Firma digital CAdES vs XAdES en Java: las diferencias que importan cuando tu CA te pide una y vos tenés la otra
Una firma digital es básicamente como un sello de lacre con ADN: no importa si el sobre viaja en tren, avión o en la mochila de alguien — si el destinatario rompe el lacre o cambia el contenido, se nota. El problema es que hay dos tipos de lacre en el mundo CMS/XML, se llaman igual en los folletos ("firma avanzada ETSI"), pero no los podés mezclar. Y cuando la Autoridad de Certificación te devuelve un error de validación, la respuesta no está en el mensaje de error — está en haber elegido el formato equivocado desde el principio.
Ya me pasó: mandé un .p7s detached a un sistema que esperaba un nodo <ds:Signature> embebido en el XML. La firma era criptográficamente perfecta. El receptor la rechazó igual, porque no la sabía leer. Ahí entendí que el problema nunca es la criptografía — es el contrato de formato.
Mi tesis es concreta: CAdES y XAdES no son variantes del mismo estándar, son formatos para dominios distintos con estructuras y perfiles de confianza distintos. Elegir mal no es un detalle técnico: es un documento que no pasa validación del otro lado, aunque la firma sea impecable. Y esa decisión se toma antes de escribir código, no debugueando después.
Qué es CAdES y qué es XAdES — sin folklore
Antes de entrar al código, vale la pena separar lo que los estándares realmente dicen de lo que se repite en foros.
CAdES (CMS Advanced Electronic Signatures) es la extensión del formato CMS/PKCS#7 para firmas avanzadas. El estándar de referencia es ETSI EN 319 122. Produce un archivo binario — típicamente .p7s o .p7m — que puede contener el documento original (firma enveloping) o referenciar un archivo externo (firma detached).
XAdES (XML Advanced Electronic Signatures) es la extensión del formato XMLDSig para firmas avanzadas. Produce un XML. Puede envolver el contenido original dentro del XML (enveloping), estar dentro del documento XML que firma (enveloped) o referenciar el documento externamente (detached).
La diferencia estructural es la que más duele en la práctica:
| Característica | CAdES | XAdES |
|---|---|---|
| Formato base | CMS / PKCS#7 (binario) | XMLDSig (XML) |
| Extensión típica |
.p7s, .p7m, .csig
|
.xades, .xml
|
| Documento firmable | Cualquier binario o texto | Nativamente XML; binarios como Base64 |
| Perfil común en LATAM | Firma de documentos PDF, archivos | Facturación electrónica, contratos XML |
| Referencia ETSI | EN 319 122 | EN 319 132 |
Esta tabla no es de marketing. Es el primer punto de decisión cuando la CA te pide "una firma avanzada" y no especifica el formato.
Cuándo la CA dice "firma avanzada" y no dice cuál
El error más común que vi (y que cometí) es asumir que si algo cumple ETSI, sirve para cualquier caso de uso. No es así — ETSI certifica el estándar de firma, no el contrato de formato que espera el sistema receptor.
Hay contextos donde el formato está prescripto por regulación o por el sistema receptor:
-
Facturación electrónica en muchos países de la región usa XAdES porque el comprobante ya es XML. El sistema de recepción espera encontrar un nodo
<Signature>dentro del XML, no un.p7sadjunto. - Firma de archivos binarios (PDFs que no usan PAdES, ejecutables, ZIPs de expedientes) tiende a usar CAdES porque puede envolver cualquier binario sin transformarlo.
- Interoperabilidad con sistemas europeos (eIDAS, TSL): la mayoría de los perfiles de validación reconocen ambos, pero los flujos de firma de documentos suelen usar CAdES o PAdES, no XAdES.
El punto práctico: antes de abrir el IDE, preguntale a la CA qué formato esperan en el campo SignedData o ds:Signature, y cuál es el perfil esperado (B, T, LT o LTA). Esa pregunta te ahorra horas de debug después.
DSS: la librería que implementa ambos en Java
La librería DSS de la Comisión Europea es la implementación de referencia en Java para CAdES, XAdES, PAdES y JAdES. Es open source (LGPL), está mantenida activamente y tiene un cookbook oficial con ejemplos ejecutables.
Es lo que usan los sistemas de firma de varios estados europeos. Si trabajás en un contexto que requiere interoperabilidad con infraestructuras eIDAS, DSS es prácticamente el estándar de facto.
Agregalo al proyecto:
<!-- pom.xml -->
<dependency>
<!-- Módulo CAdES de la librería DSS -->
<groupId>eu.europa.esig.dss</groupId>
<artifactId>dss-cades</artifactId>
<version>5.13</version>
</dependency>
<dependency>
<!-- Módulo XAdES -->
<groupId>eu.europa.esig.dss</groupId>
<artifactId>dss-xades</artifactId>
<version>5.13</version>
</dependency>
Nota: La versión 5.13 es la más reciente estable al momento de escribir esto. Verificá la versión actual en el repositorio oficial DSS.
Firmar con CAdES en Java — ejemplo mínimo reproducible
// Firma CAdES-B (baseline, sin timestamp) usando DSS
import eu.europa.esig.dss.cades.CAdESSignatureParameters;
import eu.europa.esig.dss.cades.signature.CAdESService;
import eu.europa.esig.dss.enumerations.DigestAlgorithm;
import eu.europa.esig.dss.enumerations.SignatureLevel;
import eu.europa.esig.dss.enumerations.SignaturePackaging;
import eu.europa.esig.dss.model.DSSDocument;
import eu.europa.esig.dss.model.FileDocument;
import eu.europa.esig.dss.model.SignatureValue;
import eu.europa.esig.dss.model.ToBeSigned;
import eu.europa.esig.dss.token.DSSPrivateKeyEntry;
import eu.europa.esig.dss.token.Pkcs12SignatureToken;
import java.io.File;
import java.io.IOException;
import java.security.KeyStore;
public class CadesSignerDemo {
public static DSSDocument firmarConCAdES(
File archivoBinario,
File archivoPkcs12,
String password
) throws IOException {
// 1. Cargar el token PKCS#12 con la clave privada
try (Pkcs12SignatureToken token = new Pkcs12SignatureToken(
archivoPkcs12, new KeyStore.PasswordProtection(password.toCharArray()))) {
DSSPrivateKeyEntry clavePrivada = token.getKeys().get(0);
// 2. Definir parámetros de firma CAdES
CAdESSignatureParameters parametros = new CAdESSignatureParameters();
parametros.setSignatureLevel(SignatureLevel.CAdES_BASELINE_B); // perfil B sin TSA
parametros.setSignaturePackaging(SignaturePackaging.DETACHED); // no envuelve el archivo
parametros.setDigestAlgorithm(DigestAlgorithm.SHA256);
parametros.setSigningCertificate(clavePrivada.getCertificate());
parametros.setCertificateChain(clavePrivada.getCertificateChain());
// 3. Cargar el documento a firmar
DSSDocument documento = new FileDocument(archivoBinario);
// 4. Calcular el hash que se va a firmar (ToBeSigned)
CAdESService servicio = new CAdESService(null); // null = sin validación de cadena
ToBeSigned datosAFirmar = servicio.getDataToSign(documento, parametros);
// 5. Firmar con la clave privada
SignatureValue valorFirma = token.sign(
datosAFirmar, parametros.getDigestAlgorithm(), clavePrivada);
// 6. Construir el documento firmado (.p7s detached)
return servicio.signDocument(documento, parametros, valorFirma);
}
}
}
Firmar con XAdES en Java — mismo flujo, distinto formato
// Firma XAdES-B (baseline) — el documento es XML o cualquier contenido como detached
import eu.europa.esig.dss.xades.XAdESSignatureParameters;
import eu.europa.esig.dss.xades.signature.XAdESService;
import eu.europa.esig.dss.enumerations.SignatureLevel;
import eu.europa.esig.dss.enumerations.SignaturePackaging;
public class XadesSignerDemo {
public static DSSDocument firmarConXAdES(
File archivoXML,
File archivoPkcs12,
String password
) throws IOException {
try (Pkcs12SignatureToken token = new Pkcs12SignatureToken(
archivoPkcs12, new KeyStore.PasswordProtection(password.toCharArray()))) {
DSSPrivateKeyEntry clavePrivada = token.getKeys().get(0);
// XAdES-ENVELOPED: la firma queda dentro del XML original
XAdESSignatureParameters parametros = new XAdESSignatureParameters();
parametros.setSignatureLevel(SignatureLevel.XAdES_BASELINE_B);
parametros.setSignaturePackaging(SignaturePackaging.ENVELOPED); // nodo dentro del XML
parametros.setDigestAlgorithm(DigestAlgorithm.SHA256);
parametros.setSigningCertificate(clavePrivada.getCertificate());
parametros.setCertificateChain(clavePrivada.getCertificateChain());
DSSDocument documento = new FileDocument(archivoXML);
XAdESService servicio = new XAdESService(null);
ToBeSigned datosAFirmar = servicio.getDataToSign(documento, parametros);
SignatureValue valorFirma = token.sign(
datosAFirmar, parametros.getDigestAlgorithm(), clavePrivada);
// El resultado es un XML con el nodo <ds:Signature> embebido
return servicio.signDocument(documento, parametros, valorFirma);
}
}
}
El código es casi idéntico entre los dos casos. La divergencia real está en el SignaturePackaging y en lo que produce cada servicio: CAdES te da un binario CMS, XAdES un XML con la firma adentro.
Los errores de validación más comunes — y por qué aparecen
1. Perfil equivocado: B cuando la CA pide LT
El perfil Baseline-B no incluye timestamp ni información de revocación incorporada. Si la CA valida contra el perfil LT o LTA, el documento va a fallar con algo parecido a INDETERMINATE / NO_REVOCATION_DATA. Para LT necesitás una TSA (Timestamp Authority) configurada en el servicio:
// Configurar TSA para obtener perfil CAdES-LT (incluye timestamp ETSI)
OnlineTSPSource tspSource = new OnlineTSPSource("http://timestamp.digicert.com");
CAdESService servicio = new CAdESService(validacionCadena);
servicio.setTspSource(tspSource);
// Cambiar el nivel al momento de firmar
parametros.setSignatureLevel(SignatureLevel.CAdES_BASELINE_LT);
2. CAdES enveloping sobre un XML — el error silencioso
Si usás SignaturePackaging.ENVELOPING en CAdES sobre un XML, el XML queda tratado como un blob binario dentro del CMS. El receptor que espera un <ds:Signature> en el XML no lo va a encontrar. No hay error de firma — la firma es técnicamente válida. El error es semántico: el formato no coincide con lo que el sistema receptor sabe parsear. Este es el caso que mencioné al principio, y el que más tiempo me hizo perder.
3. Canonicalización en XAdES Enveloped
XAdES Enveloped requiere una transformación de canonicalización (c14n) antes de hashear el contenido. Si el XML tiene declaraciones de namespace inconsistentes o el parser lo reordena, el hash difiere del original y la validación falla con FAILED / HASH_FAILURE. DSS lo maneja automáticamente, pero si armás el XML manualmente y lo pasás a DSS, asegurate de no tocar el árbol DOM entre la canonicalización y el paso de firma.
4. La cadena de certificados incompleta
Tanto CAdES como XAdES necesitan la cadena completa de certificados en el sobre (signingCertificate + certificateChain). Si omitís el certificado intermedio, la validación del lado receptor puede fallar con INDETERMINATE / NO_CERTIFICATE_CHAIN_FOUND, aunque la firma criptográfica sea correcta. DSS tiene métodos para incluir la cadena; no los te la salteés.
Matriz de decisión: CAdES o XAdES
Antes de escribir una línea de código, pasá por este checklist:
¿Cuál es el tipo de documento que firmás?
- Binario arbitrario (PDF sin PAdES, ZIP, imagen, ejecutable) → CAdES detached o enveloping
- XML nativo (comprobante fiscal, contrato estructurado, mensaje SOAP) → XAdES enveloped o enveloping
¿Qué espera el sistema receptor?
- Un archivo
.p7sseparado del documento → CAdES detached - El XML con la firma adentro → XAdES enveloped
- Un único archivo que contenga firma + documento → CAdES enveloping o XAdES enveloping
¿Cuál es el perfil de confianza requerido?
- Solo firma (sin timestamp) → Baseline-B
- Firma + timestamp → Baseline-T
- Firma + timestamp + datos de revocación incrustados → Baseline-LT
- Long-Term Archival (para períodos mayores a la vida del certificado) → Baseline-LTA
¿La CA o regulación especifican el formato?
- Si la CA te da un spec, seguila. El análisis técnico propio sirve para entender por qué, no para contradecirla.
Lo que no podés concluir sin experimento propio
Esta guía tiene límites que prefiero decir en voz alta:
- No afirmo que un perfil sea "más seguro" que el otro. Ambos son firmas avanzadas bajo ETSI; la seguridad real depende de la implementación del algoritmo, la custodia de la clave y la TSA usada.
-
Los ejemplos de código usan
nullcomo validador de cadena. En un escenario de validación real necesitás configurar unCertificateVerifiercon fuentes de CRL/OCSP. Ese componente depende de la infraestructura de la CA específica. - El comportamiento de DSS puede variar según la versión. Verificá el cookbook oficial para la versión que uses — los APIs cambian entre versiones menores, y ya me mordió alguna vez un método deprecado sin aviso.
-
No todos los sistemas receptores implementan la validación igual. Que el documento pase en el
dss-demo-webappno garantiza que pase en el sistema de la CA receptora si tiene una implementación propia.
FAQ
¿Puedo usar CAdES para firmar un XML?
Sí, técnicamente. CAdES puede firmar cualquier binario, incluyendo XML. El problema es semántico: si el sistema receptor espera un XAdES con un nodo <ds:Signature> dentro del XML, un .p7s externo no va a satisfacer ese contrato, aunque la firma sea criptográficamente válida.
¿DSS soporta ambos formatos con el mismo token PKCS#12?
Sí. El Pkcs12SignatureToken de DSS es agnóstico al formato de firma. La misma clave privada puede usarse para CAdES, XAdES, PAdES o JAdES. Lo que cambia es el SignatureParameters y el Service que construís.
¿Cuál es la diferencia entre CAdES-B y CAdES-LT en términos de validación?
CAdES-B solo incluye la firma y el certificado de firma. CAdES-LT agrega un timestamp ETSI y los datos de revocación (CRL u OCSP) incrustados en el sobre CMS. Esto permite validar el documento incluso si el certificado ya expiró o la CA no está disponible al momento de la verificación.
¿Qué significa "detached" vs "enveloping" en la práctica?
En una firma detached, el documento original no cambia: la firma es un archivo separado. En una firma enveloping, el documento original queda encapsulado dentro del sobre de firma. Para archivos que necesitás preservar intactos (por ejemplo, para otros sistemas que los procesan), detached es más seguro.
¿Por qué la validación pasa en mi código pero falla en el sistema de la CA?
Las razones más frecuentes son: (1) perfil distinto al esperado (B vs LT), (2) cadena de certificados incompleta en el sobre, (3) el sistema receptor implementa validación custom y no soporta todas las extensiones del estándar, (4) canonicalización incorrecta en XAdES. El primer paso de diagnóstico es correr el documento contra el validador de referencia DSS antes de enviarlo.
¿XAdES enveloped modifica el XML original?
Sí. XAdES enveloped agrega un nodo <ds:Signature> al árbol XML original. Si el documento tiene un esquema XSD estricto que no prevé ese nodo, la firma puede invalidar la validación estructural del XML. En esos casos, XAdES detached o enveloping son alternativas más seguras.
Conclusión: la pregunta correcta antes del primer getDataToSign()
No es "¿cuál formato es mejor?". Es "¿qué espera el sistema receptor y en qué perfil?".
CAdES y XAdES resuelven el mismo problema criptográfico bajo ETSI, pero para dominios de documentos distintos: uno viene del mundo CMS/PKCS#7, donde cualquier binario entra sin transformación; el otro viene del mundo XML, donde la firma convive con el contenido en el mismo árbol. Mezclarlos no rompe la criptografía, pero rompe el contrato con el receptor — y esa es la parte que ningún log te va a explicar sola.
Mi recomendación práctica, la que aplico yo antes de tocar el teclado: pedile a la CA el perfil exacto (formato + nivel baseline), chequeá el tipo de documento que firmás y configurá el validador de DSS con fuentes de CRL/OCSP reales, no con null. El código de firma es la parte fácil. Lo que se lleva el tiempo real es entender qué espera el otro lado de la conexión — y esa pregunta hay que hacerla antes, no después del primer rechazo.
Si estás construyendo una integración más amplia donde la firma es solo una capa — por ejemplo, junto con autenticación, logging o caching — los posts sobre Web Crypto API en browser vs Node.js y arquitectura de identidad digital te dan el contexto de cómo encajan estas piezas en un sistema más grande.
Fuentes originales:
- European Commission DSS library: https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/Digital+Signature+Service+-++DSS
- ETSI EN 319 122 – CAdES standard: https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf
Este artículo fue publicado originalmente en juanchi.dev
Top comments (0)