feat(xml): implementar assinatura digital no padrão XAdES-BES (ETSI TS 101 903 / ICP-Brasil)

This commit is contained in:
Claudecio Martins
2026-10-11 11:28:35 -03:00
parent 0cd4025f3c
commit 3fab68c118
9 changed files with 626 additions and 93 deletions
@@ -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.
*/
@@ -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);
@@ -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);
}
}
@@ -0,0 +1,37 @@
package br.com.cybercore.signer.core.xml;
/**
* Perfis de assinatura digital para documentos XML suportados pelo CyberCore Signer.
*/
public enum XmlSignatureProfile {
/**
* Padrão XAdES-BES (XML Advanced Electronic Signatures - Basic Electronic Signature).
* Em conformidade com a especificação ETSI TS 101 903 e padrão ICP-Brasil DOC-ICP-15.03 (AD-RB).
* Inclui propriedades qualificadas assinadas: data/hora de assinatura (SigningTime)
* e referência criptográfica do certificado do signatário (SigningCertificate).
*/
XADES_BES("XAdES-BES (Padrão ICP-Brasil / ETSI)", "Assinatura avançada com data/hora e certificado qualificado vinculados"),
/**
* Padrão W3C XMLDSig convencional (Enveloped Signature sem propriedades qualificadas XAdES).
* Mantido para compatibilidade com layouts fiscais estritos (ex: NF-e, CT-e, MDF-e SEFAZ legados).
*/
XMLDSIG("XMLDSig Clássico (W3C)", "Assinatura enveloped básica sem propriedades qualificadas");
private final String displayName;
private final String description;
XmlSignatureProfile(String displayName, String description) {
this.displayName = displayName;
this.description = description;
}
public String getDisplayName() {
return displayName;
}
public String getDescription() {
return description;
}
}
@@ -17,12 +17,14 @@ import javax.xml.crypto.KeySelector;
import javax.xml.crypto.KeySelectorException;
import javax.xml.crypto.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.
*/
@@ -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();
}
}