fix(pdf): assinatura visual PAdES em conformidade ITI via SignatureOptions e nome limpo do assinante sem CPF
This commit is contained in:
+101
-68
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -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() + ")");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user