Technischer Artikel

PAdES-Signaturen in Delphi erstellen und prüfen

Eine PAdES-Signatur zu validieren bedeutet, drei unabhängige Dinge zu prüfen, und ein grünes Häkchen in einem Viewer sagt nur etwas über das dritte aus. Erstens muss das /ByteRange-Array die richtigen Bytes abdecken: Die benannten Bereiche müssen exakt die Eingabe rekonstruieren, über die der CMS-Digest gebildet wurde, ohne dass signierte Bytes außerhalb liegen. Zweitens muss das Zertifikat im CMS zu einer Wurzel führen, der Sie vertrauen, und das signierte Signing-Certificate-Attribut tragen, das PAdES verlangt. Drittens muss, wenn das Profil einen Zeitstempel beansprucht, ein RFC-3161-Token den Signaturwert an einen Zeitpunkt vor Ablauf des Zertifikats binden. Acrobat fasst alle drei in einem Symbol zusammen; ein Konformitätsprüfer hält sie getrennt, und das sollte auch der Code tun, der diese Dateien erzeugt. losLab PDF Library (PDF Library for Delphi) liefert Ihnen die Signaturseite davon, das erneute Einbetten des Zeitstempels und die Audit-Aufrufe, um eine ByteRange zu untersuchen, bevor Sie ihr vertrauen

Eine Unterscheidung bringt fast jede erste PAdES-Implementierung zu Fall, daher lohnt es sich, sie vor jedem Code festzuhalten. Eine mit /SubFilter /adbe.pkcs7.detached geschriebene Signatur ist eine vollkommen korrekte Signatur nach ISO 32000-1 §12.8, die Acrobat als gültig meldet. Sie ist zugleich keine PAdES-Signatur, weil ETSI EN 319 142-1 auf jeder Baseline-Stufe ETSI.CAdES.detached verlangt. Ein eIDAS-Konformitätsprüfer lehnt die erste ab und akzeptiert die zweite, obwohl die Kryptographie identisch ist. Das Profil ist eine Aussage, die das Dokument über sich selbst trifft, und diese Aussage richtig zu setzen ist in PDF Library for Delphi ein einziger Aufruf

Was eine PDF-Signatur zur PAdES-Signatur macht

ETSI EN 319 142-1 definiert vier Baseline-Stufen, die auf dem CMS-Format aufsetzen. PAdES-B-B ist der Einstieg: eine CAdES-Signatur in einem PDF-Signaturfeld mit dem SubFilter ETSI.CAdES.detached 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 rückdatieren kann. PAdES-B-LT bettet die für die Validierung nötigen Zertifikate, CRLs und OCSP-Antworten 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 Dokument-Zeitstempel ab, der die angesammelten Nachweise erneut schützt, wenn Algorithmen schwächer werden

PDF Library for Delphi bildet diese Konzepte auf seine Sign-Process-API ab. Der Profilmarker ist SetSignProcessCustomSubFilter. Verlangt Ihre Richtlinie eine Commitment-Type-Angabe (Herkunftsnachweis, Genehmigungsnachweis oder einer der übrigen ETSI-Bezeichner mit den Nummern 1 bis 6), läuft das über SetSignProcessCommitmentType. Eine explizite Signaturrichtlinie wird mit SetSignProcessSignaturePolicy angehängt, das die Richtlinien-OID und ihren Digest entgegennimmt. Ein Standardwert verdient Aufmerksamkeit: Bleibt der Digest-Algorithmus auf Auto, wählt die Bibliothek SHA-256 für ETSI- und adbe.pkcs7.detached-Signaturen und fällt nur auf dem veralteten adbe.pkcs7.sha1-Pfad auf SHA-1 zurück. Setzen Sie ihn trotzdem explizit. Auditoren fragen, welchen Hash Sie verwendet haben, und ein expliziter Wert im Code ist leichter zu verteidigen als ein Standard, für dessen Erklärung Sie erst im Handbuch nachlesen müssen

Leiter der PAdES-Baseline-Stufen B-B, B-T, B-LT und B-LTA, erstellt mit PDF Library for Delphi, die zeigt, wie jede Stufe Zeitstempel, DSS-Nachweise oder einen erneuerbaren Dokument-Zeitstempel auf den ETSI.CAdES.detached-Kern aufsetzt
Jede ETSI-Baseline-Stufe legt eine weitere Garantie auf denselben CAdES-Kern, von signierten Attributen bis zum erneuerbaren Dokument-Zeitstempel

Die Baseline-Signatur erzeugen

Die flache API steuert das Signieren als einmalige Zustandsmaschine: einen Prozess auf der Quelldatei öffnen, ihn konfigurieren, in eine Ausgabedatei abschließen, den Ergebniscode lesen. Die folgende Sequenz erzeugt eine PAdES-B-B-Signatur mit SHA-256. Die wichtigste Zeile hat mit der Signatur selbst nichts zu tun. Es ist die absichtlich überdimensionierte /Contents-Reservierung, denn das ist das Einzige, was Sie später nicht mehr ändern können, falls dieser Signatur jemals ein Zeitstempel hinzugefügt werden muss

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);  // Platz für einen späteren Zeitstempel
    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 sich die Quelle gar nicht öffnen lässt. Danach trennt GetSignProcessResult die Fehlerarten, die in der Produktion tatsächlich vorkommen: 4 bedeutet ein falsches PDF-Passwort, 7 ein falsches PFX-Passwort, 9 eine Zertifikatsdatei ohne privaten Schlüssel, 10 einen nicht beschreibbaren Ausgabepfad, 11 einen Fehler beim Anwenden der Signaturbytes. Den numerischen Code neben dem Namen der Eingabedatei zu protokollieren, verwandelt ein vages Support-Ticket in eine Diagnose von einer Minute

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

PDF Library for Delphi liefert keinen TSA-Client mit, und das ist eine bewusste Grenze, keine Lücke. Die Bibliothek berechnet den Hash, den die Zeitstempelstelle gegenzeichnen muss, und bettet das erweiterte CMS anschließend erneut ein; der HTTP-Austausch und die CMS-Operation dazwischen gehören dem Aufrufer. Für die Aufteilung gibt es einen harten technischen Grund. Das Windows-CryptoAPI-Steuerkommando, das nominell unsignierte Attribute hinzufügt, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, schlägt beim Detached-SignedData-Layout, das PAdES verwendet, mit CRYPT_E_INVALID_INDEX fehl. Das erweiterte CMS muss also aus einem CMS-Encoder unter Ihrer eigenen Kontrolle kommen. Keine Bibliothek kann das Token still mit einem einzigen Systemaufruf einfalten, und jede, die das behauptet, führt den Eingriff an einer Stelle durch, die Sie nicht sehen können

Pipeline zum Hinzufügen eines RFC-3161-Zeitstempels zu einer PAdES-Signatur in Delphi, die das Hashing und Einbetten durch PDF Library for Delphi von der TSA-Anfrage und CMS-Neukodierung des Aufrufers innerhalb des reservierten /Contents-Bereichs trennt
Die Bibliothek hasht und bettet erneut ein, während Ihr Code das Token abruft und den CMS-Eingriff vornimmt, und das Ergebnis muss in die 8192-Byte-Reservierung von /Contents passen
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);
    // beide folgenden Aufrufe sind Anwendungscode: ein HTTP-POST an Ihre TSA
    // und eine CMS-Neukodierung, die das Token als unsigniertes Attribut anhängt
    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 Ergebniscodes: 12 bedeutet, dass das benannte Signaturfeld nicht existiert, 11, dass das vorhandene CMS nicht geparst werden konnte, und 13, dass das erweiterte CMS nicht mehr in den reservierten /Contents-Platzhalter passt. Code 13 ist der schmerzhafte, denn die einzige Abhilfe ist erneutes Signieren: Ein typisches Zeitstempel-Token mit seiner Zertifikatskette umfasst 4 bis 6 KB, und die im B-B-Schritt angelegte Reservierung von 8192 Byte existiert genau dafür, dass dieser Schritt Platz zum Landen hat

Validierung beginnt bei der ByteRange, nicht bei der Zertifikatskette

Ein grünes Häkchen in einem Viewer ist eine Vertrauensentscheidung gegen den Zertifikatsspeicher dieses Rechners, kein strukturelles Urteil über die Datei. Programmatische Validierung sollte tiefer ansetzen, bei der Frage, die inkrementelle Updates heikel machen: Welche Bytes deckt jede Signatur tatsächlich ab? Jede hier besprochene Erweiterung, ob zweite Signatur, DSS-Dictionary oder Dokument-Zeitstempel, kommt per inkrementellem Update, und jedes Update hängt Bytes außerhalb der /ByteRange der früheren Signatur an. Diese angehängten Bytes sind legitim. Ein Validator muss sie dennoch gegen die Änderungsrichtlinie des Dokuments einordnen, und die feldbezogene DocMDP-Stufe, in der diese Richtlinie lebt, lässt sich mit GetSignatureDocMDPLevelByName lesen

Byte-Layout-Audit einer signierten PDF in Delphi mit den von der ByteRange abgedeckten Bereichen, den ausgeschlossenen /Contents-Bytes, angehängten inkrementellen Updates außerhalb des Bereichs und dem Abdeckungsurteil gegenüber der Dateigröße
Zwei abgedeckte Bereiche unter Ausschluss der eigenen Signaturbytes erzählen die wahre Abdeckungsgeschichte, und angehängte Updates werden gegen die DocMDP-Richtlinie eingeordnet statt gefürchtet
var
  Doc: TPDFlibSignDoc;
  Names: TStringList;
  I: Integer;
  B0, B1, B2, B3, FileSize: Int64;
begin
  FileSize := TFile.GetSize('invoice-bt.pdf');  // vor Open: SignDoc hält eine Share-Sperre
  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 bedeutet tatsächlich signiert
        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;

In diesem Audit-Pfad lauern zwei Fallen. TPDFlibSignDoc.Open hält die Datei mit einer exklusiven Share-Sperre, sodass ein Validator, der die rohen Dateibytes auch für die CMS-Verifikation hashen will, die Datei in den Speicher lesen muss, bevor er sie für das Audit öffnet. Kehren Sie diese Reihenfolge um, scheitert das Lesen an einer Sperre, die Sie selbst gesetzt haben. Die zweite Falle ist still statt laut: Das Gegenstück in der flachen API, GetSignProcessByteRange, gibt Integer zurück, während die zugrunde liegenden Offsets Int64 sind, sodass der flache Aufruf jenseits von 2 GB kommentarlos abschneidet; deshalb zieht dieses Beispiel die Offsets stattdessen über die Audit-Klasse. Eine Abwesenheit verdient ebenfalls Erwähnung. Die flache Schicht hat überhaupt keinen VerifySignature-Wrapper. Kryptographische Urteile kommen vom klassenbasierten TPDFlibSignatureVerifier, der vsValid, vsInvalid oder vsUnknown zurückgibt, oder von einem externen Validator, dem Ihre Compliance-Richtlinie bereits vertraut

Langzeitvalidierung: DSS, VRI und der Dokument-Zeitstempel

PAdES-B-LT existiert, weil Sperrinfrastruktur sterblich ist. ETSI EN 319 142-1 §5.4.2.2 spezifiziert den Document Security Store: ein Dictionary auf Dokumentebene, das Zertifikate, CRLs und OCSP-Antworten trägt, optional pro Signatur über VRI-Einträge indiziert, deren Schlüssel der Hash des /Contents jeder Signatur ist. Der Ablauf in PDF Library for Delphi spiegelt das Zeitstempel-Design. NewPAdESDSSProcessFromFile öffnet den Prozess; AddPAdESDSSCertificate, AddPAdESDSSCRL und AddPAdESDSSOCSP nehmen DER-Blobs entgegen; AddPAdESDSSVRI bindet ausgewähltes Material an eine Signatur; EndPAdESDSSProcessToFile schreibt alles als inkrementelles Update. Der schwierige Teil bleibt auf Ihrer Seite. Das Sperrmaterial abzurufen und zu beurteilen, ob es frisch genug ist, um eingebettet zu werden, ist Aufgabe des Aufrufers. Die Bibliothek garantiert, dass die Dictionarys strukturell konform sind; sie kann nicht garantieren, dass Ihr OCSP-Responder die Wahrheit gesagt hat

Der Archiv-Endpunkt B-LTA fügt einen Dokument-Zeitstempel hinzu: ein separates Signaturfeld vom Typ DocTimeStamp statt Sig, erzeugt über SetSignProcessDocTimeStamp mit reservierter Signaturlänge. Er ersetzt nicht den Signatur-Zeitstempel aus dem B-T-Schritt. Der Signatur-Zeitstempel beweist, wann eine bestimmte Signatur existierte; der Dokument-Zeitstempel schützt die gesamte Datei einschließlich DSS-Nachweisen und ist das Element, das ein Langzeitarchiv alle paar Jahre erneuert, wenn Algorithmen schwächer werden. Ein ausgereiftes Archivprofil trägt beide. Für Reader, die älter sind als diese Strukturen, trägt TPDFlibSignDoc.EnsurePAdESExtensions die ESIC-Entwicklererweiterung im Dokumentkatalog ein und kündigt an, dass die Datei ETSI-definierte Funktionen verwendet

Einer Reaktion auf all das sollte man vorbeugen, weil sie wie ein Fehler aussieht und keiner ist. Ein Viewer meldet oft „Gültigkeit unbekannt“ bei einer Datei, deren PAdES-Struktur völlig korrekt ist. Vertrauen und Struktur sind unabhängige Achsen. Der Viewer kann den Signierer auf diesem Rechner schlicht nicht zu einer Wurzel verketten, der er vertraut, was bei privaten CAs und Testzertifikaten Routine ist, selbst wenn ByteRange-Audit und CMS-Verifikation beide bestehen. Die Abhilfe ist, das Wurzelzertifikat ordentlich zu verteilen oder gegen die EU-Vertrauenslisten zu prüfen, wenn qualifizierter eIDAS-Status das eigentliche Ziel ist, statt den Signaturcode anzufassen

Für die Audit-Perspektive, also das Aufzählen von Signaturfeldern über einen Korpus, das Ausgeben von ByteRange-Layouts und das massenhafte Lesen von DocMDP-Stufen, siehe den Begleitartikel zur Compliance- und Signatur-Workbench. Signierte Dokumente, die zusätzlich eine Archivrichtlinie erfüllen müssen, gehören in den Workflow aus PDF/A- und PDF/UA-Preflight in Delphi. Die vollständige API-Dokumentation und Test-Downloads finden Sie auf der Produktseite von losLab PDF Library for Delphi