Technischer Artikel

PDF-Digitalsignaturen und PAdES in Delphi mit HotPDF

Bevor ein Viewer das grüne Häkchen auf einem signierten PDF anzeigt, tut er drei mechanische Dinge: Er liest das /ByteRange-Array aus dem Signatur-Dictionary, hasht exakt die zwei Bytebereiche, die dieses Array beschreibt, und verifiziert die CMS-Signatur, die als Hexwert im /Contents-Eintrag zwischen diesen Bereichen liegt. Fast jeder produktive Signaturfehler lässt sich darauf zurückführen, dass einer dieser drei Schritte falsch angeordnet wurde: ein Platzhalter, der für das endgültige Signaturblob zu klein ist, ein Hash über die falschen Bytes oder ein Speichern nach der Signatur, das bereits abgedeckte Bytebereiche neu geschrieben hat. Die Kryptografie scheitert fast nie. Die Byte-Buchhaltung tut es.

HotPDF gibt Delphi- und C++Builder-Anwendungen drei Ebenen der Signaturunterstützung: PFX-Signatur in einem Aufruf, einen External-Signer-Workflow für HSMs und Signaturdienste sowie PAdES-Profilfelder mit Dokumentzeitstempeln. Sie werden hier in dieser Reihenfolge vorgestellt, weil jede Ebene einen Fehlermodus der vorherigen adressiert.

Der ByteRange-Vertrag in ISO 32000-1 §12.8

Eine PDF-Signatur muss in der Datei leben, die sie signiert. Das erzeugt ein Henne-Ei-Problem: Der Signaturwert kann sich nicht selbst abdecken. Das Format löst es mit einem Loch. Der Writer reserviert einen /Contents-Eintrag fester Größe, gefüllt mit Nullen, und /ByteRange zeichnet zwei Bereiche auf: alles vor dem Loch und alles danach. Der Signierer hasht diese zwei Bereiche, und die daraus entstehende CMS-Struktur wird als Hexadezimalwert in das Loch geschrieben. Die Konsequenz, über die Ingenieure stolpern: Die Reservierungsgröße ist vor dem Signieren eingefroren, also muss die endgültige Signatur mit allen Zertifikaten in ein früher gewähltes Loch passen. Rund 8 KB reichen für eine detached CMS-Signatur mit kurzer Zertifikatskette.

HotPDF stellt diese Unterscheidung direkt bereit. AddSignatureField erzeugt ein leeres sichtbares Feld, das später jemand in einem Viewer signieren kann; AddSignedSignatureField erzeugt das Feld und reserviert zugleich das /Contents-Loch für eine programmgesteuerte Fertigstellung. Die falsche Methode zu wählen ist ein klassischer Fehler der ersten Woche: Ein leeres Feld gibt einem externen Signierer nichts, was er füllen könnte.

Ein Aufruf, wenn der Schlüssel in einer PFX-Datei liegt

Wenn Signaturzertifikat und privater Schlüssel in einer PFX/PKCS#12-Datei liegen, reduziert sich die gesamte Pipeline auf eine Klassenfunktion:

if THotPDF.SignPDFWithPFX('invoice-unsigned.pdf', 'invoice-signed.pdf',
    'company-cert.pfx', 'pfx-password') then
  Writeln('Signed: invoice-signed.pdf')
else
  raise Exception.Create('PFX signing failed');

Der Fehler, der hier den meisten Supportverkehr erzeugt, hat nichts mit dem PDF zu tun: Es ist der PFX-Container selbst. HotPDF liest PFX-Dateien, die mit PBES2 geschützt sind, also PBKDF2-Key-Derivation mit AES-256-CBC. Container, die von älteren Windows-Zertifikat-Wizards oder von OpenSSL-Versionen vor 3.0 exportiert wurden, verwenden standardmäßig Legacy-RC2/3DES-Schutz und werden nicht geparst. Die Abhilfe ist ein einmaliger Re-Export des Containers mit modernen Parametern (aktuelles OpenSSL tut das standardmäßig), keine Codeänderung. Prüfen Sie dies zuerst, wenn das Signieren bei einem Zertifikat sofort fehlschlägt, das „überall sonst funktioniert“.

Externes Signieren: reservieren, hashen, einfügen

Der Ein-Aufruf-Pfad nimmt an, dass der private Schlüssel eine Datei ist, die Ihr Prozess lesen darf. Produktive Signaturschlüssel sind das zunehmend nicht. Sie liegen in einem HSM, auf einem USB-Token oder in einem entfernten Signaturdienst, und keine Bibliothek kann sie direkt aufrufen. Für diese Topologie teilt HotPDF den Workflow in Byte-Schritte: ein Platzhalterdokument schreiben, die Hashbereiche berechnen, die Hash-Eingabe an die Stelle übergeben, die den Schlüssel hält, und das zurückgelieferte CMS wieder einfügen.

var
  Doc: THotPDF;
  Fs: TFileStream;
  PdfBytes, HashInput, SigHex: AnsiString;
  R1Start, R1Len, R2Start, R2Len, CStart, CLen: Integer;
begin
  // 1. Write the document with a reserved /Contents hole
  Doc := THotPDF.Create(nil);
  try
    Doc.FileName := 'placeholder.pdf';
    Doc.BeginDoc;
    Doc.CurrentPage.AddSignedSignatureField('Sig1',
      Rect(50, 100, 350, 150), 8192, 'adbe.pkcs7.detached',
      'Contract approval', 'Boston, MA', 'legal@example.com');
    Doc.EndDoc;
  finally
    Doc.Free;
  end;

  // 2. Load the saved bytes; the returned offsets are 0-based
  Fs := TFileStream.Create('placeholder.pdf', fmOpenRead);
  try
    SetLength(PdfBytes, Fs.Size);
    Fs.ReadBuffer(PdfBytes[1], Fs.Size);
  finally
    Fs.Free;
  end;
  THotPDF.PreparePDFForSigning(PdfBytes, R1Start, R1Len, R2Start, R2Len,
    CStart, CLen);

  // 3. Hash both spans and sign externally (HSM, token, service)
  HashInput := Copy(PdfBytes, R1Start + 1, R1Len) +
               Copy(PdfBytes, R2Start + 1, R2Len);
  SigHex := SignWithHsm(HashInput);  // your integration: returns CMS as hex

  // 4. Splice the signature into the reserved hole
  THotPDF.InsertSignatureHex(PdfBytes, SigHex);
  Fs := TFileStream.Create('signed.pdf', fmCreate);
  try
    Fs.WriteBuffer(PdfBytes[1], Length(PdfBytes));
  finally
    Fs.Free;
  end;
end;

Zwei Einschränkungen in dieser Sequenz verursachen intermittierende Produktionsfehler, wenn sie übersehen werden. Erstens arbeitet PreparePDFForSigning auf den Bytes einer vollständig gespeicherten Datei. Das Platzhalterdokument muss fertig geschrieben sein, bevor die Bereiche irgendeine Bedeutung haben; Bereiche gegen einen noch laufenden Stream zu berechnen erzeugt Offsets, die nicht mehr zur finalen Serialisierung passen. Zweitens muss die 8192-Byte-Reservierung das endgültige CMS aufnehmen. Eine Signatur, die Zwischenzertifikate einbettet, oder eine von einem Dienst zurückgegebene Signatur mit zusätzlichen signed attributes, kann darüber hinausgehen, und InsertSignatureHex kann das Loch nicht vergrößern. Das Symptom ist ein Job, der mit einem Zertifikat gelingt und mit einem anderen scheitert. Die Korrektur ist, den Platzhalter mit größerer Reservierung neu zu erzeugen, bemessen an einer echten Signatur des tatsächlichen Signierers.

PAdES-Baselines und Dokumentzeitstempel

Die europäische Signaturregulierung baut auf ETSI EN 319 142-1 auf, das vier PAdES-Baseline-Stufen definiert: B-B ist die Basissignatur; B-T ergänzt einen vertrauenswürdigen Zeitstempel, der den Erstellungszeitpunkt belegt; B-LT bettet das Validierungsmaterial, also Zertifikate und Sperrdaten, in das Dokument ein; B-LTA ergänzt periodische Dokumentzeitstempel, damit die Beweiskette algorithmische Alterung übersteht. HotPDF erzeugt die dokumentseitigen Strukturen für diesen Lebenszyklus:

// PAdES baseline signature field (ETSI EN 319 142-1)
Pdf.CurrentPage.AddPAdESSignatureField(
  'ApprovalSig', Rect(50, 100, 350, 150), 'B-B',
  'Contract approval', 'Boston, MA', 'legal@example.com');

// Document timestamp: larger reservation for the TSA token and chain
Pdf.CurrentPage.AddDocumentTimestampSignature('ArchiveTS', 16384);

Beachten Sie die 16384-Byte-Reservierung beim Zeitstempel: Das Token einer Timestamp Authority trägt seine eigene Zertifikatskette und wächst deshalb regelmäßig über die 8 KB hinaus, die für eine einfache Signatur genügen. Dokumentzeitstempel sind außerdem der Mechanismus hinter der B-LTA-Wartung. Ein signiertes Archiv alle paar Jahre mit aktuellen Algorithmen erneut zu zeitstempeln, hält eine Signatur von 2026 im Jahr 2040 überprüfbar.

Die Reason-, Location- und Contact-Strings, die beide Feldaufrufe akzeptieren, verdienen einen Richtlinienhinweis: Sie werden als einfache Dictionary-Einträge gespeichert und in die sichtbare Appearance gerendert, aber nichts verifiziert sie. Sie dokumentieren die Absicht für menschliche Leser, etwa „Contract approval“, eine Stadt oder ein Postfach, und Prüfer werden sie lesen. Füllen Sie sie deshalb konsistent aus Workflow-Daten. Behandeln Sie den sichtbaren Text nur nie als Beweis: Die kryptografische Aussage liegt vollständig im CMS und seiner Zertifikatskette, und ein Validator ignoriert die Appearance vollständig.

Warum signierte Dateien nur wachsen dürfen

Sobald eine Signatur existiert, sind die Bytes innerhalb ihrer Bereiche für immer eingefroren. ISO 32000-1 §7.5.6 inkrementelle Updates sind der einzige legitime Weg, die Datei danach zu ändern: Neue und geänderte Objekte werden nach den ursprünglichen Bytes angehängt, mit einem neuen Cross-Reference-Abschnitt, der zurückverkettet. Die Signatur bleibt für ihre Revision gültig, und Viewer melden den ehrlichen Zustand, etwa „signierte Revision intakt, Dokument danach geändert“. Eine vollständige Neu-Serialisierung schreibt dagegen die signierten Bereiche neu und zerstört die Signatur direkt, selbst wenn sich kein sichtbares Element geändert hat. Die Mechanik von Append-only-Speicherungen und wann sie zu komprimieren sind, behandelt der Artikel über Objektstreams und inkrementelle Updates.

Eine Einschränkung auf Bibliotheksebene rundet die Planung ab: HotPDFs PDF/A-Ausgabemodus weist Signaturfelder zurück. Archivkonformität und eingebettete Signaturen müssen daher auf getrennte Liefergegenstände verteilt und nicht in einer Datei kombiniert werden. Außerdem ist Signieren orthogonal zu Vertraulichkeit. Eine Signatur beweist Ursprung und Integrität, verbirgt aber nichts; das ist das Gebiet von AES-256-Verschlüsselung und Berechtigungsrichtlinien.

Abnahmetests für eine Signaturpipeline sollten unabhängig vom Code sein, der die Datei erzeugt. Öffnen Sie die Ausgabe in Acrobats Signaturpanel und bestätigen Sie drei Zustände: Die Signatur ist gültig, die Identität kettet zur erwarteten Root, und das Panel meldet keine Änderungen nach dem Signieren. Beschädigen Sie danach ein Byte innerhalb des signierten Bereichs einer Kopie und bestätigen Sie, dass dasselbe Panel das Dokument als verändert meldet. Eine Pipeline, bei der Validierung nie sichtbar scheitern durfte, ist eine Pipeline, deren Validierung nicht wirklich getestet wurde.

Fragen, die in Signatur-Code-Reviews auftreten

Wie groß sollte die /Contents-Reservierung sein?

8192 Bytes für ein detached CMS mit kurzer Kette; 16384, wenn Zeitstempel oder eingebettete Zwischenzertifikate beteiligt sind. Messen Sie das CMS, das Ihr echter Signierer erzeugt, und geben Sie Reserve dazu. Die Reservierung kann später nicht wachsen.

Kann ein Dokument zwei Signaturen tragen?

Ja. Jede Signatur lebt in ihrer eigenen inkrementellen Revision, und die Bereiche der zweiten Signatur decken die erste ab. Genau so entstehen Counter-signing-Workflows.

Schützt Signieren den Dokumentinhalt?

Nein. Eine Signatur liefert Integritäts- und Ursprungsnachweise; jeder kann die Datei weiterhin lesen. Vertraulichkeit erfordert Verschlüsselung, die unabhängig konfiguriert wird.

Alle drei Signaturschichten werden mit HotPDF Component für Delphi und C++Builder ausgeliefert; die Produktseite verlinkt die vollständige Signatur-API-Referenz.