6 Commits
24 changed files with 2971 additions and 892 deletions
+81 -19
View File
@@ -1,16 +1,27 @@
# Assinaturas XML (XMLDSig e ICP-Brasil) # Assinaturas XML no Padrão XAdES e ICP-Brasil
Este documento descreve a especificação e o funcionamento da assinatura digital de arquivos XML no padrão **W3C XMLDSig (XML Digital Signatures)** e **ICP-Brasil** no CyberCore Signer. Este documento descreve a especificação, conformidade e o funcionamento da assinatura digital de arquivos XML no padrão avançado **XAdES (XML Advanced Electronic Signatures - ETSI TS 101 903 e ICP-Brasil DOC-ICP-15.03)** e **W3C XMLDSig** no CyberCore Signer.
--- ---
## 1. Visão Geral ## 1. Visão Geral
O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando a engine padronizada e segura da plataforma Java (`javax.xml.crypto.dsig`). O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando a engine padronizada e segura da plataforma Java (`javax.xml.crypto.dsig`), garantindo conformidade com o padrão **XAdES-BES** adotado pelo ITI (Instituto Nacional de Tecnologia da Informação) na infraestrutura ICP-Brasil (perfil AD-RB).
### 1.1 Casos de Uso Suportados ### 1.1 Perfis Suportados
- **Documento Inteiro Envelopado (`URI=""`):** Contratos, despachos, declarações e relatórios em formato XML. * **`XAdES-BES` (Padrão Recomendado / ICP-Brasil):**
- Conforme especificação técnica **ETSI TS 101 903** e norma **DOC-ICP-15.03**.
- Encapsula o elemento `<ds:Object>` contendo `<xades:QualifyingProperties>` e `<xades:SignedProperties>`.
- Inclui propriedade qualificada de data/hora da assinatura (`<xades:SigningTime>`) em formato UTC ISO-8601.
- Inclui vínculo criptográfico irrefutável do certificado do signatário (`<xades:SigningCertificate>`) contendo o digest SHA-256 do certificado DER e referências do emissor/serial (`<xades:IssuerSerial>`).
- Protege as propriedades qualificadas através de uma `<ds:Reference>` em `<ds:SignedInfo>` com `Type="http://uri.etsi.org/01903#SignedProperties"`.
* **`XMLDSig` Clássico (W3C Enveloped):**
- Mantido para compatibilidade com layouts estritos de notas fiscais eletrônicas legadas (ex: NF-e, CT-e, MDF-e SEFAZ).
### 1.2 Casos de Uso Suportados
- **Documento Inteiro Envelopado (`URI=""`):** Contratos, despachos, declarações, laudos e relatórios em XML.
- **Assinatura de Elementos Específicos por ID:** - **Assinatura de Elementos Específicos por ID:**
- **NF-e (Nota Fiscal Eletrônica):** Assinatura da tag `<infNFe Id="NFe...">` - **NF-e (Nota Fiscal Eletrônica):** Assinatura da tag `<infNFe Id="NFe...">`
- **CT-e (Conhecimento de Transporte Eletrônico):** Assinatura da tag `<infCte Id="CTe...">` - **CT-e (Conhecimento de Transporte Eletrônico):** Assinatura da tag `<infCte Id="CTe...">`
@@ -19,19 +30,69 @@ O CyberCore Signer fornece suporte nativo a assinaturas digitais XML utilizando
--- ---
## 2. Padrões Criptográficos ## 2. Estrutura Criptográfica do XAdES-BES
- **Canonicalização (C14N):** `http://www.w3.org/TR/2001/REC-xml-c14n-20010315` (C14N inclusiva sem comentários). ```xml
- **Transformações:** <ds:Signature Id="sig-[UUID]" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
- `http://www.w3.org/2000/09/xmldsig#enveloped-signature` (remove a assinatura do cálculo do digest). <ds:SignedInfo>
- C14N padrão. <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
- **Algoritmo de Digest:** SHA-256 (`http://www.w3.org/2001/04/xmlenc#sha256`). <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
- **Algoritmo de Assinatura:** RSA-SHA256 (`http://www.w3.org/2001/04/xmldsig-more#rsa-sha256`).
- **KeyInfo:** Informações do certificado X.509 embutidas em `<X509Data><X509Certificate>`. <!-- Referência 0: Dados de Negócio (Documento completo ou elemento específico) -->
<ds:Reference URI="#[ElementoId]">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>[Digest do nó em Base64]</ds:DigestValue>
</ds:Reference>
<!-- Referência 1: Propriedades Qualificadas Assinadas XAdES -->
<ds:Reference URI="#signed-props-[UUID]" Type="http://uri.etsi.org/01903#SignedProperties">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>[Digest das SignedProperties em Base64]</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>[Assinatura RSA-SHA256 em Base64]</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>[Certificado DER do signatário em Base64]</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<ds:Object>
<xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Target="#sig-[UUID]">
<xades:SignedProperties Id="signed-props-[UUID]" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xades="http://uri.etsi.org/01903/v1.3.2#">
<xades:SignedSignatureProperties>
<xades:SigningTime>2026-10-11T14:05:00Z</xades:SigningTime>
<xades:SigningCertificate>
<xades:Cert>
<xades:CertDigest>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>[SHA-256 do Certificado]</ds:DigestValue>
</xades:CertDigest>
<xades:IssuerSerial>
<ds:X509IssuerName>[Emissor X.500]</ds:X509IssuerName>
<ds:X509SerialNumber>[Número Serial]</ds:X509SerialNumber>
</xades:IssuerSerial>
</xades:Cert>
</xades:SigningCertificate>
</xades:SignedSignatureProperties>
</xades:SignedProperties>
</xades:QualifyingProperties>
</ds:Object>
</ds:Signature>
```
--- ---
## 3. Segurança e Mitigação contra XXE ## 3. Segurança e Mitigação contra Ataques XXE
O parser XML é configurado com blindagem estrita contra ataques **XML External Entity (XXE)** e expansões recursivas de entidades: O parser XML é configurado com blindagem estrita contra ataques **XML External Entity (XXE)** e expansões recursivas de entidades:
@@ -47,10 +108,11 @@ dbf.setExpandEntityReferences(false);
--- ---
## 4. Validação Pós-Assinatura ## 4. Validação Pós-Assinatura e Auditoria
O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` permite auditar a assinatura imediatamente após sua emissão, verificando: O serviço `XmlSigningService.validateXmlSignature(byte[] xmlBytes)` e o `SignatureValidationService` realizam:
1. A integridade criptográfica da assinatura (`SignedInfo`). 1. **Validação criptográfica de SignedInfo:** Confere se o `SignatureValue` foi emitido pela chave pública correspondente.
2. A integridade dos digests de todas as referências apontadas (`Reference.validate()`). 2. **Validação de integridade dos dados de negócio:** Confere o digest do conteúdo ou elemento assinado.
3. O certificado X.509 público extraído da chave. 3. **Validação de integridade do XAdES:** Confere o digest das propriedades qualificadas (`SignedProperties`), rejeitando qualquer adulteração no horário de assinatura (`SigningTime`) ou no certificado declarado.
4. **Inspeção de signatário e datação:** Extração de nome, documento (CPF/CNPJ), autoridade certificadora emissora e datação auditável.
@@ -25,7 +25,8 @@ public record PdfSignatureOptions(
Integer targetPageNumber, Integer targetPageNumber,
CpfDisplayMode cpfDisplayMode, CpfDisplayMode cpfDisplayMode,
Boolean includeDateTime, Boolean includeDateTime,
String customExtraInfo String customExtraInfo,
Double customSealScale
) { ) {
public PdfSignatureOptions { public PdfSignatureOptions {
@@ -51,6 +52,21 @@ public record PdfSignatureOptions(
if (includeDateTime == null) { if (includeDateTime == null) {
includeDateTime = sealTheme.defaultIncludeDateTime(); includeDateTime = sealTheme.defaultIncludeDateTime();
} }
if (customSealScale == null || customSealScale <= 0) {
customSealScale = 1.0;
}
}
/**
* Construtor para compatibilidade com assinaturas anteriores a customSealScale.
*/
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
String reason, String location, Float customX, Float customY,
Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime,
String customExtraInfo) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location,
customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, customExtraInfo, 1.0);
} }
/** /**
@@ -61,7 +77,7 @@ public record PdfSignatureOptions(
String reason, String location, Float customX, Float customY, String reason, String location, Float customX, Float customY,
Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) { Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location,
customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, null); customX, customY, targetPageNumber, cpfDisplayMode, includeDateTime, null, 1.0);
} }
/** /**
@@ -70,7 +86,7 @@ public record PdfSignatureOptions(
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal, public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice, PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
String reason, String location) { String reason, String location) {
this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, null, null, null, null, null, null); this(certificateId, pinOrPassword, visibleSeal, sealTheme, position, pageChoice, reason, location, null, null, null, null, null, null, 1.0);
} }
public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal, public PdfSignatureOptions(String certificateId, char[] pinOrPassword, boolean visibleSeal,
@@ -143,6 +159,7 @@ public record PdfSignatureOptions(
} }
private String customExtraInfo; private String customExtraInfo;
private Double customSealScale = 1.0;
public Builder includeDateTime(boolean include) { public Builder includeDateTime(boolean include) {
this.includeDateTime = include; this.includeDateTime = include;
@@ -154,6 +171,11 @@ public record PdfSignatureOptions(
return this; return this;
} }
public Builder customSealScale(Double scale) {
this.customSealScale = scale;
return this;
}
public Builder customPosition(Float x, Float y, Integer pageNumber) { public Builder customPosition(Float x, Float y, Integer pageNumber) {
this.customX = x; this.customX = x;
this.customY = y; this.customY = y;
@@ -179,7 +201,8 @@ public record PdfSignatureOptions(
targetPageNumber, targetPageNumber,
cpfDisplayMode, cpfDisplayMode,
includeDateTime, includeDateTime,
customExtraInfo customExtraInfo,
customSealScale
); );
} }
} }
@@ -1,5 +1,6 @@
package br.com.cybercore.signer.core.pdf; package br.com.cybercore.signer.core.pdf;
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
import br.com.cybercore.signer.core.domain.ManagedCertificate; import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.domain.SigningKeyMaterial; import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
import br.com.cybercore.signer.core.pdf.seal.PdfSealData; import br.com.cybercore.signer.core.pdf.seal.PdfSealData;
@@ -12,10 +13,19 @@ import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.bouncycastle.cert.jcajce.JcaCertStore; import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray; import org.bouncycastle.cms.CMSProcessableByteArray;
@@ -66,6 +76,8 @@ public class PdfSigningService {
this.certificateManager = Objects.requireNonNull(certificateManager, "CertificateManager não pode ser nulo."); this.certificateManager = Objects.requireNonNull(certificateManager, "CertificateManager não pode ser nulo.");
} }
public record PageDimensions(float width, float height) {}
/** /**
* Retorna a quantidade total de páginas contidas no documento PDF. * Retorna a quantidade total de páginas contidas no documento PDF.
*/ */
@@ -76,6 +88,21 @@ public class PdfSigningService {
} }
} }
/**
* Retorna as dimensões em pontos (pt) da página informada do documento PDF.
*/
public static PageDimensions getPageDimensions(byte[] pdfBytes, int pageIndex0Based) throws IOException {
Objects.requireNonNull(pdfBytes, "Bytes do PDF não podem ser nulos.");
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
if (pageIndex0Based < 0 || pageIndex0Based >= doc.getNumberOfPages()) {
throw new IllegalArgumentException("Índice de página inválido: " + pageIndex0Based + ". Total: " + doc.getNumberOfPages());
}
PDPage page = doc.getPage(pageIndex0Based);
PDRectangle mediaBox = page.getMediaBox();
return new PageDimensions(mediaBox.getWidth(), mediaBox.getHeight());
}
}
/** /**
* Renderiza uma página específica do PDF para BufferedImage na resolução DPI informada. * Renderiza uma página específica do PDF para BufferedImage na resolução DPI informada.
*/ */
@@ -155,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();
@@ -166,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,
@@ -178,92 +209,78 @@ public class PdfSigningService {
effectiveExtraInfo effectiveExtraInfo
); );
// 4. Carrega o PDF e estampa o selo se solicitado // 4. Carrega o PDF original e aplica a assinatura PAdES via atualização incremental (Incremental Update),
byte[] intermediatePdf; // preservando intactas quaisquer assinaturas digitais pré-existentes. Em conformidade com a ISO 32000-1 / ETSI PAdES,
if (options.visibleSeal()) { // o selo visual é associado diretamente ao widget da assinatura (/Widget /AP /N) sem alterar o stream /Contents
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData); // das páginas existentes, garantindo que o validador do ITI aprove a verificação incremental sem indeterminação.
intermediatePdf = applyVisualSealToPdf(pdfBytes, sealPng, theme, options);
} else {
intermediatePdf = pdfBytes;
}
// 5. Aplica a assinatura PAdES criptográfica via PDFBox e Bouncy Castle
ByteArrayOutputStream finalOut = new ByteArrayOutputStream(); ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
try (PDDocument doc = Loader.loadPDF(intermediatePdf)) { try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
PDSignature signature = new PDSignature(); PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName(cert.commonName()); signature.setName(signerName);
signature.setLocation(options.location()); signature.setLocation(options.location());
signature.setReason(options.reason()); signature.setReason(options.reason());
signature.setSignDate(Calendar.getInstance()); signature.setSignDate(Calendar.getInstance());
SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial); SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial);
if (options.visibleSeal()) {
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
int targetPage = resolveTargetPage(doc, options);
PDRectangle rect = calculateSealRectangle(doc, targetPage, theme, options);
try (InputStream visualTemplate = createVisualSignatureTemplate(doc, targetPage, rect, sealPng);
SignatureOptions signatureOptions = new SignatureOptions()) {
signatureOptions.setVisualSignature(visualTemplate);
signatureOptions.setPage(targetPage);
doc.addSignature(signature, signer, signatureOptions);
doc.saveIncremental(finalOut);
}
} else {
doc.addSignature(signature, signer); doc.addSignature(signature, signer);
doc.saveIncremental(finalOut); doc.saveIncremental(finalOut);
} }
}
logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size()); logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
return finalOut.toByteArray(); return finalOut.toByteArray();
} }
private byte[] applyVisualSealToPdf(byte[] pdfBytes, byte[] sealPng, PdfSealTheme theme, private int resolveTargetPage(PDDocument doc, PdfSignatureOptions options) {
PdfSignatureOptions options) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
int totalPages = doc.getNumberOfPages(); int totalPages = doc.getNumberOfPages();
if (totalPages == 0) { if (totalPages <= 0) {
return pdfBytes; return 0;
} }
PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal");
float sealWidth = theme.width() * 0.72f;
float sealHeight = theme.height() * 0.72f;
float margin = 20f;
// Se coordenadas customizadas foram informadas (clique ou drag & drop na página)
if (options.customX() != null && options.customY() != null) {
int targetPageIdx = totalPages - 1;
if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) { if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) {
targetPageIdx = options.targetPageNumber() - 1; return options.targetPageNumber() - 1;
} }
PDPage page = doc.getPage(targetPageIdx);
PDRectangle mediaBox = page.getMediaBox();
float pageW = mediaBox.getWidth();
float pageH = mediaBox.getHeight();
float x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
float y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
}
doc.save(baos);
return baos.toByteArray();
}
// Caso contrário, usa posicionamento por enum (BOTTOM_RIGHT, etc.)
int startPage = 0;
int endPage = totalPages - 1;
if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) { if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) {
endPage = 0; return 0;
} else if (options.pageChoice() == PdfSealPageChoice.LAST_PAGE) { }
startPage = totalPages - 1; return totalPages - 1;
} }
for (int i = startPage; i <= endPage; i++) { private PDRectangle calculateSealRectangle(PDDocument doc, int pageIndex, PdfSealTheme theme, PdfSignatureOptions options) {
PDPage page = doc.getPage(i); PDPage page = doc.getPage(pageIndex);
PDRectangle mediaBox = page.getMediaBox(); PDRectangle mediaBox = page.getMediaBox();
float pageW = mediaBox.getWidth(); float pageW = mediaBox.getWidth();
float pageH = mediaBox.getHeight(); float pageH = mediaBox.getHeight();
float scale = (options.customSealScale() != null && options.customSealScale() > 0)
? options.customSealScale().floatValue()
: 1.0f;
float sealWidth = (theme.width() * 0.72f) * scale;
float sealHeight = (theme.height() * 0.72f) * scale;
float margin = 20f;
float x; float x;
float y; float y;
if (options.customX() != null && options.customY() != null) {
x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
} else {
switch (options.position()) { switch (options.position()) {
case BOTTOM_LEFT -> { case BOTTOM_LEFT -> {
x = margin; x = margin;
@@ -282,15 +299,58 @@ public class PdfSigningService {
y = margin; y = margin;
} }
} }
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
}
} }
return new PDRectangle(x, y, sealWidth, sealHeight);
}
/**
* Cria um documento template PAdES para o SignatureOptions do PDFBox, incorporando o selo visual
* como a Aparência Normal (/AP /N) da anotação da assinatura (PDAnnotationWidget).
* O stream /Contents da página do documento original permanece completamente inalterado.
*/
private InputStream createVisualSignatureTemplate(PDDocument srcDoc, int pageNum,
PDRectangle rect, byte[] sealPngBytes) throws IOException {
try (PDDocument doc = new PDDocument()) {
PDPage page = new PDPage(srcDoc.getPage(pageNum).getMediaBox());
doc.addPage(page);
PDAcroForm acroForm = new PDAcroForm(doc);
doc.getDocumentCatalog().setAcroForm(acroForm);
PDSignatureField signatureField = new PDSignatureField(acroForm);
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
acroForm.getFields().add(signatureField);
acroForm.setSignaturesExist(true);
acroForm.setAppendOnly(true);
acroForm.getCOSObject().setDirect(true);
widget.setRectangle(rect);
PDStream stream = new PDStream(doc);
PDFormXObject form = new PDFormXObject(stream);
PDResources res = new PDResources();
form.setResources(res);
form.setFormType(1);
PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight());
form.setBBox(bbox);
PDAppearanceDictionary appearance = new PDAppearanceDictionary();
appearance.getCOSObject().setDirect(true);
PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject());
appearance.setNormalAppearance(appearanceStream);
widget.setAppearance(appearance);
try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
PDImageXObject img = PDImageXObject.createFromByteArray(doc, sealPngBytes, "cybercore_seal");
cs.drawImage(img, 0, 0, rect.getWidth(), rect.getHeight());
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
doc.save(baos); doc.save(baos);
return new ByteArrayInputStream(baos.toByteArray());
} }
return baos.toByteArray();
} }
private static String bytesToHex(byte[] bytes) { private static String bytesToHex(byte[] bytes) {
@@ -0,0 +1,269 @@
package br.com.cybercore.signer.core.security;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* Armazenamento local seguro e criptografado de PINs/senhas de certificados.
* <p>
* Utiliza criptografia simétrica AES-256-GCM com chave derivada exclusivamente
* a partir de atributos do usuário e da máquina local (PBKDF2WithHmacSHA256).
* Os dados são gravados com permissões restritas em {@code ~/.cybercore/secure-pins.dat}.
*/
public final class SecurePinStorage {
private static final Logger logger = LoggerFactory.getLogger(SecurePinStorage.class);
private static final String FILE_NAME = "secure-pins.dat";
private static final String SALT_FILE_NAME = "secure-pins.salt";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
private static final int PBKDF2_ITERATIONS = 65536;
private static final int KEY_LENGTH_BITS = 256;
private static final SecurePinStorage INSTANCE = new SecurePinStorage();
private final Path storageDirectory;
private final Path dataFile;
private final Path saltFile;
private final ObjectMapper objectMapper;
private final Map<String, char[]> memoryCache = new ConcurrentHashMap<>();
public static SecurePinStorage getInstance() {
return INSTANCE;
}
public SecurePinStorage() {
this(resolveDefaultDirectory());
}
public SecurePinStorage(Path storageDirectory) {
this.storageDirectory = Objects.requireNonNull(storageDirectory);
this.dataFile = storageDirectory.resolve(FILE_NAME);
this.saltFile = storageDirectory.resolve(SALT_FILE_NAME);
this.objectMapper = new ObjectMapper();
loadFromDisk();
}
private static Path resolveDefaultDirectory() {
String userHome = System.getProperty("user.home", ".");
return Path.of(userHome, ".cybercore");
}
/**
* Armazena de forma persistente e criptografada o PIN associado ao ID do certificado.
*/
public synchronized void savePin(String certId, char[] pin) {
Objects.requireNonNull(certId, "certId não pode ser nulo");
if (pin == null || pin.length == 0) {
removePin(certId);
return;
}
memoryCache.put(certId, pin.clone());
PinSessionManager.getInstance().savePin(certId, pin);
persistToDisk();
logger.info("PIN do certificado '{}' armazenado com segurança neste computador.", certId);
}
/**
* Retorna o PIN armazenado para o certificado ou {@code null} se não houver.
*/
public synchronized char[] getPin(String certId) {
if (certId == null) {
return null;
}
// 1. Tenta cache em memória
char[] cached = memoryCache.get(certId);
if (cached != null) {
return cached.clone();
}
// 2. Tenta carregar do PinSessionManager
char[] sessionPin = PinSessionManager.getInstance().getPin(certId);
if (sessionPin != null) {
memoryCache.put(certId, sessionPin.clone());
return sessionPin.clone();
}
return null;
}
/**
* Verifica se existe um PIN armazenado para o certificado.
*/
public synchronized boolean hasPin(String certId) {
if (certId == null) {
return false;
}
return memoryCache.containsKey(certId) || PinSessionManager.getInstance().hasPin(certId);
}
/**
* Remove o PIN salvo para o certificado e zera a memória.
*/
public synchronized void removePin(String certId) {
if (certId == null) {
return;
}
char[] old = memoryCache.remove(certId);
if (old != null) {
Arrays.fill(old, ' ');
}
PinSessionManager.getInstance().clearPin(certId);
persistToDisk();
logger.info("PIN do certificado '{}' removido do armazenamento seguro.", certId);
}
/**
* Remove todos os PINs armazenados em disco e zera todas as memórias.
*/
public synchronized void clearAll() {
for (char[] pin : memoryCache.values()) {
if (pin != null) {
Arrays.fill(pin, ' ');
}
}
memoryCache.clear();
PinSessionManager.getInstance().clearAll();
try {
Files.deleteIfExists(dataFile);
logger.info("Arquivo de credenciais criptografadas removido com sucesso.");
} catch (Exception e) {
logger.warn("Falha ao remover arquivo de credenciais: {}", e.getMessage());
}
}
public synchronized int getStoredCount() {
return memoryCache.size();
}
// =========================================================================
// CRIPTOGRAFIA AES-256-GCM E DERIVAÇÃO DE CHAVE
// =========================================================================
private void persistToDisk() {
try {
Files.createDirectories(storageDirectory);
if (memoryCache.isEmpty()) {
Files.deleteIfExists(dataFile);
return;
}
Map<String, String> plainMap = new HashMap<>();
for (Map.Entry<String, char[]> entry : memoryCache.entrySet()) {
plainMap.put(entry.getKey(), new String(entry.getValue()));
}
byte[] jsonBytes = objectMapper.writeValueAsBytes(plainMap);
byte[] salt = getOrCreateSalt();
SecretKey secretKey = deriveKey(salt);
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
byte[] ciphertext = cipher.doFinal(jsonBytes);
// Grava: [12 bytes IV] + [ciphertext + tag]
try (FileOutputStream fos = new FileOutputStream(dataFile.toFile())) {
fos.write(iv);
fos.write(ciphertext);
}
} catch (Exception e) {
logger.error("Erro ao persistir PINs criptografados no disco: {}", e.getMessage(), e);
}
}
private void loadFromDisk() {
if (!Files.exists(dataFile) || !Files.exists(saltFile)) {
return;
}
try {
byte[] allBytes = Files.readAllBytes(dataFile);
if (allBytes.length <= GCM_IV_LENGTH) {
return;
}
byte[] iv = Arrays.copyOfRange(allBytes, 0, GCM_IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(allBytes, GCM_IV_LENGTH, allBytes.length);
byte[] salt = Files.readAllBytes(saltFile);
SecretKey secretKey = deriveKey(salt);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
byte[] plainBytes = cipher.doFinal(ciphertext);
Map<String, String> plainMap = objectMapper.readValue(plainBytes, new TypeReference<>() {});
memoryCache.clear();
for (Map.Entry<String, String> entry : plainMap.entrySet()) {
char[] pin = entry.getValue().toCharArray();
memoryCache.put(entry.getKey(), pin);
PinSessionManager.getInstance().savePin(entry.getKey(), pin);
}
logger.info("Carregados {} PIN(s) do armazenamento seguro local.", memoryCache.size());
} catch (Exception e) {
logger.warn("Não foi possível carregar credenciais salvas em disco: {}", e.getMessage());
}
}
private byte[] getOrCreateSalt() throws Exception {
if (Files.exists(saltFile)) {
return Files.readAllBytes(saltFile);
}
byte[] salt = new byte[32];
new SecureRandom().nextBytes(salt);
Files.write(saltFile, salt);
return salt;
}
private SecretKey deriveKey(byte[] salt) throws Exception {
// Combina identificadores estáveis exclusivos da máquina e usuário
String hostIdentity = String.join(":",
System.getProperty("user.name", "unknown"),
System.getProperty("user.home", "unknown"),
System.getProperty("os.name", "unknown"),
System.getProperty("os.arch", "unknown"),
"cybercore-signer-hardware-pin-binding"
);
KeySpec spec = new PBEKeySpec(hostIdentity.toCharArray(), salt, PBKDF2_ITERATIONS, KEY_LENGTH_BITS);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
return new SecretKeySpec(keyBytes, "AES");
}
}
@@ -15,6 +15,7 @@ import org.bouncycastle.util.Store;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import javax.xml.crypto.dsig.XMLSignature; import javax.xml.crypto.dsig.XMLSignature;
@@ -221,32 +222,59 @@ public final class SignatureValidationService {
} }
/** /**
* Validação de assinaturas embutidas em documentos XML (XMLDSig). * Validação de assinaturas embutidas em documentos XML (XAdES ou XMLDSig).
*/ */
private ValidationResult validateXml(byte[] xmlBytes, String fileName) { private ValidationResult validateXml(byte[] xmlBytes, String fileName) {
try { try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true); dbf.setNamespaceAware(true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xmlBytes)); Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(xmlBytes));
// Registro de atributos Id no DOM
registerAllIdAttributes(doc.getDocumentElement());
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
if (nl.getLength() == 0) {
nl = doc.getElementsByTagName("Signature");
}
if (nl.getLength() == 0) { if (nl.getLength() == 0) {
return new ValidationResult( return new ValidationResult(
ValidationStatus.INVALID, ValidationStatus.INVALID,
ValidationType.XML_DSIG, ValidationType.XML_DSIG,
"Nenhum elemento <Signature> XMLDSig encontrado no documento.", "Nenhum elemento <Signature> XML encontrado no documento.",
List.of(), List.of(),
null, null,
fileName fileName
); );
} }
// Identificação de padrão XAdES
NodeList qpList = doc.getElementsByTagNameNS("http://uri.etsi.org/01903/v1.3.2#", "QualifyingProperties");
if (qpList.getLength() == 0) {
qpList = doc.getElementsByTagName("QualifyingProperties");
}
boolean isXades = qpList.getLength() > 0;
ValidationType valType = isXades ? ValidationType.XADES_XML : ValidationType.XML_DSIG;
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
List<ValidationSignerInfo> signers = new ArrayList<>(); List<ValidationSignerInfo> signers = new ArrayList<>();
boolean allValid = true; boolean allValid = true;
for (int i = 0; i < nl.getLength(); i++) { for (int i = 0; i < nl.getLength(); i++) {
DOMValidateContext valContext = new DOMValidateContext(new KeyValueKeySelector(), nl.item(i)); Element sigElement = (Element) nl.item(i);
DOMValidateContext valContext = new DOMValidateContext(new KeyValueKeySelector(), sigElement);
// Registra nós com atributo Id no valContext
NodeList allNodes = doc.getElementsByTagName("*");
for (int n = 0; n < allNodes.getLength(); n++) {
if (allNodes.item(n) instanceof Element e && e.hasAttribute("Id")) {
valContext.setIdAttributeNS(e, null, "Id");
}
}
XMLSignature signature = fac.unmarshalXMLSignature(valContext); XMLSignature signature = fac.unmarshalXMLSignature(valContext);
boolean coreValidity = signature.validate(valContext); boolean coreValidity = signature.validate(valContext);
@@ -254,22 +282,66 @@ public final class SignatureValidationService {
allValid = false; allValid = false;
} }
// Extração de metadados do signatário a partir do KeyInfo
String signerName = "Signatário XML #" + (i + 1);
String identifier = "";
String issuer = isXades ? "ICP-Brasil / XAdES" : "Assinatura Enveloped XML";
String certSubject = "XML Signature Element";
if (signature.getKeyInfo() != null) {
for (Object kiContent : signature.getKeyInfo().getContent()) {
if (kiContent instanceof javax.xml.crypto.dsig.keyinfo.X509Data x509Data) {
for (Object o : x509Data.getContent()) {
if (o instanceof X509Certificate cert) {
certSubject = cert.getSubjectX500Principal().getName();
signerName = X509CertificateUtils.extractCommonName(certSubject);
String cpf = X509CertificateUtils.extractCpf(cert, signerName);
if (cpf != null) {
identifier = "CPF: " + cpf;
} else {
String cnpj = X509CertificateUtils.extractCnpj(cert, signerName);
if (cnpj != null) {
identifier = "CNPJ: " + cnpj;
}
}
issuer = X509CertificateUtils.extractCommonName(cert.getIssuerX500Principal().getName());
break;
}
}
}
}
}
// Data de assinatura (SigningTime se XAdES, senão data atual)
String dateStr = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date());
if (isXades) {
NodeList stList = doc.getElementsByTagNameNS("http://uri.etsi.org/01903/v1.3.2#", "SigningTime");
if (stList.getLength() > 0) {
dateStr = stList.item(0).getTextContent();
}
}
signers.add(new ValidationSignerInfo( signers.add(new ValidationSignerInfo(
"Signatário XML #" + (i + 1), signerName,
"", identifier,
"Assinatura Enveloped XML", issuer,
new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()), dateStr,
coreValidity, coreValidity,
coreValidity, coreValidity,
signature.getSignedInfo().getSignatureMethod().getAlgorithm(), signature.getSignedInfo().getSignatureMethod().getAlgorithm(),
"XMLDSig Element", certSubject,
"Validade estrutural e criptográfica: " + (coreValidity ? "OK" : "FALHA") (isXades ? "Padrão XAdES-BES (ETSI TS 101 903 / ICP-Brasil)" : "Padrão XMLDSig W3C")
+ " • " + (coreValidity ? "Assinatura íntegra" : "Integridade violada")
)); ));
} }
ValidationStatus status = allValid ? ValidationStatus.VALID : ValidationStatus.INVALID; ValidationStatus status = allValid ? ValidationStatus.VALID : ValidationStatus.INVALID;
String msg = allValid ? "Documento XML assinado e válido conforme padrão W3C XML-DSig." : "Assinatura XML inválida ou com integridade violada."; String msg = allValid
return new ValidationResult(status, ValidationType.XML_DSIG, msg, signers, null, fileName); ? (isXades
? "Documento XML assinado e íntegro no padrão avançado XAdES-BES (ICP-Brasil)."
: "Documento XML assinado e válido conforme padrão W3C XMLDSig.")
: "Assinatura XML inválida ou com integridade violada.";
return new ValidationResult(status, valType, msg, signers, null, fileName);
} catch (Exception e) { } catch (Exception e) {
logger.error("Erro ao validar XML: {}", e.getMessage(), e); logger.error("Erro ao validar XML: {}", e.getMessage(), e);
@@ -284,6 +356,23 @@ public final class SignatureValidationService {
} }
} }
private static void registerAllIdAttributes(Element root) {
if (root == null) return;
if (root.hasAttribute("Id")) {
root.setIdAttribute("Id", true);
} else if (root.hasAttribute("id")) {
root.setIdAttribute("id", true);
} else if (root.hasAttribute("ID")) {
root.setIdAttribute("ID", true);
}
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
if (children.item(i) instanceof Element child) {
registerAllIdAttributes(child);
}
}
}
/** /**
* Processa e valida criptograficamente os signatários de um pacote CMSSignedData. * Processa e valida criptograficamente os signatários de um pacote CMSSignedData.
*/ */
@@ -7,6 +7,7 @@ public enum ValidationType {
DETACHED_CMS("Assinatura Destacada (CAdES / PKCS#7)", false), DETACHED_CMS("Assinatura Destacada (CAdES / PKCS#7)", false),
ATTACHED_CMS("Assinatura Incorporada (CMS / P7M)", true), ATTACHED_CMS("Assinatura Incorporada (CMS / P7M)", true),
PADES_PDF("Assinatura Incorporada em PDF (PAdES)", true), PADES_PDF("Assinatura Incorporada em PDF (PAdES)", true),
XADES_XML("Assinatura Incorporada em XML (XAdES / ICP-Brasil)", true),
XML_DSIG("Assinatura Incorporada em XML (XML-DSig)", true), XML_DSIG("Assinatura Incorporada em XML (XML-DSig)", true),
UNKNOWN("Formato Desconhecido", false); UNKNOWN("Formato Desconhecido", false);
@@ -3,30 +3,43 @@ package br.com.cybercore.signer.core.xml;
import java.util.Objects; import java.util.Objects;
/** /**
* Parâmetros de configuração para a operação de assinatura digital XMLDSig. * Parâmetros de configuração para a operação de assinatura digital XML (XAdES ou XMLDSig).
*/ */
public record XmlSignatureOptions( public record XmlSignatureOptions(
String certificateId, String certificateId,
char[] pinOrPassword, char[] pinOrPassword,
String targetElementTag, String targetElementTag,
String targetElementId, String targetElementId,
String signatureAlgorithm String signatureAlgorithm,
XmlSignatureProfile profile
) { ) {
public static final String DEFAULT_ALGORITHM = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; public static final String DEFAULT_ALGORITHM = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
public static final XmlSignatureProfile DEFAULT_PROFILE = XmlSignatureProfile.XADES_BES;
public XmlSignatureOptions { public XmlSignatureOptions {
Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo."); Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo.");
if (signatureAlgorithm == null || signatureAlgorithm.isBlank()) { if (signatureAlgorithm == null || signatureAlgorithm.isBlank()) {
signatureAlgorithm = DEFAULT_ALGORITHM; signatureAlgorithm = DEFAULT_ALGORITHM;
} }
if (profile == null) {
profile = DEFAULT_PROFILE;
}
} }
public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword) { public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword) {
return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM); return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM, DEFAULT_PROFILE);
}
public static XmlSignatureOptions wholeDocument(String certificateId, char[] pinOrPassword, XmlSignatureProfile profile) {
return new XmlSignatureOptions(certificateId, pinOrPassword, null, null, DEFAULT_ALGORITHM, profile);
} }
public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id) { public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id) {
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM); return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM, DEFAULT_PROFILE);
}
public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id, XmlSignatureProfile profile) {
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM, profile);
} }
} }
@@ -0,0 +1,37 @@
package br.com.cybercore.signer.core.xml;
/**
* Perfis de assinatura digital para documentos XML suportados pelo CyberCore Signer.
*/
public enum XmlSignatureProfile {
/**
* Padrão XAdES-BES (XML Advanced Electronic Signatures - Basic Electronic Signature).
* Em conformidade com a especificação ETSI TS 101 903 e padrão ICP-Brasil DOC-ICP-15.03 (AD-RB).
* Inclui propriedades qualificadas assinadas: data/hora de assinatura (SigningTime)
* e referência criptográfica do certificado do signatário (SigningCertificate).
*/
XADES_BES("XAdES-BES (Padrão ICP-Brasil / ETSI)", "Assinatura avançada com data/hora e certificado qualificado vinculados"),
/**
* Padrão W3C XMLDSig convencional (Enveloped Signature sem propriedades qualificadas XAdES).
* Mantido para compatibilidade com layouts fiscais estritos (ex: NF-e, CT-e, MDF-e SEFAZ legados).
*/
XMLDSIG("XMLDSig Clássico (W3C)", "Assinatura enveloped básica sem propriedades qualificadas");
private final String displayName;
private final String description;
XmlSignatureProfile(String displayName, String description) {
this.displayName = displayName;
this.description = description;
}
public String getDisplayName() {
return displayName;
}
public String getDescription() {
return description;
}
}
@@ -17,12 +17,14 @@ import javax.xml.crypto.KeySelector;
import javax.xml.crypto.KeySelectorException; import javax.xml.crypto.KeySelectorException;
import javax.xml.crypto.KeySelectorResult; import javax.xml.crypto.KeySelectorResult;
import javax.xml.crypto.XMLCryptoContext; import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dom.DOMStructure;
import javax.xml.crypto.dsig.CanonicalizationMethod; import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod; import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference; import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo; import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform; import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignature; import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory; import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext; import javax.xml.crypto.dsig.dom.DOMSignContext;
@@ -43,25 +45,41 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.Key; import java.security.MessageDigest;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
/** /**
* Serviço de alto nível para Assinatura Digital de Documentos XML segundo o padrão W3C XMLDSig e ICP-Brasil. * Serviço de alto nível para Assinatura Digital de Documentos XML segundo os padrões:
* Suporta assinatura enveloped de documento completo e assinatura de elementos por ID (NF-e, SPED, etc.). * - XAdES-BES (ETSI TS 101 903 / ICP-Brasil DOC-ICP-15.03 - Padrão Avançado)
* - W3C XMLDSig convencional (Enveloped / NF-e / SPED legado)
*
* Utiliza o suporte de fábrica nativo e seguro da plataforma Java (javax.xml.crypto.dsig). * Utiliza o suporte de fábrica nativo e seguro da plataforma Java (javax.xml.crypto.dsig).
*/ */
public class XmlSigningService { public class XmlSigningService {
private static final Logger logger = LoggerFactory.getLogger(XmlSigningService.class); private static final Logger logger = LoggerFactory.getLogger(XmlSigningService.class);
public static final String XADES_132_NS = "http://uri.etsi.org/01903/v1.3.2#";
public static final String XADES_SIGNED_PROPERTIES_TYPE = "http://uri.etsi.org/01903#SignedProperties";
public static final String XMLDSIG_NS = XMLSignature.XMLNS;
private static final DateTimeFormatter ISO_INSTANT_FORMATTER =
DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC);
private final CertificateManager certificateManager; private final CertificateManager certificateManager;
public XmlSigningService(CertificateManager certificateManager) { public XmlSigningService(CertificateManager certificateManager) {
@@ -85,7 +103,7 @@ public class XmlSigningService {
try (FileOutputStream fos = new FileOutputStream(targetXml)) { try (FileOutputStream fos = new FileOutputStream(targetXml)) {
fos.write(signedBytes); fos.write(signedBytes);
} }
logger.info("XML assinado com sucesso e salvo em: {}", targetXml.getAbsolutePath()); logger.info("XML assinado com sucesso ({}) e salvo em: {}", options.profile().getDisplayName(), targetXml.getAbsolutePath());
} }
/** /**
@@ -117,7 +135,8 @@ public class XmlSigningService {
} }
SigningKeyMaterial keyMaterial = keyMaterialOpt.get(); SigningKeyMaterial keyMaterial = keyMaterialOpt.get();
logger.info("Iniciando assinatura XMLDSig com certificado '{}' [tipo={}]", cert.commonName(), cert.type()); logger.info("Iniciando assinatura XML [perfil={}] com certificado '{}' [tipo={}]",
options.profile(), cert.commonName(), cert.type());
// 2. Parse Seguro do XML (Prevenção rigorosa de XXE) // 2. Parse Seguro do XML (Prevenção rigorosa de XXE)
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
@@ -134,13 +153,13 @@ public class XmlSigningService {
// 3. Configuração do XMLSignatureFactory // 3. Configuração do XMLSignatureFactory
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
// Transformações padrão: Enveloped + C14N // Transformações padrão para os dados de negócio: Enveloped + C14N
List<Transform> transforms = new ArrayList<>(); List<Transform> dataTransforms = new ArrayList<>();
transforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null)); dataTransforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null));
transforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null)); dataTransforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null));
// Determinação da Referência e Ponto de Inserção // Determinação da Referência de Dados e Ponto de Inserção da Assinatura
Reference ref; Reference dataRef;
Node insertionParent; Node insertionParent;
if (options.targetElementId() != null && !options.targetElementId().isBlank()) { if (options.targetElementId() != null && !options.targetElementId().isBlank()) {
@@ -150,7 +169,7 @@ public class XmlSigningService {
throw new IllegalArgumentException("Elemento XML com ID '" + elemId + "' não encontrado no documento."); throw new IllegalArgumentException("Elemento XML com ID '" + elemId + "' não encontrado no documento.");
} }
targetElement.setIdAttribute("Id", true); targetElement.setIdAttribute("Id", true);
ref = fac.newReference("#" + elemId, fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("#" + elemId, fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = (targetElement.getParentNode() != null) ? targetElement.getParentNode() : doc.getDocumentElement(); insertionParent = (targetElement.getParentNode() != null) ? targetElement.getParentNode() : doc.getDocumentElement();
} else if (options.targetElementTag() != null && !options.targetElementTag().isBlank()) { } else if (options.targetElementTag() != null && !options.targetElementTag().isBlank()) {
NodeList list = doc.getElementsByTagName(options.targetElementTag()); NodeList list = doc.getElementsByTagName(options.targetElementTag());
@@ -160,42 +179,87 @@ public class XmlSigningService {
if (list.getLength() > 0 && list.item(0) instanceof Element elem && elem.hasAttribute("Id")) { if (list.getLength() > 0 && list.item(0) instanceof Element elem && elem.hasAttribute("Id")) {
String idVal = elem.getAttribute("Id"); String idVal = elem.getAttribute("Id");
elem.setIdAttribute("Id", true); elem.setIdAttribute("Id", true);
ref = fac.newReference("#" + idVal, fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("#" + idVal, fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = (elem.getParentNode() != null) ? elem.getParentNode() : doc.getDocumentElement(); insertionParent = (elem.getParentNode() != null) ? elem.getParentNode() : doc.getDocumentElement();
} else { } else {
ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = doc.getDocumentElement(); insertionParent = doc.getDocumentElement();
} }
} else { } else {
// Assinatura Enveloped do documento completo // Assinatura Enveloped do documento completo
ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null); dataRef = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null), dataTransforms, null, null);
insertionParent = doc.getDocumentElement(); insertionParent = doc.getDocumentElement();
} }
// 4. Montagem do SignedInfo List<Reference> references = new ArrayList<>();
references.add(dataRef);
List<XMLObject> xmlObjects = new ArrayList<>();
String signatureUniqueId = "sig-" + UUID.randomUUID();
Element signedPropertiesElem = null;
// 4. Se for perfil XAdES-BES, constrói as QualifyingProperties e a Reference correspondente
if (options.profile() == XmlSignatureProfile.XADES_BES) {
String signedPropsId = "signed-props-" + UUID.randomUUID();
signedPropertiesElem = buildXadesSignedProperties(doc, signedPropsId, keyMaterial.signingCertificate(), Instant.now());
Element qualifyingProperties = doc.createElementNS(XADES_132_NS, "xades:QualifyingProperties");
qualifyingProperties.setAttribute("Target", "#" + signatureUniqueId);
qualifyingProperties.appendChild(signedPropertiesElem);
XMLStructure xadesStructure = new DOMStructure(qualifyingProperties);
XMLObject xadesObject = fac.newXMLObject(Collections.singletonList(xadesStructure), null, null, null);
xmlObjects.add(xadesObject);
// Adiciona Referência Criptográfica cobrindo as SignedProperties
List<Transform> xadesTransforms = Collections.singletonList(
fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null)
);
Reference xadesRef = fac.newReference(
"#" + signedPropsId,
fac.newDigestMethod(DigestMethod.SHA256, null),
xadesTransforms,
XADES_SIGNED_PROPERTIES_TYPE,
null
);
references.add(xadesRef);
}
// 5. Montagem do SignedInfo
SignedInfo si = fac.newSignedInfo( SignedInfo si = fac.newSignedInfo(
fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null), fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) null),
fac.newSignatureMethod(options.signatureAlgorithm(), null), fac.newSignatureMethod(options.signatureAlgorithm(), null),
Collections.singletonList(ref) references
); );
// 5. Inclusão da Cadeia de Certificados no KeyInfo // 6. Inclusão da Cadeia de Certificados no KeyInfo
KeyInfoFactory kif = fac.getKeyInfoFactory(); KeyInfoFactory kif = fac.getKeyInfoFactory();
List<Object> x509Content = new ArrayList<>(); List<Object> x509Content = new ArrayList<>();
x509Content.add(keyMaterial.signingCertificate()); x509Content.add(keyMaterial.signingCertificate());
X509Data x509Data = kif.newX509Data(x509Content); X509Data x509Data = kif.newX509Data(x509Content);
KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509Data)); KeyInfo ki = kif.newKeyInfo(Collections.singletonList(x509Data));
// 6. Execução da Assinatura DOM // 7. Execução da Assinatura DOM
DOMSignContext dsc = new DOMSignContext(keyMaterial.privateKey(), insertionParent); DOMSignContext dsc = new DOMSignContext(keyMaterial.privateKey(), insertionParent);
if (keyMaterial.jcaProvider() != null) { if (keyMaterial.jcaProvider() != null) {
dsc.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", keyMaterial.jcaProvider()); dsc.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", keyMaterial.jcaProvider());
} }
XMLSignature signature = fac.newXMLSignature(si, ki); if (signedPropertiesElem != null) {
dsc.setIdAttributeNS(signedPropertiesElem, null, "Id");
}
XMLSignature signature = fac.newXMLSignature(
si,
ki,
xmlObjects,
signatureUniqueId,
null
);
signature.sign(dsc); signature.sign(dsc);
// 7. Serialização do XML Assinado // 8. Serialização do XML Assinado
TransformerFactory tf = TransformerFactory.newInstance(); TransformerFactory tf = TransformerFactory.newInstance();
Transformer trans = tf.newTransformer(); Transformer trans = tf.newTransformer();
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
@@ -205,12 +269,13 @@ public class XmlSigningService {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
trans.transform(new DOMSource(doc), new StreamResult(baos)); trans.transform(new DOMSource(doc), new StreamResult(baos));
logger.info("Assinatura XMLDSig gerada com sucesso. Tamanho: {} bytes", baos.size()); logger.info("Assinatura XML ({}) gerada com sucesso. Tamanho final: {} bytes",
options.profile(), baos.size());
return baos.toByteArray(); return baos.toByteArray();
} }
/** /**
* Valida a integridade criptográfica de um documento XML assinado com XMLDSig. * Valida a integridade criptográfica de um documento XML assinado com XAdES ou XMLDSig.
*/ */
public boolean validateXmlSignature(byte[] signedXmlBytes) throws Exception { public boolean validateXmlSignature(byte[] signedXmlBytes) throws Exception {
Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos."); Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos.");
@@ -218,24 +283,17 @@ public class XmlSigningService {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true); dbf.setNamespaceAware(true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes)); Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes));
// Registra nós com atributo Id/id/ID para que o ResourceResolver do XMLDSig localize a referência // Registra nós com atributo Id/id/ID para que o ResourceResolver localize as referências
NodeList allElements = doc.getElementsByTagName("*"); registerIdAttributes(doc.getDocumentElement());
for (int i = 0; i < allElements.getLength(); i++) {
if (allElements.item(i) instanceof Element e) {
if (e.hasAttribute("Id")) {
e.setIdAttribute("Id", true);
} else if (e.hasAttribute("id")) {
e.setIdAttribute("id", true);
} else if (e.hasAttribute("ID")) {
e.setIdAttribute("ID", true);
}
}
}
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); NodeList nl = doc.getElementsByTagNameNS(XMLDSIG_NS, "Signature");
if (nl.getLength() == 0) { if (nl.getLength() == 0) {
nl = doc.getElementsByTagName("Signature"); nl = doc.getElementsByTagName("Signature");
} }
@@ -248,8 +306,168 @@ public class XmlSigningService {
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
DOMValidateContext valContext = new DOMValidateContext(new X509KeySelector(), sigElement); DOMValidateContext valContext = new DOMValidateContext(new X509KeySelector(), sigElement);
// Registra explicitamente elementos com atributo Id no valContext
NodeList allNodes = doc.getElementsByTagName("*");
for (int i = 0; i < allNodes.getLength(); i++) {
if (allNodes.item(i) instanceof Element e) {
if (e.hasAttribute("Id")) {
valContext.setIdAttributeNS(e, null, "Id");
}
}
}
XMLSignature signature = fac.unmarshalXMLSignature(valContext); XMLSignature signature = fac.unmarshalXMLSignature(valContext);
return signature.validate(valContext); boolean coreValidity = signature.validate(valContext);
if (!coreValidity) {
boolean sv = signature.getSignatureValue().validate(valContext);
logger.warn("Validação XML falhou. SignatureValue válido: {}", sv);
int refIdx = 0;
for (Object o : signature.getSignedInfo().getReferences()) {
if (o instanceof Reference r) {
boolean rValid = r.validate(valContext);
byte[] calcDigest = r.getCalculatedDigestValue();
byte[] storedDigest = r.getDigestValue();
String calcB64 = calcDigest != null ? Base64.getEncoder().encodeToString(calcDigest) : "null";
String storedB64 = storedDigest != null ? Base64.getEncoder().encodeToString(storedDigest) : "null";
logger.warn("Reference[{}] URI='{}' válida: {} | calcDigest={} | storedDigest={}",
refIdx++, r.getURI(), rValid, calcB64, storedB64);
}
}
}
return coreValidity;
}
/**
* Inspeciona e extrai informações detalhadas da assinatura XML (XAdES ou XMLDSig).
*/
public XmlSignatureDetails inspectXmlSignature(byte[] signedXmlBytes) throws Exception {
Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos.");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Document doc = dbf.newDocumentBuilder().parse(new ByteArrayInputStream(signedXmlBytes));
registerIdAttributes(doc.getDocumentElement());
NodeList nl = doc.getElementsByTagNameNS(XMLDSIG_NS, "Signature");
if (nl.getLength() == 0) {
nl = doc.getElementsByTagName("Signature");
}
if (nl.getLength() == 0) {
return null;
}
Element sigElem = (Element) nl.item(0);
String sigId = sigElem.getAttribute("Id");
// Verifica presença de XAdES
NodeList qpNodes = doc.getElementsByTagNameNS(XADES_132_NS, "QualifyingProperties");
boolean isXades = qpNodes.getLength() > 0;
String signingTime = null;
if (isXades) {
NodeList timeNodes = doc.getElementsByTagNameNS(XADES_132_NS, "SigningTime");
if (timeNodes.getLength() > 0) {
signingTime = timeNodes.item(0).getTextContent();
}
}
// Validação criptográfica
boolean isValid = false;
try {
isValid = validateXmlSignature(signedXmlBytes);
} catch (Exception ex) {
logger.warn("Validação da assinatura XML falhou durante inspeção: {}", ex.getMessage());
}
String sigMethod = "";
NodeList smNodes = sigElem.getElementsByTagNameNS(XMLDSIG_NS, "SignatureMethod");
if (smNodes.getLength() > 0 && smNodes.item(0) instanceof Element smElem) {
sigMethod = smElem.getAttribute("Algorithm");
}
return new XmlSignatureDetails(
isValid,
isXades ? XmlSignatureProfile.XADES_BES : XmlSignatureProfile.XMLDSIG,
sigMethod,
signingTime,
sigId
);
}
/**
* Constrói a estrutura DOM das propriedades assinadas do XAdES (SignedProperties).
*/
private Element buildXadesSignedProperties(Document doc, String signedPropsId, X509Certificate cert, Instant signInstant) throws Exception {
Element signedProperties = doc.createElementNS(XADES_132_NS, "xades:SignedProperties");
signedProperties.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xades", XADES_132_NS);
signedProperties.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:ds", XMLDSIG_NS);
signedProperties.setAttribute("Id", signedPropsId);
signedProperties.setIdAttribute("Id", true);
Element signedSignatureProperties = doc.createElementNS(XADES_132_NS, "xades:SignedSignatureProperties");
// 1. Data e Hora UTC da Assinatura
Element signingTime = doc.createElementNS(XADES_132_NS, "xades:SigningTime");
signingTime.setTextContent(ISO_INSTANT_FORMATTER.format(signInstant));
signedSignatureProperties.appendChild(signingTime);
// 2. Vínculo Criptográfico do Certificado do Signatário (SigningCertificate)
Element signingCertificate = doc.createElementNS(XADES_132_NS, "xades:SigningCertificate");
Element certElem = doc.createElementNS(XADES_132_NS, "xades:Cert");
// Digest SHA-256 do Certificado DER
Element certDigest = doc.createElementNS(XADES_132_NS, "xades:CertDigest");
Element digestMethod = doc.createElementNS(XMLDSIG_NS, "ds:DigestMethod");
digestMethod.setAttribute("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256");
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] certHash = md.digest(cert.getEncoded());
String certHashB64 = Base64.getEncoder().encodeToString(certHash);
Element digestValue = doc.createElementNS(XMLDSIG_NS, "ds:DigestValue");
digestValue.setTextContent(certHashB64);
certDigest.appendChild(digestMethod);
certDigest.appendChild(digestValue);
certElem.appendChild(certDigest);
// Dados do Emissor e Serial do Certificado
Element issuerSerial = doc.createElementNS(XADES_132_NS, "xades:IssuerSerial");
Element x509IssuerName = doc.createElementNS(XMLDSIG_NS, "ds:X509IssuerName");
x509IssuerName.setTextContent(cert.getIssuerX500Principal().getName());
Element x509SerialNumber = doc.createElementNS(XMLDSIG_NS, "ds:X509SerialNumber");
x509SerialNumber.setTextContent(cert.getSerialNumber().toString());
issuerSerial.appendChild(x509IssuerName);
issuerSerial.appendChild(x509SerialNumber);
certElem.appendChild(issuerSerial);
signingCertificate.appendChild(certElem);
signedSignatureProperties.appendChild(signingCertificate);
signedProperties.appendChild(signedSignatureProperties);
return signedProperties;
}
private static void registerIdAttributes(Element root) {
if (root == null) return;
if (root.hasAttribute("Id")) {
root.setIdAttribute("Id", true);
} else if (root.hasAttribute("id")) {
root.setIdAttribute("id", true);
} else if (root.hasAttribute("ID")) {
root.setIdAttribute("ID", true);
}
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
if (children.item(i) instanceof Element child) {
registerIdAttributes(child);
}
}
} }
private static Element findElementById(Document doc, String id, String hintTag) { private static Element findElementById(Document doc, String id, String hintTag) {
@@ -264,6 +482,17 @@ public class XmlSigningService {
return doc.getElementById(id); return doc.getElementById(id);
} }
/**
* DTO de detalhes de uma assinatura XML.
*/
public record XmlSignatureDetails(
boolean valid,
XmlSignatureProfile profile,
String algorithm,
String signingTime,
String signatureId
) {}
/** /**
* Seletor de chave pública baseado no certificado X.509 embutido no KeyInfo. * Seletor de chave pública baseado no certificado X.509 embutido no KeyInfo.
*/ */
@@ -18,6 +18,8 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import br.com.cybercore.signer.core.validation.SignatureValidationService;
import br.com.cybercore.signer.core.validation.ValidationResult;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -41,6 +43,7 @@ class PdfSigningServiceTest {
private CertificateManager certificateManager; private CertificateManager certificateManager;
private PdfSigningService pdfSigningService; private PdfSigningService pdfSigningService;
private String testCertId; private String testCertId;
private String testCert2Id;
@BeforeEach @BeforeEach
void setUp() throws Exception { void setUp() throws Exception {
@@ -74,13 +77,29 @@ class PdfSigningServiceTest {
char[] pwd = "password123".toCharArray(); char[] pwd = "password123".toCharArray();
ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert}); ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert});
// Gera segundo par de chaves e certificado para co-assinatura
KeyPair kp2 = kpg.generateKeyPair();
X500Name subject2 = new X500Name("CN=Maria Souza:98765432100, O=CyberCore Testes, C=BR");
X509v3CertificateBuilder certBuilder2 = new JcaX509v3CertificateBuilder(
subject2,
BigInteger.valueOf(System.currentTimeMillis() + 100),
notBefore,
notAfter,
subject2,
kp2.getPublic()
);
ContentSigner signer2 = new JcaContentSignerBuilder("SHA256withRSA").build(kp2.getPrivate());
X509Certificate cert2 = new JcaX509CertificateConverter().getCertificate(certBuilder2.build(signer2));
ks.setKeyEntry("test-cert-2", kp2.getPrivate(), pwd, new Certificate[]{cert2});
Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste"); Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste");
provider.setKeyStore(ks, pwd); provider.setKeyStore(ks, pwd);
certificateManager.registerProvider(provider); certificateManager.registerProvider(provider);
List<ManagedCertificate> list = certificateManager.discoverCertificates(); List<ManagedCertificate> list = certificateManager.discoverCertificates();
assertThat(list).isNotEmpty(); assertThat(list).hasSize(2);
this.testCertId = list.get(0).id(); this.testCertId = list.stream().filter(c -> c.alias().equals("test-cert")).findFirst().orElseThrow().id();
this.testCert2Id = list.stream().filter(c -> c.alias().equals("test-cert-2")).findFirst().orElseThrow().id();
this.pdfSigningService = new PdfSigningService(certificateManager); this.pdfSigningService = new PdfSigningService(certificateManager);
} }
@@ -215,4 +234,59 @@ class PdfSigningServiceTest {
byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f); byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f);
assertThat(pngBytes).isNotEmpty(); assertThat(pngBytes).isNotEmpty();
} }
@Test
@DisplayName("Deve manter ambas as assinaturas válidas ao assinar um PDF que já possui assinatura digital (Co-assinatura PAdES)")
void shouldKeepBothSignaturesValidWhenSigningAlreadySignedPdf() throws Exception {
byte[] originalPdf = createSamplePdf();
// 1ª Assinatura com o primeiro certificado
PdfSignatureOptions firstOptions = PdfSignatureOptions.builder(testCertId)
.visibleSeal(true)
.position(PdfSealPosition.BOTTOM_RIGHT)
.reason("Primeira Aprovação")
.location("Brasília, DF")
.build();
byte[] firstSignedPdf = pdfSigningService.signPdf(originalPdf, firstOptions);
assertThat(firstSignedPdf).isNotNull();
// 2ª Assinatura sobre o documento já assinado com o segundo certificado
PdfSignatureOptions secondOptions = PdfSignatureOptions.builder(testCert2Id)
.visibleSeal(true)
.position(PdfSealPosition.TOP_LEFT)
.reason("Segunda Aprovação Conjunta")
.location("São Paulo, SP")
.build();
byte[] doubleSignedPdf = pdfSigningService.signPdf(firstSignedPdf, secondOptions);
assertThat(doubleSignedPdf).isNotNull();
assertThat(doubleSignedPdf.length).isGreaterThan(firstSignedPdf.length);
// Validação estrutural das assinaturas no PDDocument
try (PDDocument doc = Loader.loadPDF(doubleSignedPdf)) {
List<PDSignature> signatures = doc.getSignatureDictionaries();
assertThat(signatures).hasSize(2);
assertThat(signatures.get(0).getName()).contains("Joao da Silva");
assertThat(signatures.get(0).getReason()).isEqualTo("Primeira Aprovação");
assertThat(signatures.get(1).getName()).contains("Maria Souza");
assertThat(signatures.get(1).getReason()).isEqualTo("Segunda Aprovação Conjunta");
}
// Validação criptográfica rigorosa via SignatureValidationService (ambas as assinaturas devem ser íntegras)
SignatureValidationService validationService = new SignatureValidationService();
ValidationResult validationResult = validationService.validateAttached(doubleSignedPdf, "duplo_assinado.pdf");
assertThat(validationResult.isValid()).isTrue();
assertThat(validationResult.signers()).hasSize(2);
// Ambas as assinaturas e digests devem ser 100% válidos
assertThat(validationResult.signers().get(0).signatureValid()).isTrue();
assertThat(validationResult.signers().get(0).digestValid()).isTrue();
assertThat(validationResult.signers().get(0).signerName()).contains("Joao da Silva");
assertThat(validationResult.signers().get(1).signatureValid()).isTrue();
assertThat(validationResult.signers().get(1).digestValid()).isTrue();
assertThat(validationResult.signers().get(1).signerName()).contains("Maria Souza");
}
} }
@@ -0,0 +1,77 @@
package br.com.cybercore.signer.core.security;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class SecurePinStorageTest {
@TempDir
Path tempDir;
private SecurePinStorage storage;
@BeforeEach
void setUp() {
storage = new SecurePinStorage(tempDir);
}
@AfterEach
void tearDown() {
storage.clearAll();
}
@Test
void shouldSaveAndRetrievePinCorrectly() {
String certId = "cert-test-123";
char[] pin = "123456".toCharArray();
storage.savePin(certId, pin);
assertThat(storage.hasPin(certId)).isTrue();
assertThat(storage.getPin(certId)).isEqualTo(pin);
}
@Test
void shouldPersistAndReloadFromDisk() {
String certId = "token-a3-999";
char[] pin = "senhaForte987".toCharArray();
storage.savePin(certId, pin);
// Cria nova instância apontando para o mesmo diretório
SecurePinStorage reloaded = new SecurePinStorage(tempDir);
assertThat(reloaded.hasPin(certId)).isTrue();
assertThat(reloaded.getPin(certId)).isEqualTo(pin);
}
@Test
void shouldRemoveSpecificPin() {
storage.savePin("cert-1", "1111".toCharArray());
storage.savePin("cert-2", "2222".toCharArray());
storage.removePin("cert-1");
assertThat(storage.hasPin("cert-1")).isFalse();
assertThat(storage.getPin("cert-1")).isNull();
assertThat(storage.hasPin("cert-2")).isTrue();
}
@Test
void shouldClearAllPins() {
storage.savePin("cert-1", "1111".toCharArray());
storage.savePin("cert-2", "2222".toCharArray());
storage.clearAll();
assertThat(storage.hasPin("cert-1")).isFalse();
assertThat(storage.hasPin("cert-2")).isFalse();
assertThat(storage.getStoredCount()).isEqualTo(0);
}
}
@@ -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();
}
} }
@@ -104,7 +104,7 @@ public class CyberCoreDesktopApplication extends Application {
mainAppView = new MainAppView(primaryStage); mainAppView = new MainAppView(primaryStage);
// Ações da Grade de Assinatura e Validação // Ações da Grade de Assinatura e Validação
mainAppView.setOnSignPdf(() -> new PdfSignDialog(primaryStage, certificateManager).show()); mainAppView.setOnSignPdf(() -> handleStartPdfSigning(primaryStage));
mainAppView.setOnSignXml(() -> new XmlSignDialog(primaryStage, certificateManager).show()); mainAppView.setOnSignXml(() -> new XmlSignDialog(primaryStage, certificateManager).show());
mainAppView.setOnSignFile(() -> new FileSignDialog(primaryStage, "Assinar Arquivo", "*.*", SvgIcons.fileIcon(), certificateManager, signingService).show()); mainAppView.setOnSignFile(() -> new FileSignDialog(primaryStage, "Assinar Arquivo", "*.*", SvgIcons.fileIcon(), certificateManager, signingService).show());
mainAppView.setOnBatchSign(() -> new BatchSigningDialog(primaryStage, certificateManager, signingService).show()); mainAppView.setOnBatchSign(() -> new BatchSigningDialog(primaryStage, certificateManager, signingService).show());
@@ -155,6 +155,19 @@ public class CyberCoreDesktopApplication extends Application {
} }
} }
private void handleStartPdfSigning(Stage ownerStage) {
FileChooser chooser = new FileChooser();
chooser.setTitle("Selecionar Documento PDF para Assinar");
chooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"),
new FileChooser.ExtensionFilter("Todos os Arquivos (*.*)", "*.*")
);
File selectedFile = chooser.showOpenDialog(ownerStage);
if (selectedFile != null && selectedFile.exists()) {
new PdfSignDialog(ownerStage, certificateManager, selectedFile).show();
}
}
private void openSettingsDialog(Stage owner) { private void openSettingsDialog(Stage owner) {
Stage dlg = new Stage(); Stage dlg = new Stage();
dlg.initModality(Modality.APPLICATION_MODAL); dlg.initModality(Modality.APPLICATION_MODAL);
@@ -178,8 +191,9 @@ public class CyberCoreDesktopApplication extends Application {
VBox.setVgrow(scrollPane, Priority.ALWAYS); VBox.setVgrow(scrollPane, Priority.ALWAYS);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary"); btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(95); btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> dlg.close()); btnClose.setOnAction(e -> dlg.close());
HBox footer = new HBox(btnClose); HBox footer = new HBox(btnClose);
@@ -206,8 +220,9 @@ public class CyberCoreDesktopApplication extends Application {
AboutView aboutView = new AboutView(); AboutView aboutView = new AboutView();
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-primary"); btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(95); btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> dlg.close()); btnClose.setOnAction(e -> dlg.close());
HBox footer = new HBox(btnClose); HBox footer = new HBox(btnClose);
@@ -729,4 +729,103 @@ public final class SvgIcons {
box.getChildren().add(cross); box.getChildren().add(cross);
return box; return box;
} }
/**
* Ícone vetorial de Seta para a Direita / Avançar.
*/
public static Node arrowRightIcon(double size) {
return arrowRightIcon(size, "#ffffff");
}
public static Node arrowRightIcon(double size, String colorHex) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath arrow = new SVGPath();
arrow.setContent("M 5 3 L 10.5 8 L 5 13");
arrow.setStroke(Color.web(colorHex));
arrow.setStrokeWidth(2.2);
arrow.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
arrow.setStrokeLineJoin(javafx.scene.shape.StrokeLineJoin.ROUND);
arrow.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
arrow.setScaleX(scale);
arrow.setScaleY(scale);
box.getChildren().add(arrow);
return box;
}
/**
* Ícones vetoriais de Zoom In (+), Zoom Out (-) e Ajustar à Tela.
*/
public static Node zoomInIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath path = new SVGPath();
path.setContent("M 6.5 2 A 4.5 4.5 0 1 0 9.7 9.7 L 13.5 13.5 M 4.5 6.5 L 8.5 6.5 M 6.5 4.5 L 6.5 8.5");
path.setStroke(Color.web("#0284c7"));
path.setStrokeWidth(1.8);
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
path.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
path.setScaleX(scale);
path.setScaleY(scale);
box.getChildren().add(path);
return box;
}
public static Node zoomOutIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath path = new SVGPath();
path.setContent("M 6.5 2 A 4.5 4.5 0 1 0 9.7 9.7 L 13.5 13.5 M 4.5 6.5 L 8.5 6.5");
path.setStroke(Color.web("#0284c7"));
path.setStrokeWidth(1.8);
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
path.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
path.setScaleX(scale);
path.setScaleY(scale);
box.getChildren().add(path);
return box;
}
public static Node zoomFitIcon(double size) {
StackPane box = new StackPane();
box.setPrefSize(size, size);
box.setMaxSize(size, size);
box.setMinSize(size, size);
box.setAlignment(Pos.CENTER);
SVGPath path = new SVGPath();
path.setContent("M 2 5 L 2 2 L 5 2 M 11 2 L 14 2 L 14 5 M 14 11 L 14 14 L 11 14 M 5 14 L 2 14 L 2 11");
path.setStroke(Color.web("#0284c7"));
path.setStrokeWidth(1.8);
path.setStrokeLineCap(javafx.scene.shape.StrokeLineCap.ROUND);
path.setFill(Color.TRANSPARENT);
double scale = size / 16.0;
path.setScaleX(scale);
path.setScaleY(scale);
box.getChildren().add(path);
return box;
}
} }
@@ -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 =
@@ -129,8 +129,9 @@ public class CertificateDetailsDialog {
actionsRow.setAlignment(Pos.CENTER_RIGHT); actionsRow.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary"); btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(95); btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close()); btnClose.setOnAction(e -> stage.close());
actionsRow.getChildren().add(btnClose); actionsRow.getChildren().add(btnClose);
@@ -63,12 +63,15 @@ public final class LogsViewerDialog {
footer.setAlignment(Pos.CENTER_RIGHT); footer.setAlignment(Pos.CENTER_RIGHT);
Button btnRefresh = new Button("Atualizar"); Button btnRefresh = new Button("Atualizar");
btnRefresh.getStyleClass().add("btn-secondary"); btnRefresh.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnRefresh.setPrefWidth(110);
btnRefresh.setMinWidth(110);
btnRefresh.setOnAction(e -> loadInitialLogs()); btnRefresh.setOnAction(e -> loadInitialLogs());
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-primary"); btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(95); btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close()); btnClose.setOnAction(e -> stage.close());
footer.getChildren().addAll(btnRefresh, btnClose); footer.getChildren().addAll(btnRefresh, btnClose);
@@ -0,0 +1,590 @@
package br.com.cybercore.signer.ui.view;
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
import br.com.cybercore.signer.core.domain.CertificateType;
import br.com.cybercore.signer.core.domain.ManagedCertificate;
import br.com.cybercore.signer.core.pdf.PdfSignatureOptions;
import br.com.cybercore.signer.core.pdf.PdfSigningService;
import br.com.cybercore.signer.core.pdf.seal.CpfDisplayMode;
import br.com.cybercore.signer.core.pdf.seal.PdfSealTheme;
import br.com.cybercore.signer.core.provider.CertificateManager;
import br.com.cybercore.signer.core.security.PinSessionManager;
import br.com.cybercore.signer.core.security.SecurePinStorage;
import br.com.cybercore.signer.ui.components.SvgIcons;
import br.com.cybercore.signer.ui.theme.ThemeManager;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Desktop;
import java.io.File;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Diálogo dedicado e compacto para Seleção de Certificado Digital ICP-Brasil, inserção de PIN/senha,
* definição do arquivo de destino e conclusão da Assinatura Digital PAdES.
*/
@SuppressWarnings("this-escape")
public final class PdfCertificateSignDialog {
private static final Logger logger = LoggerFactory.getLogger(PdfCertificateSignDialog.class);
public record SealConfig(
boolean visibleSeal,
PdfSealTheme sealTheme,
CpfDisplayMode cpfDisplayMode,
boolean includeDateTime,
String customExtraInfo,
PdfSignDialog.PositionOption positionOption,
PdfSignDialog.PageOption pageOption,
boolean isCustomPosition,
Float customX,
Float customY,
Integer customPageNumber,
String reason,
String location,
Double customSealScale
) {
public SealConfig(
boolean visibleSeal,
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 CertificateManager certManager;
private final PdfSigningService pdfSigningService;
private final File sourcePdfFile;
private final SealConfig sealConfig;
private final String sealSummaryText;
private ComboBox<ManagedCertificate> certCombo;
private Label lblPinTitle;
private PasswordField pinField;
private Button btnClearRememberedPin;
private Label lblPinNotice;
private CheckBox chkRememberPin;
private ProgressIndicator progressIndicator;
private Label lblStatus;
private Button btnCancel;
private Button btnSign;
public PdfCertificateSignDialog(Stage owner,
CertificateManager certManager,
PdfSigningService pdfSigningService,
File sourcePdfFile,
SealConfig sealConfig,
String sealSummaryText) {
this.certManager = Objects.requireNonNull(certManager, "CertificateManager não pode ser nulo.");
this.pdfSigningService = Objects.requireNonNull(pdfSigningService, "PdfSigningService não pode ser nulo.");
this.sourcePdfFile = Objects.requireNonNull(sourcePdfFile, "sourcePdfFile não pode ser nulo.");
this.sealConfig = Objects.requireNonNull(sealConfig, "sealConfig não pode ser nulo.");
this.sealSummaryText = sealSummaryText != null ? sealSummaryText : "Selo Visual PAdES";
this.stage = new Stage();
this.stage.initModality(Modality.APPLICATION_MODAL);
if (owner != null) {
this.stage.initOwner(owner);
}
this.stage.setResizable(true);
this.stage.setMinWidth(700);
this.stage.setMinHeight(490);
this.stage.setTitle("Seleção de Certificado Digital — CyberCore Signer");
initLayout();
}
private HBox windowsStoreBox;
private HBox pinRow;
private void initLayout() {
VBox root = new VBox(14);
root.setPadding(new Insets(18, 20, 18, 20));
root.getStyleClass().add("dialog-container");
// Topo / Cabeçalho
HBox header = new HBox(12);
header.setAlignment(Pos.CENTER_LEFT);
VBox titleBox = new VBox(2);
Label lblTitle = new Label("SELEÇÃO DE CERTIFICADO DIGITAL");
lblTitle.getStyleClass().add("dialog-header");
Label lblSub = new Label("Escolha o certificado ICP-Brasil e informe o PIN para concluir a assinatura digital.");
lblSub.getStyleClass().add("dialog-subtitle");
titleBox.getChildren().addAll(lblTitle, lblSub);
header.getChildren().addAll(SvgIcons.certificatesIcon(), titleBox);
// 1. Resumo do Documento e do Selo
VBox summaryBox = new VBox(8);
summaryBox.getStyleClass().addAll("dialog-box", "document-summary-box");
summaryBox.setPadding(new Insets(12, 14, 12, 14));
Label lblSummaryHeader = new Label("DOCUMENTO CONFIGURADO");
lblSummaryHeader.getStyleClass().add("dialog-field-title");
GridPane summaryGrid = new GridPane();
summaryGrid.setHgap(12);
summaryGrid.setVgap(6);
Label lblDocTitle = new Label("Arquivo:");
lblDocTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
Label lblDocValue = new Label(sourcePdfFile.getName() + " (" + (sourcePdfFile.length() / 1024) + " KB)");
lblDocValue.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-primary; -fx-font-size: 11.5px;");
lblDocValue.setWrapText(true);
Label lblSealTitle = new Label("Selo Visual:");
lblSealTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
Label lblSealValue = new Label(sealSummaryText);
lblSealValue.setStyle("-fx-text-fill: #0284c7; -fx-font-weight: bold; -fx-font-size: 11.5px;");
lblSealValue.setWrapText(true);
summaryGrid.add(lblDocTitle, 0, 0);
summaryGrid.add(lblDocValue, 1, 0);
summaryGrid.add(lblSealTitle, 0, 1);
summaryGrid.add(lblSealValue, 1, 1);
summaryBox.getChildren().addAll(lblSummaryHeader, summaryGrid);
// 2. Card de Certificado ICP-Brasil
VBox certSection = new VBox(10);
certSection.getStyleClass().add("dialog-box");
certSection.setPadding(new Insets(14));
VBox.setVgrow(certSection, Priority.ALWAYS);
HBox certHeader = new HBox(10);
certHeader.setAlignment(Pos.CENTER_LEFT);
Label lblCertTitle = new Label("CERTIFICADO DIGITAL ICP-BRASIL");
lblCertTitle.getStyleClass().add("dialog-field-title");
HBox.setHgrow(lblCertTitle, Priority.ALWAYS);
Button btnRefresh = new Button("↻ Atualizar Lista");
btnRefresh.getStyleClass().addAll("btn-secondary", "btn-compact");
btnRefresh.setOnAction(e -> {
populateCertificates();
updatePinFieldVisibility();
});
certHeader.getChildren().addAll(lblCertTitle, btnRefresh);
certCombo = new ComboBox<>();
certCombo.setMaxWidth(Double.MAX_VALUE);
populateCertificates();
certCombo.setOnAction(e -> updatePinFieldVisibility());
certCombo.setCellFactory(lv -> new ListCell<>() {
@Override
protected void updateItem(ManagedCertificate item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
String holderName = X509CertificateUtils.extractHolderName(item.commonName() != null ? item.commonName() : item.alias());
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
setText(holderName + doc + " (" + item.type() + ")");
}
}
});
certCombo.setButtonCell(new ListCell<>() {
@Override
protected void updateItem(ManagedCertificate item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
String holderName = X509CertificateUtils.extractHolderName(item.commonName() != null ? item.commonName() : item.alias());
String doc = item.cpf() != null ? " [CPF: " + item.cpf() + "]" : (item.cnpj() != null ? " [CNPJ: " + item.cnpj() + "]" : "");
setText(holderName + doc + " (" + item.type() + ")");
}
}
});
// Banner informativo para certificados da Loja do Windows (sem PIN)
windowsStoreBox = new HBox(8);
windowsStoreBox.setAlignment(Pos.CENTER_LEFT);
windowsStoreBox.setPadding(new Insets(10, 12, 10, 12));
windowsStoreBox.setStyle("-fx-background-color: #ecfdf5; -fx-border-color: #a7f3d0; -fx-border-radius: 6px; -fx-background-radius: 6px;");
Label lblWinNotice = new Label("✓ Certificado instalado no computador — Gerenciado nativamente pelo Windows (PIN não necessário)");
lblWinNotice.setStyle("-fx-text-fill: #065f46; -fx-font-weight: bold; -fx-font-size: 11.5px;");
lblWinNotice.setWrapText(true);
HBox.setHgrow(lblWinNotice, Priority.ALWAYS);
windowsStoreBox.getChildren().add(lblWinNotice);
// Linha de PIN / Senha para Tokens A3 e Arquivos A1
pinRow = new HBox(10);
pinRow.setAlignment(Pos.CENTER_LEFT);
lblPinTitle = new Label("PIN / Senha:");
lblPinTitle.getStyleClass().add("dialog-field-title");
pinField = new PasswordField();
pinField.setPromptText("PIN ou senha do certificado...");
pinField.setPrefWidth(220);
btnClearRememberedPin = new Button("Alterar PIN");
btnClearRememberedPin.getStyleClass().addAll("btn-secondary", "btn-compact");
btnClearRememberedPin.setStyle("-fx-font-size: 11px;");
btnClearRememberedPin.setVisible(false);
btnClearRememberedPin.setOnAction(e -> {
ManagedCertificate sel = certCombo.getValue();
if (sel != null) {
SecurePinStorage.getInstance().removePin(sel.id());
updatePinFieldVisibility();
}
});
lblPinNotice = new Label();
lblPinNotice.setStyle("-fx-font-size: 11.5px;");
lblPinNotice.setWrapText(true);
HBox.setHgrow(lblPinNotice, Priority.ALWAYS);
pinRow.getChildren().addAll(lblPinTitle, pinField, btnClearRememberedPin, lblPinNotice);
chkRememberPin = new CheckBox("Lembrar PIN/senha com segurança neste computador");
chkRememberPin.setStyle("-fx-font-size: 11.5px; -fx-text-fill: -fx-text-primary;");
chkRememberPin.setSelected(true);
VBox tipBox = new VBox(2);
tipBox.setPadding(new Insets(8, 10, 8, 10));
tipBox.setStyle("-fx-background-color: -fx-bg-primary; -fx-border-color: -fx-border-light; -fx-border-radius: 6px; -fx-background-radius: 6px;");
Label lblTip = new Label("🔒 Segurança: O PIN é criptografado localmente com AES-256-GCM vinculado à máquina e nunca é transmitido na rede.");
lblTip.setStyle("-fx-font-size: 11px; -fx-text-fill: -fx-text-secondary;");
lblTip.setWrapText(true);
lblTip.setMaxWidth(Double.MAX_VALUE);
tipBox.getChildren().add(lblTip);
certSection.getChildren().addAll(certHeader, certCombo, windowsStoreBox, pinRow, chkRememberPin, tipBox);
// 3. Rodapé
HBox footer = new HBox(12);
footer.setAlignment(Pos.CENTER_RIGHT);
footer.setPadding(new Insets(4, 0, 0, 0));
progressIndicator = new ProgressIndicator();
progressIndicator.setPrefSize(22, 22);
progressIndicator.setVisible(false);
lblStatus = new Label();
lblStatus.setStyle("-fx-font-size: 12px;");
HBox.setHgrow(lblStatus, Priority.ALWAYS);
btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnCancel.setMinWidth(110);
btnCancel.setOnAction(e -> stage.close());
btnSign = new Button("Assinar Documento PDF");
btnSign.getStyleClass().addAll("btn-primary", "btn-dialog-wide");
btnSign.setMinWidth(220);
btnSign.setPrefWidth(Region.USE_COMPUTED_SIZE);
btnSign.setDefaultButton(true);
btnSign.setOnAction(e -> handleSign());
footer.getChildren().addAll(progressIndicator, lblStatus, btnCancel, btnSign);
root.getChildren().addAll(header, summaryBox, certSection, footer);
Scene scene = new Scene(root, 720, 500);
ThemeManager.applyTheme(scene);
stage.setScene(scene);
updatePinFieldVisibility();
}
private void populateCertificates() {
try {
List<ManagedCertificate> certs = certManager.getAvailableCertificates();
certCombo.setItems(FXCollections.observableArrayList(certs));
if (!certs.isEmpty()) {
certCombo.setValue(certs.get(0));
}
} catch (Exception e) {
logger.error("Erro ao listar certificados", e);
}
}
private void updatePinFieldVisibility() {
ManagedCertificate cert = certCombo.getValue();
if (cert == null) {
pinField.setDisable(true);
lblPinNotice.setText("");
chkRememberPin.setVisible(false);
chkRememberPin.setManaged(false);
btnClearRememberedPin.setVisible(false);
windowsStoreBox.setVisible(false);
windowsStoreBox.setManaged(false);
pinRow.setVisible(true);
pinRow.setManaged(true);
return;
}
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
if (isWindowsStore) {
// Certificado instalado no repositório do Windows (não exige PIN no aplicativo)
windowsStoreBox.setVisible(true);
windowsStoreBox.setManaged(true);
pinRow.setVisible(false);
pinRow.setManaged(false);
chkRememberPin.setVisible(false);
chkRememberPin.setManaged(false);
btnClearRememberedPin.setVisible(false);
return;
}
// Token USB (PKCS#11) ou Arquivo A1 (PKCS#12)
windowsStoreBox.setVisible(false);
windowsStoreBox.setManaged(false);
pinRow.setVisible(true);
pinRow.setManaged(true);
char[] storedPin = SecurePinStorage.getInstance().getPin(cert.id());
if (storedPin != null && storedPin.length > 0) {
pinField.setText(new String(storedPin));
pinField.setDisable(true);
chkRememberPin.setVisible(false);
chkRememberPin.setManaged(false);
btnClearRememberedPin.setVisible(true);
lblPinNotice.setText("✓ PIN lembrado com segurança neste computador");
lblPinNotice.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold; -fx-font-size: 11.5px;");
} else {
pinField.clear();
pinField.setDisable(false);
chkRememberPin.setVisible(true);
chkRememberPin.setManaged(true);
btnClearRememberedPin.setVisible(false);
boolean isA3 = cert.type() == CertificateType.A3;
lblPinNotice.setText(isA3 ? "Requer PIN do token" : "Requer senha do certificado");
lblPinNotice.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 11.5px;");
}
}
private void handleSign() {
ManagedCertificate cert = certCombo.getValue();
if (cert == null) {
showAlert(Alert.AlertType.WARNING, "Certificado Não Selecionado", "Por favor, selecione um certificado digital.");
return;
}
boolean isWindowsStore = "provider-win-store".equalsIgnoreCase(cert.providerId());
char[] storedPin = !isWindowsStore ? SecurePinStorage.getInstance().getPin(cert.id()) : null;
if (!isWindowsStore && (storedPin == null || storedPin.length == 0) && (pinField.getText() == null || pinField.getText().trim().isEmpty())) {
showAlert(Alert.AlertType.WARNING, "PIN / Senha Obrigatório",
cert.type() == CertificateType.A3
? "Por favor, informe o PIN do seu dispositivo A3 para autorizar a assinatura."
: "Por favor, informe a senha do certificado A1 para autorizar a assinatura.");
pinField.requestFocus();
return;
}
FileChooser chooser = new FileChooser();
chooser.setTitle("Salvar PDF Assinado Como...");
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Documentos PDF (*.pdf)", "*.pdf"));
String originalName = sourcePdfFile.getName();
String baseName = originalName.toLowerCase().endsWith(".pdf")
? originalName.substring(0, originalName.length() - 4)
: originalName;
chooser.setInitialFileName(baseName + "_assinado.pdf");
if (sourcePdfFile.getParentFile() != null && sourcePdfFile.getParentFile().exists()) {
chooser.setInitialDirectory(sourcePdfFile.getParentFile());
}
File targetFile = chooser.showSaveDialog(stage);
if (targetFile == null) {
return;
}
char[] pinToUse = null;
if (!isWindowsStore) {
if (storedPin != null && storedPin.length > 0) {
pinToUse = storedPin;
} else if (!pinField.getText().isEmpty()) {
pinToUse = pinField.getText().toCharArray();
}
}
final char[] enteredPin = (!isWindowsStore && pinField.getText() != null && !pinField.getText().isEmpty())
? pinField.getText().toCharArray()
: null;
PdfSignatureOptions.Builder b = PdfSignatureOptions.builder(cert.id())
.pinOrPassword(pinToUse)
.visibleSeal(sealConfig.visibleSeal())
.sealTheme(sealConfig.sealTheme())
.cpfDisplayMode(sealConfig.cpfDisplayMode())
.includeDateTime(sealConfig.includeDateTime())
.customExtraInfo(sealConfig.customExtraInfo())
.reason(sealConfig.reason())
.location(sealConfig.location())
.customSealScale(sealConfig.customSealScale());
if (sealConfig.isCustomPosition() && sealConfig.customX() != null && sealConfig.customY() != null) {
b.customPosition(sealConfig.customX(), sealConfig.customY(), sealConfig.customPageNumber());
} else if (sealConfig.positionOption() != null) {
b.position(sealConfig.positionOption().toPosition());
if (sealConfig.pageOption() != null) {
b.pageChoice(sealConfig.pageOption().toPageChoice());
}
}
PdfSignatureOptions options = b.build();
setFormDisabled(true);
progressIndicator.setVisible(true);
lblStatus.setText("Assinando documento PDF com envelope PAdES...");
lblStatus.setStyle("-fx-text-fill: -fx-accent;");
Task<File> signTask = new Task<>() {
@Override
protected File call() throws Exception {
pdfSigningService.signPdfFile(sourcePdfFile, targetFile, options);
return targetFile;
}
};
signTask.setOnSucceeded(e -> {
setFormDisabled(false);
progressIndicator.setVisible(false);
lblStatus.setText("✓ Documento assinado com sucesso!");
lblStatus.setStyle("-fx-text-fill: #10b981; -fx-font-weight: bold;");
// Salva o PIN de forma segura se solicitado pelo usuário
if (!isWindowsStore && chkRememberPin.isSelected() && enteredPin != null && enteredPin.length > 0) {
SecurePinStorage.getInstance().savePin(cert.id(), enteredPin);
}
showSuccessDialog(targetFile);
stage.close();
});
signTask.setOnFailed(e -> {
setFormDisabled(false);
progressIndicator.setVisible(false);
Throwable ex = signTask.getException();
logger.error("Falha ao assinar PDF", ex);
// Se o PIN estava incorreto, remove do armazenamento seguro
String msg = ex != null ? ex.getMessage() : "";
if (msg.contains("PIN_INCORRECT") || msg.contains("senha") || msg.contains("PIN")) {
SecurePinStorage.getInstance().removePin(cert.id());
updatePinFieldVisibility();
}
lblStatus.setText("✕ Falha na assinatura");
lblStatus.setStyle("-fx-text-fill: #ef4444; -fx-font-weight: bold;");
showAlert(Alert.AlertType.ERROR, "Erro na Assinatura do PDF",
"Não foi possível concluir a assinatura digital:\n" + (ex != null ? ex.getMessage() : "Erro desconhecido"));
});
Thread signThread = new Thread(signTask, "cybercore-pdf-cert-signer");
signThread.setDaemon(true);
signThread.start();
}
private void showSuccessDialog(File targetFile) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Assinatura Concluída");
alert.setHeaderText("PDF Assinado com Sucesso!");
alert.setContentText("O documento foi digitalmente assinado no padrão PAdES ICP-Brasil e salvo no destino solicitado.\n\n"
+ "Arquivo gerado:\n" + targetFile.getAbsolutePath());
ButtonType btnOpen = new ButtonType("Abrir PDF");
ButtonType btnFolder = new ButtonType("Abrir Pasta");
ButtonType btnClose = new ButtonType("Fechar", ButtonBar.ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(btnOpen, btnFolder, btnClose);
ThemeManager.applyTheme(alert.getDialogPane().getScene());
Optional<ButtonType> result = alert.showAndWait();
if (result.isPresent()) {
if (result.get() == btnOpen) {
openFile(targetFile);
} else if (result.get() == btnFolder) {
openFolder(targetFile);
}
}
}
private void openFile(File file) {
try {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
Desktop.getDesktop().open(file);
}
} catch (Exception ex) {
logger.warn("Não foi possível abrir o arquivo automaticamente: {}", ex.getMessage());
}
}
private void openFolder(File file) {
try {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
File parent = file.getParentFile();
if (parent != null) {
Desktop.getDesktop().open(parent);
}
}
} catch (Exception ex) {
logger.warn("Não foi possível abrir a pasta do arquivo: {}", ex.getMessage());
}
}
private void setFormDisabled(boolean disabled) {
btnSign.setDisable(disabled);
btnCancel.setDisable(disabled);
certCombo.setDisable(disabled);
pinField.setDisable(disabled);
}
private void showAlert(Alert.AlertType type, String title, String message) {
Alert alert = new Alert(type);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
ThemeManager.applyTheme(alert.getDialogPane().getScene());
alert.showAndWait();
}
public void show() {
stage.centerOnScreen();
stage.show();
stage.toFront();
}
}
File diff suppressed because it is too large Load Diff
@@ -222,8 +222,9 @@ public final class SealManagementDialog {
footer.setAlignment(Pos.CENTER_RIGHT); footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13)); Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-secondary"); btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(95); btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close()); btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose); footer.getChildren().add(btnClose);
@@ -124,7 +124,7 @@ public final class SettingsView extends VBox {
HBox pinActionsBox = new HBox(12); HBox pinActionsBox = new HBox(12);
pinActionsBox.setAlignment(Pos.CENTER_LEFT); pinActionsBox.setAlignment(Pos.CENTER_LEFT);
Button btnClearPins = new Button("Limpar PINs Salvos na Sessão"); Button btnClearPins = new Button("Limpar PINs Salvos (Sessão e Computador)");
btnClearPins.getStyleClass().add("btn-secondary"); btnClearPins.getStyleClass().add("btn-secondary");
btnClearPins.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;"); btnClearPins.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;");
@@ -133,8 +133,9 @@ public final class SettingsView extends VBox {
btnClearPins.setOnAction(e -> { btnClearPins.setOnAction(e -> {
PinSessionManager.getInstance().clearAll(); PinSessionManager.getInstance().clearAll();
br.com.cybercore.signer.core.security.SecurePinStorage.getInstance().clearAll();
SmartCardSessionManager.resetCardSessions(); SmartCardSessionManager.resetCardSessions();
pinStatusLabel.setText("✓ Todos os PINs e sessões de tokens foram zerados."); pinStatusLabel.setText("✓ Todos os PINs salvos em memória e neste computador foram zerados.");
}); });
pinActionsBox.getChildren().addAll(btnClearPins, pinStatusLabel); pinActionsBox.getChildren().addAll(btnClearPins, pinStatusLabel);
@@ -193,6 +194,8 @@ public final class SettingsView extends VBox {
private void refreshSealsList() { private void refreshSealsList() {
sealsContainer.getChildren().clear(); sealsContainer.getChildren().clear();
List<PdfSealTheme> themes = PdfSealThemeRegistry.listThemes(); List<PdfSealTheme> themes = PdfSealThemeRegistry.listThemes();
AgentConfigurationService configService = AgentConfigurationService.getInstance();
String currentDefaultId = configService.getConfig().defaultSealThemeId();
for (PdfSealTheme theme : themes) { for (PdfSealTheme theme : themes) {
HBox item = new HBox(10); HBox item = new HBox(10);
@@ -200,6 +203,8 @@ public final class SettingsView extends VBox {
item.getStyleClass().add("diagnostic-item"); item.getStyleClass().add("diagnostic-item");
item.setPadding(new Insets(6, 12, 6, 12)); item.setPadding(new Insets(6, 12, 6, 12));
boolean isDefault = theme.id().equalsIgnoreCase(currentDefaultId);
VBox info = new VBox(2); VBox info = new VBox(2);
Label lblName = new Label(theme.name() + " (" + theme.id() + ")"); Label lblName = new Label(theme.name() + " (" + theme.id() + ")");
lblName.getStyleClass().add("cert-title"); lblName.getStyleClass().add("cert-title");
@@ -211,10 +216,34 @@ public final class SettingsView extends VBox {
info.getChildren().addAll(lblName, lblDesc); info.getChildren().addAll(lblName, lblDesc);
HBox.setHgrow(info, Priority.ALWAYS); HBox.setHgrow(info, Priority.ALWAYS);
Label badge = new Label(theme.isCustom() ? "Personalizado" : "Sistema"); Label badgeType = new Label(theme.isCustom() ? "Personalizado" : "Sistema");
badge.getStyleClass().add(theme.isCustom() ? "diagnostic-value-warn" : "diagnostic-value-ok"); badgeType.getStyleClass().add(theme.isCustom() ? "diagnostic-value-warn" : "diagnostic-value-ok");
item.getChildren().addAll(info, badgeType);
item.getChildren().addAll(info, badge); if (isDefault) {
Label badgeDefault = new Label("★ PADRÃO");
badgeDefault.setStyle("-fx-background-color: #0284c7; -fx-text-fill: #ffffff; -fx-padding: 3px 8px; -fx-background-radius: 4px; -fx-font-weight: bold; -fx-font-size: 11px;");
item.getChildren().add(badgeDefault);
} else {
Button btnSetDefault = new Button("★ Definir Padrão");
btnSetDefault.getStyleClass().addAll("btn-secondary", "btn-compact");
btnSetDefault.setStyle("-fx-padding: 3px 8px; -fx-font-size: 11px;");
btnSetDefault.setOnAction(e -> {
AgentConfiguration current = configService.getConfig();
configService.updateConfig(new AgentConfiguration(
current.requireConfirmation(),
current.alwaysPromptPin(),
current.autoStart(),
theme.id(),
current.defaultCpfDisplayMode(),
current.defaultIncludeDateTime(),
current.serverPort()
));
refreshSealsList();
notifySave("Selo '" + theme.name() + "' definido como o padrão para assinaturas.");
});
item.getChildren().add(btnSetDefault);
}
if (theme.isCustom()) { if (theme.isCustom()) {
Button btnDelete = new Button("Excluir"); Button btnDelete = new Button("Excluir");
@@ -77,8 +77,9 @@ public final class TutorialDialog {
HBox footer = new HBox(); HBox footer = new HBox();
footer.setAlignment(Pos.CENTER_RIGHT); footer.setAlignment(Pos.CENTER_RIGHT);
Button btnClose = new Button(" Entendi", SvgIcons.closeIcon(13)); Button btnClose = new Button(" Entendi", SvgIcons.closeIcon(13));
btnClose.getStyleClass().add("btn-primary"); btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnClose.setPrefWidth(95); btnClose.setPrefWidth(110);
btnClose.setMinWidth(110);
btnClose.setOnAction(e -> stage.close()); btnClose.setOnAction(e -> stage.close());
footer.getChildren().add(btnClose); footer.getChildren().add(btnClose);
@@ -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,11 +26,14 @@ 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.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Modality; import javafx.stage.Modality;
@@ -66,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;
@@ -92,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();
} }
@@ -109,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);
@@ -196,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();
@@ -214,6 +234,16 @@ public final class XmlSignDialog {
elementGrid.setHgap(12); elementGrid.setHgap(12);
elementGrid.setVgap(8); elementGrid.setVgap(8);
ColumnConstraints colTagLabel = new ColumnConstraints();
colTagLabel.setMinWidth(140);
colTagLabel.setPrefWidth(145);
colTagLabel.setHgrow(Priority.NEVER);
ColumnConstraints colTagField = new ColumnConstraints();
colTagField.setHgrow(Priority.ALWAYS);
elementGrid.getColumnConstraints().addAll(colTagLabel, colTagField);
Label lblTag = new Label("Tag do Elemento:"); Label lblTag = new Label("Tag do Elemento:");
lblTag.getStyleClass().add("dialog-field-title"); lblTag.getStyleClass().add("dialog-field-title");
txtElementTag = new TextField("infNFe"); txtElementTag = new TextField("infNFe");
@@ -235,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);
@@ -254,11 +293,12 @@ public final class XmlSignDialog {
HBox.setHgrow(lblStatus, Priority.ALWAYS); HBox.setHgrow(lblStatus, Priority.ALWAYS);
btnCancel = new Button("Cancelar"); btnCancel = new Button("Cancelar");
btnCancel.getStyleClass().add("btn-secondary"); btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
btnCancel.setOnAction(e -> stage.close()); btnCancel.setOnAction(e -> stage.close());
btnSign = new Button("Assinar Documento XML"); btnSign = new Button("Assinar Documento XML");
btnSign.getStyleClass().add("btn-primary"); btnSign.getStyleClass().addAll("btn-primary", "btn-dialog");
btnSign.setMinWidth(Region.USE_PREF_SIZE);
btnSign.setDefaultButton(true); btnSign.setDefaultButton(true);
btnSign.setOnAction(e -> executeXmlSignature()); btnSign.setOnAction(e -> executeXmlSignature());
@@ -376,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();
@@ -383,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();
@@ -853,6 +853,11 @@
3. BOTÕES E COMPONENTES COMUNS (DUAL THEME) 3. BOTÕES E COMPONENTES COMUNS (DUAL THEME)
========================================================================== */ ========================================================================== */
/* Botões Base e Proteção contra Truncamento com Reticências */
.button {
-fx-min-width: -fx-pref-size;
}
.btn-primary { .btn-primary {
-fx-background-color: #0284c7; -fx-background-color: #0284c7;
-fx-text-fill: #ffffff; -fx-text-fill: #ffffff;
@@ -860,6 +865,8 @@
-fx-padding: 8px 18px; -fx-padding: 8px 18px;
-fx-background-radius: 6px; -fx-background-radius: 6px;
-fx-cursor: hand; -fx-cursor: hand;
-fx-min-width: -fx-pref-size;
-fx-alignment: center;
-fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1); -fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1);
} }
@@ -873,6 +880,8 @@
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-padding: 8px 18px; -fx-padding: 8px 18px;
-fx-background-radius: 6px; -fx-background-radius: 6px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center;
-fx-cursor: hand; -fx-cursor: hand;
} }
@@ -901,6 +910,8 @@
-fx-text-fill: #475569; -fx-text-fill: #475569;
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-padding: 8px 18px; -fx-padding: 8px 18px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center;
-fx-cursor: hand; -fx-cursor: hand;
} }
@@ -919,6 +930,29 @@
-fx-text-fill: #f87171; -fx-text-fill: #f87171;
} }
/* Botões Padronizados para Diálogos e Modais */
.btn-dialog {
-fx-min-width: 110px;
-fx-pref-width: -fx-pref-size;
-fx-pref-height: 34px;
-fx-alignment: center;
}
.btn-dialog-wide {
-fx-min-width: 220px;
-fx-pref-width: -fx-pref-size;
-fx-pref-height: 36px;
-fx-alignment: center;
}
/* Botões Compactos (barras de ferramentas e atalhos em linha) */
.btn-compact {
-fx-padding: 5px 12px;
-fx-font-size: 11.5px;
-fx-min-width: -fx-pref-size;
-fx-alignment: center;
}
/* Botão de Perigo / Exclusão / Remoção */ /* Botão de Perigo / Exclusão / Remoção */
.btn-danger { .btn-danger {
-fx-background-color: #fee2e2; -fx-background-color: #fee2e2;
@@ -931,6 +965,7 @@
-fx-font-weight: bold; -fx-font-weight: bold;
-fx-padding: 6px 14px; -fx-padding: 6px 14px;
-fx-alignment: center; -fx-alignment: center;
-fx-min-width: -fx-pref-size;
-fx-cursor: hand; -fx-cursor: hand;
} }
@@ -1063,3 +1098,131 @@
-fx-background-color: #0284c7; -fx-background-color: #0284c7;
-fx-background-radius: 4px; -fx-background-radius: 4px;
} }
/* =========================================================================
WIZARD STEPPER (FLUXO MULTI-ETAPAS)
========================================================================= */
.wizard-stepper {
-fx-alignment: center-left;
-fx-padding: 2px 0 6px 0;
}
.wizard-step-pill {
-fx-alignment: center;
-fx-padding: 4px 12px;
-fx-background-radius: 14px;
-fx-border-radius: 14px;
-fx-border-width: 1px;
}
.theme-light .wizard-step-pill {
-fx-background-color: #f1f5f9;
-fx-border-color: #cbd5e1;
}
.theme-dark .wizard-step-pill {
-fx-background-color: #1e293b;
-fx-border-color: #334155;
}
.theme-light .wizard-step-pill-active {
-fx-background-color: #e0f2fe;
-fx-border-color: #0284c7;
}
.theme-dark .wizard-step-pill-active {
-fx-background-color: #0c4a6e;
-fx-border-color: #38bdf8;
}
.wizard-step-badge {
-fx-font-size: 11px;
-fx-font-weight: bold;
-fx-alignment: center;
-fx-min-width: 20px;
-fx-min-height: 20px;
-fx-background-radius: 10px;
-fx-text-fill: #ffffff;
}
.theme-light .wizard-step-badge-active {
-fx-background-color: #0284c7;
-fx-text-fill: #ffffff;
}
.theme-dark .wizard-step-badge-active {
-fx-background-color: #38bdf8;
-fx-text-fill: #0f172a;
}
.theme-light .wizard-step-badge-inactive {
-fx-background-color: #94a3b8;
-fx-text-fill: #ffffff;
}
.theme-dark .wizard-step-badge-inactive {
-fx-background-color: #475569;
-fx-text-fill: #e2e8f0;
}
.wizard-step-text {
-fx-font-size: 11.5px;
-fx-font-weight: bold;
}
.theme-light .wizard-step-text-active {
-fx-text-fill: #0369a1;
}
.theme-dark .wizard-step-text-active {
-fx-text-fill: #38bdf8;
}
.theme-light .wizard-step-text-inactive {
-fx-text-fill: #64748b;
}
.theme-dark .wizard-step-text-inactive {
-fx-text-fill: #94a3b8;
}
.wizard-step-separator {
-fx-pref-width: 28px;
-fx-min-width: 20px;
-fx-pref-height: 2px;
}
.theme-light .wizard-step-separator {
-fx-background-color: #cbd5e1;
}
.theme-dark .wizard-step-separator {
-fx-background-color: #334155;
}
.theme-light .wizard-step-separator-active {
-fx-background-color: #0284c7;
}
.theme-dark .wizard-step-separator-active {
-fx-background-color: #38bdf8;
}
.document-summary-box {
-fx-padding: 10px 14px;
-fx-border-radius: 8px;
-fx-background-radius: 8px;
-fx-border-width: 1px;
-fx-alignment: center-left;
}
.theme-light .document-summary-box {
-fx-background-color: #f8fafc;
-fx-border-color: #e2e8f0;
}
.theme-dark .document-summary-box {
-fx-background-color: #0f172a;
-fx-border-color: #1e293b;
}