Compare commits
4
Commits
c997afbfd4
...
3fab68c118
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fab68c118 | ||
|
|
0cd4025f3c | ||
|
|
5360caaddf | ||
|
|
4c30649598 |
+81
-19
@@ -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.
|
||||||
|
|||||||
+27
-4
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+125
-82
@@ -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,119 +209,148 @@ 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);
|
||||||
doc.addSignature(signature, signer);
|
|
||||||
doc.saveIncremental(finalOut);
|
if (options.visibleSeal()) {
|
||||||
|
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
|
||||||
|
int targetPage = resolveTargetPage(doc, options);
|
||||||
|
PDRectangle rect = calculateSealRectangle(doc, targetPage, theme, options);
|
||||||
|
|
||||||
|
try (InputStream visualTemplate = createVisualSignatureTemplate(doc, targetPage, rect, sealPng);
|
||||||
|
SignatureOptions signatureOptions = new SignatureOptions()) {
|
||||||
|
signatureOptions.setVisualSignature(visualTemplate);
|
||||||
|
signatureOptions.setPage(targetPage);
|
||||||
|
doc.addSignature(signature, signer, signatureOptions);
|
||||||
|
doc.saveIncremental(finalOut);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doc.addSignature(signature, signer);
|
||||||
|
doc.saveIncremental(finalOut);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
|
logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
|
||||||
return finalOut.toByteArray();
|
return finalOut.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] applyVisualSealToPdf(byte[] pdfBytes, byte[] sealPng, PdfSealTheme theme,
|
private int resolveTargetPage(PDDocument doc, PdfSignatureOptions options) {
|
||||||
PdfSignatureOptions options) throws IOException {
|
int totalPages = doc.getNumberOfPages();
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
if (totalPages <= 0) {
|
||||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
return 0;
|
||||||
int totalPages = doc.getNumberOfPages();
|
}
|
||||||
if (totalPages == 0) {
|
if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) {
|
||||||
return pdfBytes;
|
return options.targetPageNumber() - 1;
|
||||||
}
|
}
|
||||||
|
if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return totalPages - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PDRectangle calculateSealRectangle(PDDocument doc, int pageIndex, PdfSealTheme theme, PdfSignatureOptions options) {
|
||||||
|
PDPage page = doc.getPage(pageIndex);
|
||||||
|
PDRectangle mediaBox = page.getMediaBox();
|
||||||
|
float pageW = mediaBox.getWidth();
|
||||||
|
float pageH = mediaBox.getHeight();
|
||||||
|
|
||||||
PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal");
|
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 sealWidth = theme.width() * 0.72f;
|
float x;
|
||||||
float sealHeight = theme.height() * 0.72f;
|
float y;
|
||||||
float margin = 20f;
|
|
||||||
|
|
||||||
// Se coordenadas customizadas foram informadas (clique ou drag & drop na página)
|
if (options.customX() != null && options.customY() != null) {
|
||||||
if (options.customX() != null && options.customY() != null) {
|
x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
|
||||||
int targetPageIdx = totalPages - 1;
|
y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
|
||||||
if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) {
|
} else {
|
||||||
targetPageIdx = options.targetPageNumber() - 1;
|
switch (options.position()) {
|
||||||
|
case BOTTOM_LEFT -> {
|
||||||
|
x = margin;
|
||||||
|
y = margin;
|
||||||
|
}
|
||||||
|
case TOP_RIGHT -> {
|
||||||
|
x = pageW - sealWidth - margin;
|
||||||
|
y = pageH - sealHeight - margin;
|
||||||
}
|
}
|
||||||
|
case TOP_LEFT -> {
|
||||||
|
x = margin;
|
||||||
|
y = pageH - sealHeight - margin;
|
||||||
|
}
|
||||||
|
default -> { // BOTTOM_RIGHT
|
||||||
|
x = pageW - sealWidth - margin;
|
||||||
|
y = margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PDPage page = doc.getPage(targetPageIdx);
|
return new PDRectangle(x, y, sealWidth, sealHeight);
|
||||||
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));
|
* 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);
|
||||||
|
|
||||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
PDAcroForm acroForm = new PDAcroForm(doc);
|
||||||
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
doc.getDocumentCatalog().setAcroForm(acroForm);
|
||||||
}
|
|
||||||
|
|
||||||
doc.save(baos);
|
|
||||||
return baos.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caso contrário, usa posicionamento por enum (BOTTOM_RIGHT, etc.)
|
PDSignatureField signatureField = new PDSignatureField(acroForm);
|
||||||
int startPage = 0;
|
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
|
||||||
int endPage = totalPages - 1;
|
acroForm.getFields().add(signatureField);
|
||||||
|
acroForm.setSignaturesExist(true);
|
||||||
|
acroForm.setAppendOnly(true);
|
||||||
|
acroForm.getCOSObject().setDirect(true);
|
||||||
|
|
||||||
if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) {
|
widget.setRectangle(rect);
|
||||||
endPage = 0;
|
|
||||||
} else if (options.pageChoice() == PdfSealPageChoice.LAST_PAGE) {
|
|
||||||
startPage = totalPages - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = startPage; i <= endPage; i++) {
|
PDStream stream = new PDStream(doc);
|
||||||
PDPage page = doc.getPage(i);
|
PDFormXObject form = new PDFormXObject(stream);
|
||||||
PDRectangle mediaBox = page.getMediaBox();
|
PDResources res = new PDResources();
|
||||||
float pageW = mediaBox.getWidth();
|
form.setResources(res);
|
||||||
float pageH = mediaBox.getHeight();
|
form.setFormType(1);
|
||||||
|
|
||||||
float x;
|
PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight());
|
||||||
float y;
|
form.setBBox(bbox);
|
||||||
|
|
||||||
switch (options.position()) {
|
PDAppearanceDictionary appearance = new PDAppearanceDictionary();
|
||||||
case BOTTOM_LEFT -> {
|
appearance.getCOSObject().setDirect(true);
|
||||||
x = margin;
|
PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject());
|
||||||
y = margin;
|
appearance.setNormalAppearance(appearanceStream);
|
||||||
}
|
widget.setAppearance(appearance);
|
||||||
case TOP_RIGHT -> {
|
|
||||||
x = pageW - sealWidth - margin;
|
|
||||||
y = pageH - sealHeight - margin;
|
|
||||||
}
|
|
||||||
case TOP_LEFT -> {
|
|
||||||
x = margin;
|
|
||||||
y = pageH - sealHeight - margin;
|
|
||||||
}
|
|
||||||
default -> { // BOTTOM_RIGHT
|
|
||||||
x = pageW - sealWidth - margin;
|
|
||||||
y = margin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
|
||||||
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
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) {
|
||||||
|
|||||||
+100
-11
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
@@ -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);
|
||||||
|
|
||||||
|
|||||||
+17
-4
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+268
-39
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+76
-2
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+80
-9
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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 =
|
||||||
|
|||||||
+29
-5
@@ -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;
|
||||||
@@ -66,8 +67,28 @@ public final class PdfCertificateSignDialog {
|
|||||||
Float customY,
|
Float customY,
|
||||||
Integer customPageNumber,
|
Integer customPageNumber,
|
||||||
String reason,
|
String reason,
|
||||||
String location
|
String location,
|
||||||
) {}
|
Double customSealScale
|
||||||
|
) {
|
||||||
|
public 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
|
||||||
|
) {
|
||||||
|
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());
|
||||||
|
|||||||
+270
-40
@@ -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)";
|
||||||
|
|||||||
+41
-10
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user