From 0cd4025f3c16189b480c5ee13acea74ab5cbd0a9 Mon Sep 17 00:00:00 2001 From: Claudecio Martins Date: Sun, 11 Oct 2026 10:57:19 -0300 Subject: [PATCH] fix(pdf): assinatura visual PAdES em conformidade ITI via SignatureOptions e nome limpo do assinante sem CPF --- .../signer/core/pdf/PdfSigningService.java | 179 +++++++++++------- .../ui/view/PdfCertificateSignDialog.java | 7 +- 2 files changed, 111 insertions(+), 75 deletions(-) diff --git a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java index 260b2cb..199cd5a 100644 --- a/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java +++ b/modules/cybercore-core/src/main/java/br/com/cybercore/signer/core/pdf/PdfSigningService.java @@ -1,5 +1,6 @@ package br.com.cybercore.signer.core.pdf; +import br.com.cybercore.signer.core.crypto.X509CertificateUtils; import br.com.cybercore.signer.core.domain.ManagedCertificate; import br.com.cybercore.signer.core.domain.SigningKeyMaterial; import br.com.cybercore.signer.core.pdf.seal.PdfSealData; @@ -12,10 +13,19 @@ import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.common.PDStream; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; import org.apache.pdfbox.rendering.PDFRenderer; import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cms.CMSProcessableByteArray; @@ -172,6 +182,10 @@ public class PdfSigningService { String issuerCn = extractIssuerCn(cert.issuerDn()); String signingTimeStr = DATE_TIME_FORMATTER.format(Instant.now()); + // Extrai o nome amigável do titular (removendo qualquer sufixo como :CPF ou :CNPJ para o nome do assinante) + String rawName = cert.commonName() != null ? cert.commonName() : cert.alias(); + String signerName = X509CertificateUtils.extractHolderName(rawName); + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); byte[] pdfDigest = sha256.digest(pdfBytes); String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase(); @@ -183,7 +197,7 @@ public class PdfSigningService { // 3. Monta dados do selo visual PdfSealData sealData = new PdfSealData( - cert.commonName() != null ? cert.commonName() : cert.alias(), + signerName, signerDoc, signingTimeStr, issuerCn, @@ -195,41 +209,63 @@ public class PdfSigningService { effectiveExtraInfo ); - // 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. + // 4. Carrega o PDF original e aplica a assinatura PAdES via atualização incremental (Incremental Update), + // preservando intactas quaisquer assinaturas digitais pré-existentes. Em conformidade com a ISO 32000-1 / ETSI PAdES, + // o selo visual é associado diretamente ao widget da assinatura (/Widget /AP /N) sem alterar o stream /Contents + // das páginas existentes, garantindo que o validador do ITI aprove a verificação incremental sem indeterminação. ByteArrayOutputStream finalOut = new ByteArrayOutputStream(); 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); - signature.setName(cert.commonName()); + signature.setName(signerName); signature.setLocation(options.location()); signature.setReason(options.reason()); signature.setSignDate(Calendar.getInstance()); SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial); - doc.addSignature(signature, signer); - doc.saveIncremental(finalOut); + + if (options.visibleSeal()) { + byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData); + int targetPage = resolveTargetPage(doc, options); + PDRectangle rect = calculateSealRectangle(doc, targetPage, theme, options); + + try (InputStream visualTemplate = createVisualSignatureTemplate(doc, targetPage, rect, sealPng); + SignatureOptions signatureOptions = new SignatureOptions()) { + signatureOptions.setVisualSignature(visualTemplate); + signatureOptions.setPage(targetPage); + doc.addSignature(signature, signer, signatureOptions); + doc.saveIncremental(finalOut); + } + } else { + doc.addSignature(signature, signer); + doc.saveIncremental(finalOut); + } } logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size()); return finalOut.toByteArray(); } - private void applyVisualSealToDocument(PDDocument doc, byte[] sealPng, PdfSealTheme theme, - PdfSignatureOptions options) throws IOException { + private int resolveTargetPage(PDDocument doc, PdfSignatureOptions options) { int totalPages = doc.getNumberOfPages(); - if (totalPages == 0) { - return; + if (totalPages <= 0) { + return 0; } + if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) { + return options.targetPageNumber() - 1; + } + if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) { + return 0; + } + return totalPages - 1; + } - PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal"); + private PDRectangle calculateSealRectangle(PDDocument doc, int pageIndex, PdfSealTheme theme, PdfSignatureOptions options) { + PDPage page = doc.getPage(pageIndex); + PDRectangle mediaBox = page.getMediaBox(); + float pageW = mediaBox.getWidth(); + float pageH = mediaBox.getHeight(); float scale = (options.customSealScale() != null && options.customSealScale() > 0) ? options.customSealScale().floatValue() @@ -238,48 +274,13 @@ public class PdfSigningService { float sealHeight = (theme.height() * 0.72f) * scale; float margin = 20f; - // Se coordenadas customizadas foram informadas (clique ou drag & drop na página) + float x; + float y; + 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(); - - 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); - } - - markPageAsUpdated(page); - return; - } - - // 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; - } - - 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; - + x = Math.max(0, Math.min(options.customX(), pageW - sealWidth)); + y = Math.max(0, Math.min(options.customY(), pageH - sealHeight)); + } else { switch (options.position()) { case BOTTOM_LEFT -> { x = margin; @@ -298,25 +299,57 @@ public class PdfSigningService { y = margin; } } - - try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) { - cs.drawImage(pdImage, x, y, sealWidth, sealHeight); - } - - markPageAsUpdated(page); } + + return new PDRectangle(x, y, sealWidth, sealHeight); } - 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); + /** + * Cria um documento template PAdES para o SignatureOptions do PDFBox, incorporando o selo visual + * como a Aparência Normal (/AP /N) da anotação da assinatura (PDAnnotationWidget). + * O stream /Contents da página do documento original permanece completamente inalterado. + */ + private InputStream createVisualSignatureTemplate(PDDocument srcDoc, int pageNum, + PDRectangle rect, byte[] sealPngBytes) throws IOException { + try (PDDocument doc = new PDDocument()) { + PDPage page = new PDPage(srcDoc.getPage(pageNum).getMediaBox()); + doc.addPage(page); + + PDAcroForm acroForm = new PDAcroForm(doc); + doc.getDocumentCatalog().setAcroForm(acroForm); + + PDSignatureField signatureField = new PDSignatureField(acroForm); + PDAnnotationWidget widget = signatureField.getWidgets().get(0); + acroForm.getFields().add(signatureField); + acroForm.setSignaturesExist(true); + acroForm.setAppendOnly(true); + acroForm.getCOSObject().setDirect(true); + + widget.setRectangle(rect); + + PDStream stream = new PDStream(doc); + PDFormXObject form = new PDFormXObject(stream); + PDResources res = new PDResources(); + form.setResources(res); + form.setFormType(1); + + PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight()); + form.setBBox(bbox); + + PDAppearanceDictionary appearance = new PDAppearanceDictionary(); + appearance.getCOSObject().setDirect(true); + PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject()); + appearance.setNormalAppearance(appearanceStream); + widget.setAppearance(appearance); + + try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) { + PDImageXObject img = PDImageXObject.createFromByteArray(doc, sealPngBytes, "cybercore_seal"); + cs.drawImage(img, 0, 0, rect.getWidth(), rect.getHeight()); } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + doc.save(baos); + return new ByteArrayInputStream(baos.toByteArray()); } } diff --git a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java index c936f9c..c9d7bde 100644 --- a/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java +++ b/modules/cybercore-ui/src/main/java/br/com/cybercore/signer/ui/view/PdfCertificateSignDialog.java @@ -1,5 +1,6 @@ package br.com.cybercore.signer.ui.view; +import br.com.cybercore.signer.core.crypto.X509CertificateUtils; import br.com.cybercore.signer.core.domain.CertificateType; import br.com.cybercore.signer.core.domain.ManagedCertificate; import br.com.cybercore.signer.core.pdf.PdfSignatureOptions; @@ -216,8 +217,9 @@ public final class PdfCertificateSignDialog { if (empty || item == null) { setText(null); } else { + String holderName = X509CertificateUtils.extractHolderName(item.commonName() != null ? item.commonName() : item.alias()); String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : ""); - setText(item.commonName() + doc + " (" + item.type() + ")"); + setText(holderName + doc + " (" + item.type() + ")"); } } }); @@ -228,8 +230,9 @@ public final class PdfCertificateSignDialog { if (empty || item == null) { setText(null); } else { + String holderName = X509CertificateUtils.extractHolderName(item.commonName() != null ? item.commonName() : item.alias()); String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : ""); - setText(item.commonName() + doc + " (" + item.type() + ")"); + setText(holderName + doc + " (" + item.type() + ")"); } } });