Compare commits
6
Commits
main
...
3fab68c118
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fab68c118 | ||
|
|
0cd4025f3c | ||
|
|
5360caaddf | ||
|
|
4c30649598 | ||
|
|
c997afbfd4 | ||
|
|
7f6d8a4a08 |
+81
-19
@@ -1,16 +1,27 @@
|
||||
# Assinaturas XML (XMLDSig e ICP-Brasil)
|
||||
# Assinaturas XML no Padrão XAdES e ICP-Brasil
|
||||
|
||||
Este documento descreve a especificação e o funcionamento da assinatura digital de arquivos XML no padrão **W3C XMLDSig (XML Digital Signatures)** e **ICP-Brasil** no CyberCore Signer.
|
||||
Este documento descreve a especificação, conformidade e o funcionamento da assinatura digital de arquivos XML no padrão avançado **XAdES (XML Advanced Electronic Signatures - ETSI TS 101 903 e ICP-Brasil DOC-ICP-15.03)** e **W3C XMLDSig** no CyberCore Signer.
|
||||
|
||||
---
|
||||
|
||||
## 1. Visão Geral
|
||||
|
||||
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:**
|
||||
- **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...">`
|
||||
@@ -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).
|
||||
- **Transformações:**
|
||||
- `http://www.w3.org/2000/09/xmldsig#enveloped-signature` (remove a assinatura do cálculo do digest).
|
||||
- C14N padrão.
|
||||
- **Algoritmo de Digest:** SHA-256 (`http://www.w3.org/2001/04/xmlenc#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>`.
|
||||
```xml
|
||||
<ds:Signature Id="sig-[UUID]" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
||||
<ds:SignedInfo>
|
||||
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
|
||||
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
|
||||
|
||||
<!-- 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:
|
||||
|
||||
@@ -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`).
|
||||
2. A integridade dos digests de todas as referências apontadas (`Reference.validate()`).
|
||||
3. O certificado X.509 público extraído da chave.
|
||||
1. **Validação criptográfica de SignedInfo:** Confere se o `SignatureValue` foi emitido pela chave pública correspondente.
|
||||
2. **Validação de integridade dos dados de negócio:** Confere o digest do conteúdo ou elemento assinado.
|
||||
3. **Validação de integridade do XAdES:** Confere o digest das propriedades qualificadas (`SignedProperties`), rejeitando qualquer adulteração no horário de assinatura (`SigningTime`) ou no certificado declarado.
|
||||
4. **Inspeção de signatário e datação:** Extração de nome, documento (CPF/CNPJ), autoridade certificadora emissora e datação auditável.
|
||||
|
||||
+27
-4
@@ -25,7 +25,8 @@ public record PdfSignatureOptions(
|
||||
Integer targetPageNumber,
|
||||
CpfDisplayMode cpfDisplayMode,
|
||||
Boolean includeDateTime,
|
||||
String customExtraInfo
|
||||
String customExtraInfo,
|
||||
Double customSealScale
|
||||
) {
|
||||
|
||||
public PdfSignatureOptions {
|
||||
@@ -51,6 +52,21 @@ public record PdfSignatureOptions(
|
||||
if (includeDateTime == null) {
|
||||
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,
|
||||
Integer targetPageNumber, CpfDisplayMode cpfDisplayMode, Boolean includeDateTime) {
|
||||
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,
|
||||
PdfSealTheme sealTheme, PdfSealPosition position, PdfSealPageChoice pageChoice,
|
||||
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,
|
||||
@@ -143,6 +159,7 @@ public record PdfSignatureOptions(
|
||||
}
|
||||
|
||||
private String customExtraInfo;
|
||||
private Double customSealScale = 1.0;
|
||||
|
||||
public Builder includeDateTime(boolean include) {
|
||||
this.includeDateTime = include;
|
||||
@@ -154,6 +171,11 @@ public record PdfSignatureOptions(
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder customSealScale(Double scale) {
|
||||
this.customSealScale = scale;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder customPosition(Float x, Float y, Integer pageNumber) {
|
||||
this.customX = x;
|
||||
this.customY = y;
|
||||
@@ -179,7 +201,8 @@ public record PdfSignatureOptions(
|
||||
targetPageNumber,
|
||||
cpfDisplayMode,
|
||||
includeDateTime,
|
||||
customExtraInfo
|
||||
customExtraInfo,
|
||||
customSealScale
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+142
-82
@@ -1,5 +1,6 @@
|
||||
package br.com.cybercore.signer.core.pdf;
|
||||
|
||||
import br.com.cybercore.signer.core.crypto.X509CertificateUtils;
|
||||
import br.com.cybercore.signer.core.domain.ManagedCertificate;
|
||||
import br.com.cybercore.signer.core.domain.SigningKeyMaterial;
|
||||
import br.com.cybercore.signer.core.pdf.seal.PdfSealData;
|
||||
@@ -12,10 +13,19 @@ import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
|
||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
|
||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||
@@ -66,6 +76,8 @@ public class PdfSigningService {
|
||||
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.
|
||||
*/
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -155,6 +182,10 @@ public class PdfSigningService {
|
||||
String issuerCn = extractIssuerCn(cert.issuerDn());
|
||||
String signingTimeStr = DATE_TIME_FORMATTER.format(Instant.now());
|
||||
|
||||
// Extrai o nome amigável do titular (removendo qualquer sufixo como :CPF ou :CNPJ para o nome do assinante)
|
||||
String rawName = cert.commonName() != null ? cert.commonName() : cert.alias();
|
||||
String signerName = X509CertificateUtils.extractHolderName(rawName);
|
||||
|
||||
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
byte[] pdfDigest = sha256.digest(pdfBytes);
|
||||
String hashShort = bytesToHex(pdfDigest).substring(0, 16).toUpperCase();
|
||||
@@ -166,7 +197,7 @@ public class PdfSigningService {
|
||||
|
||||
// 3. Monta dados do selo visual
|
||||
PdfSealData sealData = new PdfSealData(
|
||||
cert.commonName() != null ? cert.commonName() : cert.alias(),
|
||||
signerName,
|
||||
signerDoc,
|
||||
signingTimeStr,
|
||||
issuerCn,
|
||||
@@ -178,119 +209,148 @@ public class PdfSigningService {
|
||||
effectiveExtraInfo
|
||||
);
|
||||
|
||||
// 4. Carrega o PDF e estampa o selo se solicitado
|
||||
byte[] intermediatePdf;
|
||||
if (options.visibleSeal()) {
|
||||
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
|
||||
intermediatePdf = applyVisualSealToPdf(pdfBytes, sealPng, theme, options);
|
||||
} else {
|
||||
intermediatePdf = pdfBytes;
|
||||
}
|
||||
|
||||
// 5. Aplica a assinatura PAdES criptográfica via PDFBox e Bouncy Castle
|
||||
// 4. Carrega o PDF original e aplica a assinatura PAdES via atualização incremental (Incremental Update),
|
||||
// preservando intactas quaisquer assinaturas digitais pré-existentes. Em conformidade com a ISO 32000-1 / ETSI PAdES,
|
||||
// o selo visual é associado diretamente ao widget da assinatura (/Widget /AP /N) sem alterar o stream /Contents
|
||||
// das páginas existentes, garantindo que o validador do ITI aprove a verificação incremental sem indeterminação.
|
||||
ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
|
||||
try (PDDocument doc = Loader.loadPDF(intermediatePdf)) {
|
||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
||||
PDSignature signature = new PDSignature();
|
||||
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
||||
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
|
||||
signature.setName(cert.commonName());
|
||||
signature.setName(signerName);
|
||||
signature.setLocation(options.location());
|
||||
signature.setReason(options.reason());
|
||||
signature.setSignDate(Calendar.getInstance());
|
||||
|
||||
SignatureInterface signer = new PadesBouncyCastleSigner(keyMaterial);
|
||||
doc.addSignature(signature, signer);
|
||||
doc.saveIncremental(finalOut);
|
||||
|
||||
if (options.visibleSeal()) {
|
||||
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
|
||||
int targetPage = resolveTargetPage(doc, options);
|
||||
PDRectangle rect = calculateSealRectangle(doc, targetPage, theme, options);
|
||||
|
||||
try (InputStream visualTemplate = createVisualSignatureTemplate(doc, targetPage, rect, sealPng);
|
||||
SignatureOptions signatureOptions = new SignatureOptions()) {
|
||||
signatureOptions.setVisualSignature(visualTemplate);
|
||||
signatureOptions.setPage(targetPage);
|
||||
doc.addSignature(signature, signer, signatureOptions);
|
||||
doc.saveIncremental(finalOut);
|
||||
}
|
||||
} else {
|
||||
doc.addSignature(signature, signer);
|
||||
doc.saveIncremental(finalOut);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Assinatura PAdES concluída com sucesso. Tamanho final: {} bytes", finalOut.size());
|
||||
return finalOut.toByteArray();
|
||||
}
|
||||
|
||||
private byte[] applyVisualSealToPdf(byte[] pdfBytes, byte[] sealPng, PdfSealTheme theme,
|
||||
PdfSignatureOptions options) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
||||
int totalPages = doc.getNumberOfPages();
|
||||
if (totalPages == 0) {
|
||||
return pdfBytes;
|
||||
}
|
||||
private int resolveTargetPage(PDDocument doc, PdfSignatureOptions options) {
|
||||
int totalPages = doc.getNumberOfPages();
|
||||
if (totalPages <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) {
|
||||
return options.targetPageNumber() - 1;
|
||||
}
|
||||
if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) {
|
||||
return 0;
|
||||
}
|
||||
return totalPages - 1;
|
||||
}
|
||||
|
||||
private PDRectangle calculateSealRectangle(PDDocument doc, int pageIndex, PdfSealTheme theme, PdfSignatureOptions options) {
|
||||
PDPage page = doc.getPage(pageIndex);
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
float pageW = mediaBox.getWidth();
|
||||
float pageH = mediaBox.getHeight();
|
||||
|
||||
PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal");
|
||||
float scale = (options.customSealScale() != null && options.customSealScale() > 0)
|
||||
? options.customSealScale().floatValue()
|
||||
: 1.0f;
|
||||
float sealWidth = (theme.width() * 0.72f) * scale;
|
||||
float sealHeight = (theme.height() * 0.72f) * scale;
|
||||
float margin = 20f;
|
||||
|
||||
float sealWidth = theme.width() * 0.72f;
|
||||
float sealHeight = theme.height() * 0.72f;
|
||||
float margin = 20f;
|
||||
float x;
|
||||
float y;
|
||||
|
||||
// Se coordenadas customizadas foram informadas (clique ou drag & drop na página)
|
||||
if (options.customX() != null && options.customY() != null) {
|
||||
int targetPageIdx = totalPages - 1;
|
||||
if (options.targetPageNumber() != null && options.targetPageNumber() >= 1 && options.targetPageNumber() <= totalPages) {
|
||||
targetPageIdx = options.targetPageNumber() - 1;
|
||||
if (options.customX() != null && options.customY() != null) {
|
||||
x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
|
||||
y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
|
||||
} else {
|
||||
switch (options.position()) {
|
||||
case BOTTOM_LEFT -> {
|
||||
x = margin;
|
||||
y = margin;
|
||||
}
|
||||
case TOP_RIGHT -> {
|
||||
x = pageW - sealWidth - margin;
|
||||
y = pageH - sealHeight - margin;
|
||||
}
|
||||
case TOP_LEFT -> {
|
||||
x = margin;
|
||||
y = pageH - sealHeight - margin;
|
||||
}
|
||||
default -> { // BOTTOM_RIGHT
|
||||
x = pageW - sealWidth - margin;
|
||||
y = margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PDPage page = doc.getPage(targetPageIdx);
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
float pageW = mediaBox.getWidth();
|
||||
float pageH = mediaBox.getHeight();
|
||||
return new PDRectangle(x, y, sealWidth, sealHeight);
|
||||
}
|
||||
|
||||
float x = Math.max(0, Math.min(options.customX(), pageW - sealWidth));
|
||||
float y = Math.max(0, Math.min(options.customY(), pageH - sealHeight));
|
||||
/**
|
||||
* Cria um documento template PAdES para o SignatureOptions do PDFBox, incorporando o selo visual
|
||||
* como a Aparência Normal (/AP /N) da anotação da assinatura (PDAnnotationWidget).
|
||||
* O stream /Contents da página do documento original permanece completamente inalterado.
|
||||
*/
|
||||
private InputStream createVisualSignatureTemplate(PDDocument srcDoc, int pageNum,
|
||||
PDRectangle rect, byte[] sealPngBytes) throws IOException {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage page = new PDPage(srcDoc.getPage(pageNum).getMediaBox());
|
||||
doc.addPage(page);
|
||||
|
||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
||||
}
|
||||
|
||||
doc.save(baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
PDAcroForm acroForm = new PDAcroForm(doc);
|
||||
doc.getDocumentCatalog().setAcroForm(acroForm);
|
||||
|
||||
// Caso contrário, usa posicionamento por enum (BOTTOM_RIGHT, etc.)
|
||||
int startPage = 0;
|
||||
int endPage = totalPages - 1;
|
||||
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);
|
||||
|
||||
if (options.pageChoice() == PdfSealPageChoice.FIRST_PAGE) {
|
||||
endPage = 0;
|
||||
} else if (options.pageChoice() == PdfSealPageChoice.LAST_PAGE) {
|
||||
startPage = totalPages - 1;
|
||||
}
|
||||
widget.setRectangle(rect);
|
||||
|
||||
for (int i = startPage; i <= endPage; i++) {
|
||||
PDPage page = doc.getPage(i);
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
float pageW = mediaBox.getWidth();
|
||||
float pageH = mediaBox.getHeight();
|
||||
PDStream stream = new PDStream(doc);
|
||||
PDFormXObject form = new PDFormXObject(stream);
|
||||
PDResources res = new PDResources();
|
||||
form.setResources(res);
|
||||
form.setFormType(1);
|
||||
|
||||
float x;
|
||||
float y;
|
||||
PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight());
|
||||
form.setBBox(bbox);
|
||||
|
||||
switch (options.position()) {
|
||||
case BOTTOM_LEFT -> {
|
||||
x = margin;
|
||||
y = margin;
|
||||
}
|
||||
case TOP_RIGHT -> {
|
||||
x = pageW - sealWidth - margin;
|
||||
y = pageH - sealHeight - margin;
|
||||
}
|
||||
case TOP_LEFT -> {
|
||||
x = margin;
|
||||
y = pageH - sealHeight - margin;
|
||||
}
|
||||
default -> { // BOTTOM_RIGHT
|
||||
x = pageW - sealWidth - margin;
|
||||
y = margin;
|
||||
}
|
||||
}
|
||||
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, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
||||
}
|
||||
try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
|
||||
PDImageXObject img = PDImageXObject.createFromByteArray(doc, sealPngBytes, "cybercore_seal");
|
||||
cs.drawImage(img, 0, 0, rect.getWidth(), rect.getHeight());
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
doc.save(baos);
|
||||
return new ByteArrayInputStream(baos.toByteArray());
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
|
||||
+269
@@ -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");
|
||||
}
|
||||
}
|
||||
+100
-11
@@ -15,6 +15,7 @@ import org.bouncycastle.util.Store;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
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) {
|
||||
try {
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
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));
|
||||
|
||||
// Registro de atributos Id no DOM
|
||||
registerAllIdAttributes(doc.getDocumentElement());
|
||||
|
||||
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
|
||||
if (nl.getLength() == 0) {
|
||||
nl = doc.getElementsByTagName("Signature");
|
||||
}
|
||||
if (nl.getLength() == 0) {
|
||||
return new ValidationResult(
|
||||
ValidationStatus.INVALID,
|
||||
ValidationType.XML_DSIG,
|
||||
"Nenhum elemento <Signature> XMLDSig encontrado no documento.",
|
||||
"Nenhum elemento <Signature> XML encontrado no documento.",
|
||||
List.of(),
|
||||
null,
|
||||
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");
|
||||
List<ValidationSignerInfo> signers = new ArrayList<>();
|
||||
boolean allValid = true;
|
||||
|
||||
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);
|
||||
boolean coreValidity = signature.validate(valContext);
|
||||
|
||||
@@ -254,22 +282,66 @@ public final class SignatureValidationService {
|
||||
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(
|
||||
"Signatário XML #" + (i + 1),
|
||||
"",
|
||||
"Assinatura Enveloped XML",
|
||||
new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()),
|
||||
signerName,
|
||||
identifier,
|
||||
issuer,
|
||||
dateStr,
|
||||
coreValidity,
|
||||
coreValidity,
|
||||
signature.getSignedInfo().getSignatureMethod().getAlgorithm(),
|
||||
"XMLDSig Element",
|
||||
"Validade estrutural e criptográfica: " + (coreValidity ? "OK" : "FALHA")
|
||||
certSubject,
|
||||
(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;
|
||||
String msg = allValid ? "Documento XML assinado e válido conforme padrão W3C XML-DSig." : "Assinatura XML inválida ou com integridade violada.";
|
||||
return new ValidationResult(status, ValidationType.XML_DSIG, msg, signers, null, fileName);
|
||||
String msg = allValid
|
||||
? (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) {
|
||||
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.
|
||||
*/
|
||||
|
||||
+1
@@ -7,6 +7,7 @@ public enum ValidationType {
|
||||
DETACHED_CMS("Assinatura Destacada (CAdES / PKCS#7)", false),
|
||||
ATTACHED_CMS("Assinatura Incorporada (CMS / P7M)", 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),
|
||||
UNKNOWN("Formato Desconhecido", false);
|
||||
|
||||
|
||||
+17
-4
@@ -3,30 +3,43 @@ package br.com.cybercore.signer.core.xml;
|
||||
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(
|
||||
String certificateId,
|
||||
char[] pinOrPassword,
|
||||
String targetElementTag,
|
||||
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 XmlSignatureProfile DEFAULT_PROFILE = XmlSignatureProfile.XADES_BES;
|
||||
|
||||
public XmlSignatureOptions {
|
||||
Objects.requireNonNull(certificateId, "ID do certificado não pode ser nulo.");
|
||||
if (signatureAlgorithm == null || signatureAlgorithm.isBlank()) {
|
||||
signatureAlgorithm = DEFAULT_ALGORITHM;
|
||||
}
|
||||
if (profile == null) {
|
||||
profile = DEFAULT_PROFILE;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM);
|
||||
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM, DEFAULT_PROFILE);
|
||||
}
|
||||
|
||||
public static XmlSignatureOptions forElement(String certificateId, char[] pinOrPassword, String tag, String id, XmlSignatureProfile profile) {
|
||||
return new XmlSignatureOptions(certificateId, pinOrPassword, tag, id, DEFAULT_ALGORITHM, profile);
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package br.com.cybercore.signer.core.xml;
|
||||
|
||||
/**
|
||||
* Perfis de assinatura digital para documentos XML suportados pelo CyberCore Signer.
|
||||
*/
|
||||
public enum XmlSignatureProfile {
|
||||
|
||||
/**
|
||||
* Padrão XAdES-BES (XML Advanced Electronic Signatures - Basic Electronic Signature).
|
||||
* Em conformidade com a especificação ETSI TS 101 903 e padrão ICP-Brasil DOC-ICP-15.03 (AD-RB).
|
||||
* Inclui propriedades qualificadas assinadas: data/hora de assinatura (SigningTime)
|
||||
* e referência criptográfica do certificado do signatário (SigningCertificate).
|
||||
*/
|
||||
XADES_BES("XAdES-BES (Padrão ICP-Brasil / ETSI)", "Assinatura avançada com data/hora e certificado qualificado vinculados"),
|
||||
|
||||
/**
|
||||
* Padrão W3C XMLDSig convencional (Enveloped Signature sem propriedades qualificadas XAdES).
|
||||
* Mantido para compatibilidade com layouts fiscais estritos (ex: NF-e, CT-e, MDF-e SEFAZ legados).
|
||||
*/
|
||||
XMLDSIG("XMLDSig Clássico (W3C)", "Assinatura enveloped básica sem propriedades qualificadas");
|
||||
|
||||
private final String displayName;
|
||||
private final String description;
|
||||
|
||||
XmlSignatureProfile(String displayName, String description) {
|
||||
this.displayName = displayName;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
+268
-39
@@ -17,12 +17,14 @@ import javax.xml.crypto.KeySelector;
|
||||
import javax.xml.crypto.KeySelectorException;
|
||||
import javax.xml.crypto.KeySelectorResult;
|
||||
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.DigestMethod;
|
||||
import javax.xml.crypto.dsig.Reference;
|
||||
import javax.xml.crypto.dsig.SignatureMethod;
|
||||
import javax.xml.crypto.dsig.SignedInfo;
|
||||
import javax.xml.crypto.dsig.Transform;
|
||||
import javax.xml.crypto.dsig.XMLObject;
|
||||
import javax.xml.crypto.dsig.XMLSignature;
|
||||
import javax.xml.crypto.dsig.XMLSignatureFactory;
|
||||
import javax.xml.crypto.dsig.dom.DOMSignContext;
|
||||
@@ -43,25 +45,41 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.Key;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.PublicKey;
|
||||
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.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
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.
|
||||
* Suporta assinatura enveloped de documento completo e assinatura de elementos por ID (NF-e, SPED, etc.).
|
||||
* Serviço de alto nível para Assinatura Digital de Documentos XML segundo os padrões:
|
||||
* - 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).
|
||||
*/
|
||||
public class XmlSigningService {
|
||||
|
||||
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;
|
||||
|
||||
public XmlSigningService(CertificateManager certificateManager) {
|
||||
@@ -85,7 +103,7 @@ public class XmlSigningService {
|
||||
try (FileOutputStream fos = new FileOutputStream(targetXml)) {
|
||||
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();
|
||||
|
||||
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)
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
@@ -134,13 +153,13 @@ public class XmlSigningService {
|
||||
// 3. Configuração do XMLSignatureFactory
|
||||
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
|
||||
|
||||
// Transformações padrão: Enveloped + C14N
|
||||
List<Transform> transforms = new ArrayList<>();
|
||||
transforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null));
|
||||
transforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null));
|
||||
// Transformações padrão para os dados de negócio: Enveloped + C14N
|
||||
List<Transform> dataTransforms = new ArrayList<>();
|
||||
dataTransforms.add(fac.newTransform(Transform.ENVELOPED, (TransformParameterSpec) null));
|
||||
dataTransforms.add(fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null));
|
||||
|
||||
// Determinação da Referência e Ponto de Inserção
|
||||
Reference ref;
|
||||
// Determinação da Referência de Dados e Ponto de Inserção da Assinatura
|
||||
Reference dataRef;
|
||||
Node insertionParent;
|
||||
|
||||
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.");
|
||||
}
|
||||
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();
|
||||
} else if (options.targetElementTag() != null && !options.targetElementTag().isBlank()) {
|
||||
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")) {
|
||||
String idVal = elem.getAttribute("Id");
|
||||
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();
|
||||
} 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();
|
||||
}
|
||||
} else {
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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(
|
||||
fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, (C14NMethodParameterSpec) 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();
|
||||
List<Object> x509Content = new ArrayList<>();
|
||||
x509Content.add(keyMaterial.signingCertificate());
|
||||
X509Data x509Data = kif.newX509Data(x509Content);
|
||||
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);
|
||||
if (keyMaterial.jcaProvider() != null) {
|
||||
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);
|
||||
|
||||
// 7. Serialização do XML Assinado
|
||||
// 8. Serialização do XML Assinado
|
||||
TransformerFactory tf = TransformerFactory.newInstance();
|
||||
Transformer trans = tf.newTransformer();
|
||||
trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
|
||||
@@ -205,12 +269,13 @@ public class XmlSigningService {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
Objects.requireNonNull(signedXmlBytes, "Bytes do XML assinado não podem ser nulos.");
|
||||
@@ -218,24 +283,17 @@ public class XmlSigningService {
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
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);
|
||||
dbf.setXIncludeAware(false);
|
||||
dbf.setExpandEntityReferences(false);
|
||||
|
||||
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
|
||||
NodeList allElements = doc.getElementsByTagName("*");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Registra nós com atributo Id/id/ID para que o ResourceResolver localize as referências
|
||||
registerIdAttributes(doc.getDocumentElement());
|
||||
|
||||
NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
|
||||
NodeList nl = doc.getElementsByTagNameNS(XMLDSIG_NS, "Signature");
|
||||
if (nl.getLength() == 0) {
|
||||
nl = doc.getElementsByTagName("Signature");
|
||||
}
|
||||
@@ -248,8 +306,168 @@ public class XmlSigningService {
|
||||
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
|
||||
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);
|
||||
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) {
|
||||
@@ -264,6 +482,17 @@ public class XmlSigningService {
|
||||
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.
|
||||
*/
|
||||
|
||||
+76
-2
@@ -18,6 +18,8 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
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.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -41,6 +43,7 @@ class PdfSigningServiceTest {
|
||||
private CertificateManager certificateManager;
|
||||
private PdfSigningService pdfSigningService;
|
||||
private String testCertId;
|
||||
private String testCert2Id;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
@@ -74,13 +77,29 @@ class PdfSigningServiceTest {
|
||||
char[] pwd = "password123".toCharArray();
|
||||
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");
|
||||
provider.setKeyStore(ks, pwd);
|
||||
certificateManager.registerProvider(provider);
|
||||
|
||||
List<ManagedCertificate> list = certificateManager.discoverCertificates();
|
||||
assertThat(list).isNotEmpty();
|
||||
this.testCertId = list.get(0).id();
|
||||
assertThat(list).hasSize(2);
|
||||
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);
|
||||
}
|
||||
@@ -215,4 +234,59 @@ class PdfSigningServiceTest {
|
||||
byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
+77
@@ -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);
|
||||
}
|
||||
}
|
||||
+80
-9
@@ -72,8 +72,8 @@ class XmlSigningServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve assinar documento XML completo com XMLDSig Enveloped e validar com sucesso")
|
||||
void shouldSignWholeXmlDocumentAndValidate() throws Exception {
|
||||
@DisplayName("Deve assinar documento XML no padrão XAdES-BES por padrão e validar com sucesso")
|
||||
void shouldSignWholeXmlDocumentWithXadesBesByDefaultAndValidate() throws Exception {
|
||||
String xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<pedido id="ped-1001">
|
||||
@@ -86,19 +86,38 @@ class XmlSigningServiceTest {
|
||||
byte[] signedXmlBytes = xmlSigningService.signXml(xml.getBytes(StandardCharsets.UTF_8), options);
|
||||
|
||||
String signedXmlStr = new String(signedXmlBytes, StandardCharsets.UTF_8);
|
||||
|
||||
// Verifica tags essenciais do XMLDSig
|
||||
assertThat(signedXmlStr).contains("<Signature");
|
||||
assertThat(signedXmlStr).contains("<SignedInfo");
|
||||
assertThat(signedXmlStr).contains("<SignatureValue");
|
||||
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);
|
||||
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
|
||||
@DisplayName("Deve assinar nó XML específico com atributo Id (estilo NF-e) e validar")
|
||||
void shouldSignSpecificXmlElementById() throws Exception {
|
||||
@DisplayName("Deve assinar nó XML específico com atributo Id no padrão XAdES-BES e validar")
|
||||
void shouldSignSpecificXmlElementWithXadesBes() throws Exception {
|
||||
String nfeXml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<NFe xmlns="http://www.portalfiscal.inf.br/nfe">
|
||||
@@ -117,21 +136,48 @@ class XmlSigningServiceTest {
|
||||
testCertId,
|
||||
null,
|
||||
"infNFe",
|
||||
"NFe35261012345678000195550010000000011000000010"
|
||||
"NFe35261012345678000195550010000000011000000010",
|
||||
XmlSignatureProfile.XADES_BES
|
||||
);
|
||||
|
||||
byte[] signedNfe = xmlSigningService.signXml(nfeXml.getBytes(StandardCharsets.UTF_8), options);
|
||||
String signedStr = new String(signedNfe, StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(signedStr).contains("URI=\"#NFe35261012345678000195550010000000011000000010\"");
|
||||
assertThat(signedStr).contains("QualifyingProperties");
|
||||
assertThat(signedStr).contains("SignedProperties");
|
||||
|
||||
boolean isValid = xmlSigningService.validateXmlSignature(signedNfe);
|
||||
assertThat(isValid).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Deve detectar adulteração em XML assinado e reprovar na validação")
|
||||
void shouldDetectTamperingInSignedXml() throws Exception {
|
||||
@DisplayName("Deve assinar documento XML no perfil XMLDSig clássico quando explicitamente solicitado")
|
||||
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 = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<contrato>
|
||||
@@ -142,11 +188,36 @@ class XmlSigningServiceTest {
|
||||
XmlSignatureOptions options = XmlSignatureOptions.wholeDocument(testCertId, null);
|
||||
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)
|
||||
.replace("R$ 100,00", "R$ 999.999,00");
|
||||
|
||||
boolean isValid = xmlSigningService.validateXmlSignature(tampered.getBytes(StandardCharsets.UTF_8));
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
+20
-5
@@ -104,7 +104,7 @@ public class CyberCoreDesktopApplication extends Application {
|
||||
mainAppView = new MainAppView(primaryStage);
|
||||
|
||||
// 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.setOnSignFile(() -> new FileSignDialog(primaryStage, "Assinar Arquivo", "*.*", SvgIcons.fileIcon(), 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) {
|
||||
Stage dlg = new Stage();
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
@@ -178,8 +191,9 @@ public class CyberCoreDesktopApplication extends Application {
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
|
||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||
btnClose.getStyleClass().add("btn-secondary");
|
||||
btnClose.setPrefWidth(95);
|
||||
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||
btnClose.setPrefWidth(110);
|
||||
btnClose.setMinWidth(110);
|
||||
btnClose.setOnAction(e -> dlg.close());
|
||||
|
||||
HBox footer = new HBox(btnClose);
|
||||
@@ -206,8 +220,9 @@ public class CyberCoreDesktopApplication extends Application {
|
||||
|
||||
AboutView aboutView = new AboutView();
|
||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||
btnClose.getStyleClass().add("btn-primary");
|
||||
btnClose.setPrefWidth(95);
|
||||
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||
btnClose.setPrefWidth(110);
|
||||
btnClose.setMinWidth(110);
|
||||
btnClose.setOnAction(e -> dlg.close());
|
||||
|
||||
HBox footer = new HBox(btnClose);
|
||||
|
||||
+99
@@ -729,4 +729,103 @@ public final class SvgIcons {
|
||||
box.getChildren().add(cross);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -386,7 +386,7 @@ public final class BatchSigningDialog {
|
||||
Path dest = targetDir.resolve(outPdfName);
|
||||
pdfService.signPdfFile(item.getFile(), dest.toFile(), options);
|
||||
} 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 =
|
||||
new br.com.cybercore.signer.core.xml.XmlSigningService(certManager);
|
||||
br.com.cybercore.signer.core.xml.XmlSignatureOptions options =
|
||||
|
||||
+3
-2
@@ -129,8 +129,9 @@ public class CertificateDetailsDialog {
|
||||
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||
btnClose.getStyleClass().add("btn-secondary");
|
||||
btnClose.setPrefWidth(95);
|
||||
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||
btnClose.setPrefWidth(110);
|
||||
btnClose.setMinWidth(110);
|
||||
btnClose.setOnAction(e -> stage.close());
|
||||
|
||||
actionsRow.getChildren().add(btnClose);
|
||||
|
||||
+6
-3
@@ -63,12 +63,15 @@ public final class LogsViewerDialog {
|
||||
footer.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
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());
|
||||
|
||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||
btnClose.getStyleClass().add("btn-primary");
|
||||
btnClose.setPrefWidth(95);
|
||||
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||
btnClose.setPrefWidth(110);
|
||||
btnClose.setMinWidth(110);
|
||||
btnClose.setOnAction(e -> stage.close());
|
||||
|
||||
footer.getChildren().addAll(btnRefresh, btnClose);
|
||||
|
||||
+590
@@ -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();
|
||||
}
|
||||
}
|
||||
+818
-690
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -222,8 +222,9 @@ public final class SealManagementDialog {
|
||||
footer.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
Button btnClose = new Button(" Fechar", SvgIcons.closeIcon(13));
|
||||
btnClose.getStyleClass().add("btn-secondary");
|
||||
btnClose.setPrefWidth(95);
|
||||
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||
btnClose.setPrefWidth(110);
|
||||
btnClose.setMinWidth(110);
|
||||
btnClose.setOnAction(e -> stage.close());
|
||||
|
||||
footer.getChildren().add(btnClose);
|
||||
|
||||
+34
-5
@@ -124,7 +124,7 @@ public final class SettingsView extends VBox {
|
||||
HBox pinActionsBox = new HBox(12);
|
||||
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.setStyle("-fx-padding: 6px 14px; -fx-font-size: 12px;");
|
||||
|
||||
@@ -133,8 +133,9 @@ public final class SettingsView extends VBox {
|
||||
|
||||
btnClearPins.setOnAction(e -> {
|
||||
PinSessionManager.getInstance().clearAll();
|
||||
br.com.cybercore.signer.core.security.SecurePinStorage.getInstance().clearAll();
|
||||
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);
|
||||
@@ -193,6 +194,8 @@ public final class SettingsView extends VBox {
|
||||
private void refreshSealsList() {
|
||||
sealsContainer.getChildren().clear();
|
||||
List<PdfSealTheme> themes = PdfSealThemeRegistry.listThemes();
|
||||
AgentConfigurationService configService = AgentConfigurationService.getInstance();
|
||||
String currentDefaultId = configService.getConfig().defaultSealThemeId();
|
||||
|
||||
for (PdfSealTheme theme : themes) {
|
||||
HBox item = new HBox(10);
|
||||
@@ -200,6 +203,8 @@ public final class SettingsView extends VBox {
|
||||
item.getStyleClass().add("diagnostic-item");
|
||||
item.setPadding(new Insets(6, 12, 6, 12));
|
||||
|
||||
boolean isDefault = theme.id().equalsIgnoreCase(currentDefaultId);
|
||||
|
||||
VBox info = new VBox(2);
|
||||
Label lblName = new Label(theme.name() + " (" + theme.id() + ")");
|
||||
lblName.getStyleClass().add("cert-title");
|
||||
@@ -211,10 +216,34 @@ public final class SettingsView extends VBox {
|
||||
info.getChildren().addAll(lblName, lblDesc);
|
||||
HBox.setHgrow(info, Priority.ALWAYS);
|
||||
|
||||
Label badge = new Label(theme.isCustom() ? "Personalizado" : "Sistema");
|
||||
badge.getStyleClass().add(theme.isCustom() ? "diagnostic-value-warn" : "diagnostic-value-ok");
|
||||
Label badgeType = new Label(theme.isCustom() ? "Personalizado" : "Sistema");
|
||||
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()) {
|
||||
Button btnDelete = new Button("Excluir");
|
||||
|
||||
+3
-2
@@ -77,8 +77,9 @@ public final class TutorialDialog {
|
||||
HBox footer = new HBox();
|
||||
footer.setAlignment(Pos.CENTER_RIGHT);
|
||||
Button btnClose = new Button(" Entendi", SvgIcons.closeIcon(13));
|
||||
btnClose.getStyleClass().add("btn-primary");
|
||||
btnClose.setPrefWidth(95);
|
||||
btnClose.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||
btnClose.setPrefWidth(110);
|
||||
btnClose.setMinWidth(110);
|
||||
btnClose.setOnAction(e -> stage.close());
|
||||
footer.getChildren().add(btnClose);
|
||||
|
||||
|
||||
+56
-12
@@ -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.security.PinSessionManager;
|
||||
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.ui.components.SvgIcons;
|
||||
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.RadioButton;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
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;
|
||||
@@ -66,7 +70,9 @@ public final class XmlSignDialog {
|
||||
private PasswordField pinField;
|
||||
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 rbSpecificElement;
|
||||
private TextField txtElementTag;
|
||||
@@ -92,10 +98,10 @@ public final class XmlSignDialog {
|
||||
if (owner != null) {
|
||||
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.setMinWidth(720);
|
||||
this.stage.setMinHeight(580);
|
||||
this.stage.setMinHeight(600);
|
||||
|
||||
initLayout();
|
||||
}
|
||||
@@ -109,9 +115,9 @@ public final class XmlSignDialog {
|
||||
HBox header = new HBox(14);
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
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");
|
||||
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");
|
||||
titleBox.getChildren().addAll(lblTitle, lblSub);
|
||||
header.getChildren().addAll(SvgIcons.xmlIcon(), titleBox);
|
||||
@@ -196,7 +202,21 @@ public final class XmlSignDialog {
|
||||
// 3. Seção: Modo de Assinatura XMLDSig
|
||||
VBox xmlModeSection = new VBox(12);
|
||||
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");
|
||||
|
||||
ToggleGroup modeGroup = new ToggleGroup();
|
||||
@@ -214,6 +234,16 @@ public final class XmlSignDialog {
|
||||
elementGrid.setHgap(12);
|
||||
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:");
|
||||
lblTag.getStyleClass().add("dialog-field-title");
|
||||
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.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);
|
||||
scrollPane.setContent(formContent);
|
||||
@@ -254,11 +293,12 @@ public final class XmlSignDialog {
|
||||
HBox.setHgrow(lblStatus, Priority.ALWAYS);
|
||||
|
||||
btnCancel = new Button("Cancelar");
|
||||
btnCancel.getStyleClass().add("btn-secondary");
|
||||
btnCancel.getStyleClass().addAll("btn-secondary", "btn-dialog");
|
||||
btnCancel.setOnAction(e -> stage.close());
|
||||
|
||||
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.setOnAction(e -> executeXmlSignature());
|
||||
|
||||
@@ -376,6 +416,10 @@ public final class XmlSignDialog {
|
||||
pin = pinField.getText().toCharArray();
|
||||
}
|
||||
|
||||
XmlSignatureProfile profile = rbXadesBes.isSelected()
|
||||
? XmlSignatureProfile.XADES_BES
|
||||
: XmlSignatureProfile.XMLDSIG;
|
||||
|
||||
XmlSignatureOptions options;
|
||||
if (rbSpecificElement.isSelected()) {
|
||||
String tag = txtElementTag.getText().trim();
|
||||
@@ -383,14 +427,14 @@ public final class XmlSignDialog {
|
||||
if (tag.isBlank()) {
|
||||
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 {
|
||||
options = XmlSignatureOptions.wholeDocument(cert.id(), pin);
|
||||
options = XmlSignatureOptions.wholeDocument(cert.id(), pin, profile);
|
||||
}
|
||||
|
||||
setFormDisabled(true);
|
||||
progressIndicator.setVisible(true);
|
||||
lblStatus.setText("Assinando documento XML (XMLDSig)...");
|
||||
lblStatus.setText("Assinando documento XML (" + profile.getDisplayName() + ")...");
|
||||
lblStatus.setStyle("-fx-text-fill: -fx-accent;");
|
||||
|
||||
boolean validate = chkValidateAfter.isSelected();
|
||||
|
||||
@@ -853,6 +853,11 @@
|
||||
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 {
|
||||
-fx-background-color: #0284c7;
|
||||
-fx-text-fill: #ffffff;
|
||||
@@ -860,6 +865,8 @@
|
||||
-fx-padding: 8px 18px;
|
||||
-fx-background-radius: 6px;
|
||||
-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);
|
||||
}
|
||||
|
||||
@@ -873,6 +880,8 @@
|
||||
-fx-font-weight: bold;
|
||||
-fx-padding: 8px 18px;
|
||||
-fx-background-radius: 6px;
|
||||
-fx-min-width: -fx-pref-size;
|
||||
-fx-alignment: center;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
@@ -901,6 +910,8 @@
|
||||
-fx-text-fill: #475569;
|
||||
-fx-font-weight: bold;
|
||||
-fx-padding: 8px 18px;
|
||||
-fx-min-width: -fx-pref-size;
|
||||
-fx-alignment: center;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
@@ -919,6 +930,29 @@
|
||||
-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 */
|
||||
.btn-danger {
|
||||
-fx-background-color: #fee2e2;
|
||||
@@ -931,6 +965,7 @@
|
||||
-fx-font-weight: bold;
|
||||
-fx-padding: 6px 14px;
|
||||
-fx-alignment: center;
|
||||
-fx-min-width: -fx-pref-size;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
@@ -1063,3 +1098,131 @@
|
||||
-fx-background-color: #0284c7;
|
||||
-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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user