Tutorial: XAdES-XML-Signierung in Delphi

Eine praxisnahe Anleitung, die aus einer einfachen XML-Rechnung ein XAdES-B-T-Dokument macht — das von FatturaPA, FACTUR-X und den meisten E-Invoicing-Portalen in Europa geforderte Niveau. Du wählst zwischen Enveloped-, Enveloping- und Detached-Verpackung, hängst einen RFC-3161-Zeitstempel an und verifizierst das Ergebnis, sodass du genau weißt, was deine Gegenseite sieht.

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

Drei Verpackungsmodi, eine Engine

XAdES erweitert die W3C-XML-DSig-Spezifikation um Zeitstempelung und Langzeit-Validierungsdaten. Bevor du irgendetwas signierst, entscheide, wo das ds:Signature-Element liegen soll.

Enveloped

Das ds:Signature-Element wird innerhalb der Dokument-Wurzel angehängt. Das ist das Format, das FatturaPA, FACTUR-X-XAdES und die meisten E-Invoicing-Flows verwenden. Das signierte XML bleibt eine gültige Kopie des Originals — Leser ignorieren das Signatur-Element, das sie nicht verstehen.

Enveloping

Das ursprüngliche XML wird zu einem ds:Object-Kind innerhalb der ds:Signature. Praktisch, wenn du einen einzelnen, in sich geschlossenen Umschlag brauchst und es egal ist, ob bestehende Konsumenten das Dokument weiter parsen können.

Detached

Die Signatur liegt in einer separaten Datei und verweist über eine URI-Referenz auf das Originaldokument. Üblich bei SAML, ebXML und überall dort, wo das Quelldokument byte-genau unangetastet bleiben muss.

Units einbinden

XAdES benötigt den XML-Signer, das XAdES-Profil und einen Key Provider. In diesem Tutorial nutzen wir PFX; tausche einen der 10 Provider ein, wenn du eine Smartcard oder ein Cloud-HSM hast.

Delphi-uses-Klausel

sgcSign_XML ist der Kern für XML-Signaturen. sgcSign_Profile_XAdES stellt Level, Transforms und Zeitstempel-Konfiguration bereit. sgcSign_KeyProvider_PFX lädt eine PKCS#12-Datei von der Festplatte — der häufigste Ausgangspunkt.

Für die Long-Term-Level (B-LT, B-LTA) benötigst du zusätzlich sgcSign_OCSP und sgcSign_CRL, damit Sperrinformationen geholt und eingebettet werden können.

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

XML und Schlüssel laden

Zwei Eingaben — das XML, das du signieren willst, und das PFX-Zertifikat, mit dem du signierst.

PFX-Key-Provider

TsgcPFXKeyProvider importiert die PKCS#12-Datei über Windows CNG, sodass du ein modernes Signier-Handle bekommst — unabhängig davon, für welchen CSP das Zertifikat ursprünglich ausgestellt wurde. Derselbe Provider funktioniert ab Windows 7, in 32-Bit und 64-Bit.

Halte den Provider während der gesamten Signieroperation am Leben — der XML-Signer referenziert das zugrunde liegende Key-Handle, bis SignXML zurückkehrt.

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;

XAdES-B-T oder XAdES-B-LT wählen

B-T fügt einen Signatur-Zeitstempel hinzu. B-LT bettet zusätzlich die CA-Kette und Sperrinformationen ein, sodass die Signatur jahrelang verifizierbar bleibt.

Profil-Konfiguration

Level wählt das AdES-Level: xalB, xalT, xalLT, xalLTA. Packaging wählt xpkEnveloped (Standard), xpkEnveloping oder xpkDetached.

Die Liste Transforms ist standardmäßig xtEnvelopedSignature + xtC14NExclusive, was die meisten E-Invoicing-Schemas vorschreiben. Überschreibe sie nur, wenn ein bestimmtes Länderprofil inklusives C14N oder eine eigene XSLT-Transformation verlangt.

Für B-LT weist OCSP.AutoFetch := True den Signer an, OCSP-Antworten für jedes Zertifikat in der Kette abzurufen und im RevocationValues-Element einzubetten. Das signierte XML trägt dann alles in sich, was ein Validator braucht — zur Validierungszeit ist kein Netzwerkaufruf nötig.

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;

Signieren — Enveloped, Enveloping und Detached

Dieselbe Komponente TsgcSignXML handhabt alle drei Verpackungsmodi — nur die Eigenschaft Packaging unterscheidet sich.

Enveloped-Signatur

Der Standard für Rechnungen. Die Signatur wird innerhalb der Dokument-Wurzel angehängt und verwendet die Enveloped-Signature-Transformation, um sich selbst aus dem Digest auszuschließen.

Bei schema-bewussten Payloads (FatturaPA, TicketBAI, KSeF) setze RootNamespace, damit die Signatur den richtigen XML-Namespace erbt — die Länderprofile setzen das automatisch.

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 und Detached

Stelle das Packaging im Profil um, lass alles andere unverändert. Für Detached-Signaturen setze DetachedURI auf den Pfad oder die URL des signierten Dokuments; der Validator braucht diese Referenz, um die Daten zu laden.

Wenn du eine Detached-Signatur zurückerhältst, übergib das Signatur-XML und die Bytes des Originaldokuments an VerifyDetached — sgcSign berechnet den Digest neu und bestätigt die Bindung.

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

Das signierte XML verifizieren

Jedes signierte Dokument muss verifiziert werden, bevor du den Workflow ausrollst.

Ein Aufruf, vollständiger Bericht

VerifyXML liefert ein TsgcSignatureReport mit erkanntem AdES-Level, Subject des Signierenden, Signatur-Zeitstempel und etwaigen Problemen mit Kette oder Sperrinformationen. Bei Enveloped-Signaturen findet der Validator das ds:Signature-Element automatisch; bei Detached-Signaturen nutze VerifyDetached und übergib die Bytes des Originaldokuments.

Wenn Status gleich svValid ist, passt der Digest, die Zertifikatskette mündet in einer vertrauenswürdigen Wurzel und der Zeitstempel ist intakt. svInvalid mit Begründung in StatusDetail ist der typische Fehlerfall; svUnknown bedeutet, dass der Validator weder OCSP-Responder noch CRL-Verteilpunkt erreichen konnte.

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;

Was üblicherweise schiefgeht

Probleme, die wir am häufigsten sehen, wenn Entwickler ihren ersten XAdES-Umschlag signieren.

Whitespace und Kanonikalisierung

Ein signiertes XML hübsch zu formatieren — im Texteditor, in einer IDE oder durch eine XSLT — zerstört den Digest. Exclusive C14N ist gegenüber jedem Byte empfindlich. Speichere das signierte XML als Bytes, transportiere es als Bytes, formatiere es nie nach.

BOM im Eingabedokument

Ein UTF-8-BOM am Anfang des XML ist eine häufige Ursache für nicht passende Digests, vor allem wenn das Dokument von Notepad geschrieben wurde. Entferne das BOM vor dem Signieren — sgcSign normalisiert intern, andere Validatoren möglicherweise nicht.

FatturaPA benötigt xalLT

Das italienische E-Invoicing-Portal lehnt XAdES-B-T ab, weil es zur Archivzeit keine Langzeit-Sperrabfragen durchführen kann. Verwende xalLT mit OCSP.AutoFetch = True, damit die OCSP-Antwort mit dem Dokument reist.

Detached-Signaturen und URI-Auflösung

Ist DetachedURI ein relativer Pfad, löst der Validator ihn gegen sein eigenes Arbeitsverzeichnis auf. Für maschinen­übergreifende Abläufe verwende eine absolute URL oder übergib die Bytes des Dokuments direkt an VerifyDetached.

Wie es weitergeht

Länderprofile übernehmen die XAdES-Konfiguration für dich. Der Server zentralisiert das Signieren über die gesamte Build-Farm.

Länderprofile

VeriFactu, FatturaPA, KSeF, FACTUR-X, TicketBAI — eine Konstante schaltet jede Transformation, jeden Algorithmus und jedes Pflichtattribut um.

Mehr erfahren →

PAdES-Tutorial

Dieselbe Engine, angewandt auf PDF statt XML. Lies den PAdES-Walkthrough, um die beiden Formate direkt zu vergleichen.

Mehr erfahren →

sgcSign-Server

REST-API, GitHub-Actions-Integration, Docker und Helm. Verlagere das Signieren von einzelnen Entwicklern zu einem kontrollierten Dienst.

Mehr erfahren →

Bereit, dein erstes XML zu signieren?

Lade die Testversion herunter, fahre dieses Tutorial gegen deine eigene Rechnung und lizenziere sgcSign, sobald du ausgelieferst.