Technical Article

Sign PDFs with PAdES B-B in Delphi Using PDFium

Die PDFium-Komponente signiert eine PDF-Datei mit einer digitalen PAdES B-B-Signatur über ihre Methode SignPades: Sie lädt das Dokument, hasht den signierten Byte-Bereich, baut eine CAdES-CMS-Struktur auf und hängt die Signatur als inkrementelles Update an. Das kryptografische Backend läuft ausschließlich unter Windows, weshalb Sie jeden Aufruf mit PadesCryptoAvailable absichern sollten, bevor Sie signieren

Die Situation ist bekannt: Ein Vertragsentwurf als PDF liegt auf Ihrem Schreibtisch. Die Rechtsabteilung verlangt eine digitale Signatur, bevor das Dokument verschickt wird. Sie greifen zu derselben PDFium-Komponente, die Sie bereits zum Rendern und Prüfen von Dokumenten nutzen — nur um festzustellen, dass PDFium überhaupt keine Signaturen schreiben kann. Seine Signatur-API ist ausschließlich lesend ausgelegt. Die PDFium-Komponente schließt diese Lücke, indem sie die gesamte Signier-Pipeline in Pascal abbildet, von der Hash-Funktion bis hin zur Injektion auf Byte-Ebene. Dieser Artikel führt Sie durch diesen Ablauf

Warum kann PDFium keine digitalen Signaturen schreiben?

PDFium stellt Signaturen als schreibgeschützte Objekte dar und bietet keine Funktionen zu deren Erstellung. Mit den Methoden der FPDFSignatureObj_*-Gruppe können Sie eine vorhandene Signatur auflisten, deren Eintrag /Contents auslesen und den /ByteRange prüfen. Es gibt jedoch kein Gegenstück, das ein Signaturverzeichnis erstellt, einen Platzhalter für /Contents reserviert oder einen Byte-Bereich schreibt. Inkrementelles Speichern ist zwar vorhanden (FPDF_SaveAsCopy mit FPDF_INCREMENTAL), bietet jedoch keine Schnittstelle für Signaturen. Jede Komponente, die auf PDFium aufbauend eine PDF signiert, muss daher alle Signatur-Bytes selbst erzeugen. Aus diesem Grund baut die PDFium-Komponente diese Logik aus drei Units in reinem Pascal auf. Da FPC 3.2.2 zwar MD5 und SHA-1, aber kein SHA-2 mitbringt und die Delphi-Klasse System.Hash nicht quellkompatibel zu FPC ist, implementiert FPdfSha256 den FIPS 180-4-Standard eigenständig. So laufen alle CMS-Codepfade über denselben Typ TSHA256Digest ohne Compiler-Weichen. FPdfAsn1 liefert den DER-Codierer und -Decoder, den die CMS-Strukturen benötigen, und FPdfCms setzt die CAdES-SignedData-Struktur darauf auf

Wie signiert man eine PDF in Delphi digital?

Laden Sie das Dokument und rufen Sie SignPades mit dem Fingerabdruck (Thumbprint) eines Zertifikats auf. Die PDFium-Komponente sucht diesen Fingerabdruck im persönlichen Zertifikatsspeicher des aktuellen Benutzers („MY“), lädt das passende Zertifikat samt privatem Schlüssel und schreibt eine signierte Kopie in den angegebenen Pfad

uses
  PDFium, FPdfCrypto;

procedure SignContract(const AThumbprint: string);
var
  Pdf: TPdf;
begin
  if not PadesCryptoAvailable then
    raise Exception.Create('PAdES signing requires the Windows CNG backend');

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';   // the document to sign
    Pdf.Active := True;
    // Second argument: SHA-1 thumbprint of a certificate in the Current
    // User "MY" store. First argument: destination for the signed copy.
    if not Pdf.SignPades('contract-signed.pdf', AThumbprint) then
      raise Exception.Create('Signing failed');
  finally
    Pdf.Free;
  end;
end;

PadesCryptoAvailable ist die Prüfung, die Sie vorab immer ausführen sollten. Unter Windows liefert sie True zurück und die CNG-Schnittstellen (crypt32/ncrypt) sind aktiv. Auf allen anderen Plattformen liefert sie False zurück und ein Aufruf zum Signieren würde eine EPadesCrypto-Exception auslösen. Das konsequente Nutzen dieser Abfrage verhindert, dass ein Linux- oder macOS-Build zur Laufzeit auf einem Pfad fehlschlägt, der dort nicht funktionieren kann. Der Fingerabdruck ist der SHA-1-Hash des Zertifikats — derselbe Wert, den die Windows-Zertifikatsverwaltung auf der Registerkarte Details anzeigt —, und identifiziert den Aussteller eindeutig, ohne dass Schlüsseldaten in Ihrem Quellcode stehen müssen

Was in das CMS gehört: Signierte Attribute und RFC 5652

Eine PAdES-Baseline-Signatur ist keine einfache RSA-Signatur über das gesamte PDF-Dokument, sondern eine CAdES-CMS-SignedData-Struktur mit einer Reihe vorgeschriebener signierter Attribute. FPdfCms.BuildSignedData erzeugt genau diese Attribute: content-type, message-digest und signing-certificate-v2 (das ESS-Attribut, das die Signatur über einen Hash fest an das Unterzeichner-Zertifikat bindet). Ein Detail stellt dabei fast jede manuelle CMS-Implementierung vor Probleme: RFC 5652 §5.4 verlangt, dass der Hashwert der signierten Attribute über deren DER-SET-OF-Codierung (Tag 0x31) berechnet wird, während dieselben Attribute in der SignerInfo-Struktur unter dem Tag IMPLICIT [0] (0xA0) transportiert werden. Die PDFium-Komponente codiert die Attribute einmal, berechnet den Hash über die 0x31-Struktur und schreibt anschließend nur das führende Tag-Byte für die Ausgabe auf 0xA0 um, sodass ein Puffer für beide Aufgaben ohne erneutes Durchlaufen der Daten genutzt wird

var
  Pdf: TPdf;
  Opts: TPadesSignOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;

    Opts := TPadesSignOptions.Default;
    Opts.CertificateThumbprint := 'a1b2c3d4e5f6...';  // signer in the MY store
    Opts.Reason := 'I approve this agreement';
    Opts.Location := 'Berlin, DE';
    Opts.ContentsSize := 16384;                        // hex width of /Contents

    if not Pdf.SignPades('contract-signed.pdf', Opts) then
      raise Exception.Create('Signing failed');
  finally
    Pdf.Free;
  end;
end;

Die Überladung mit Optionen fügt die im Standard ISO 32000-1 §12.8.1 definierten Metadaten für das Signaturverzeichnis hinzu: Reason, Location, ContactInfo und Name. Diese sind alle optional und werden in das Signatur-Dictionary geschrieben. Eine Einschränkung sollte beachtet werden: Wenn Sie CommitmentTypeOid setzen (um das signierte CAdES-Attribut commitment-type-indication hinzuzufügen), sollten Sie nicht gleichzeitig Reason angeben. ETSI EN 319 142-1 §6.3 verbietet das gemeinsame Auftreten beider Angaben, da sie denselben Zweck mit unterschiedlichen Mitteln ausdrücken

Wie ByteRange und der /Contents-Platzhalter ineinandergreifen

Eine Signatur muss die gesamte Datei abdecken, mit Ausnahme der Bytes, die die Signatur selbst enthalten. PAdES löst diesen Zirkelbezug mit einem Platzhalter fester Breite, den SignPadesBytes präzise verwaltet. Es reserviert eine hexadezimale Zeichenfolge für /Contents mit einer Breite von ContentsSize Bytes (standardmäßig 16384, was deutlich über der typischen CMS-SignedData-Größe liegt), serialisiert das inkrementelle Update, um die genaue Position des Platzhalters zu bestimmen, und berechnet dann den /ByteRange als zwei Abschnitte um diesen Platzhalter herum: alles vor dem öffnenden Trennzeichen der hexadezimalen Zeichenfolge und alles nach dem schließenden Trennzeichen. Der SHA-256-Hash wird ausschließlich über diese beiden Abschnitte berechnet. Die fertigen CMS-Daten werden hex-codiert in den reservierten Platzhalter geschrieben, mit Nullen auf die feste Breite aufgefüllt und das aktualisierte Querverzeichnis (Cross-Reference) angehängt. Da die Breite vorab festgelegt ist, verschiebt das Schreiben der Signatur keine nachgelagerten Bytes, weshalb der Byte-Bereich gültig bleibt. Die ursprünglichen Bytes des Dokuments werden unverändert beibehalten, sodass eine zuvor auf der Datei angebrachte Signatur unbeschädigt erhalten bleibt, genau wie es ISO 32000-1 §12.8.1 für das inkrementelle Signieren vorschreibt

Das Windows-CNG-Backend und seine Grenzen

Die PDFium-Komponente signiert ausschließlich unter Windows. Dies ist eine bewusste Entscheidung. FPdfCryptoWin bindet crypt32.dll und ncrypt.dll dynamisch ein, ohne eine feste Abhängigkeit zur Compile-Zeit zu erzeugen. Der Signiervorgang läuft über die Standard-CNG-Schnittstellen: den Speicher „MY“ öffnen, das Zertifikat über seinen Hash suchen, das Handle für den privaten Schlüssel über CryptAcquireCertificatePrivateKey anfordern und die Signierung über NCryptSignHash ausführen. Unterstützt werden RSA mit PKCS#1 v1.5, RSA-PSS und ECDSA. ECDSA erfordert dabei einen zusätzlichen Zwischenschritt: NCryptSignHash liefert die rohen IEEE P1363-Werte (r und s), während CMS eine DER-codierte ECDSA-Sig-Value-Struktur (SEQUENCE) erwartet. Daher codiert das Backend das Ergebnis gemäß RFC 5480 um

var
  Pdf: TPdf;
  Opts: TPadesSignOptions;
  Output: TFileStream;
begin
  if not PadesCryptoAvailable then
    Exit;   // no signing backend on this platform

  Opts := TPadesSignOptions.Default;
  Opts.CertificateThumbprint := ReadThumbprintFromConfig;

  Pdf := TPdf.Create(nil);
  Output := TFileStream.Create('contract-signed.pdf', fmCreate);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;
    Pdf.SignPadesToStream(Output, Opts);
  finally
    Output.Free;
    Pdf.Free;
  end;
end;

Die praktische Konsequenz lautet, dass der private Schlüssel im Windows-Zertifikatsspeicher liegen muss. Ein Zertifikat in einer PFX-Datei kann erst genutzt werden, nachdem es in den Speicher des aktuellen Benutzers importiert wurde. Dessen Fingerabdruck ist der Wert, den Sie an SignPades übergeben. Diese Version bietet keine direkte PKCS#11- oder HSM-Unterstützung und keinen dateibasierten Soft-Key-Pfad. Wenn PadesCryptoAvailable also False zurückgibt, kann auf diesem System nicht signiert werden

Wo PAdES B-B an seine Grenzen stößt

PAdES B-B bildet das Fundament, die unterste der vier PAdES-Ebenen: Es beweist, wer unterzeichnet hat und dass die Bytes seither unverändert geblieben sind, aber nicht mehr. Eine B-B-Signatur enthält keinen vertrauenswürdigen Zeitstempel, kann also den Zeitpunkt der Signierung nicht manipulationssicher belegen. Sie bettet auch keine Widerrufsdaten ein, sodass eine Prüfstelle Jahre später die Zertifikatskette und deren Status selbst ermitteln muss. Diese Lücken schließen die höheren Ebenen. Wenn Sie einen audit-konformen Signaturzeitpunkt benötigen, führt das Hinzufügen eines RFC 3161-Zeitstempels und DSS für die Langzeit-Validierung die Signatur auf B-T und darüber hinaus. Möchten Sie eine fertige Signatur einlesen und deren Niveau überprüfen, ist das Prüfen von PDF-Signaturen und PAdES-Ebenen der passende Weg. Bevor Sie ein Dokument signieren, zeigt das Auditieren von PDF-Sicherheitsrisiken auf, worauf Sie Ihren Namen setzen

Die hier gezeigten SignPades-Methoden werden mit der PDFium-Komponente für Delphi und C++Builder ausgeliefert, zusammen mit der schreibgeschützten Signatur-Prüfung, die PDFium von Haus aus bereitstellt