fix(pdf): assinatura visual PAdES em conformidade ITI via SignatureOptions e nome limpo do assinante sem CPF

This commit is contained in:
Claudecio Martins
2026-10-11 10:57:19 -03:00
parent 5360caaddf
commit 0cd4025f3c
2 changed files with 111 additions and 75 deletions
@@ -1,5 +1,6 @@
package br.com.cybercore.signer.core.pdf; 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.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningKeyMaterial; import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
import br.com.cybercore.signer.core.pdf.seal.PdfSealData; 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.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; 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.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.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.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; 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.apache.pdfbox.rendering.PDFRenderer;
import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray; import org.bouncycastle.cms.CMSProcessableByteArray;
@@ -172,6 +182,10 @@ public class PdfSigningService {
String issuerCn = extractIssuerCn(cert.issuerDn()); String issuerCn = extractIssuerCn(cert.issuerDn());
String signingTimeStr = DATE_TIME_FORMATTER.format(Instant.now()); 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"); MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] pdfDigest = sha256.digest(pdfBytes); byte[] pdfDigest = sha256.digest(pdfBytes);
String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase(); String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase();
@@ -183,7 +197,7 @@ public class PdfSigningService {
// 3. Monta dados do selo visual // 3. Monta dados do selo visual
PdfSealData sealData = new PdfSealData( PdfSealData sealData = new PdfSealData(
cert.commonName() != null ? cert.commonName() : cert.alias(), signerName,
signerDoc, signerDoc,
signingTimeStr, signingTimeStr,
issuerCn, issuerCn,
@@ -195,41 +209,63 @@ public class PdfSigningService {
effectiveExtraInfo effectiveExtraInfo
); );
// 4. Carrega o PDF original uma única vez, estampa o selo se solicitado // 4. Carrega o PDF original e aplica a assinatura PAdES via atualização incremental (Incremental Update),
// e aplica a nova assinatura PAdES via atualização incremental (Incremental Update), // preservando intactas quaisquer assinaturas digitais pré-existentes. Em conformidade com a ISO 32000-1 / ETSI PAdES,
// preservando intactas quaisquer assinaturas digitais pré-existentes. // 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(); ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
try (PDDocument doc = Loader.loadPDF(pdfBytes)) { try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
if (options.visibleSeal()) {
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
applyVisualSealToDocument(doc, sealPng, theme, options);
}
PDSignature signature = new PDSignature(); PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName(cert.commonName()); signature.setName(signerName);
signature.setLocation(options.location()); signature.setLocation(options.location());
signature.setReason(options.reason()); signature.setReason(options.reason());
signature.setSignDate(Calendar.getInstance()); signature.setSignDate(Calendar.getInstance());
SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial); 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()); logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
return finalOut.toByteArray(); return finalOut.toByteArray();
} }
private void applyVisualSealToDocument(PDDocument doc, byte[] sealPng, PdfSealTheme theme, private int resolveTargetPage(PDDocument doc, PdfSignatureOptions options) {
PdfSignatureOptions options) throws IOException {
int totalPages = doc.getNumberOfPages(); int totalPages = doc.getNumberOfPages();
if (totalPages == 0) { if (totalPages <= 0) {
return; 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) float scale = (options.customSealScale() != null && options.customSealScale() > 0)
? options.customSealScale().floatValue() ? options.customSealScale().floatValue()
@@ -238,48 +274,13 @@ public class PdfSigningService {
float sealHeight = (theme.height() * 0.72f) * scale; float sealHeight = (theme.height() * 0.72f) * scale;
float margin = 20f; 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) { if (options.customX() != null && options.customY() != null) {
int targetPageIdx = totalPages - 1; x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) { y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
targetPageIdx = options.targetPageNumber() - 1; } else {
}
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;
switch (options.position()) { switch (options.position()) {
case BOTTOM_LEFT -> { case BOTTOM_LEFT -> {
x = margin; x = margin;
@@ -298,25 +299,57 @@ public class PdfSigningService {
y = margin; 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) { * Cria um documento template PAdES para o SignatureOptions do PDFBox, incorporando o selo visual
page.getCOSObject().setNeedToBeUpdated(true); * como a Aparência Normal (/AP /N) da anotação da assinatura (PDAnnotationWidget).
if (page.getResources() != null && page.getResources().getCOSObject() != null) { * O stream /Contents da página do documento original permanece completamente inalterado.
page.getResources().getCOSObject().setNeedToBeUpdated(true); */
} private InputStream createVisualSignatureTemplate(PDDocument srcDoc, int pageNum,
org.apache.pdfbox.cos.COSBase contents = page.getCOSObject().getDictionaryObject(org.apache.pdfbox.cos.COSName.CONTENTS); PDRectangle rect, byte[] sealPngBytes) throws IOException {
if (contents instanceof org.apache.pdfbox.cos.COSUpdateInfo updateInfo) { try (PDDocument doc = new PDDocument()) {
updateInfo.setNeedToBeUpdated(true); 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());
} }
} }
@@ -1,5 +1,6 @@
package br.com.cybercore.signer.ui.view; 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.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate; import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.pdf.PdfSignatureOptions; import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
@@ -216,8 +217,9 @@ public final class PdfCertificateSignDialog {
if (empty || item == null) { if (empty || item == null) {
setText(null); setText(null);
} else { } 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() + "]" : ""); 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) { if (empty || item == null) {
setText(null); setText(null);
} else { } 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() + "]" : ""); 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() + ")");
} }
} }
}); });