Technischer Artikel

PDF-Signaturen und PAdES-Level in Delphi inspizieren

Sie haben eine signierte PDF erhalten und müssen in Ihrem Betrachter zeigen, wer sie signiert hat, wann sie signiert wurde, ob die Signatur die ganze Datei abdeckt und wie weit sie in Richtung Langzeit-Konformität reicht. Die PDFium Component für Delphi und Lazarus beantwortet alle vier Fragen mit Nur-Lese-Aufrufen: FPDF_GetSignatureCount und die FPDFSignatureObj_*-Familie legen das Signatur-Dictionary offen, und TPdf.ValidatePades klassifiziert das PAdES-Baseline-Level. Dies ist der erste von drei Artikeln über PDF-Signaturen mit PDFium; die beiden folgenden behandeln das Erstellen einer B-B-Signatur und das Hinzufügen von Langzeit-Zeitstempeln. Eine Grenze gehört jedoch an den Anfang: Alles hier ist Inspektion, und zu lesen, was eine Signatur behauptet, ist eine andere Aufgabe, als ihre Kryptographie zu verifizieren oder zu entscheiden, ob Sie dem Signierer vertrauen

Warum ein PDF-Signatur-Dictionary nicht bloß ein Byte-Klumpen ist

Eine PDF-Signatur ist ein Dictionary, kein undurchsichtiger Anhang, und ihre zwei wichtigsten Einträge sagen Ihnen, wie viel von der Datei tatsächlich geschützt ist. ISO 32000-1 §12.8 definiert das Signatur-Dictionary mit einem /ByteRange- und einem /Contents-Eintrag. /Contents enthält eine hex-kodierte CMS-SignedData-Struktur (RFC 5652), den kryptographischen Umschlag, der das Signiererzertifikat, die signierten Attribute und den Signaturwert selbst trägt. /ByteRange ist der Teil, den Entwickler unterschätzen: Es ist ein Array aus zwei Offset-Länge-Spannen, die zusammen die gesamte Datei außer der /Contents-Hex-Zeichenkette abdecken. Diese Lücke ist genau der Ort, an dem die Signaturbytes sitzen, und die beiden Spannen zu beiden Seiten sind exakt das, worauf sich die Signatur festlegt

Das ByteRange-Design ist es, was ein inkrementelles Speichern prüfbar macht. Weil ein Signierer keine Signaturbytes hashen kann, die noch nicht existieren, wird die Datei um den /Contents-Platzhalter herum geteilt und alles andere in die Signatur gehasht. Eine Signatur, deren ByteRange nicht bis zum Dateiende reicht, ist ein Warnzeichen: Inhalt, der nach dem abgedeckten Bereich durch ein späteres inkrementelles Update angehängt wurde, würde die Signatur nicht brechen, obwohl er ändert, was der Leser sieht. Das Erste, was ein ernsthafter Inspektor prüft, ist also nicht, wer signiert hat, sondern ob die Signatur die Bytes abdeckt, die sie zu bestätigen scheint

PDFium legt eine signierte PDF als Signatur-Dictionary offen, dessen zwei /ByteRange-Spannen die ganze Datei um die /Contents-CMS-Hex-Zeichenkette herum hashen, während nach dem Signieren angehängte Bytes unabgedeckt bleiben
Die zwei /ByteRange-Spannen hashen alles um die /Contents-Hex-Zeichenkette herum, sodass angehängte Bytes ändern, was Leser sehen, ohne die Signatur zu brechen

Das Signatur-Dictionary mit PDFiums Nur-Lese-API lesen

Die PDFium Component stellt das Signatur-Dictionary über zwei Nur-Lese-Member bereit: SignatureCount und den Signature[Index]-Record. Unter der Haube rufen sie FPDF_GetSignatureCount, FPDF_GetSignatureObject und die FPDFSignatureObj_*-Accessoren für /SubFilter, /ByteRange, /Contents, /Reason und die Signierzeit auf. Nur-Lese ist hier das entscheidende Wort: PDFium kann Signaturen aufzählen und lesen, hat aber keine API, um eine zu erstellen oder zu schreiben, weshalb die Signierseite dieser Serie von der Bibliothek selbst implementiert wird und nicht von PDFium

var
  Pdf: TPdf;
  i: Integer;
  Sig: TPdfSignature;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    for i := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[i];
      Writeln('SubFilter : ', Sig.Encoding);        // ETSI.CAdES.detached, adbe.pkcs7.detached, ...
      Writeln('Signed at : ', Sig.Time);            // Datumszeichenkette des Signierers, z. B. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // rohe DER-SignedData aus /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = keine Zertifizierung, 1..3 = MDP-Stufe
    end;
  finally
    Pdf.Free;
  end;
end;

Jeder TPdfSignature-Record bildet direkt auf das Dictionary ab. Encoding ist der /SubFilter, das einzelne diagnostischste Feld, weil es den Signatur-Handler benennt und eine moderne ETSI.CAdES.detached-Signatur sofort von einer veralteten oder verbotenen trennt. Time ist die vom Autor angegebene Signierzeit als PDF-Datumszeichenkette, was eine Behauptung des Signierers ist und keine vertrauenswürdige Zeit. Content ist die rohe CMS-SignedData, und Permission legt die DocMDP-Zertifizierungsstufe offen (0 für eine gewöhnliche Genehmigungssignatur, 1 bis 3 für eine Zertifizierungssignatur, die spätere Änderungen einschränkt). Das eine Feld, das der Record nicht bereitstellt, ist die geparste ByteRange, und diese Auslassung ist beabsichtigt, denn ValidatePades erledigt die Arithmetik der ByteRange-Abdeckung für Sie, statt Sie sie von Hand wiederholen zu lassen

Was ist der Unterschied zwischen PAdES B-B, B-T, B-LT und B-LTA?

Die vier PAdES-Baseline-Level bilden eine Leiter von einer minimal gültigen Signatur bis zu einer, die Jahrzehnte der Archivierung überstehen soll, und jedes Level enthält strikt das darunterliegende. ETSI EN 319 142-1 definiert sie als B-B, B-T, B-LT und B-LTA. B-B (Basic) ist die Signatur plus die verpflichtenden signierten Attribute und nichts weiter. B-T (Timestamp) fügt einen vertrauenswürdigen RFC-3161-Zeitstempel über die Signatur hinzu, sodass der Zeitpunkt des Signierens von einer Zeitstempelstelle bestätigt statt von der Uhr des Signierers behauptet wird. B-LT (Long-Term) bettet das Validierungsmaterial — die Zertifikatskette und optional OCSP- oder CRL-Antworten — in die Datei ein, sodass die Signatur auch Jahre später noch validiert werden kann, wenn die ausstellende Infrastruktur verschwunden ist. B-LTA (Long-Term with Archive timestamp) legt einen Dokument-Zeitstempel um dieses Material, schützt die Langzeitdaten selbst und gibt Ihnen einen Punkt, an dem Sie erneut zeitstempeln können, bevor die zugrunde liegende Kryptographie altert

Die praktische Lesart dreht sich um den Zeithorizont. Eine B-B-Signatur beantwortet „hat jemand das signiert“. B-T beantwortet „und wann, beweisbar“. B-LT beantwortet „und kann ich das nach Ablauf der Zertifikate noch prüfen“. B-LTA beantwortet „und hält diese Prüfung in zwanzig Jahren noch stand“. Regulatorische Profile wählen eine Sprosse: Viele E-Rechnungs- und eIDAS-Kontexte verlangen mindestens B-T, und Archivierungsvorgaben greifen nach B-LT oder B-LTA. Zu wissen, welche Sprosse ein Dokument tatsächlich erreicht, bevor Sie es annehmen oder ablehnen, ist der ganze Sinn des Inspektionsschritts

Die PAdES-Baseline-Leiter steigt von B-B über B-T und B-LT bis B-LTA, wobei jedes Level Zeitstempel oder Langzeit-Validierungsmaterial hinzufügt, das TPdf.ValidatePades in Delphi erkennen kann
Jedes PAdES-Baseline-Level enthält strikt das darunterliegende, und ValidatePades meldet, welche Sprosse ein Dokument tatsächlich erreicht

Das Baseline-Level mit TPdf.ValidatePades erkennen

Die PDFium Component reduziert die gesamte Level-Frage auf einen Aufruf. TPdf.ValidatePades gibt einen TPadesValidationResult-Record zurück, dessen Feld Level ein TPadesLevel ist — plNone, plUnknown, plB_B, plB_T, plB_LT oder plB_LTA — neben einer Menge von Problemen, einer Signaturanzahl und einer Anzahl von Dokument-Zeitstempeln. Das Level wird monoton abgeleitet: Der Validator stellt zuerst B-B fest, stuft dann auf B-T hoch, wenn ein Signatur-Zeitstempel oder ein Dokument-Zeitstempel vorhanden ist, auf B-LT, wenn der Katalog ein /DSS mit Zertifikaten und die /Extensions /ESIC-Level-1-Markierung trägt, und auf B-LTA, wenn ein Dokument-Zeitstempel und die ESIC-Level-2-Markierung beide vorhanden sind. Zwei Helfer machen das Ergebnis handlungsfähig: IsCompliant ist nur dann True, wenn das Level mindestens B-B erreicht und die Problemmenge leer ist, und IsCompliantAt lässt Sie eine Richtlinien-Untergrenze wie plB_T per Assertion prüfen

var
  Pdf: TPdf;
  R: TPadesValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract-signed.pdf';
    Pdf.Active := True;
    R := Pdf.ValidatePades;
    case R.Level of
      plNone:    Writeln('No PAdES signature present');
      plUnknown: Writeln('Signature present but level undeterminable');
      plB_B:     Writeln('PAdES B-B   (basic)');
      plB_T:     Writeln('PAdES B-T   (trusted timestamp)');
      plB_LT:    Writeln('PAdES B-LT  (long-term material embedded)');
      plB_LTA:   Writeln('PAdES B-LTA (archive timestamp)');
    end;
    Writeln('Signatures   : ', R.SignatureCount);
    Writeln('DocTimeStamps: ', R.DocTimeStampCount);
    if R.IsCompliantAt(plB_T) then
      Writeln('Meets the B-T policy floor')
    else
      Writeln('Below the required B-T level');
  finally
    Pdf.Free;
  end;
end;

Warum ist adbe.pkcs7.sha1 ein verbotener SubFilter?

Weil SHA-1 gebrochen ist und der adbe.pkcs7.sha1-Handler es fest einbaut. Dieser SubFilter hasht das Dokument mit SHA-1 vor, bevor er es in PKCS#7 verpackt, und SHA-1 ist seit Jahren kollisionsanfällig, weshalb EN 319 142-1 Klausel 6.3 ihn für eine Baseline-Signatur rundheraus verbietet. ValidatePades meldet ppeiForbiddenSubFilter, wenn es adbe.pkcs7.sha1 oder adbe.x509.rsa_sha1 sieht, und es meldet ppeiBadDigestAlgorithm, wenn das CMS selbst MD5 oder SHA-1 als Message Digest verwendet (Klausel 6.2.1). Das sind zwei getrennte Prüfungen, die dieselbe Schwächeklasse auf zwei verschiedenen Ebenen fangen

Die Problemmenge hat insgesamt 26 Mitglieder, und diejenigen, denen Sie am häufigsten begegnen werden, gruppieren sich um Struktur und Abdeckung. ppeiByteRangeNotCoveringFile ist die zuvor beschriebene Abdeckungsprüfung. ppeiForbiddenCertKey feuert, wenn das Signatur-Dictionary einen /Cert-Eintrag trägt, was PAdES verbietet, weil die Kette stattdessen innerhalb der CMS-SignedData.certificates liegen muss. ppeiMissingSigningCertificate, ppeiMissingContentType und ppeiMissingMessageDigest markieren fehlende verpflichtende signierte Attribute, und ppeiDetachedContentViolation fängt eine Signatur, die den signierten Inhalt fälschlich einbettet, statt ihn abzutrennen. Das Aufzählen der Menge verwandelt eine bloße Ablehnung in eine Diagnose, die Sie protokollieren können

var
  R: TPadesValidationResult;
  Issue: TPadesValidationIssue;
begin
  R := Pdf.ValidatePades;
  if R.Issues <> [] then
    for Issue := Low(TPadesValidationIssue) to High(TPadesValidationIssue) do
      if Issue in R.Issues then
        Writeln('Issue: ',
          GetEnumName(TypeInfo(TPadesValidationIssue), Ord(Issue)));
end;

Was ValidatePades nicht prüft

ValidatePades validiert Struktur, nicht Vertrauen, und das eine mit dem anderen zu verwechseln ist der gefährliche Fehler. Ein Ergebnis von plB_LTA bedeutet, dass das Dokument eine wohlgeformte B-LTA-Signatur mit allen vorgeschriebenen Attributen, Materialien und Zeitstempeln an den richtigen Stellen enthält — es bedeutet nicht, dass die Signatur kryptographisch gültig ist, dass das Zertifikat zu einer Wurzel führt, der Sie vertrauen, oder dass kein Zertifikat in der Kette widerrufen wurde. Der Validator führt bewusst keine kryptographische Verifikation durch: Er berechnet die Signatur über die ByteRange nicht neu, baut oder bewertet die Vertrauenskette nicht und prüft keinen OCSP- oder CRL-Widerrufsstatus. Diese Trennung ist beabsichtigt und nützlich, denn strukturelle Inspektion ist schnell, vollständig deterministisch und braucht keine Schlüssel, kein Netzwerk und keine Plattform-Kryptographie, sodass ValidatePades auf Windows, Linux und macOS identisch als reines Pascal über die Dateibytes läuft. Die Validierung der Vertrauenskette ist dagegen untrennbar mit Richtlinien verbunden — welchen Wurzeln Sie vertrauen, wie Sie Widerrufsinformationen abrufen, wie streng Ihre Zeitstempeltoleranz ist — und gehört zu einer späteren Stufe, die vom Zertifikatsspeicher der Plattform abhängt; behandeln Sie ein bestandenes ValidatePades daher als das notwendige Tor dafür, dass eine Signatur korrekt geformt ist, und übergeben Sie eine strukturell einwandfreie Signatur dann an eine echte kryptographische Verifikation, bevor Sie sich auf sie verlassen

TPdf.ValidatePades prüft in Delphi die PAdES-Struktur wie SubFilter, ByteRange-Abdeckung und Zeitstempel und übergibt eine strukturell einwandfreie Signatur dann an eine separate kryptographische Vertrauensverifikation
Strukturelle Inspektion ist schnell und deterministisch, während Kettenvertrauen und Widerruf zu einer späteren Verifikationsstufe gehören

Dieser strukturelle Durchlauf ist der richtige Ausgangspunkt, und er passt natürlich zu den breiteren Nur-Lese-Prüfungen in Prüfung von PDF-Sicherheitsrisiken mit der PDFium Component und zu Arbeiten an der Formatkonformität wie der Validierung druckfertiger PDF/X-Dokumente. Sobald Sie eine Signatur lesen und klassifizieren können, ist der nächste Schritt, eine zu erzeugen: Der zweite Artikel dieser Serie behandelt das Signieren von PDFs mit PAdES B-B, und der dritte erweitert das um vertrauenswürdige Zeitstempel und langfristige B-LT- und B-LTA-Signaturen. Die hier gezeigte Nur-Lese-Signaturinspektion und der ValidatePades-Klassifikator sind Teil der PDFium Component für Delphi, C++Builder und Lazarus