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;
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);
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)
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;
if (options.customX() != null && options.customY() != null) {
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());
}
}
@@ -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() + ")");
}
}
});