Technischer Artikel

PAdES-Digitalsignaturen in Delphi: Signieren und Validieren mit PDFlibPas

Adobe Acrobat meldete jede Signatur im Stapel als gültig. Der eIDAS-Konformitätsprüfer des Kunden wies jede einzelne zurück. Die Lücke lag an einem einzigen Namen im Signaturwörterbuch: Die Dateien trugen /SubFilter /adbe.pkcs7.detached, was eine vollkommen solide ISO-32000-1-§12.8-Signatur und zugleich eine nicht konforme PAdES-Signatur erzeugt, weil ETSI EN 319 142-1 auf jeder Baseline-Stufe ETSI.CAdES.detached verlangt. Die Kryptografie war fehlerfrei; das Dokument beanspruchte schlicht nicht das Profil, das der Regulator forderte. Wenn Ihre Delphi-Anwendung Rechnungen, Verträge oder Laborberichte signiert, die europäische Validierungspolitik überstehen müssen, ist diese Unterscheidung das Erste, was stimmen muss — und in losLab PDF Library (PDFlibPas) ist es ein Aufruf. Dieser Artikel arbeitet die Signatur-, Zeitstempel- und DSS-Werkzeugkette von der Basissignatur bis zur Langzeitvalidierung durch.

Was eine PDF-Signatur zur PAdES-Signatur macht

ETSI EN 319 142-1 definiert vier Baseline-Stufen, die auf dem CMS-Format aufbauen. PAdES-B-B ist der Einstieg: eine CAdES-Signatur in einem PDF-Signaturfeld mit dem ETSI.CAdES.detached-SubFilter und einem signierten Signing-Certificate-Attribut. PAdES-B-T fügt einen RFC-3161-Zeitstempel über den Signaturwert hinzu und beweist, dass die Signatur vor einem Zeitpunkt existierte, den niemand zurückdatieren kann. PAdES-B-LT bettet die Zertifikate, CRLs und OCSP-Antworten, die zur Validierung nötig sind, in einen Document Security Store ein, sodass die Datei prüfbar bleibt, nachdem die ausstellende CA ihre Infrastruktur abgeschaltet hat. PAdES-B-LTA schließt den Stapel mit einem Dokumentzeitstempel ab, der die gesammelten Belege erneut schützt, wenn Algorithmen schwächer werden.

PDFlibPas bildet diese Konzepte auf seine Sign-Process-API ab. Der Profilmarker ist SetSignProcessCustomSubFilter; ETSI Commitment-Type-Indications — Proof of Origin, Proof of Approval und die anderen ETSI-definierten Kennungen 1 bis 6 — laufen über SetSignProcessCommitmentType; eine explizite Signaturpolicy wird mit SetSignProcessSignaturePolicy angehängt, das die Policy-OID und ihren Digest nimmt. Eine Vorgabe verdient Aufmerksamkeit: Wenn der Digest-Algorithmus auf Auto bleibt, wählt die Bibliothek SHA-256 für ETSI- und adbe.pkcs7.detached-Signaturen und fällt nur auf dem Legacy-Pfad adbe.pkcs7.sha1 auf SHA-1 zurück. Setzen Sie ihn trotzdem explizit; Auditoren fragen, und explizit schlägt abgeleitet in jeder Compliance-Prüfung.

Die Basissignatur erzeugen

Die flache API steuert Signieren als One-Shot-Zustandsmaschine: Prozess auf der Quelldatei öffnen, konfigurieren, in eine Ausgabedatei abschließen, Ergebniscode lesen. Die folgende Sequenz erzeugt eine PAdES-B-B-Signatur mit SHA-256 — und reserviert bewusst zusätzlichen Platz im /Contents-Platzhalter, die eine Zeile, die Sie später nicht nachrüsten können, wenn dieser Signatur jemals ein Zeitstempel hinzugefügt werden soll.

var
  Pdf: TPDFlib;
  SignId: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    SignId := Pdf.NewSignProcessFromFile('invoice.pdf', '');
    if SignId = 0 then
      raise Exception.Create('cannot open source PDF');
    Pdf.SetSignProcessField(SignId, 'Sig1');
    Pdf.SetSignProcessPFXFromFile(SignId, 'company.pfx', PfxPassword);
    Pdf.SetSignProcessInfo(SignId, 'Approved', 'Vienna', 'billing@example.com');
    Pdf.SetSignProcessCustomSubFilter(SignId, 'ETSI.CAdES.detached');
    Pdf.SetSignProcessDigestAlgorithm(SignId, 2);          // SHA-256
    Pdf.SetSignProcessReserveContentsBytes(SignId, 8192);  // room for a timestamp later
    Pdf.EndSignProcessToFile(SignId, 'invoice-signed.pdf');
    if Pdf.GetSignProcessResult(SignId) <> 1 then
      raise Exception.CreateFmt('signing failed, code %d',
        [Pdf.GetSignProcessResult(SignId)]);
    Pdf.ReleaseSignProcess(SignId);
  finally
    Pdf.Free;
  end;
end;

NewSignProcessFromFile gibt 0 zurück, wenn die Quelle überhaupt nicht geöffnet werden kann. Danach trennt GetSignProcessResult die Fehlerarten, die in Produktion wirklich auftreten: 4 bedeutet falsches PDF-Passwort, 7 falsches PFX-Passwort, 9 Zertifikatsdatei ohne privaten Schlüssel, 10 nicht beschreibbarer Ausgabepfad, 11 Fehler beim Anwenden der Signaturbytes. Den numerischen Code neben dem Eingabedateinamen zu loggen verwandelt ein vages Supportticket in eine Ein-Minuten-Diagnose.

Den RFC-3161-Zeitstempel hinzufügen, den die Bibliothek nicht für Sie abholt

PDFlibPas liefert keinen TSA-Client, und das ist eine bewusste Grenze statt einer Lücke. Die Bibliothek berechnet den Hash, den die Zeitstempelstelle gegensignieren muss, und bettet das erweiterte CMS anschließend wieder ein; der HTTP-Austausch und die CMS-Operation dazwischen gehören dem Aufrufer. Dafür gibt es einen harten technischen Grund: Das Windows-CryptoAPI-Control, das nominell unsignierte Attribute hinzufügt, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, scheitert mit CRYPT_E_INVALID_INDEX auf dem detached SignedData-Layout, das PAdES verwendet. Das erweiterte CMS muss daher aus einem CMS-Encoder unter Ihrer Kontrolle kommen — keine Bibliothek kann es still mit einem Systemaufruf erledigen.

var
  Pdf: TPDFlib;
  StsId: Integer;
  HashHex, TstDer, TsAttr, AugmentedCms: AnsiString;
begin
  Pdf := TPDFlib.Create;
  try
    StsId := Pdf.NewPAdESSignatureTimeStampProcessFromFile('invoice-signed.pdf', '');
    Pdf.SetPAdESSignatureTimeStampField(StsId, 'Sig1');
    Pdf.SetPAdESSignatureTimeStampDigestAlgorithm(StsId, 2);
    HashHex := Pdf.GetPAdESSignatureValueHashHex(StsId);
    // both calls below are application code: an HTTP POST to your TSA,
    // and a CMS re-encode that attaches the token as an unsigned attribute
    TstDer := RequestTimeStampToken(HashHex);
    TsAttr := Pdf.BuildPAdESSignatureTimeStampAttribute(TstDer);
    AugmentedCms := AttachUnsignedAttribute(Pdf.GetPAdESSignatureCMSBytes(StsId), TsAttr);
    Pdf.SetPAdESSignatureCMSBytes(StsId, AugmentedCms);
    Pdf.EndPAdESSignatureTimeStampProcessToFile(StsId, 'invoice-bt.pdf');
    if Pdf.GetPAdESSignatureTimeStampProcessResult(StsId) <> 1 then
      raise Exception.Create('timestamp embedding failed');
    Pdf.ReleasePAdESSignatureTimeStampProcess(StsId);
  finally
    Pdf.Free;
  end;
end;

Achten Sie hier auf die Ergebnis-Codes: 12 bedeutet, dass das benannte Signaturfeld nicht existiert, 11, dass das bestehende CMS nicht geparst werden konnte, und 13, dass das erweiterte CMS nicht mehr in den reservierten /Contents-Platzhalter passt. Code 13 schmerzt, denn die einzige Reparatur ist Neusignieren: Ein typisches Zeitstempel-Token mit Zertifikatskette liegt bei 4 bis 6 KB, und die im B-B-Schritt gesetzte 8192-Byte-Reservierung existiert genau, damit dieser Schritt landen kann.

Validierung beginnt bei der ByteRange, nicht bei der Zertifikatskette

Ein grünes Häkchen in einem Viewer ist eine Vertrauensentscheidung gegen den Zertifikatsspeicher dieser Maschine, kein strukturelles Urteil über die Datei. Programmatische Validierung sollte tiefer beginnen, mit der Frage, die inkrementelle Updates subtil machen: Welche Bytes deckt jede Signatur tatsächlich ab? Jede hier diskutierte Erweiterung — zweite Signaturen, DSS-Wörterbücher, Dokumentzeitstempel — kommt per inkrementellem Update hinzu, und jedes Update hängt Bytes außerhalb der /ByteRange der früheren Signatur an. Diese angehängten Bytes sind legitim, aber ein Validator muss sie gegen die Änderungs-Policy des Dokuments klassifizieren; die DocMDP-Stufe pro Feld ist mit GetSignatureDocMDPLevelByName lesbar.

var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // before Open: SignDoc holds a share lock
  Doc := TPDFlibSignDoc.Create;
  try
    if not Doc.Open('invoice-bt.pdf', '', False) then
      raise Exception.Create('cannot open for audit');
    Names := TStringList.Create;
    try
      Doc.GetSignatureFieldNames(Names);
      for I := 0 to Names.Count - 1 do
        if Doc.GetSignatureValueObjNum(Names[I]) > 0 then   // >0 means actually signed
        begin
          B0 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 11)));
          B1 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 12)));
          B2 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 13)));
          B3 := StrToInt64(string(Doc.GetSignatureValueByName(Names[I], 14)));
          if (B0 = 0) and (B2 + B3 = FileSize) then
            Writeln(Names[I], ': covers the file to EOF')
          else
            Writeln(Names[I], ': earlier revision, or unexpected ByteRange layout');
        end;
    finally
      Names.Free;
    end;
    Doc.Close;
  finally
    Doc.Free;
  end;
end;

Zwei Fallen liegen in diesem Audit-Pfad. TPDFlibSignDoc.Open hält die Datei mit einem exklusiven Share-Lock, also muss ein Validator, der rohe Dateibytes für die CMS-Verifikation hashen will, die Datei vor dem Audit-Open in den Speicher lesen — die Reihenfolge zählt. Und das flache API-Gegenstück GetSignProcessByteRange gibt Integer zurück, während die zugrunde liegenden Offsets Int64 sind: jenseits von 2 GB schneidet der flache Aufruf still ab, weshalb dieses Beispiel die Offsets über die Auditklasse zieht. Beachten Sie auch, was in der flachen Schicht absichtlich fehlt: Es gibt keinen VerifySignature-Wrapper. Kryptografische Urteile kommen von der Klassenebene TPDFlibSignatureVerifier, die vsValid, vsInvalid oder vsUnknown zurückgibt, oder von einem externen Validator, dem Ihre Compliance-Policy bereits vertraut.

Langzeitvalidierung: DSS, VRI und der Dokumentzeitstempel

PAdES-B-LT existiert, weil Sperrinfrastruktur sterblich ist. ETSI EN 319 142-1 §5.4.2.2 spezifiziert den Document Security Store: ein dokumentweites Wörterbuch mit Zertifikaten, CRLs und OCSP-Antworten, optional pro Signatur über VRI-Einträge indexiert, die durch den Hash des /Contents jeder Signatur geschlüsselt sind. Der PDFlibPas-Ablauf spiegelt das Zeitstempeldesign: NewPAdESDSSProcessFromFile öffnet den Prozess; AddPAdESDSSCertificate, AddPAdESDSSCRL und AddPAdESDSSOCSP akzeptieren DER-Blobs; AddPAdESDSSVRI bindet ausgewähltes Material an eine Signatur; EndPAdESDSSProcessToFile schreibt alles als inkrementelles Update. Das Abrufen des Sperrmaterials — und das Urteil, ob es frisch genug zum Einbetten ist — bleibt Verantwortung des Aufrufers; die Bibliothek garantiert strukturell konforme Wörterbücher, nicht, dass Ihr OCSP-Responder die Wahrheit sagte.

Der Archivendpunkt, B-LTA, fügt einen Dokumentzeitstempel hinzu: ein separates Signaturfeld, dessen Typ DocTimeStamp statt Sig ist, erzeugt über SetSignProcessDocTimeStamp mit reservierter Signaturlänge. Für Reader, die älter sind als diese Strukturen, trägt TPDFlibSignDoc.EnsurePAdESExtensions die ESIC-Developer-Extension in den Dokumentkatalog ein und kündigt an, dass die Datei ETSI-definierte Features nutzt.

Häufige Fragen

Warum sagt Acrobat „Gültigkeit unbekannt“, obwohl die PAdES-Struktur korrekt ist?

Weil Vertrauen und Struktur unabhängig sind. Der Viewer kann den Unterzeichner auf dieser Maschine nicht zu einer vertrauenswürdigen Root verketten — bei privaten CAs und Testzertifikaten Routine —, während ByteRange-Audit und CMS-Verifikation gleichzeitig bestehen. Verteilen Sie das Root-Zertifikat korrekt oder werten Sie gegen die EU Trusted Lists aus, wenn eIDAS-Qualifikation das eigentliche Ziel ist.

Kann einer Signatur, die keinen zusätzlichen Contents-Platz reserviert hat, ein Zeitstempel hinzugefügt werden?

Meist nicht. Das erweiterte CMS muss in den ursprünglichen Platzhalter passen, und ein Platzhalter in Standardgröße passt knapp für die ursprüngliche Signatur. Rechnen Sie mit Ergebniscode 13 und planen Sie von Anfang an, mit SetSignProcessReserveContentsBytes neu zu signieren.

Ersetzt ein Dokumentzeitstempel den Signaturzeitstempel?

Nein. Der Signaturzeitstempel beweist, wann eine Signatur existierte; der Dokumentzeitstempel schützt die ganze Datei einschließlich ihrer DSS-Belege und ist das Element, das über Jahrzehnte erneuert wird. Archivprofile tragen am Ende beide.

Für die Audit-Perspektive — Signaturfelder über ein Korpus enumerieren, ByteRange-Layouts ausgeben und DocMDP-Stufen gesammelt lesen — siehe das Begleitstück zur Compliance- und Signatur-Workbench. Signierte Dokumente, die zusätzlich Archivpolitik erfüllen müssen, gehören in den Workflow aus PDF/A- und PDF/UA-Preflight in Delphi. Vollständige API-Dokumentation und Evaluierungsdownloads stehen auf der Produktseite losLab PDF Library for Delphi.