Tutorial: XAdES XML ondertekenen in Delphi

Een praktische doorloop die een platte XML-factuur neemt en een XAdES-B-T-document produceert — het niveau dat wordt vereist door FatturaPA, FACTUR-X en de meeste e-facturatie-portals in Europa. Je kiest tussen enveloped-, enveloping- en detached-handtekening-packaging, koppelt een RFC 3161-timestamp, en verifieert het resultaat zodat je precies weet wat je counter-party zal zien.

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

Drie packaging-modi, een engine

XAdES breidt de W3C XML-DSig-specificatie uit met timestamping en langetermijn-validatie-data. Voordat je iets ondertekent, beslis waar het ds:Signature-element zal wonen.

Enveloped

Het ds:Signature-element wordt binnen de documentroot toegevoegd. Dit is het formaat gebruikt door FatturaPA, FACTUR-X-XAdES en de meeste e-facturatie-flows. De ondertekende XML is nog steeds een geldige kopie van het oorspronkelijke document — lezers negeren het signature-element dat ze niet begrijpen.

Enveloping

De oorspronkelijke XML wordt een ds:Object-kind binnen de ds:Signature. Handig wanneer je een enkele self-contained envelope nodig hebt en niet uitmaakt of bestaande consumers het document kunnen blijven parsen.

Detached

De handtekening zit in een apart bestand en wijst naar het oorspronkelijke document via een URI-referentie. Gangbaar in SAML, ebXML en elke flow waar het brondocument byte-voor-byte onaangeroerd moet blijven.

Voeg de units toe

XAdES heeft de XML-signer, het XAdES-profiel en een key-provider nodig. We gebruiken PFX in deze tutorial; wissel naar een van de 10 providers als je een smartcard of cloud-HSM hebt.

Delphi uses-clausule

sgcSign_XML is de XML-signing-kern. sgcSign_Profile_XAdES stelt het niveau, de transforms en timestamp-configuratie bloot. sgcSign_KeyProvider_PFX laadt een PKCS#12-bestand van disk — het meest gangbare startpunt.

Voor langetermijn-niveaus (B-LT, B-LTA) heb je ook sgcSign_OCSP en sgcSign_CRL nodig zodat revocation-data kan worden opgehaald en ingebed.

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

Laad de XML en sleutel

Twee inputs — de XML die je wilt ondertekenen en het PFX-certificaat waarmee je ondertekent.

PFX-key-provider

TsgcPFXKeyProvider importeert het PKCS#12-bestand via Windows CNG, wat betekent dat je een moderne signing-handle krijgt ongeacht voor welke CSP het certificaat oorspronkelijk werd uitgegeven. Dezelfde provider werkt op Windows 7 en hoger, 32-bit en 64-bit.

Houd de provider levend gedurende de volledige signing-operatie — de XML-signer refereert naar het onderliggende key-handle tot SignXML terugkeert.

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;

Kies XAdES-B-T of XAdES-B-LT

B-T voegt een signature-timestamp toe. B-LT bedt daarnaast de CA-chain en revocation-data in zodat de handtekening jarenlang verifieerbaar blijft.

Profiel-configuratie

Level kiest het AdES-niveau: xalB, xalT, xalLT, xalLTA. Packaging kiest xpkEnveloped (standaard), xpkEnveloping of xpkDetached.

De Transforms-lijst staat standaard op xtEnvelopedSignature + xtC14NExclusive, wat de meeste e-facturatie-schema’s vereisen. Overschrijf alleen wanneer een specifiek landenprofiel om inclusive C14N of een custom XSLT-transform vraagt.

Voor B-LT vertelt OCSP.AutoFetch := True de signer om OCSP-responses op te halen voor elk certificaat in de chain en in te bedden in het RevocationValues-element. De ondertekende XML draagt dan alles wat een verifier nodig heeft — geen netwerkaanroep vereist bij validatietijd.

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;

Onderteken — enveloped, enveloping en detached

Dezelfde TsgcSignXML-component handelt alle drie de packaging-modi af — alleen de Packaging-property verschilt.

Enveloped-handtekening

De standaard voor facturen. De handtekening wordt binnen de documentroot toegevoegd en gebruikt de enveloped-signature-transform om zichzelf uit het digest te sluiten.

Voor schema-bewuste payloads (FatturaPA, TicketBAI, KSeF), stel RootNamespace in zodat de handtekening de juiste XML-namespace erft — de landenprofielen stellen dit automatisch in.

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 en detached

Schakel de packaging op het profiel, houd al het andere hetzelfde. Voor detached-handtekeningen zet DetachedURI op het pad of de URL van het te ondertekenen document; de verifier heeft die referentie nodig om de data op te halen.

Wanneer je een detached-handtekening terugkrijgt, geef je zowel de signature-XML als de oorspronkelijke documentbytes door aan VerifyDetached — sgcSign berekent het digest opnieuw en bevestigt de binding.

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);

Verifieer de ondertekende XML

Elk ondertekend document moet worden geverifieerd voordat je de workflow live brengt.

Een aanroep, volledig rapport

VerifyXML geeft een TsgcSignatureReport terug met het gedetecteerde AdES-niveau, de signer-subject, de signature-timestamp en eventuele chain- of revocation-problemen. Voor enveloped-handtekeningen vindt de verifier het ds:Signature-element automatisch; voor detached-handtekeningen gebruik VerifyDetached en lever de oorspronkelijke documentbytes.

Als Status svValid is, klopt het digest, verankert de certificate chain in een vertrouwde root en is de timestamp intact. svInvalid met de reden in StatusDetail is de typische failure mode; svUnknown betekent dat de verifier geen OCSP-responder of CRL-distributiepunt kon bereiken.

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;

Wat meestal misgaat

Problemen die we het vaakst zien wanneer ontwikkelaars hun eerste XAdES-envelope ondertekenen.

Whitespace en canonicalisatie

Een ondertekende XML pretty-printen — in een teksteditor, in een IDE, of via een XSLT — breekt het digest. Exclusive C14N is gevoelig voor elke byte. Sla de ondertekende XML op als bytes, transporteer hem als bytes, herformatteer nooit.

BOM in het input-document

Een UTF-8-BOM aan het begin van de XML is een frequente bron van niet-overeenkomende digests, vooral wanneer het document door Notepad is geschreven. Strip de BOM voor het ondertekenen — sgcSign normaliseert intern, maar andere verifiers misschien niet.

FatturaPA heeft xalLT nodig

De Italiaanse e-facturatie-portal weigert XAdES-B-T omdat het bij archieftijd geen langetermijn-revocation-lookups kan uitvoeren. Gebruik xalLT met OCSP.AutoFetch = True zodat de OCSP-response met het document meereist.

Detached-handtekeningen en URI-resolutie

Als de DetachedURI een relatief pad is, lost de verifier het op tegen zijn eigen werkdirectory. Voor cross-machine-flows gebruik een absolute URL of geef de documentbytes direct door aan VerifyDetached.

Waar nu verder

Landenprofielen doen de XAdES-configuratie voor je. De server centraliseert ondertekenen over de build-farm.

Landenprofielen

VeriFactu, FatturaPA, KSeF, FACTUR-X, TicketBAI — een constante schakelt elke transform, algoritme en vereist attribuut om.

Lees meer →

PAdES-tutorial

Dezelfde engine, toegepast op PDF in plaats van XML. Lees de PAdES-doorloop om de twee formaten naast elkaar te vergelijken.

Lees meer →

sgcSign Server

REST-API, GitHub Actions-integratie, Docker en Helm. Verplaats ondertekenen van individuele ontwikkelaars naar een gecontroleerde service.

Lees meer →

Klaar om je eerste XML te ondertekenen?

Download de proefversie, draai deze tutorial tegen je eigen factuur, licentieer sgcSign wanneer je live gaat.