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