Tutorial: Firma XML con XAdES en Delphi

Recorrido práctico que parte de una factura XML en claro y produce un documento XAdES-B-T — el nivel exigido por FatturaPA, FACTUR-X y la mayoría de portales de facturación electrónica en Europa. Elegirás entre empaquetado enveloped, enveloping o detached, adjuntarás un sello de tiempo RFC 3161 y verificarás el resultado para saber exactamente lo que verá tu contraparte.

XAdES-B-B / B-T / B-LT / B-LTA
Enveloped / Enveloping / Detached
Delphi 7 – RAD Studio 13

Tres modos de empaquetado, un único motor

XAdES extiende la especificación W3C XML-DSig con sellado de tiempo y datos de validación a largo plazo. Antes de firmar nada, decide dónde vivirá el elemento ds:Signature.

Enveloped

El elemento ds:Signature se añade dentro de la raíz del documento. Es el formato usado por FatturaPA, FACTUR-X-XAdES y la mayoría de flujos de facturación electrónica. El XML firmado sigue siendo una copia válida del documento original — los lectores ignoran el elemento de firma que no entienden.

Enveloping

El XML original se convierte en un hijo ds:Object dentro de la ds:Signature. Útil cuando necesitas un único sobre autocontenido y no te preocupa que los consumidores existentes puedan seguir parseando el documento.

Detached

La firma queda en un archivo separado y apunta al documento original mediante una referencia URI. Habitual en SAML, ebXML y cualquier flujo donde el documento fuente debe permanecer intacto byte a byte.

Añade las units

XAdES necesita el firmador XML, el perfil XAdES y un proveedor de claves. En este tutorial usamos PFX; sustitúyelo por cualquiera de los 10 proveedores si tienes una tarjeta inteligente o un HSM en la nube.

Cláusula uses de Delphi

sgcSign_XML es el núcleo de firma XML. sgcSign_Profile_XAdES expone el nivel, las transformadas y la configuración del sello de tiempo. sgcSign_KeyProvider_PFX carga un archivo PKCS#12 desde disco — el punto de partida más común.

Para niveles a largo plazo (B-LT, B-LTA) necesitas además sgcSign_OCSP y sgcSign_CRL para que los datos de revocación puedan obtenerse y embeberse.

uXAdESSigning.pas
uses
  Classes, SysUtils,
  // sgc
  sgcSign_KeyProvider_PFX,
  sgcSign_XML,
  sgcSign_Profile_XAdES,
  sgcSign_TSA,
  sgcSign_OCSP,
  sgcSign_CRL,
  sgcSign_Verifier;

Carga el XML y la clave

Dos entradas — el XML que quieres firmar y el certificado PFX con el que firmas.

Proveedor de claves PFX

TsgcPFXKeyProvider importa el archivo PKCS#12 vía Windows CNG, lo que significa que obtienes un handle de firma moderno independientemente del CSP con el que se emitió originalmente el certificado. El mismo proveedor funciona en Windows 7 en adelante, 32 y 64 bits.

Mantén el proveedor vivo durante toda la operación de firma — el firmador XML referencia el handle de clave subyacente hasta que SignXML retorna.

step1-load.pas
var
  vXML: string;
  vKeyProvider: TsgcPFXKeyProvider;
begin
  vXML := TFile.ReadAllText('invoice.xml', TEncoding.UTF8);

  vKeyProvider := TsgcPFXKeyProvider.Create(nil);
  vKeyProvider.FileName := 'certificate.pfx';
  vKeyProvider.Password := 'secret';
  vKeyProvider.LoadFromFile;
end;

Elige XAdES-B-T o XAdES-B-LT

B-T añade un sello de tiempo de firma. B-LT embebe además la cadena de la CA y los datos de revocación para que la firma siga siendo verificable durante años.

Configuración del perfil

Level escoge el nivel AdES: xalB, xalT, xalLT, xalLTA. Packaging escoge xpkEnveloped (por defecto), xpkEnveloping o xpkDetached.

La lista Transforms por defecto contiene xtEnvelopedSignature + xtC14NExclusive, que es lo que la mayoría de esquemas de facturación electrónica exigen. Sólo cámbialo cuando un perfil país específico pida C14N inclusivo o una transformada XSLT personalizada.

Para B-LT, OCSP.AutoFetch := True indica al firmador que obtenga respuestas OCSP para cada certificado de la cadena y las embeba dentro del elemento RevocationValues. El XML firmado lleva entonces todo lo que un verificador necesita — sin llamadas de red en tiempo de validación.

step2-profile.pas
var
  vProfile: TsgcSignProfile_XAdES;
begin
  vProfile := TsgcSignProfile_XAdES.Create(nil);
  vProfile.Level := xalT;            // or xalLT, xalLTA
  vProfile.Packaging := xpkEnveloped;
  vProfile.HashAlgorithm := shaSHA256;

  // Timestamp authority for B-T and above
  vProfile.TSA.URL := 'https://freetsa.org/tsr';
  vProfile.TSA.HashAlgorithm := shaSHA256;

  // For B-LT: embed full chain and revocation data
  vProfile.OCSP.AutoFetch := True;
  vProfile.CRL.AutoFetch := True;
end;

Firma — enveloped, enveloping y detached

El mismo componente TsgcSignXML maneja los tres modos de empaquetado — sólo cambia la propiedad Packaging.

Firma enveloped

Es el modo por defecto para facturas. La firma se añade dentro de la raíz del documento y usa la transformada enveloped-signature para excluirse a sí misma del digest.

Para cargas conscientes del esquema (FatturaPA, TicketBAI, KSeF), establece RootNamespace para que la firma herede el namespace XML correcto — los perfiles por país lo configuran automáticamente.

step3-enveloped.pas
var
  vSigner: TsgcSignXML;
  vSigned: string;
begin
  vSigner := TsgcSignXML.Create(nil);
  try
    vSigner.KeyProvider := vKeyProvider;
    vSigner.Profile := vProfile;          // Packaging = xpkEnveloped
    vSigned := vSigner.SignXML(vXML);
    TFile.WriteAllText('invoice-signed.xml', vSigned, TEncoding.UTF8);
  finally
    vSigner.Free;
  end;
end;

Enveloping y detached

Cambia el empaquetado en el perfil y deja todo lo demás igual. Para firmas detached, establece DetachedURI a la ruta o URL del documento que se firma; el verificador necesita esa referencia para obtener los datos.

Cuando recibas una firma detached, pasa tanto el XML de firma como los bytes del documento original a VerifyDetached — sgcSign recalcula el digest y confirma el vínculo.

step3-other.pas
// Enveloping: original XML wrapped in a ds:Object
vProfile.Packaging := xpkEnveloping;
vSigned := vSigner.SignXML(vXML);

// Detached: signature file points at invoice.xml
vProfile.Packaging := xpkDetached;
vProfile.DetachedURI := 'invoice.xml';
vSigned := vSigner.SignXML(vXML);
TFile.WriteAllText('invoice.sig.xml', vSigned, TEncoding.UTF8);

Verifica el XML firmado

Cada documento firmado debe verificarse antes de poner el flujo en producción.

Una llamada, informe completo

VerifyXML devuelve un TsgcSignatureReport con el nivel AdES detectado, el sujeto firmante, el sello de tiempo de firma y cualquier problema de cadena o revocación. Para firmas enveloped el verificador localiza el elemento ds:Signature automáticamente; para firmas detached usa VerifyDetached y proporciona los bytes del documento original.

Si Status es svValid, el digest coincide, la cadena de certificación ancla en una raíz confiable y el sello de tiempo está intacto. svInvalid con el motivo en StatusDetail es el modo de fallo típico; svUnknown significa que el verificador no pudo alcanzar un responder OCSP o un punto de distribución CRL.

step4-verify.pas
var
  vVerifier: TsgcSignatureVerifier;
  vReport: TsgcSignatureReport;
begin
  vVerifier := TsgcSignatureVerifier.Create(nil);
  try
    vReport := vVerifier.VerifyXML(vSigned);
    Memo1.Lines.Add('Level:   ' + vReport.Signatures[0].LevelAsString);
    Memo1.Lines.Add('Status:  ' + vReport.Signatures[0].StatusAsString);
    Memo1.Lines.Add('Signer:  ' + vReport.Signatures[0].Subject);
    Memo1.Lines.Add('TSA:     ' +
      DateTimeToStr(vReport.Signatures[0].TimestampUTC));
  finally
    vVerifier.Free;
  end;
end;

Lo que suele fallar

Problemas que más vemos cuando los desarrolladores firman su primer sobre XAdES.

Espacios en blanco y canonicalización

Aplicar pretty-print a un XML firmado — en un editor de texto, en un IDE o pasándolo por un XSLT — rompe el digest. La C14N exclusiva es sensible a cada byte. Guarda el XML firmado como bytes, transpórtalo como bytes y nunca lo reformatees.

BOM en el documento de entrada

Un BOM UTF-8 al inicio del XML es una fuente frecuente de digests desajustados, especialmente cuando el documento se escribe con Notepad. Quita el BOM antes de firmar — sgcSign normaliza internamente pero otros verificadores pueden no hacerlo.

FatturaPA necesita xalLT

El portal italiano de facturación electrónica rechaza XAdES-B-T porque no puede realizar consultas de revocación a largo plazo en el momento de archivado. Usa xalLT con OCSP.AutoFetch = True para que la respuesta OCSP viaje con el documento.

Firmas detached y resolución de URI

Si DetachedURI es una ruta relativa, el verificador la resuelve contra su propio directorio de trabajo. Para flujos entre máquinas usa una URL absoluta o pasa directamente los bytes del documento a VerifyDetached.

Por dónde seguir

Los perfiles por país hacen la configuración XAdES por ti. El servidor centraliza la firma para toda la granja de compilación.

Perfiles por país

VeriFactu, FatturaPA, KSeF, FACTUR-X, TicketBAI — una constante cambia cada transformada, algoritmo y atributo requerido.

Leer más →

Tutorial PAdES

El mismo motor, aplicado a PDF en lugar de XML. Lee el recorrido PAdES para comparar ambos formatos lado a lado.

Leer más →

sgcSign Server

API REST, integración con GitHub Actions, Docker y Helm. Descarga la firma de desarrolladores individuales a un servicio controlado.

Leer más →

¿Listo para firmar tu primer XML?

Descarga la versión de prueba, ejecuta este tutorial contra tu propia factura y licencia sgcSign cuando lo lleves a producción.