4 Commits
14 changed files with 1153 additions and 226 deletions
+81 -19
View File
@@ -1,16 +1,27 @@
# Assinaturas XML (XMLDSig e ICP-Brasil) # Assinaturas XML no Padrão XAdES e ICP-Brasil
Este documento descreve a especificação e o funcionamento da assinatura digital de arquivos XML no padrão **W3C XMLDSig (XML Digital Signatures)** e **ICP-Brasil** no CyberCore Signer. Este documento descreve a especificação, conformidade e o funcionamento da assinatura digital de arquivos XML no padrão avançado **XAdES (XML Advanced Electronic Signatures - ETSI TS 101 903 e ICP-Brasil DOC-ICP-15.03)** e **W3C XMLDSig** no CyberCore Signer.
--- ---
## 1. Visão Geral ## 1. Visão Geral
O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando a engine padronizada e segura da plataforma Java (`javax.xml.crypto.dsig`). O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando a engine padronizada e segura da plataforma Java (`javax.xml.crypto.dsig`), garantindo conformidade com o padrão **XAdES-BES** adotado pelo ITI (Instituto Nacional de Tecnologia da Informação) na infraestrutura ICP-Brasil (perfil AD-RB).
### 1.1 Casos de Uso Suportados ### 1.1 Perfis Suportados
- **Documento Inteiro Envelopado (`URI=""`):** Contratos, despachos, declarações e relatórios em formato XML. * **`XAdES-BES` (Padrão Recomendado / ICP-Brasil):**
- Conforme especificação técnica **ETSI TS 101 903** e norma **DOC-ICP-15.03**.
- Encapsula o elemento `<ds:Object>` contendo `<xades:QualifyingProperties>` e `<xades:SignedProperties>`.
- Inclui propriedade qualificada de data/hora da assinatura (`<xades:SigningTime>`) em formato UTC ISO-8601.
- Inclui vínculo criptográfico irrefutável do certificado do signatário (`<xades:SigningCertificate>`) contendo o digest SHA-256 do certificado DER e referências do emissor/serial (`<xades:IssuerSerial>`).
- Protege as propriedades qualificadas através de uma `<ds:Reference>` em `<ds:SignedInfo>` com `Type="http://uri.etsi.org/01903#SignedProperties"`.
* **`XMLDSig` Clássico (W3C Enveloped):**
- Mantido para compatibilidade com layouts estritos de notas fiscais eletrônicas legadas (ex: NF-e, CT-e, MDF-e SEFAZ).
### 1.2 Casos de Uso Suportados
- **Documento Inteiro Envelopado (`URI=""`):** Contratos, despachos, declarações, laudos e relatórios em XML.
- **Assinatura de Elementos Específicos por ID:** - **Assinatura de Elementos Específicos por ID:**
- **NF-e (Nota Fiscal Eletrônica):** Assinatura da tag `<infNFe Id="NFe...">` - **NF-e (Nota Fiscal Eletrônica):** Assinatura da tag `<infNFe Id="NFe...">`
- **CT-e (Conhecimento de Transporte Eletrônico):** Assinatura da tag `<infCte Id="CTe...">` - **CT-e (Conhecimento de Transporte Eletrônico):** Assinatura da tag `<infCte Id="CTe...">`
@@ -19,19 +30,69 @@ O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando
--- ---
## 2. Padrões Criptográficos ## 2. Estrutura Criptográfica do XAdES-BES
- **Canonicalização (C14N):** `http://www.w3.org/TR/2001/REC-xml-c14n-20010315` (C14N inclusiva sem comentários). ```xml
- **Transformações:** <ds:Signature Id="sig-[UUID]" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
- `http://www.w3.org/2000/09/xmldsig#enveloped-signature` (remove a assinatura do cálculo do digest). <ds:SignedInfo>
- C14N padrão. <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
- **Algoritmo de Digest:** SHA-256 (`http://www.w3.org/2001/04/xmlenc#sha256`). <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
- **Algoritmo de Assinatura:** RSA-SHA256 (`http://www.w3.org/2001/04/xmldsig-more#rsa-sha256`).
- **KeyInfo:** Informações do certificado X.509 embutidas em `<X509Data><X509Certificate>`. <!-- Referência 0: Dados de Negócio (Documento completo ou elemento específico) -->
<ds:Reference URI="#[ElementoId]">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>[Digest do nó em Base64]</ds:DigestValue>
</ds:Reference>
<!-- Referência 1: Propriedades Qualificadas Assinadas XAdES -->
<ds:Reference URI="#signed-props-[UUID]" Type="http://uri.etsi.org/01903#SignedProperties">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>[Digest das SignedProperties em Base64]</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>[Assinatura RSA-SHA256 em Base64]</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>[Certificado DER do signatário em Base64]</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<ds:Object>
<xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#sig-[UUID]">
<xades:SignedProperties Id="signed-props-[UUID]" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xades="http://uri.etsi.org/01903/v1.3.2#">
<xades:SignedSignatureProperties>
<xades:SigningTime>2026-10-11T14:05:00Z</xades:SigningTime>
<xades:SigningCertificate>
<xades:Cert>
<xades:CertDigest>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>[SHA-256 do Certificado]</ds:DigestValue>
</xades:CertDigest>
<xades:IssuerSerial>
<ds:X509IssuerName>[Emissor X.500]</ds:X509IssuerName>
<ds:X509SerialNumber>[Número Serial]</ds:X509SerialNumber>
</xades:IssuerSerial>
</xades:Cert>
</xades:SigningCertificate>
</xades:SignedSignatureProperties>
</xades:SignedProperties>
</xades:QualifyingProperties>
</ds:Object>
</ds:Signature>
```
--- ---
## 3. Segurança e Mitigação contra XXE ## 3. Segurança e Mitigação contra Ataques XXE
O parser XML é configurado com blindagem estrita contra ataques **XML External Entity (XXE)** e expansões recursivas de entidades: O parser XML é configurado com blindagem estrita contra ataques **XML External Entity (XXE)** e expansões recursivas de entidades:
@@ -47,10 +108,11 @@ dbf.setExpandEntityReferences(false);
--- ---
## 4. Validação Pós-Assinatura ## 4. Validação Pós-Assinatura e Auditoria
O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` permite auditar a assinatura imediatamente após sua emissão, verificando: O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` e o `SignatureValidationService` realizam:
1. A integridade criptográfica da assinatura (`SignedInfo`). 1. **Validação criptográfica de SignedInfo:** Confere se o `SignatureValue` foi emitido pela chave pública correspondente.
2. A integridade dos digests de todas as referências apontadas (`Reference.validate()`). 2. **Validação de integridade dos dados de negócio:** Confere o digest do conteúdo ou elemento assinado.
3. O certificado X.509 público extraído da chave. 3. **Validação de integridade do XAdES:** Confere o digest das propriedades qualificadas (`SignedProperties`), rejeitando qualquer adulteração no horário de assinatura (`SigningTime`) ou no certificado declarado.
4. **Inspeção de signatário e datação:** Extração de nome, documento (CPF/CNPJ), autoridade certificadora emissora e datação auditável.
@@ -25,7 +25,8 @@ public record PdfSignatureOptions(
Integer targetPageNumber, Integer targetPageNumber,
CpfDisplayMode cpfDisplayMode, CpfDisplayMode cpfDisplayMode,
Boolean includeDateTime, Boolean includeDateTime,
String customExtraInfo String customExtraInfo,
Double customSealScale
) { ) {
public PdfSignatureOptions { public PdfSignatureOptions {
@@ -51,6 +52,21 @@ public record PdfSignatureOptions(
if (includeDateTime == null) { if (includeDateTime == null) {
includeDateTime = sealTheme.defaultIncludeDateTime(); includeDateTime = sealTheme.defaultIncludeDateTime();
} }
if (customSealScale == null || customSealScale <= 0) {
customSealScale = 1.0;
}
}
/**
* Construtor para compatibilidade com assinaturas anteriores a customSealScale.
*/
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
String reason, String location, Float customX, Float customY,
Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime,
String customExtraInfo) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location,
customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, customExtraInfo, 1.0);
} }
/** /**
@@ -61,7 +77,7 @@ public record PdfSignatureOptions(
String reason, String location, Float customX, Float customY, String reason, String location, Float customX, Float customY,
Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) { Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location,
customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, null); customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, null, 1.0);
} }
/** /**
@@ -70,7 +86,7 @@ public record PdfSignatureOptions(
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal, public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice, PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
String reason, String location) { String reason, String location) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, null, null, null, null, null, null); this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, null, null, null, null, null, null, 1.0);
} }
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal, public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
@@ -143,6 +159,7 @@ public record PdfSignatureOptions(
} }
private String customExtraInfo; private String customExtraInfo;
private Double customSealScale = 1.0;
public Builder includeDateTime(boolean include) { public Builder includeDateTime(boolean include) {
this.includeDateTime = include; this.includeDateTime = include;
@@ -154,6 +171,11 @@ public record PdfSignatureOptions(
return this; return this;
} }
public Builder customSealScale(Double scale) {
this.customSealScale = scale;
return this;
}
public Builder customPosition(Float x, Float y, Integer pageNumber) { public Builder customPosition(Float x, Float y, Integer pageNumber) {
this.customX = x; this.customX = x;
this.customY = y; this.customY = y;
@@ -179,7 +201,8 @@ public record PdfSignatureOptions(
targetPageNumber, targetPageNumber,
cpfDisplayMode, cpfDisplayMode,
includeDateTime, includeDateTime,
customExtraInfo customExtraInfo,
customSealScale
); );
} }
} }
@@ -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,92 +209,78 @@ public class PdfSigningService {
effectiveExtraInfo effectiveExtraInfo
); );
// 4. Carrega o PDF e estampa o selo se solicitado // 4. Carrega o PDF original e aplica a assinatura PAdES via atualização incremental (Incremental Update),
byte[] intermediatePdf; // preservando intactas quaisquer assinaturas digitais pré-existentes. Em conformidade com a ISO 32000-1 / ETSI PAdES,
if (options.visibleSeal()) { // o selo visual é associado diretamente ao widget da assinatura (/Widget /AP /N) sem alterar o stream /Contents
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData); // das páginas existentes, garantindo que o validador do ITI aprove a verificação incremental sem indeterminação.
intermediatePdf = applyVisualSealToPdf(pdfBytes, sealPng, theme, options);
} else {
intermediatePdf = pdfBytes;
}
// 5. Aplica a assinatura PAdES criptográfica via PDFBox e Bouncy Castle
ByteArrayOutputStream finalOut = new ByteArrayOutputStream(); ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
try (PDDocument doc = Loader.loadPDF(intermediatePdf)) { try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
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);
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.addSignature(signature, signer);
doc.saveIncremental(finalOut); 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 byte[] applyVisualSealToPdf(byte[] pdfBytes, byte[] sealPng, PdfSealTheme theme, private int resolveTargetPage(PDDocument doc, PdfSignatureOptions options) {
PdfSignatureOptions options) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
int totalPages = doc.getNumberOfPages(); int totalPages = doc.getNumberOfPages();
if (totalPages == 0) { if (totalPages <= 0) {
return pdfBytes; return 0;
} }
PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal");
float sealWidth = theme.width() * 0.72f;
float sealHeight = theme.height() * 0.72f;
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) { if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) {
targetPageIdx = options.targetPageNumber() - 1; return 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);
}
doc.save(baos);
return baos.toByteArray();
}
// Caso contrário, usa posicionamento por enum (BOTTOM_RIGHT, etc.)
int startPage = 0;
int endPage = totalPages - 1;
if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) { if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) {
endPage = 0; return 0;
} else if (options.pageChoice() == PdfSealPageChoice.LAST_PAGE) { }
startPage = totalPages - 1; return totalPages - 1;
} }
for (int i = startPage; i <= endPage; i++) { private PDRectangle calculateSealRectangle(PDDocument doc, int pageIndex, PdfSealTheme theme, PdfSignatureOptions options) {
PDPage page = doc.getPage(i); PDPage page = doc.getPage(pageIndex);
PDRectangle mediaBox = page.getMediaBox(); PDRectangle mediaBox = page.getMediaBox();
float pageW = mediaBox.getWidth(); float pageW = mediaBox.getWidth();
float pageH = mediaBox.getHeight(); float pageH = mediaBox.getHeight();
float scale = (options.customSealScale() != null && options.customSealScale() > 0)
? options.customSealScale().floatValue()
: 1.0f;
float sealWidth = (theme.width() * 0.72f) * scale;
float sealHeight = (theme.height() * 0.72f) * scale;
float margin = 20f;
float x; float x;
float y; 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()) { switch (options.position()) {
case BOTTOM_LEFT -> { case BOTTOM_LEFT -> {
x = margin; x = margin;
@@ -299,15 +299,58 @@ 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);
}
} }
return new PDRectangle(x, y, sealWidth, sealHeight);
}
/**
* 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); doc.save(baos);
return new ByteArrayInputStream(baos.toByteArray());
} }
return baos.toByteArray();
} }
private static String bytesToHex(byte[] bytes) { private static String bytesToHex(byte[] bytes) {
@@ -15,6 +15,7 @@ import org.bouncycastle.util.Store;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import javax.xml.crypto.dsig.XMLSignature; import javax.xml.crypto.dsig.XMLSignature;
@@ -221,32 +222,59 @@ public final class SignatureValidationService {
} }
/** /**
* Validação de assinaturas embutidas em documentos XML (XMLDSig). * Validação de assinaturas embutidas em documentos XML (XAdES ou XMLDSig).
*/ */
private ValidationResult validateXml(byte[] xmlBytes, String fileName) { private ValidationResult validateXml(byte[] xmlBytes, String fileName) {
try { try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true); dbf.setNamespaceAware(true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xmlBytes)); Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xmlBytes));
// Registro de atributos Id no DOM
registerAllIdAttributes(doc.getDocumentElement());
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
if (nl.getLength() == 0) {
nl = doc.getElementsByTagName("Signature");
}
if (nl.getLength() == 0) { if (nl.getLength() == 0) {
return new ValidationResult( return new ValidationResult(
ValidationStatus.INVALID, ValidationStatus.INVALID,
ValidationType.XML_DSIG, ValidationType.XML_DSIG,
"Nenhum elemento <Signature> XMLDSig encontrado no documento.", "Nenhum elemento <Signature> XML encontrado no documento.",
List.of(), List.of(),
null, null,
fileName fileName
); );
} }
// Identificação de padrão XAdES
NodeList qpList = doc.getElementsByTagNameNS("http://uri.etsi.org/01903/v1.3.2#", "QualifyingProperties");
if (qpList.getLength() == 0) {
qpList = doc.getElementsByTagName("QualifyingProperties");
}
boolean isXades = qpList.getLength() > 0;
ValidationType valType = isXades ? ValidationType.XADES_XML : ValidationType.XML_DSIG;
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
List<ValidationSignerInfo> signers = new ArrayList<>(); List<ValidationSignerInfo> signers = new ArrayList<>();
boolean allValid = true; boolean allValid = true;
for (int i = 0; i < nl.getLength(); i++) { for (int i = 0; i < nl.getLength(); i++) {
DOMValidateContext valContext = new DOMValidateContext(new KeyValueKeySelector(), nl.item(i)); Element sigElement = (Element) nl.item(i);
DOMValidateContext valContext = new DOMValidateContext(new KeyValueKeySelector(), sigElement);
// Registra nós com atributo Id no valContext
NodeList allNodes = doc.getElementsByTagName("*");
for (int n = 0; n < allNodes.getLength(); n++) {
if (allNodes.item(n) instanceof Element e && e.hasAttribute("Id")) {
valContext.setIdAttributeNS(e, null, "Id");
}
}
XMLSignature signature = fac.unmarshalXMLSignature(valContext); XMLSignature signature = fac.unmarshalXMLSignature(valContext);
boolean coreValidity = signature.validate(valContext); boolean coreValidity = signature.validate(valContext);
@@ -254,22 +282,66 @@ public final class SignatureValidationService {
allValid = false; allValid = false;
} }
// Extração de metadados do signatário a partir do KeyInfo
String signerName = "Signatário XML #" + (i + 1);
String identifier = "";
String issuer = isXades ? "ICP-Brasil / XAdES" : "Assinatura Enveloped XML";
String certSubject = "XML Signature Element";
if (signature.getKeyInfo() != null) {
for (Object kiContent : signature.getKeyInfo().getContent()) {
if (kiContent instanceof javax.xml.crypto.dsig.keyinfo.X509Data x509Data) {
for (Object o : x509Data.getContent()) {
if (o instanceof X509Certificate cert) {
certSubject = cert.getSubjectX500Principal().getName();
signerName = X509CertificateUtils.extractCommonName(certSubject);
String cpf = X509CertificateUtils.extractCpf(cert, signerName);
if (cpf != null) {
identifier = "CPF: " + cpf;
} else {
String cnpj = X509CertificateUtils.extractCnpj(cert, signerName);
if (cnpj != null) {
identifier = "CNPJ: " + cnpj;
}
}
issuer = X509CertificateUtils.extractCommonName(cert.getIssuerX500Principal().getName());
break;
}
}
}
}
}
// Data de assinatura (SigningTime se XAdES, senão data atual)
String dateStr = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date());
if (isXades) {
NodeList stList = doc.getElementsByTagNameNS("http://uri.etsi.org/01903/v1.3.2#", "SigningTime");
if (stList.getLength() > 0) {
dateStr = stList.item(0).getTextContent();
}
}
signers.add(new ValidationSignerInfo( signers.add(new ValidationSignerInfo(
"Signatário XML #" + (i + 1), signerName,
"", identifier,
"Assinatura Enveloped XML", issuer,
new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()), dateStr,
coreValidity, coreValidity,
coreValidity, coreValidity,
signature.getSignedInfo().getSignatureMethod().getAlgorithm(), signature.getSignedInfo().getSignatureMethod().getAlgorithm(),
"XMLDSig Element", certSubject,
"Validade estrutural e criptográfica: " + (coreValidity ? "OK" : "FALHA") (isXades ? "Padrão XAdES-BES (ETSI TS 101 903 / ICP-Brasil)" : "Padrão XMLDSig W3C")
+ " • " + (coreValidity ? "Assinatura íntegra" : "Integridade violada")
)); ));
} }
ValidationStatus status = allValid ? ValidationStatus.VALID : ValidationStatus.INVALID; ValidationStatus status = allValid ? ValidationStatus.VALID : ValidationStatus.INVALID;
String msg = allValid ? "Documento XML assinado e válido conforme padrão W3C XML-DSig." : "Assinatura XML inválida ou com integridade violada."; String msg = allValid
return new ValidationResult(status, ValidationType.XML_DSIG, msg, signers, null, fileName); ? (isXades
? "Documento XML assinado e íntegro no padrão avançado XAdES-BES (ICP-Brasil)."
: "Documento XML assinado e válido conforme padrão W3C XMLDSig.")
: "Assinatura XML inválida ou com integridade violada.";
return new ValidationResult(status, valType, msg, signers, null, fileName);
} catch (Exception e) { } catch (Exception e) {
logger.error("Erro ao validar XML: {}", e.getMessage(), e); logger.error("Erro ao validar XML: {}", e.getMessage(), e);
@@ -284,6 +356,23 @@ public final class SignatureValidationService {
} }
} }
private static void registerAllIdAttributes(Element root) {
if (root == null) return;
if (root.hasAttribute("Id")) {
root.setIdAttribute("Id", true);
} else if (root.hasAttribute("id")) {
root.setIdAttribute("id", true);
} else if (root.hasAttribute("ID")) {
root.setIdAttribute("ID", true);
}
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
if (children.item(i) instanceof Element child) {
registerAllIdAttributes(child);
}
}
}
/** /**
* Processa e valida criptograficamente os signatários de um pacote CMSSignedData. * Processa e valida criptograficamente os signatários de um pacote CMSSignedData.
*/ */
@@ -7,6 +7,7 @@ public enum ValidationType {
DETACHED_CMS("Assinatura Destacada (CAdES / PKCS#7)", false), DETACHED_CMS("Assinatura Destacada (CAdES / PKCS#7)", false),
ATTACHED_CMS("Assinatura Incorporada (CMS / P7M)", true), ATTACHED_CMS("Assinatura Incorporada (CMS / P7M)", true),
PADES_PDF("Assinatura Incorporada em PDF (PAdES)", true), PADES_PDF("Assinatura Incorporada em PDF (PAdES)", true),
XADES_XML("Assinatura Incorporada em XML (XAdES / ICP-Brasil)", true),
XML_DSIG("Assinatura Incorporada em XML (XML-DSig)", true), XML_DSIG("Assinatura Incorporada em XML (XML-DSig)", true),
UNKNOWN("Formato Desconhecido", false); UNKNOWN("Formato Desconhecido", false);
@@ -3,30 +3,43 @@ package br.com.cybercore.signer.core.xml;
import java.util.Objects; import java.util.Objects;
/** /**
* Parâmetros de configuração para a operação de assinatura digital XMLDSig. * Parâmetros de configuração para a operação de assinatura digital XML (XAdES ou XMLDSig).
*/ */
public record XmlSignatureOptions( public record XmlSignatureOptions(
String certificateId, String certificateId,
char[] pinOrPassword, char[] pinOrPassword,
String targetElementTag, String targetElementTag,
String targetElementId, String targetElementId,
String signatureAlgorithm String signatureAlgorithm,
XmlSignatureProfile profile
) { ) {
public static final String DEFAULT_ALGORITHM = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; public static final String DEFAULT_ALGORITHM = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
public static final XmlSignatureProfile DEFAULT_PROFILE = XmlSignatureProfile.XADES_BES;
public XmlSignatureOptions { public XmlSignatureOptions {
Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo."); Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo.");
if (signatureAlgorithm == null || signatureAlgorithm.isBlank()) { if (signatureAlgorithm == null || signatureAlgorithm.isBlank()) {
signatureAlgorithm = DEFAULT_ALGORITHM; signatureAlgorithm = DEFAULT_ALGORITHM;
} }
if (profile == null) {
profile = DEFAULT_PROFILE;
}
} }
public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword) { public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword) {
return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM); return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM, DEFAULT_PROFILE);
}
public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword, XmlSignatureProfile profile) {
return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM, profile);
} }
public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id) { public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id) {
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM); return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM, DEFAULT_PROFILE);
}
public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id, XmlSignatureProfile profile) {
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM, profile);
} }
} }
@@ -0,0 +1,37 @@
package br.com.cybercore.signer.core.xml;
/**
* Perfis de assinatura digital para documentos XML suportados pelo CyberCore Signer.
*/
public enum XmlSignatureProfile {
/**
* Padrão XAdES-BES (XML Advanced Electronic Signatures - Basic Electronic Signature).
* Em conformidade com a especificação ETSI TS 101 903 e padrão ICP-Brasil DOC-ICP-15.03 (AD-RB).
* Inclui propriedades qualificadas assinadas: data/hora de assinatura (SigningTime)
* e referência criptográfica do certificado do signatário (SigningCertificate).
*/
XADES_BES("XAdES-BES (Padrão ICP-Brasil / ETSI)", "Assinatura avançada com data/hora e certificado qualificado vinculados"),
/**
* Padrão W3C XMLDSig convencional (Enveloped Signature sem propriedades qualificadas XAdES).
* Mantido para compatibilidade com layouts fiscais estritos (ex: NF-e, CT-e, MDF-e SEFAZ legados).
*/
XMLDSIG("XMLDSig Clássico (W3C)", "Assinatura enveloped básica sem propriedades qualificadas");
private final String displayName;
private final String description;
XmlSignatureProfile(String displayName, String description) {
this.displayName = displayName;
this.description = description;
}
public String getDisplayName() {
return displayName;
}
public String getDescription() {
return description;
}
}
@@ -17,12 +17,14 @@ import javax.xml.crypto.KeySelector;
import javax.xml.crypto.KeySelectorException; import javax.xml.crypto.KeySelectorException;
import javax.xml.crypto.KeySelectorResult; import javax.xml.crypto.KeySelectorResult;
import javax.xml.crypto.XMLCryptoContext; import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dom.DOMStructure;
import javax.xml.crypto.dsig.CanonicalizationMethod; import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod; import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference; import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo; import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform; import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignature; import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory; import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext; import javax.xml.crypto.dsig.dom.DOMSignContext;
@@ -43,25 +45,41 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.Key; import java.security.MessageDigest;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
/** /**
* Serviço de alto nível para Assinatura Digital de Documentos XML segundo o padrão W3C XMLDSig e ICP-Brasil. * Serviço de alto nível para Assinatura Digital de Documentos XML segundo os padrões:
* Suporta assinatura enveloped de documento completo e assinatura de elementos por ID (NF-e, SPED, etc.). * - XAdES-BES (ETSI TS 101 903 / ICP-Brasil DOC-ICP-15.03 - Padrão Avançado)
* - W3C XMLDSig convencional (Enveloped / NF-e / SPED legado)
*
* Utiliza o suporte de fábrica nativo e seguro da plataforma Java (javax.xml.crypto.dsig). * Utiliza o suporte de fábrica nativo e seguro da plataforma Java (javax.xml.crypto.dsig).
*/ */
public class XmlSigningService { public class XmlSigningService {
private static final Logger logger = LoggerFactory.getLogger(XmlSigningService.class); private static final Logger logger = LoggerFactory.getLogger(XmlSigningService.class);
public static final String XADES_132_NS = "http://uri.etsi.org/01903/v1.3.2#";
public static final String XADES_SIGNED_PROPERTIES_TYPE = "http://uri.etsi.org/01903#SignedProperties";
public static final String XMLDSIG_NS = XMLSignature.XMLNS;
private static final DateTimeFormatter ISO_INSTANT_FORMATTER =
DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
private final CertificateManager certificateManager; private final CertificateManager certificateManager;
public XmlSigningService(CertificateManager certificateManager) { public XmlSigningService(CertificateManager certificateManager) {
@@ -85,7 +103,7 @@ public class XmlSigningService {
try (FileOutputStream fos = new FileOutputStream(targetXml)) { try (FileOutputStream fos = new FileOutputStream(targetXml)) {
fos.write(signedBytes); fos.write(signedBytes);
} }
logger.info("XML assinado com sucesso e salvo em: {}", targetXml.getAbsolutePath()); logger.info("XML assinado com sucesso ({}) e salvo em: {}", options.profile().getDisplayName(), targetXml.getAbsolutePath());
} }
/** /**
@@ -117,7 +135,8 @@ public class XmlSigningService {
} }
SigningKeyMaterial keyMaterial = keyMaterialOpt.get(); SigningKeyMaterial keyMaterial = keyMaterialOpt.get();
logger.info("Iniciando assinatura XMLDSig com certificado '{}' [tipo={}]", cert.commonName(), cert.type()); logger.info("Iniciando assinatura XML [perfil={}] com certificado '{}' [tipo={}]",
options.profile(), cert.commonName(), cert.type());
// 2. Parse Seguro do XML (Prevenção rigorosa de XXE) // 2. Parse Seguro do XML (Prevenção rigorosa de XXE)
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
@@ -134,13 +153,13 @@ public class XmlSigningService {
// 3. Configuração do XMLSignatureFactory // 3. Configuração do XMLSignatureFactory
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
// Transformações padrão: Enveloped + C14N // Transformações padrão para os dados de negócio: Enveloped + C14N
List<Transform> transforms = new ArrayList<>(); List<Transform> dataTransforms = new ArrayList<>();
transforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null)); dataTransforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null));
transforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null)); dataTransforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null));
// Determinação da Referência e Ponto de Inserção // Determinação da Referência de Dados e Ponto de Inserção da Assinatura
Reference ref; Reference dataRef;
Node insertionParent; Node insertionParent;
if (options.targetElementId() != null && !options.targetElementId().isBlank()) { if (options.targetElementId() != null && !options.targetElementId().isBlank()) {
@@ -150,7 +169,7 @@ public class XmlSigningService {
throw new IllegalArgumentException("Elemento XML com ID '" + elemId + "' não encontrado no documento."); throw new IllegalArgumentException("Elemento XML com ID '" + elemId + "' não encontrado no documento.");
} }
targetElement.setIdAttribute("Id", true); targetElement.setIdAttribute("Id", true);
ref = fac.newReference("#" + elemId, fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("#" + elemId, fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = (targetElement.getParentNode() != null) ? targetElement.getParentNode() : doc.getDocumentElement(); insertionParent = (targetElement.getParentNode() != null) ? targetElement.getParentNode() : doc.getDocumentElement();
} else if (options.targetElementTag() != null && !options.targetElementTag().isBlank()) { } else if (options.targetElementTag() != null && !options.targetElementTag().isBlank()) {
NodeList list = doc.getElementsByTagName(options.targetElementTag()); NodeList list = doc.getElementsByTagName(options.targetElementTag());
@@ -160,42 +179,87 @@ public class XmlSigningService {
if (list.getLength() > 0 && list.item(0) instanceof Element elem && elem.hasAttribute("Id")) { if (list.getLength() > 0 && list.item(0) instanceof Element elem && elem.hasAttribute("Id")) {
String idVal = elem.getAttribute("Id"); String idVal = elem.getAttribute("Id");
elem.setIdAttribute("Id", true); elem.setIdAttribute("Id", true);
ref = fac.newReference("#" + idVal, fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("#" + idVal, fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = (elem.getParentNode() != null) ? elem.getParentNode() : doc.getDocumentElement(); insertionParent = (elem.getParentNode() != null) ? elem.getParentNode() : doc.getDocumentElement();
} else { } else {
ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = doc.getDocumentElement(); insertionParent = doc.getDocumentElement();
} }
} else { } else {
// Assinatura Enveloped do documento completo // Assinatura Enveloped do documento completo
ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = doc.getDocumentElement(); insertionParent = doc.getDocumentElement();
} }
// 4. Montagem do SignedInfo List<Reference> references = new ArrayList<>();
references.add(dataRef);
List<XMLObject> xmlObjects = new ArrayList<>();
String signatureUniqueId = "sig-" + UUID.randomUUID();
Element signedPropertiesElem = null;
// 4. Se for perfil XAdES-BES, constrói as QualifyingProperties e a Reference correspondente
if (options.profile() == XmlSignatureProfile.XADES_BES) {
String signedPropsId = "signed-props-" + UUID.randomUUID();
signedPropertiesElem = buildXadesSignedProperties(doc, signedPropsId, keyMaterial.signingCertificate(), Instant.now());
Element qualifyingProperties = doc.createElementNS(XADES_132_NS, "xades:QualifyingProperties");
qualifyingProperties.setAttribute("Target", "#" + signatureUniqueId);
qualifyingProperties.appendChild(signedPropertiesElem);
XMLStructure xadesStructure = new DOMStructure(qualifyingProperties);
XMLObject xadesObject = fac.newXMLObject(Collections.singletonList(xadesStructure), null, null, null);
xmlObjects.add(xadesObject);
// Adiciona Referência Criptográfica cobrindo as SignedProperties
List<Transform> xadesTransforms = Collections.singletonList(
fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null)
);
Reference xadesRef = fac.newReference(
"#" + signedPropsId,
fac.newDigestMethod(DigestMethod.SHA256, null),
xadesTransforms,
XADES_SIGNED_PROPERTIES_TYPE,
null
);
references.add(xadesRef);
}
// 5. Montagem do SignedInfo
SignedInfo si = fac.newSignedInfo( SignedInfo si = fac.newSignedInfo(
fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null), fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null),
fac.newSignatureMethod(options.signatureAlgorithm(), null), fac.newSignatureMethod(options.signatureAlgorithm(), null),
Collections.singletonList(ref) references
); );
// 5. Inclusão da Cadeia de Certificados no KeyInfo // 6. Inclusão da Cadeia de Certificados no KeyInfo
KeyInfoFactory kif = fac.getKeyInfoFactory(); KeyInfoFactory kif = fac.getKeyInfoFactory();
List<Object> x509Content = new ArrayList<>(); List<Object> x509Content = new ArrayList<>();
x509Content.add(keyMaterial.signingCertificate()); x509Content.add(keyMaterial.signingCertificate());
X509Data x509Data = kif.newX509Data(x509Content); X509Data x509Data = kif.newX509Data(x509Content);
KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509Data)); KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509Data));
// 6. Execução da Assinatura DOM // 7. Execução da Assinatura DOM
DOMSignContext dsc = new DOMSignContext(keyMaterial.privateKey(), insertionParent); DOMSignContext dsc = new DOMSignContext(keyMaterial.privateKey(), insertionParent);
if (keyMaterial.jcaProvider() != null) { if (keyMaterial.jcaProvider() != null) {
dsc.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", keyMaterial.jcaProvider()); dsc.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", keyMaterial.jcaProvider());
} }
XMLSignature signature = fac.newXMLSignature(si, ki); if (signedPropertiesElem != null) {
dsc.setIdAttributeNS(signedPropertiesElem, null, "Id");
}
XMLSignature signature = fac.newXMLSignature(
si,
ki,
xmlObjects,
signatureUniqueId,
null
);
signature.sign(dsc); signature.sign(dsc);
// 7. Serialização do XML Assinado // 8. Serialização do XML Assinado
TransformerFactory tf = TransformerFactory.newInstance(); TransformerFactory tf = TransformerFactory.newInstance();
Transformer trans = tf.newTransformer(); Transformer trans = tf.newTransformer();
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
@@ -205,12 +269,13 @@ public class XmlSigningService {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
trans.transform(new DOMSource(doc), new StreamResult(baos)); trans.transform(new DOMSource(doc), new StreamResult(baos));
logger.info("Assinatura XMLDSig gerada com sucesso. Tamanho: {} bytes", baos.size()); logger.info("Assinatura XML ({}) gerada com sucesso. Tamanho final: {} bytes",
options.profile(), baos.size());
return baos.toByteArray(); return baos.toByteArray();
} }
/** /**
* Valida a integridade criptográfica de um documento XML assinado com XMLDSig. * Valida a integridade criptográfica de um documento XML assinado com XAdES ou XMLDSig.
*/ */
public boolean validateXmlSignature(byte[] signedXmlBytes) throws Exception { public boolean validateXmlSignature(byte[] signedXmlBytes) throws Exception {
Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos."); Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos.");
@@ -218,24 +283,17 @@ public class XmlSigningService {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true); dbf.setNamespaceAware(true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes)); Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes));
// Registra nós com atributo Id/id/ID para que o ResourceResolver do XMLDSig localize a referência // Registra nós com atributo Id/id/ID para que o ResourceResolver localize as referências
NodeList allElements = doc.getElementsByTagName("*"); registerIdAttributes(doc.getDocumentElement());
for (int i = 0; i < allElements.getLength(); i++) {
if (allElements.item(i) instanceof Element e) {
if (e.hasAttribute("Id")) {
e.setIdAttribute("Id", true);
} else if (e.hasAttribute("id")) {
e.setIdAttribute("id", true);
} else if (e.hasAttribute("ID")) {
e.setIdAttribute("ID", true);
}
}
}
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); NodeList nl = doc.getElementsByTagNameNS(XMLDSIG_NS, "Signature");
if (nl.getLength() == 0) { if (nl.getLength() == 0) {
nl = doc.getElementsByTagName("Signature"); nl = doc.getElementsByTagName("Signature");
} }
@@ -248,8 +306,168 @@ public class XmlSigningService {
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
DOMValidateContext valContext = new DOMValidateContext(new X509KeySelector(), sigElement); DOMValidateContext valContext = new DOMValidateContext(new X509KeySelector(), sigElement);
// Registra explicitamente elementos com atributo Id no valContext
NodeList allNodes = doc.getElementsByTagName("*");
for (int i = 0; i < allNodes.getLength(); i++) {
if (allNodes.item(i) instanceof Element e) {
if (e.hasAttribute("Id")) {
valContext.setIdAttributeNS(e, null, "Id");
}
}
}
XMLSignature signature = fac.unmarshalXMLSignature(valContext); XMLSignature signature = fac.unmarshalXMLSignature(valContext);
return signature.validate(valContext); boolean coreValidity = signature.validate(valContext);
if (!coreValidity) {
boolean sv = signature.getSignatureValue().validate(valContext);
logger.warn("Validação XML falhou. SignatureValue válido: {}", sv);
int refIdx = 0;
for (Object o : signature.getSignedInfo().getReferences()) {
if (o instanceof Reference r) {
boolean rValid = r.validate(valContext);
byte[] calcDigest = r.getCalculatedDigestValue();
byte[] storedDigest = r.getDigestValue();
String calcB64 = calcDigest != null ? Base64.getEncoder().encodeToString(calcDigest) : "null";
String storedB64 = storedDigest != null ? Base64.getEncoder().encodeToString(storedDigest) : "null";
logger.warn("Reference[{}] URI='{}' válida: {} | calcDigest={} | storedDigest={}",
refIdx++, r.getURI(), rValid, calcB64, storedB64);
}
}
}
return coreValidity;
}
/**
* Inspeciona e extrai informações detalhadas da assinatura XML (XAdES ou XMLDSig).
*/
public XmlSignatureDetails inspectXmlSignature(byte[] signedXmlBytes) throws Exception {
Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos.");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes));
registerIdAttributes(doc.getDocumentElement());
NodeList nl = doc.getElementsByTagNameNS(XMLDSIG_NS, "Signature");
if (nl.getLength() == 0) {
nl = doc.getElementsByTagName("Signature");
}
if (nl.getLength() == 0) {
return null;
}
Element sigElem = (Element) nl.item(0);
String sigId = sigElem.getAttribute("Id");
// Verifica presença de XAdES
NodeList qpNodes = doc.getElementsByTagNameNS(XADES_132_NS, "QualifyingProperties");
boolean isXades = qpNodes.getLength() > 0;
String signingTime = null;
if (isXades) {
NodeList timeNodes = doc.getElementsByTagNameNS(XADES_132_NS, "SigningTime");
if (timeNodes.getLength() > 0) {
signingTime = timeNodes.item(0).getTextContent();
}
}
// Validação criptográfica
boolean isValid = false;
try {
isValid = validateXmlSignature(signedXmlBytes);
} catch (Exception ex) {
logger.warn("Validação da assinatura XML falhou durante inspeção: {}", ex.getMessage());
}
String sigMethod = "";
NodeList smNodes = sigElem.getElementsByTagNameNS(XMLDSIG_NS, "SignatureMethod");
if (smNodes.getLength() > 0 && smNodes.item(0) instanceof Element smElem) {
sigMethod = smElem.getAttribute("Algorithm");
}
return new XmlSignatureDetails(
isValid,
isXades ? XmlSignatureProfile.XADES_BES : XmlSignatureProfile.XMLDSIG,
sigMethod,
signingTime,
sigId
);
}
/**
* Constrói a estrutura DOM das propriedades assinadas do XAdES (SignedProperties).
*/
private Element buildXadesSignedProperties(Document doc, String signedPropsId, X509Certificate cert, Instant signInstant) throws Exception {
Element signedProperties = doc.createElementNS(XADES_132_NS, "xades:SignedProperties");
signedProperties.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xades", XADES_132_NS);
signedProperties.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:ds", XMLDSIG_NS);
signedProperties.setAttribute("Id", signedPropsId);
signedProperties.setIdAttribute("Id", true);
Element signedSignatureProperties = doc.createElementNS(XADES_132_NS, "xades:SignedSignatureProperties");
// 1. Data e Hora UTC da Assinatura
Element signingTime = doc.createElementNS(XADES_132_NS, "xades:SigningTime");
signingTime.setTextContent(ISO_INSTANT_FORMATTER.format(signInstant));
signedSignatureProperties.appendChild(signingTime);
// 2. Vínculo Criptográfico do Certificado do Signatário (SigningCertificate)
Element signingCertificate = doc.createElementNS(XADES_132_NS, "xades:SigningCertificate");
Element certElem = doc.createElementNS(XADES_132_NS, "xades:Cert");
// Digest SHA-256 do Certificado DER
Element certDigest = doc.createElementNS(XADES_132_NS, "xades:CertDigest");
Element digestMethod = doc.createElementNS(XMLDSIG_NS, "ds:DigestMethod");
digestMethod.setAttribute("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256");
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] certHash = md.digest(cert.getEncoded());
String certHashB64 = Base64.getEncoder().encodeToString(certHash);
Element digestValue = doc.createElementNS(XMLDSIG_NS, "ds:DigestValue");
digestValue.setTextContent(certHashB64);
certDigest.appendChild(digestMethod);
certDigest.appendChild(digestValue);
certElem.appendChild(certDigest);
// Dados do Emissor e Serial do Certificado
Element issuerSerial = doc.createElementNS(XADES_132_NS, "xades:IssuerSerial");
Element x509IssuerName = doc.createElementNS(XMLDSIG_NS, "ds:X509IssuerName");
x509IssuerName.setTextContent(cert.getIssuerX500Principal().getName());
Element x509SerialNumber = doc.createElementNS(XMLDSIG_NS, "ds:X509SerialNumber");
x509SerialNumber.setTextContent(cert.getSerialNumber().toString());
issuerSerial.appendChild(x509IssuerName);
issuerSerial.appendChild(x509SerialNumber);
certElem.appendChild(issuerSerial);
signingCertificate.appendChild(certElem);
signedSignatureProperties.appendChild(signingCertificate);
signedProperties.appendChild(signedSignatureProperties);
return signedProperties;
}
private static void registerIdAttributes(Element root) {
if (root == null) return;
if (root.hasAttribute("Id")) {
root.setIdAttribute("Id", true);
} else if (root.hasAttribute("id")) {
root.setIdAttribute("id", true);
} else if (root.hasAttribute("ID")) {
root.setIdAttribute("ID", true);
}
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
if (children.item(i) instanceof Element child) {
registerIdAttributes(child);
}
}
} }
private static Element findElementById(Document doc, String id, String hintTag) { private static Element findElementById(Document doc, String id, String hintTag) {
@@ -264,6 +482,17 @@ public class XmlSigningService {
return doc.getElementById(id); return doc.getElementById(id);
} }
/**
* DTO de detalhes de uma assinatura XML.
*/
public record XmlSignatureDetails(
boolean valid,
XmlSignatureProfile profile,
String algorithm,
String signingTime,
String signatureId
) {}
/** /**
* Seletor de chave pública baseado no certificado X.509 embutido no KeyInfo. * Seletor de chave pública baseado no certificado X.509 embutido no KeyInfo.
*/ */
@@ -18,6 +18,8 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import br.com.cybercore.signer.core.validation.SignatureValidationService;
import br.com.cybercore.signer.core.validation.ValidationResult;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -41,6 +43,7 @@ class PdfSigningServiceTest {
private CertificateManager certificateManager; private CertificateManager certificateManager;
private PdfSigningService pdfSigningService; private PdfSigningService pdfSigningService;
private String testCertId; private String testCertId;
private String testCert2Id;
@BeforeEach @BeforeEach
void setUp() throws Exception { void setUp() throws Exception {
@@ -74,13 +77,29 @@ class PdfSigningServiceTest {
char[] pwd = "password123".toCharArray(); char[] pwd = "password123".toCharArray();
ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert}); ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert});
// Gera segundo par de chaves e certificado para co-assinatura
KeyPair kp2 = kpg.generateKeyPair();
X500Name subject2 = new X500Name("CN=Maria Souza:98765432100, O=CyberCore Testes, C=BR");
X509v3CertificateBuilder certBuilder2 = new JcaX509v3CertificateBuilder(
subject2,
BigInteger.valueOf(System.currentTimeMillis() + 100),
notBefore,
notAfter,
subject2,
kp2.getPublic()
);
ContentSigner signer2 = new JcaContentSignerBuilder("SHA256withRSA").build(kp2.getPrivate());
X509Certificate cert2 = new JcaX509CertificateConverter().getCertificate(certBuilder2.build(signer2));
ks.setKeyEntry("test-cert-2", kp2.getPrivate(), pwd, new Certificate[]{cert2});
Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste"); Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste");
provider.setKeyStore(ks, pwd); provider.setKeyStore(ks, pwd);
certificateManager.registerProvider(provider); certificateManager.registerProvider(provider);
List<ManagedCertificate> list = certificateManager.discoverCertificates(); List<ManagedCertificate> list = certificateManager.discoverCertificates();
assertThat(list).isNotEmpty(); assertThat(list).hasSize(2);
this.testCertId = list.get(0).id(); this.testCertId = list.stream().filter(c -> c.alias().equals("test-cert")).findFirst().orElseThrow().id();
this.testCert2Id = list.stream().filter(c -> c.alias().equals("test-cert-2")).findFirst().orElseThrow().id();
this.pdfSigningService = new PdfSigningService(certificateManager); this.pdfSigningService = new PdfSigningService(certificateManager);
} }
@@ -215,4 +234,59 @@ class PdfSigningServiceTest {
byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f); byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f);
assertThat(pngBytes).isNotEmpty(); assertThat(pngBytes).isNotEmpty();
} }
@Test
@DisplayName("Deve manter ambas as assinaturas válidas ao assinar um PDF que já possui assinatura digital (Co-assinatura PAdES)")
void shouldKeepBothSignaturesValidWhenSigningAlreadySignedPdf() throws Exception {
byte[] originalPdf = createSamplePdf();
// 1ª Assinatura com o primeiro certificado
PdfSignatureOptions firstOptions = PdfSignatureOptions.builder(testCertId)
.visibleSeal(true)
.position(PdfSealPosition.BOTTOM_RIGHT)
.reason("Primeira Aprovação")
.location("Brasília, DF")
.build();
byte[] firstSignedPdf = pdfSigningService.signPdf(originalPdf, firstOptions);
assertThat(firstSignedPdf).isNotNull();
// 2ª Assinatura sobre o documento já assinado com o segundo certificado
PdfSignatureOptions secondOptions = PdfSignatureOptions.builder(testCert2Id)
.visibleSeal(true)
.position(PdfSealPosition.TOP_LEFT)
.reason("Segunda Aprovação Conjunta")
.location("São Paulo, SP")
.build();
byte[] doubleSignedPdf = pdfSigningService.signPdf(firstSignedPdf, secondOptions);
assertThat(doubleSignedPdf).isNotNull();
assertThat(doubleSignedPdf.length).isGreaterThan(firstSignedPdf.length);
// Validação estrutural das assinaturas no PDDocument
try (PDDocument doc = Loader.loadPDF(doubleSignedPdf)) {
List<PDSignature> signatures = doc.getSignatureDictionaries();
assertThat(signatures).hasSize(2);
assertThat(signatures.get(0).getName()).contains("Joao da Silva");
assertThat(signatures.get(0).getReason()).isEqualTo("Primeira Aprovação");
assertThat(signatures.get(1).getName()).contains("Maria Souza");
assertThat(signatures.get(1).getReason()).isEqualTo("Segunda Aprovação Conjunta");
}
// Validação criptográfica rigorosa via SignatureValidationService (ambas as assinaturas devem ser íntegras)
SignatureValidationService validationService = new SignatureValidationService();
ValidationResult validationResult = validationService.validateAttached(doubleSignedPdf, "duplo_assinado.pdf");
assertThat(validationResult.isValid()).isTrue();
assertThat(validationResult.signers()).hasSize(2);
// Ambas as assinaturas e digests devem ser 100% válidos
assertThat(validationResult.signers().get(0).signatureValid()).isTrue();
assertThat(validationResult.signers().get(0).digestValid()).isTrue();
assertThat(validationResult.signers().get(0).signerName()).contains("Joao da Silva");
assertThat(validationResult.signers().get(1).signatureValid()).isTrue();
assertThat(validationResult.signers().get(1).digestValid()).isTrue();
assertThat(validationResult.signers().get(1).signerName()).contains("Maria Souza");
}
} }
@@ -72,8 +72,8 @@ class XmlSigningServiceTest {
} }
@Test @Test
@DisplayName("Deve assinar documento XML completo com XMLDSig Enveloped e validar com sucesso") @DisplayName("Deve assinar documento XML no padrão XAdES-BES por padrão e validar com sucesso")
void shouldSignWholeXmlDocumentAndValidate() throws Exception { void shouldSignWholeXmlDocumentWithXadesBesByDefaultAndValidate() throws Exception {
String xml = """ String xml = """
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<pedido id="ped-1001"> <pedido id="ped-1001">
@@ -86,19 +86,38 @@ class XmlSigningServiceTest {
byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options); byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8); String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
// Verifica tags essenciais do XMLDSig
assertThat(signedXmlStr).contains("<Signature"); assertThat(signedXmlStr).contains("<Signature");
assertThat(signedXmlStr).contains("<SignedInfo"); assertThat(signedXmlStr).contains("<SignedInfo");
assertThat(signedXmlStr).contains("<SignatureValue"); assertThat(signedXmlStr).contains("<SignatureValue");
assertThat(signedXmlStr).contains("<KeyInfo"); assertThat(signedXmlStr).contains("<KeyInfo");
// Validação criptográfica // Verifica propriedades qualificadas do XAdES-BES (ETSI TS 101 903 / ICP-Brasil)
assertThat(signedXmlStr).contains("QualifyingProperties");
assertThat(signedXmlStr).contains("SignedProperties");
assertThat(signedXmlStr).contains("SignedSignatureProperties");
assertThat(signedXmlStr).contains("SigningTime");
assertThat(signedXmlStr).contains("SigningCertificate");
assertThat(signedXmlStr).contains("CertDigest");
assertThat(signedXmlStr).contains("IssuerSerial");
assertThat(signedXmlStr).contains("Type=\"http://uri.etsi.org/01903#SignedProperties\"");
// Validação criptográfica do documento e das propriedades XAdES
boolean isValid = xmlSigningService.validateXmlSignature(signedXmlBytes); boolean isValid = xmlSigningService.validateXmlSignature(signedXmlBytes);
assertThat(isValid).isTrue(); assertThat(isValid).isTrue();
// Inspeção detalhada
XmlSigningService.XmlSignatureDetails details = xmlSigningService.inspectXmlSignature(signedXmlBytes);
assertThat(details).isNotNull();
assertThat(details.valid()).isTrue();
assertThat(details.profile()).isEqualTo(XmlSignatureProfile.XADES_BES);
assertThat(details.signingTime()).isNotBlank();
} }
@Test @Test
@DisplayName("Deve assinar nó XML específico com atributo Id (estilo NF-e) e validar") @DisplayName("Deve assinar nó XML específico com atributo Id no padrão XAdES-BES e validar")
void shouldSignSpecificXmlElementById() throws Exception { void shouldSignSpecificXmlElementWithXadesBes() throws Exception {
String nfeXml = """ String nfeXml = """
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<NFe xmlns="http://www.portalfiscal.inf.br/nfe"> <NFe xmlns="http://www.portalfiscal.inf.br/nfe">
@@ -117,21 +136,48 @@ class XmlSigningServiceTest {
testCertId, testCertId,
null, null,
"infNFe", "infNFe",
"NFe35261012345678000195550010000000011000000010" "NFe35261012345678000195550010000000011000000010",
XmlSignatureProfile.XADES_BES
); );
byte[] signedNfe = xmlSigningService.signXml(nfeXml.getBytes(StandardCharsets.UTF_8), options); byte[] signedNfe = xmlSigningService.signXml(nfeXml.getBytes(StandardCharsets.UTF_8), options);
String signedStr = new String(signedNfe, StandardCharsets.UTF_8); String signedStr = new String(signedNfe, StandardCharsets.UTF_8);
assertThat(signedStr).contains("URI=\"#NFe35261012345678000195550010000000011000000010\""); assertThat(signedStr).contains("URI=\"#NFe35261012345678000195550010000000011000000010\"");
assertThat(signedStr).contains("QualifyingProperties");
assertThat(signedStr).contains("SignedProperties");
boolean isValid = xmlSigningService.validateXmlSignature(signedNfe); boolean isValid = xmlSigningService.validateXmlSignature(signedNfe);
assertThat(isValid).isTrue(); assertThat(isValid).isTrue();
} }
@Test @Test
@DisplayName("Deve detectar adulteração em XML assinado e reprovar na validação") @DisplayName("Deve assinar documento XML no perfil XMLDSig clássico quando explicitamente solicitado")
void shouldDetectTamperingInSignedXml() throws Exception { void shouldSignWithClassicXmlDsigWhenConfigured() throws Exception {
String xml = """
<?xml version="1.0" encoding="UTF-8"?>
<documento>
<campo>Valor de teste</campo>
</documento>
""";
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null, XmlSignatureProfile.XMLDSIG);
byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
assertThat(signedXmlStr).contains("<Signature");
assertThat(signedXmlStr).doesNotContain("QualifyingProperties");
boolean isValid = xmlSigningService.validateXmlSignature(signedXmlBytes);
assertThat(isValid).isTrue();
XmlSigningService.XmlSignatureDetails details = xmlSigningService.inspectXmlSignature(signedXmlBytes);
assertThat(details.profile()).isEqualTo(XmlSignatureProfile.XMLDSIG);
}
@Test
@DisplayName("Deve detectar adulteração no conteúdo do XML assinado com XAdES")
void shouldDetectTamperingInSignedXmlContent() throws Exception {
String xml = """ String xml = """
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<contrato> <contrato>
@@ -142,11 +188,36 @@ class XmlSigningServiceTest {
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null); XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null);
byte[] signedXml = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options); byte[] signedXml = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
// Adulteração intencional do conteúdo original // Adulteração no payload de negócio
String tampered = new String(signedXml, StandardCharsets.UTF_8) String tampered = new String(signedXml, StandardCharsets.UTF_8)
.replace("R$ 100,00", "R$ 999.999,00"); .replace("R$ 100,00", "R$ 999.999,00");
boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8)); boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8));
assertThat(isValid).isFalse(); assertThat(isValid).isFalse();
} }
@Test
@DisplayName("Deve detectar adulteração nas propriedades qualificadas do XAdES (SigningTime)")
void shouldDetectTamperingInXadesSignedProperties() throws Exception {
String xml = """
<?xml version="1.0" encoding="UTF-8"?>
<contrato>
<clausula>Assinado conforme regras XAdES</clausula>
</contrato>
""";
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null);
byte[] signedXml = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
String signedStr = new String(signedXml, StandardCharsets.UTF_8);
// Adulteração fraudulenta da data da assinatura no nó XAdES
String tampered = signedStr.replaceFirst(
"<xades:SigningTime>[^<]+</xades:SigningTime>",
"<xades:SigningTime>1999-01-01T00:00:00Z</xades:SigningTime>"
);
boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8));
assertThat(isValid).isFalse();
}
} }
@@ -386,7 +386,7 @@ public final class BatchSigningDialog {
Path dest = targetDir.resolve(outPdfName); Path dest = targetDir.resolve(outPdfName);
pdfService.signPdfFile(item.getFile(), dest.toFile(), options); pdfService.signPdfFile(item.getFile(), dest.toFile(), options);
} else if (lowerName.endsWith(".xml")) { } else if (lowerName.endsWith(".xml")) {
// Assinatura XMLDSig de documento XML // Assinatura XAdES-BES de documento XML (ETSI TS 101 903 / ICP-Brasil)
br.com.cybercore.signer.core.xml.XmlSigningService xmlService = br.com.cybercore.signer.core.xml.XmlSigningService xmlService =
new br.com.cybercore.signer.core.xml.XmlSigningService(certManager); new br.com.cybercore.signer.core.xml.XmlSigningService(certManager);
br.com.cybercore.signer.core.xml.XmlSignatureOptions options = br.com.cybercore.signer.core.xml.XmlSignatureOptions options =
@@ -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;
@@ -54,6 +55,22 @@ public final class PdfCertificateSignDialog {
private static final Logger logger = LoggerFactory.getLogger(PdfCertificateSignDialog.class); private static final Logger logger = LoggerFactory.getLogger(PdfCertificateSignDialog.class);
public record SealConfig( public record SealConfig(
boolean visibleSeal,
PdfSealTheme sealTheme,
CpfDisplayMode cpfDisplayMode,
boolean includeDateTime,
String customExtraInfo,
PdfSignDialog.PositionOption positionOption,
PdfSignDialog.PageOption pageOption,
boolean isCustomPosition,
Float customX,
Float customY,
Integer customPageNumber,
String reason,
String location,
Double customSealScale
) {
public SealConfig(
boolean visibleSeal, boolean visibleSeal,
PdfSealTheme sealTheme, PdfSealTheme sealTheme,
CpfDisplayMode cpfDisplayMode, CpfDisplayMode cpfDisplayMode,
@@ -67,7 +84,11 @@ public final class PdfCertificateSignDialog {
Integer customPageNumber, Integer customPageNumber,
String reason, String reason,
String location String location
) {} ) {
this(visibleSeal, sealTheme, cpfDisplayMode, includeDateTime, customExtraInfo, positionOption,
pageOption, isCustomPosition, customX, customY, customPageNumber, reason, location, 1.0);
}
}
private final Stage stage; private final Stage stage;
private final CertificateManager certManager; private final CertificateManager certManager;
@@ -196,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() + ")");
} }
} }
}); });
@@ -208,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() + ")");
} }
} }
}); });
@@ -429,7 +452,8 @@ public final class PdfCertificateSignDialog {
.includeDateTime(sealConfig.includeDateTime()) .includeDateTime(sealConfig.includeDateTime())
.customExtraInfo(sealConfig.customExtraInfo()) .customExtraInfo(sealConfig.customExtraInfo())
.reason(sealConfig.reason()) .reason(sealConfig.reason())
.location(sealConfig.location()); .location(sealConfig.location())
.customSealScale(sealConfig.customSealScale());
if (sealConfig.isCustomPosition() && sealConfig.customX() != null && sealConfig.customY() != null) { if (sealConfig.isCustomPosition() && sealConfig.customX() != null && sealConfig.customY() != null) {
b.customPosition(sealConfig.customX(), sealConfig.customY(), sealConfig.customPageNumber()); b.customPosition(sealConfig.customX(), sealConfig.customY(), sealConfig.customPageNumber());
@@ -46,6 +46,8 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
@@ -127,6 +129,24 @@ public final class PdfSignDialog {
private ComboBox<PageOption> pageCombo; private ComboBox<PageOption> pageCombo;
private ImageView sealPreviewImageView; private ImageView sealPreviewImageView;
// Controle de Escala / Redimensionamento do Selo (1.0 = 100%)
private double sealScale = 1.0;
private static final double MIN_SEAL_SCALE = 0.40;
private static final double MAX_SEAL_SCALE = 1.80;
private boolean isResizeModeActive = false;
// Componentes do grupo do selo e alça interativa
private Pane sealGroupPane;
private Rectangle sealSelectionBorder;
private Circle resizeHandle;
private Button btnToggleResize;
private Button btnResetSealScale;
private Label lblSealScaleIndicator;
private double resizeStartSceneX;
private double resizeStartBaseWidth;
private boolean isResizingSealInProgress = false;
private long lastResizeActionTimestamp = 0;
// Visualizador Interativo da Folha // Visualizador Interativo da Folha
private ScrollPane sheetScroll; private ScrollPane sheetScroll;
private StackPane pageSheetContainer; private StackPane pageSheetContainer;
@@ -463,13 +483,34 @@ public final class PdfSignDialog {
zoomBox.getChildren().addAll(btnZoomOut, lblZoomValue, btnZoomIn, btnZoomFit); zoomBox.getChildren().addAll(btnZoomOut, lblZoomValue, btnZoomIn, btnZoomFit);
// Grupo de Tamanho do Selo
HBox resizeToolBox = new HBox(6);
resizeToolBox.setAlignment(Pos.CENTER_LEFT);
resizeToolBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-padding: 2px 6px;");
btnToggleResize = new Button("📏 Redimensionar");
btnToggleResize.getStyleClass().addAll("btn-secondary", "btn-compact");
btnToggleResize.setTooltip(new Tooltip("Ativar/desativar alça interativa para alterar o tamanho do selo"));
btnToggleResize.setOnAction(e -> toggleResizeMode());
lblSealScaleIndicator = new Label("100%");
lblSealScaleIndicator.setStyle("-fx-font-size: 11px; -fx-font-weight: bold; -fx-min-width: 36px; -fx-alignment: center;");
btnResetSealScale = new Button("↺ 100%");
btnResetSealScale.getStyleClass().addAll("btn-secondary", "btn-compact");
btnResetSealScale.setTooltip(new Tooltip("Restaurar tamanho padrão original (100%)"));
btnResetSealScale.setVisible(false);
btnResetSealScale.setOnAction(e -> applySealScale(1.0));
resizeToolBox.getChildren().addAll(btnToggleResize, lblSealScaleIndicator, btnResetSealScale);
Region tbSpacer = new Region(); Region tbSpacer = new Region();
HBox.setHgrow(tbSpacer, Priority.ALWAYS); HBox.setHgrow(tbSpacer, Priority.ALWAYS);
lblPositionFeedback = new Label("👉 Clique na folha para posicionar"); lblPositionFeedback = new Label("👉 Clique na folha para posicionar");
lblPositionFeedback.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-accent; -fx-font-weight: bold;"); lblPositionFeedback.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-accent; -fx-font-weight: bold;");
toolbar.getChildren().addAll(pageNavBox, zoomBox, tbSpacer, lblPositionFeedback); toolbar.getChildren().addAll(pageNavBox, zoomBox, resizeToolBox, tbSpacer, lblPositionFeedback);
// ScrollPane da Folha de Papel // ScrollPane da Folha de Papel
sheetScroll = new ScrollPane(); sheetScroll = new ScrollPane();
@@ -508,23 +549,61 @@ public final class PdfSignDialog {
interactiveOverlayPane = new Pane(); interactiveOverlayPane = new Pane();
interactiveOverlayPane.setCursor(Cursor.CROSSHAIR); interactiveOverlayPane.setCursor(Cursor.CROSSHAIR);
interactiveOverlayPane.setOnMouseClicked(event -> { interactiveOverlayPane.setOnMouseClicked(event -> {
if (sealDragOccurred) { if (sealDragOccurred || isResizingSealInProgress || (System.currentTimeMillis() - lastResizeActionTimestamp < 400)) {
// Previne que a soltura do mouse no término do arraste cause clique residual // Previne que a soltura do mouse no término do redimensionamento ou arraste cause clique residual
sealDragOccurred = false; sealDragOccurred = false;
event.consume(); event.consume();
return; return;
} }
if (isResizeModeActive) {
// No modo redimensionamento ativo, clicar fora não deve saltar o selo acidentalmente
event.consume();
return;
}
handleSheetClick(event); handleSheetClick(event);
}); });
// Grupo visual do selo composto pela imagem, borda de seleção e alça de quina
sealGroupPane = new Pane();
sealGroupPane.setVisible(false);
sealStampOverlayView = new ImageView(); sealStampOverlayView = new ImageView();
sealStampOverlayView.setPreserveRatio(true); sealStampOverlayView.setPreserveRatio(true);
sealStampOverlayView.setCursor(Cursor.MOVE); sealStampOverlayView.setCursor(Cursor.MOVE);
DropShadow sealShadow = new DropShadow(8, 0, 2, Color.rgb(2, 132, 199, 0.65)); DropShadow sealShadow = new DropShadow(8, 0, 2, Color.rgb(2, 132, 199, 0.65));
sealStampOverlayView.setEffect(sealShadow); sealStampOverlayView.setEffect(sealShadow);
sealStampOverlayView.setVisible(false);
// Arraste com ancoragem precisa no ponto exato do clique dentro do selo (sem salto e sem drift) sealSelectionBorder = new Rectangle();
sealSelectionBorder.setFill(Color.TRANSPARENT);
sealSelectionBorder.setStroke(Color.rgb(2, 132, 199));
sealSelectionBorder.setStrokeWidth(1.8);
sealSelectionBorder.getStrokeDashArray().addAll(5.0, 4.0);
sealSelectionBorder.setVisible(false);
sealSelectionBorder.setOnMousePressed(e -> e.consume());
sealSelectionBorder.setOnMouseDragged(e -> e.consume());
sealSelectionBorder.setOnMouseReleased(e -> e.consume());
sealSelectionBorder.setOnMouseClicked(e -> e.consume());
resizeHandle = new Circle(8);
resizeHandle.setFill(Color.rgb(2, 132, 199));
resizeHandle.setStroke(Color.WHITE);
resizeHandle.setStrokeWidth(2.0);
resizeHandle.setCursor(Cursor.SE_RESIZE);
DropShadow handleShadow = new DropShadow(4, 0, 1, Color.rgb(0, 0, 0, 0.35));
resizeHandle.setEffect(handleShadow);
resizeHandle.setVisible(false);
Tooltip.install(resizeHandle, new Tooltip("Puxe a quina para redimensionar o selo proporcionalmente"));
// Duplo clique no selo para ativar/desativar o modo de redimensionamento
sealStampOverlayView.setOnMouseClicked(event -> {
sealDragOccurred = false;
if (event.getClickCount() == 2) {
toggleResizeMode();
}
event.consume();
});
// Arraste com ancoragem precisa no ponto exato do clique dentro do selo
sealStampOverlayView.setOnMousePressed(event -> { sealStampOverlayView.setOnMousePressed(event -> {
dragStartOffsetInSealX = event.getX(); dragStartOffsetInSealX = event.getX();
dragStartOffsetInSealY = event.getY(); dragStartOffsetInSealY = event.getY();
@@ -536,7 +615,6 @@ public final class PdfSignDialog {
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return; if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
sealDragOccurred = true; sealDragOccurred = true;
// Converte coordenada da cena para coordenadas locais do painel da folha
Point2D localInSheet = interactiveOverlayPane.sceneToLocal(event.getSceneX(), event.getSceneY()); Point2D localInSheet = interactiveOverlayPane.sceneToLocal(event.getSceneX(), event.getSceneY());
double targetX = localInSheet.getX() - dragStartOffsetInSealX; double targetX = localInSheet.getX() - dragStartOffsetInSealX;
double targetY = localInSheet.getY() - dragStartOffsetInSealY; double targetY = localInSheet.getY() - dragStartOffsetInSealY;
@@ -545,16 +623,47 @@ public final class PdfSignDialog {
}); });
sealStampOverlayView.setOnMouseReleased(event -> { sealStampOverlayView.setOnMouseReleased(event -> {
// Garante que o evento de soltura do mouse não borbulhe para a folha lastResizeActionTimestamp = System.currentTimeMillis();
event.consume(); event.consume();
}); });
sealStampOverlayView.setOnMouseClicked(event -> { // Eventos da Alça de Redimensionamento Interativa
sealDragOccurred = false; resizeHandle.setOnMousePressed(event -> {
isResizingSealInProgress = true;
sealDragOccurred = true;
resizeStartSceneX = event.getSceneX();
resizeStartBaseWidth = currentBaseSealWidth * zoomFactor * sealScale;
event.consume(); event.consume();
}); });
interactiveOverlayPane.getChildren().add(sealStampOverlayView); resizeHandle.setOnMouseDragged(event -> {
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
isResizingSealInProgress = true;
sealDragOccurred = true;
double deltaX = event.getSceneX() - resizeStartSceneX;
double newWidth = resizeStartBaseWidth + deltaX;
double baseW = currentBaseSealWidth * zoomFactor;
if (baseW > 0) {
applySealScalePreservingOrigin(newWidth / baseW);
}
event.consume();
});
resizeHandle.setOnMouseReleased(event -> {
isResizingSealInProgress = false;
lastResizeActionTimestamp = System.currentTimeMillis();
sealDragOccurred = true;
event.consume();
});
resizeHandle.setOnMouseClicked(event -> {
isResizingSealInProgress = false;
lastResizeActionTimestamp = System.currentTimeMillis();
event.consume();
});
sealGroupPane.getChildren().addAll(sealStampOverlayView, sealSelectionBorder, resizeHandle);
interactiveOverlayPane.getChildren().add(sealGroupPane);
pageSheetContainer.getChildren().addAll(pageImageView, interactiveOverlayPane); pageSheetContainer.getChildren().addAll(pageImageView, interactiveOverlayPane);
sheetWrapper.getChildren().add(pageSheetContainer); sheetWrapper.getChildren().add(pageSheetContainer);
sheetScroll.setContent(sheetWrapper); sheetScroll.setContent(sheetWrapper);
@@ -563,6 +672,98 @@ public final class PdfSignDialog {
return column; return column;
} }
private void toggleResizeMode() {
this.isResizeModeActive = !this.isResizeModeActive;
this.lastResizeActionTimestamp = System.currentTimeMillis();
if (sealSelectionBorder != null) {
sealSelectionBorder.setVisible(isResizeModeActive);
}
if (resizeHandle != null) {
resizeHandle.setVisible(isResizeModeActive);
}
if (btnToggleResize != null) {
if (isResizeModeActive) {
btnToggleResize.setText("✓ Concluir");
btnToggleResize.setStyle("-fx-background-color: #0284c7; -fx-text-fill: white; -fx-font-weight: bold;");
lblPositionFeedback.setText("👉 Puxe a alça azul no canto do selo para ajustar o tamanho");
} else {
btnToggleResize.setText("📏 Redimensionar");
btnToggleResize.setStyle("");
updateFeedbackLabel();
}
}
}
private void applySealScalePreservingOrigin(double newScale) {
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
double currentX = sealGroupPane != null ? sealGroupPane.getLayoutX() : 0;
double currentY = sealGroupPane != null ? sealGroupPane.getLayoutY() : 0;
this.sealScale = Math.max(MIN_SEAL_SCALE, Math.min(MAX_SEAL_SCALE, Math.round(newScale * 100.0) / 100.0));
if (lblSealScaleIndicator != null) {
lblSealScaleIndicator.setText(String.format("%.0f%%", sealScale * 100));
}
if (btnResetSealScale != null) {
btnResetSealScale.setVisible(Math.abs(sealScale - 1.0) > 0.02);
}
updateContainerZoomDimensions();
double sheetW = currentBaseSheetWidth * zoomFactor;
double sheetH = currentBaseSheetHeight * zoomFactor;
double sealW = currentBaseSealWidth * zoomFactor * sealScale;
double sealH = currentBaseSealHeight * zoomFactor * sealScale;
// Mantém a quina superior esquerda ancorada no mesmo ponto exato
double clampedX = Math.max(0, Math.min(currentX, sheetW - sealW));
double clampedY = Math.max(0, Math.min(currentY, sheetH - sealH));
if (sealGroupPane != null) {
sealGroupPane.setLayoutX(clampedX);
sealGroupPane.setLayoutY(clampedY);
sealGroupPane.setVisible(true);
}
this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0;
this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0;
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
float basePdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
float basePdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
float scaledPdfW = basePdfW * (float) sealScale;
float scaledPdfH = basePdfH * (float) sealScale;
double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0;
float calculatedPdfX = (float) (clampedX / scale);
float calculatedPdfY = (float) (currentPagePdfHeight - (clampedY / scale) - scaledPdfH);
this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - scaledPdfW));
this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - scaledPdfH));
this.customPageNumber = currentPageIndex + 1;
this.isCustomPositionActive = true;
if (positionCombo.getValue() != PositionOption.CUSTOM) {
positionCombo.setValue(PositionOption.CUSTOM);
}
updateFeedbackLabel();
}
private void applySealScale(double newScale) {
this.sealScale = Math.max(MIN_SEAL_SCALE, Math.min(MAX_SEAL_SCALE, Math.round(newScale * 100.0) / 100.0));
if (lblSealScaleIndicator != null) {
lblSealScaleIndicator.setText(String.format("%.0f%%", sealScale * 100));
}
if (btnResetSealScale != null) {
btnResetSealScale.setVisible(Math.abs(sealScale - 1.0) > 0.02);
}
updateContainerZoomDimensions();
repositionSealByRelativeCoords();
updateFeedbackLabel();
}
private void adjustZoom(double delta) { private void adjustZoom(double delta) {
setZoom(Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.zoomFactor + delta))); setZoom(Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, this.zoomFactor + delta)));
} }
@@ -592,8 +793,19 @@ public final class PdfSignDialog {
pageImageView.setFitHeight(currentH); pageImageView.setFitHeight(currentH);
} }
if (sealStampOverlayView != null) { if (sealStampOverlayView != null) {
sealStampOverlayView.setFitWidth(currentBaseSealWidth * zoomFactor); double sealW = currentBaseSealWidth * zoomFactor * sealScale;
sealStampOverlayView.setFitHeight(currentBaseSealHeight * zoomFactor); double sealH = currentBaseSealHeight * zoomFactor * sealScale;
sealStampOverlayView.setFitWidth(sealW);
sealStampOverlayView.setFitHeight(sealH);
if (sealSelectionBorder != null) {
sealSelectionBorder.setWidth(sealW);
sealSelectionBorder.setHeight(sealH);
}
if (resizeHandle != null) {
resizeHandle.setCenterX(sealW);
resizeHandle.setCenterY(sealH);
}
} }
} }
@@ -602,15 +814,17 @@ public final class PdfSignDialog {
double sheetW = currentBaseSheetWidth * zoomFactor; double sheetW = currentBaseSheetWidth * zoomFactor;
double sheetH = currentBaseSheetHeight * zoomFactor; double sheetH = currentBaseSheetHeight * zoomFactor;
double sealW = currentBaseSealWidth * zoomFactor; double sealW = currentBaseSealWidth * zoomFactor * sealScale;
double sealH = currentBaseSealHeight * zoomFactor; double sealH = currentBaseSealHeight * zoomFactor * sealScale;
double posX = Math.max(0, Math.min(sealRelativeX * sheetW, sheetW - sealW)); double posX = Math.max(0, Math.min(sealRelativeX * sheetW, sheetW - sealW));
double posY = Math.max(0, Math.min(sealRelativeY * sheetH, sheetH - sealH)); double posY = Math.max(0, Math.min(sealRelativeY * sheetH, sheetH - sealH));
sealStampOverlayView.setLayoutX(posX); if (sealGroupPane != null) {
sealStampOverlayView.setLayoutY(posY); sealGroupPane.setLayoutX(posX);
sealStampOverlayView.setVisible(true); sealGroupPane.setLayoutY(posY);
sealGroupPane.setVisible(true);
}
} }
// ========================================================================= // =========================================================================
@@ -653,7 +867,9 @@ public final class PdfSignDialog {
renderCurrentPage(); renderCurrentPage();
if (chkVisibleSeal.isSelected()) { if (chkVisibleSeal.isSelected()) {
sealStampOverlayView.setVisible(true); if (sealGroupPane != null) {
sealGroupPane.setVisible(true);
}
resetSealToCornerPosition(PositionOption.BOTTOM_RIGHT); resetSealToCornerPosition(PositionOption.BOTTOM_RIGHT);
} }
} catch (Exception ex) { } catch (Exception ex) {
@@ -751,38 +967,42 @@ public final class PdfSignDialog {
// ========================================================================= // =========================================================================
private void handleSheetClick(MouseEvent event) { private void handleSheetClick(MouseEvent event) {
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return; if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
double sealW = currentBaseSealWidth * zoomFactor; double sealW = currentBaseSealWidth * zoomFactor * sealScale;
double sealH = currentBaseSealHeight * zoomFactor; double sealH = currentBaseSealHeight * zoomFactor * sealScale;
updateSealPositionFromDrag(event.getX() - sealW / 2.0, event.getY() - sealH / 2.0); updateSealPositionFromDrag(event.getX() - sealW / 2.0, event.getY() - sealH / 2.0);
} }
private void updateSealPositionFromDrag(double newLayoutX, double newLayoutY) { private void updateSealPositionFromDrag(double newLayoutX, double newLayoutY) {
double sheetW = currentBaseSheetWidth * zoomFactor; double sheetW = currentBaseSheetWidth * zoomFactor;
double sheetH = currentBaseSheetHeight * zoomFactor; double sheetH = currentBaseSheetHeight * zoomFactor;
double sealW = currentBaseSealWidth * zoomFactor; double sealW = currentBaseSealWidth * zoomFactor * sealScale;
double sealH = currentBaseSealHeight * zoomFactor; double sealH = currentBaseSealHeight * zoomFactor * sealScale;
double clampedX = Math.max(0, Math.min(newLayoutX, sheetW - sealW)); double clampedX = Math.max(0, Math.min(newLayoutX, sheetW - sealW));
double clampedY = Math.max(0, Math.min(newLayoutY, sheetH - sealH)); double clampedY = Math.max(0, Math.min(newLayoutY, sheetH - sealH));
sealStampOverlayView.setLayoutX(clampedX); if (sealGroupPane != null) {
sealStampOverlayView.setLayoutY(clampedY); sealGroupPane.setLayoutX(clampedX);
sealStampOverlayView.setVisible(true); sealGroupPane.setLayoutY(clampedY);
sealGroupPane.setVisible(true);
}
this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0; this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0;
this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0; this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0;
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme(); PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
float sealPdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0); float basePdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
float sealPdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0); float basePdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
float scaledPdfW = basePdfW * (float) sealScale;
float scaledPdfH = basePdfH * (float) sealScale;
// Conversão direta e exata da escala visual na tela para os pontos do PDF // Conversão direta e exata da escala visual na tela para os pontos do PDF
double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0; double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0;
float calculatedPdfX = (float) (clampedX / scale); float calculatedPdfX = (float) (clampedX / scale);
float calculatedPdfY = (float) (currentPagePdfHeight - (clampedY / scale) - sealPdfH); float calculatedPdfY = (float) (currentPagePdfHeight - (clampedY / scale) - scaledPdfH);
this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - sealPdfW)); this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - scaledPdfW));
this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - sealPdfH)); this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - scaledPdfH));
this.customPageNumber = currentPageIndex + 1; this.customPageNumber = currentPageIndex + 1;
this.isCustomPositionActive = true; this.isCustomPositionActive = true;
@@ -799,8 +1019,10 @@ public final class PdfSignDialog {
} }
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme(); PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
float sealPdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0); float basePdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
float sealPdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0); float basePdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
float sealPdfW = basePdfW * (float) sealScale;
float sealPdfH = basePdfH * (float) sealScale;
float margin = 20f; float margin = 20f;
float targetPdfX; float targetPdfX;
@@ -853,16 +1075,22 @@ public final class PdfSignDialog {
private void updateFeedbackLabel() { private void updateFeedbackLabel() {
if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) { if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) {
lblPositionFeedback.setText(String.format("📍 Selo na Pág. %d [X: %.0f pt, Y: %.0f pt]", String scaleStr = (Math.abs(sealScale - 1.0) > 0.02) ? String.format(" | Escala: %.0f%%", sealScale * 100) : "";
customPageNumber, customPdfX, customPdfY)); lblPositionFeedback.setText(String.format("📍 Selo na Pág. %d [X: %.0f pt, Y: %.0f pt]%s",
customPageNumber, customPdfX, customPdfY, scaleStr));
} }
} }
private void toggleVisibleSeal(boolean active) { private void toggleVisibleSeal(boolean active) {
positionCombo.setDisable(!active); positionCombo.setDisable(!active);
pageCombo.setDisable(!active); pageCombo.setDisable(!active);
if (btnToggleResize != null) {
btnToggleResize.setDisable(!active);
}
sealPreviewImageView.setOpacity(active ? 1.0 : 0.3); sealPreviewImageView.setOpacity(active ? 1.0 : 0.3);
sealStampOverlayView.setVisible(active && selectedPdfBytes != null); if (sealGroupPane != null) {
sealGroupPane.setVisible(active && selectedPdfBytes != null);
}
} }
private void updateSealPreview() { private void updateSealPreview() {
@@ -927,19 +1155,21 @@ public final class PdfSignDialog {
customPdfY, customPdfY,
customPageNumber, customPageNumber,
DEFAULT_REASON, DEFAULT_REASON,
DEFAULT_LOCATION DEFAULT_LOCATION,
sealScale
); );
String sealSummary; String sealSummary;
if (chkVisibleSeal.isSelected()) { if (chkVisibleSeal.isSelected()) {
String themeName = theme.name(); String themeName = theme.name();
String scaleInfo = (Math.abs(sealScale - 1.0) > 0.02) ? String.format(" [Escala: %.0f%%]", sealScale * 100) : "";
if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) { if (isCustomPositionActive && customPageNumber != null && customPdfX != null && customPdfY != null) {
sealSummary = String.format("Selo Estampado (%s) — Pág. %d [X: %.0f pt, Y: %.0f pt]", sealSummary = String.format("Selo Estampado (%s)%s — Pág. %d [X: %.0f pt, Y: %.0f pt]",
themeName, customPageNumber, customPdfX, customPdfY); themeName, scaleInfo, customPageNumber, customPdfX, customPdfY);
} else { } else {
sealSummary = String.format("Selo Estampado (%s) — %s (%s)", sealSummary = String.format("Selo Estampado (%s)%s — %s (%s)",
themeName, (pos != null ? pos.label : "Canto Inferior Direito"), (pg != null ? pg.label : "Última Página")); themeName, scaleInfo, (pos != null ? pos.label : "Canto Inferior Direito"), (pg != null ? pg.label : "Última Página"));
} }
} else { } else {
sealSummary = "Sem Selo Visual (Assinatura PAdES Invisível)"; sealSummary = "Sem Selo Visual (Assinatura PAdES Invisível)";
@@ -5,6 +5,7 @@ import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.provider.CertificateManager; import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.core.security.PinSessionManager; import br.com.cybercore.signer.core.security.PinSessionManager;
import br.com.cybercore.signer.core.xml.XmlSignatureOptions; import br.com.cybercore.signer.core.xml.XmlSignatureOptions;
import br.com.cybercore.signer.core.xml.XmlSignatureProfile;
import br.com.cybercore.signer.core.xml.XmlSigningService; import br.com.cybercore.signer.core.xml.XmlSigningService;
import br.com.cybercore.signer.ui.components.SvgIcons; import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager; import br.com.cybercore.signer.ui.theme.ThemeManager;
@@ -25,6 +26,7 @@ import javafx.scene.control.PasswordField;
import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.RadioButton; import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.ColumnConstraints;
@@ -68,7 +70,9 @@ public final class XmlSignDialog {
private PasswordField pinField; private PasswordField pinField;
private Label lblPinNotice; private Label lblPinNotice;
// Controles de Modo de Assinatura // Controles de Padrão e Modo de Assinatura
private RadioButton rbXadesBes;
private RadioButton rbXmlDsig;
private RadioButton rbWholeDocument; private RadioButton rbWholeDocument;
private RadioButton rbSpecificElement; private RadioButton rbSpecificElement;
private TextField txtElementTag; private TextField txtElementTag;
@@ -94,10 +98,10 @@ public final class XmlSignDialog {
if (owner != null) { if (owner != null) {
this.stage.initOwner(owner); this.stage.initOwner(owner);
} }
this.stage.setTitle("CyberCore Signer — Assinatura Digital de XML (XMLDSig)"); this.stage.setTitle("CyberCore Signer — Assinatura Digital de XML (XAdES / ICP-Brasil)");
this.stage.setResizable(true); this.stage.setResizable(true);
this.stage.setMinWidth(720); this.stage.setMinWidth(720);
this.stage.setMinHeight(580); this.stage.setMinHeight(600);
initLayout(); initLayout();
} }
@@ -111,9 +115,9 @@ public final class XmlSignDialog {
HBox header = new HBox(14); HBox header = new HBox(14);
header.setAlignment(Pos.CENTER_LEFT); header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(4); VBox titleBox = new VBox(4);
Label lblTitle = new Label("ASSINATURA DE DOCUMENTO XML (XMLDSIG)"); Label lblTitle = new Label("ASSINATURA DE DOCUMENTO XML (XADES / ICP-BRASIL)");
lblTitle.getStyleClass().add("dialog-header"); lblTitle.getStyleClass().add("dialog-header");
Label lblSub = new Label("Assine digitalmente documentos XML no padrão W3C XMLDSig e ICP-Brasil (NF-e, CT-e, MDF-e, eSocial, etc.)."); Label lblSub = new Label("Assine digitalmente documentos XML no padrão avançado XAdES (ETSI TS 101 903 / ICP-Brasil DOC-ICP-15.03) ou XMLDSig.");
lblSub.getStyleClass().add("dialog-subtitle"); lblSub.getStyleClass().add("dialog-subtitle");
titleBox.getChildren().addAll(lblTitle, lblSub); titleBox.getChildren().addAll(lblTitle, lblSub);
header.getChildren().addAll(SvgIcons.xmlIcon(), titleBox); header.getChildren().addAll(SvgIcons.xmlIcon(), titleBox);
@@ -198,7 +202,21 @@ public final class XmlSignDialog {
// 3. Seção: Modo de Assinatura XMLDSig // 3. Seção: Modo de Assinatura XMLDSig
VBox xmlModeSection = new VBox(12); VBox xmlModeSection = new VBox(12);
xmlModeSection.getStyleClass().add("dialog-box"); xmlModeSection.getStyleClass().add("dialog-box");
Label lblModeTitle = new Label("MODO DE ASSINATURA XMLDSIG"); Label lblProfileTitle = new Label("PADRÃO DE ASSINATURA XML");
lblProfileTitle.getStyleClass().add("dialog-field-title");
ToggleGroup profileGroup = new ToggleGroup();
rbXadesBes = new RadioButton("XAdES-BES (ETSI TS 101 903 / ICP-Brasil DOC-ICP-15.03 - Padrão Avançado)");
rbXadesBes.setToggleGroup(profileGroup);
rbXadesBes.setSelected(true);
rbXmlDsig = new RadioButton("XMLDSig Clássico W3C (Enveloped legado / NF-e SEFAZ)");
rbXmlDsig.setToggleGroup(profileGroup);
VBox profileBox = new VBox(8);
profileBox.getChildren().addAll(rbXadesBes, rbXmlDsig);
Label lblModeTitle = new Label("ESCOPO DE ASSINATURA");
lblModeTitle.getStyleClass().add("dialog-field-title"); lblModeTitle.getStyleClass().add("dialog-field-title");
ToggleGroup modeGroup = new ToggleGroup(); ToggleGroup modeGroup = new ToggleGroup();
@@ -247,7 +265,16 @@ public final class XmlSignDialog {
chkValidateAfter = new CheckBox("Validar conformidade e integridade criptográfica após gravação"); chkValidateAfter = new CheckBox("Validar conformidade e integridade criptográfica após gravação");
chkValidateAfter.setSelected(true); chkValidateAfter.setSelected(true);
xmlModeSection.getChildren().addAll(lblModeTitle, rbWholeDocument, rbSpecificElement, elementConfigBox, chkValidateAfter); xmlModeSection.getChildren().addAll(
lblProfileTitle,
profileBox,
new Separator(),
lblModeTitle,
rbWholeDocument,
rbSpecificElement,
elementConfigBox,
chkValidateAfter
);
formContent.getChildren().addAll(fileSection, certSection, xmlModeSection); formContent.getChildren().addAll(fileSection, certSection, xmlModeSection);
scrollPane.setContent(formContent); scrollPane.setContent(formContent);
@@ -389,6 +416,10 @@ public final class XmlSignDialog {
pin = pinField.getText().toCharArray(); pin = pinField.getText().toCharArray();
} }
XmlSignatureProfile profile = rbXadesBes.isSelected()
? XmlSignatureProfile.XADES_BES
: XmlSignatureProfile.XMLDSIG;
XmlSignatureOptions options; XmlSignatureOptions options;
if (rbSpecificElement.isSelected()) { if (rbSpecificElement.isSelected()) {
String tag = txtElementTag.getText().trim(); String tag = txtElementTag.getText().trim();
@@ -396,14 +427,14 @@ public final class XmlSignDialog {
if (tag.isBlank()) { if (tag.isBlank()) {
tag = "infNFe"; tag = "infNFe";
} }
options = XmlSignatureOptions.forElement(cert.id(), pin, tag, id.isBlank() ? null : id); options = XmlSignatureOptions.forElement(cert.id(), pin, tag, id.isBlank() ? null : id, profile);
} else { } else {
options = XmlSignatureOptions.wholeDocument(cert.id(), pin); options = XmlSignatureOptions.wholeDocument(cert.id(), pin, profile);
} }
setFormDisabled(true); setFormDisabled(true);
progressIndicator.setVisible(true); progressIndicator.setVisible(true);
lblStatus.setText("Assinando documento XML (XMLDSig)..."); lblStatus.setText("Assinando documento XML (" + profile.getDisplayName() + ")...");
lblStatus.setStyle("-fx-text-fill: -fx-accent;"); lblStatus.setStyle("-fx-text-fill: -fx-accent;");
boolean validate = chkValidateAfter.isSelected(); boolean validate = chkValidateAfter.isSelected();