fix(pdf): co-assinatura incremental com preservação de assinaturas anteriores e trava anti-salto no redimensionamento
This commit is contained in:
+88
-81
@@ -195,18 +195,16 @@ public class PdfSigningService {
|
||||
effectiveExtraInfo
|
||||
);
|
||||
|
||||
// 4. Carrega o PDF e estampa o selo se solicitado
|
||||
byte[] intermediatePdf;
|
||||
if (options.visibleSeal()) {
|
||||
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
|
||||
intermediatePdf = applyVisualSealToPdf(pdfBytes, sealPng, theme, options);
|
||||
} else {
|
||||
intermediatePdf = pdfBytes;
|
||||
}
|
||||
|
||||
// 5. Aplica a assinatura PAdES criptográfica via PDFBox e Bouncy Castle
|
||||
// 4. Carrega o PDF original uma única vez, estampa o selo se solicitado
|
||||
// e aplica a nova assinatura PAdES via atualização incremental (Incremental Update),
|
||||
// preservando intactas quaisquer assinaturas digitais pré-existentes.
|
||||
ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
|
||||
try (PDDocument doc = Loader.loadPDF(intermediatePdf)) {
|
||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
||||
if (options.visibleSeal()) {
|
||||
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
|
||||
applyVisualSealToDocument(doc, sealPng, theme, options);
|
||||
}
|
||||
|
||||
PDSignature signature = new PDSignature();
|
||||
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
||||
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
|
||||
@@ -224,93 +222,102 @@ public class PdfSigningService {
|
||||
return finalOut.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] applyVisualSealToPdf(byte[] pdfBytes, byte[] sealPng, PdfSealTheme theme,
|
||||
PdfSignatureOptions options) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
||||
int totalPages = doc.getNumberOfPages();
|
||||
if (totalPages == 0) {
|
||||
return pdfBytes;
|
||||
}
|
||||
private void applyVisualSealToDocument(PDDocument doc, byte[] sealPng, PdfSealTheme theme,
|
||||
PdfSignatureOptions options) throws IOException {
|
||||
int totalPages = doc.getNumberOfPages();
|
||||
if (totalPages == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal");
|
||||
PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal");
|
||||
|
||||
float scale = (options.customSealScale() != null && options.customSealScale() > 0)
|
||||
? options.customSealScale().floatValue()
|
||||
: 1.0f;
|
||||
float sealWidth = (theme.width() * 0.72f) * scale;
|
||||
float sealHeight = (theme.height() * 0.72f) * scale;
|
||||
float margin = 20f;
|
||||
float scale = (options.customSealScale() != null && options.customSealScale() > 0)
|
||||
? options.customSealScale().floatValue()
|
||||
: 1.0f;
|
||||
float sealWidth = (theme.width() * 0.72f) * scale;
|
||||
float sealHeight = (theme.height() * 0.72f) * scale;
|
||||
float margin = 20f;
|
||||
|
||||
// Se coordenadas customizadas foram informadas (clique ou drag & drop na página)
|
||||
if (options.customX() != null && options.customY() != null) {
|
||||
int targetPageIdx = totalPages - 1;
|
||||
if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) {
|
||||
targetPageIdx = options.targetPageNumber() - 1;
|
||||
}
|
||||
// Se coordenadas customizadas foram informadas (clique ou drag & drop na página)
|
||||
if (options.customX() != null && options.customY() != null) {
|
||||
int targetPageIdx = totalPages - 1;
|
||||
if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) {
|
||||
targetPageIdx = options.targetPageNumber() - 1;
|
||||
}
|
||||
|
||||
PDPage page = doc.getPage(targetPageIdx);
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
float pageW = mediaBox.getWidth();
|
||||
float pageH = mediaBox.getHeight();
|
||||
PDPage page = doc.getPage(targetPageIdx);
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
float pageW = mediaBox.getWidth();
|
||||
float pageH = mediaBox.getHeight();
|
||||
|
||||
float x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
|
||||
float y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
|
||||
float x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
|
||||
float y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
|
||||
|
||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
||||
}
|
||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
||||
}
|
||||
|
||||
doc.save(baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
markPageAsUpdated(page);
|
||||
return;
|
||||
}
|
||||
|
||||
// Caso contrário, usa posicionamento por enum (BOTTOM_RIGHT, etc.)
|
||||
int startPage = 0;
|
||||
int endPage = totalPages - 1;
|
||||
// Caso contrário, usa posicionamento por enum (BOTTOM_RIGHT, etc.)
|
||||
int startPage = 0;
|
||||
int endPage = totalPages - 1;
|
||||
|
||||
if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) {
|
||||
endPage = 0;
|
||||
} else if (options.pageChoice() == PdfSealPageChoice.LAST_PAGE) {
|
||||
startPage = totalPages - 1;
|
||||
}
|
||||
if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) {
|
||||
endPage = 0;
|
||||
} else if (options.pageChoice() == PdfSealPageChoice.LAST_PAGE) {
|
||||
startPage = totalPages - 1;
|
||||
}
|
||||
|
||||
for (int i = startPage; i <= endPage; i++) {
|
||||
PDPage page = doc.getPage(i);
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
float pageW = mediaBox.getWidth();
|
||||
float pageH = mediaBox.getHeight();
|
||||
for (int i = startPage; i <= endPage; i++) {
|
||||
PDPage page = doc.getPage(i);
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
float pageW = mediaBox.getWidth();
|
||||
float pageH = mediaBox.getHeight();
|
||||
|
||||
float x;
|
||||
float y;
|
||||
float x;
|
||||
float y;
|
||||
|
||||
switch (options.position()) {
|
||||
case BOTTOM_LEFT -> {
|
||||
x = margin;
|
||||
y = margin;
|
||||
}
|
||||
case TOP_RIGHT -> {
|
||||
x = pageW - sealWidth - margin;
|
||||
y = pageH - sealHeight - margin;
|
||||
}
|
||||
case TOP_LEFT -> {
|
||||
x = margin;
|
||||
y = pageH - sealHeight - margin;
|
||||
}
|
||||
default -> { // BOTTOM_RIGHT
|
||||
x = pageW - sealWidth - margin;
|
||||
y = margin;
|
||||
}
|
||||
switch (options.position()) {
|
||||
case BOTTOM_LEFT -> {
|
||||
x = margin;
|
||||
y = margin;
|
||||
}
|
||||
case TOP_RIGHT -> {
|
||||
x = pageW - sealWidth - margin;
|
||||
y = pageH - sealHeight - margin;
|
||||
}
|
||||
case TOP_LEFT -> {
|
||||
x = margin;
|
||||
y = pageH - sealHeight - margin;
|
||||
}
|
||||
default -> { // BOTTOM_RIGHT
|
||||
x = pageW - sealWidth - margin;
|
||||
y = margin;
|
||||
}
|
||||
}
|
||||
|
||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
||||
}
|
||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
||||
}
|
||||
|
||||
doc.save(baos);
|
||||
markPageAsUpdated(page);
|
||||
}
|
||||
}
|
||||
|
||||
private void markPageAsUpdated(PDPage page) {
|
||||
if (page != null && page.getCOSObject() != null) {
|
||||
page.getCOSObject().setNeedToBeUpdated(true);
|
||||
if (page.getResources() != null && page.getResources().getCOSObject() != null) {
|
||||
page.getResources().getCOSObject().setNeedToBeUpdated(true);
|
||||
}
|
||||
org.apache.pdfbox.cos.COSBase contents = page.getCOSObject().getDictionaryObject(org.apache.pdfbox.cos.COSName.CONTENTS);
|
||||
if (contents instanceof org.apache.pdfbox.cos.COSUpdateInfo updateInfo) {
|
||||
updateInfo.setNeedToBeUpdated(true);
|
||||
}
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
|
||||
+76
-2
@@ -18,6 +18,8 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import br.com.cybercore.signer.core.validation.SignatureValidationService;
|
||||
import br.com.cybercore.signer.core.validation.ValidationResult;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -41,6 +43,7 @@ class PdfSigningServiceTest {
|
||||
private CertificateManager certificateManager;
|
||||
private PdfSigningService pdfSigningService;
|
||||
private String testCertId;
|
||||
private String testCert2Id;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
@@ -74,13 +77,29 @@ class PdfSigningServiceTest {
|
||||
char[] pwd = "password123".toCharArray();
|
||||
ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert});
|
||||
|
||||
// Gera segundo par de chaves e certificado para co-assinatura
|
||||
KeyPair kp2 = kpg.generateKeyPair();
|
||||
X500Name subject2 = new X500Name("CN=Maria Souza:98765432100, O=CyberCore Testes, C=BR");
|
||||
X509v3CertificateBuilder certBuilder2 = new JcaX509v3CertificateBuilder(
|
||||
subject2,
|
||||
BigInteger.valueOf(System.currentTimeMillis() + 100),
|
||||
notBefore,
|
||||
notAfter,
|
||||
subject2,
|
||||
kp2.getPublic()
|
||||
);
|
||||
ContentSigner signer2 = new JcaContentSignerBuilder("SHA256withRSA").build(kp2.getPrivate());
|
||||
X509Certificate cert2 = new JcaX509CertificateConverter().getCertificate(certBuilder2.build(signer2));
|
||||
ks.setKeyEntry("test-cert-2", kp2.getPrivate(), pwd, new Certificate[]{cert2});
|
||||
|
||||
Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste");
|
||||
provider.setKeyStore(ks, pwd);
|
||||
certificateManager.registerProvider(provider);
|
||||
|
||||
List<ManagedCertificate> list = certificateManager.discoverCertificates();
|
||||
assertThat(list).isNotEmpty();
|
||||
this.testCertId = list.get(0).id();
|
||||
assertThat(list).hasSize(2);
|
||||
this.testCertId = list.stream().filter(c -> c.alias().equals("test-cert")).findFirst().orElseThrow().id();
|
||||
this.testCert2Id = list.stream().filter(c -> c.alias().equals("test-cert-2")).findFirst().orElseThrow().id();
|
||||
|
||||
this.pdfSigningService = new PdfSigningService(certificateManager);
|
||||
}
|
||||
@@ -215,4 +234,59 @@ class PdfSigningServiceTest {
|
||||
byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f);
|
||||
assertThat(pngBytes).isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve manter ambas as assinaturas válidas ao assinar um PDF que já possui assinatura digital (Co-assinatura PAdES)")
|
||||
void shouldKeepBothSignaturesValidWhenSigningAlreadySignedPdf() throws Exception {
|
||||
byte[] originalPdf = createSamplePdf();
|
||||
|
||||
// 1ª Assinatura com o primeiro certificado
|
||||
PdfSignatureOptions firstOptions = PdfSignatureOptions.builder(testCertId)
|
||||
.visibleSeal(true)
|
||||
.position(PdfSealPosition.BOTTOM_RIGHT)
|
||||
.reason("Primeira Aprovação")
|
||||
.location("Brasília, DF")
|
||||
.build();
|
||||
|
||||
byte[] firstSignedPdf = pdfSigningService.signPdf(originalPdf, firstOptions);
|
||||
assertThat(firstSignedPdf).isNotNull();
|
||||
|
||||
// 2ª Assinatura sobre o documento já assinado com o segundo certificado
|
||||
PdfSignatureOptions secondOptions = PdfSignatureOptions.builder(testCert2Id)
|
||||
.visibleSeal(true)
|
||||
.position(PdfSealPosition.TOP_LEFT)
|
||||
.reason("Segunda Aprovação Conjunta")
|
||||
.location("São Paulo, SP")
|
||||
.build();
|
||||
|
||||
byte[] doubleSignedPdf = pdfSigningService.signPdf(firstSignedPdf, secondOptions);
|
||||
assertThat(doubleSignedPdf).isNotNull();
|
||||
assertThat(doubleSignedPdf.length).isGreaterThan(firstSignedPdf.length);
|
||||
|
||||
// Validação estrutural das assinaturas no PDDocument
|
||||
try (PDDocument doc = Loader.loadPDF(doubleSignedPdf)) {
|
||||
List<PDSignature> signatures = doc.getSignatureDictionaries();
|
||||
assertThat(signatures).hasSize(2);
|
||||
assertThat(signatures.get(0).getName()).contains("Joao da Silva");
|
||||
assertThat(signatures.get(0).getReason()).isEqualTo("Primeira Aprovação");
|
||||
assertThat(signatures.get(1).getName()).contains("Maria Souza");
|
||||
assertThat(signatures.get(1).getReason()).isEqualTo("Segunda Aprovação Conjunta");
|
||||
}
|
||||
|
||||
// Validação criptográfica rigorosa via SignatureValidationService (ambas as assinaturas devem ser íntegras)
|
||||
SignatureValidationService validationService = new SignatureValidationService();
|
||||
ValidationResult validationResult = validationService.validateAttached(doubleSignedPdf, "duplo_assinado.pdf");
|
||||
|
||||
assertThat(validationResult.isValid()).isTrue();
|
||||
assertThat(validationResult.signers()).hasSize(2);
|
||||
|
||||
// Ambas as assinaturas e digests devem ser 100% válidos
|
||||
assertThat(validationResult.signers().get(0).signatureValid()).isTrue();
|
||||
assertThat(validationResult.signers().get(0).digestValid()).isTrue();
|
||||
assertThat(validationResult.signers().get(0).signerName()).contains("Joao da Silva");
|
||||
|
||||
assertThat(validationResult.signers().get(1).signatureValid()).isTrue();
|
||||
assertThat(validationResult.signers().get(1).digestValid()).isTrue();
|
||||
assertThat(validationResult.signers().get(1).signerName()).contains("Maria Souza");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user