Technical Article

Inspect PDF Signatures and PAdES Levels in Delphi

Sie haben eine signierte PDF-Datei erhalten und möchten in Ihrem Viewer anzeigen, wer sie signiert hat, wann sie signiert wurde, ob die Signatur die gesamte Datei abdeckt und wie weit sie in Richtung Langzeit-Konformität (Archivierungssicherheit) geht. Die PDFium-Komponente für Delphi und Lazarus beantwortet alle vier Fragen mit Nur-Lese-Aufrufen: FPDF_GetSignatureCount und die Funktionsfamilie FPDFSignatureObj_* machen das Signaturverzeichnis lesbar, und TPdf.ValidatePades klassifiziert das PAdES-Baseline-Niveau. Dies ist der erste von drei Artikeln über PDF-Signaturen mit PDFium; die folgenden behandeln das Erstellen einer B-B-Signatur und das Hinzufügen von vertrauenswürdigen Zeitstempeln. Eine wichtige Grenze gehört jedoch an den Anfang: Alles hier beschriebene betrifft die Struktur-Prüfung. Das Auslesen der Eigenschaften einer Signatur ist eine andere Aufgabe als die kryptografische Verifizierung der Signatur oder die Entscheidung, ob Sie dem Aussteller vertrauen

Warum ein PDF-Signaturverzeichnis nicht nur ein Haufen Bytes ist

Eine PDF-Signatur ist ein Verzeichnis-Eintrag (Dictionary) und kein undurchsichtiger Anhang. Ihre beiden wichtigsten Einträge verraten, wie viel der Datei tatsächlich geschützt ist. ISO 32000-1 §12.8 definiert das Signaturverzeichnis mit den Einträgen /ByteRange und /Contents. /Contents enthält eine hex-codierte CMS-SignedData-Struktur (RFC 5652) — das kryptografische Datenpaket, das das Zertifikat des Unterzeichners, die signierten Attribute und den eigentlichen Signaturwert transportiert. /ByteRange ist der Teil, der von Entwicklern oft unterschätzt wird: Es ist ein Array aus zwei Offset-Längen-Spannen, die zusammen die gesamte Datei abdecken, mit Ausnahme der hexadezimalen Zeichenfolge in /Contents. Diese Lücke ist genau der Ort, an dem die Signatur-Bytes sitzen. Die beiden Spannen davor und danach sind exakt die Daten, auf die sich die Signatur bezieht

Dieses ByteRange-Design macht ein nachträgliches Speichern (Incremental Save) überprüfbar. Da ein Unterzeichner die Signatur-Bytes, die noch nicht existieren, nicht hashen kann, wird die Datei um den Platzhalter /Contents herum aufgeteilt und alles andere in die Signatur gehasht. Eine Signatur, deren ByteRange nicht bis zum Ende der Datei reicht, ist ein Warnsignal: Inhalte, die nach dem abgedeckten Bereich durch ein späteres inkrementelles Update angehängt werden, würden die Signatur nicht verletzen, obwohl sie das visuelle Erscheinungsbild des Dokuments verändern. Das Erste, was eine seriöse Prüfung kontrolliert, ist also nicht, wer unterzeichnet hat, sondern ob die Signatur den Byte-Bereich abdeckt, den sie vorgibt zu schützen

Auslesen des Signaturverzeichnisses mit der Nur-Lese-API von PDFium

Die PDFium-Komponente stellt das Signaturverzeichnis über zwei Nur-Lese-Eigenschaften bereit: SignatureCount und den Record Signature[Index]. Unter der Haube rufen sie die Low-Level-Methoden FPDF_GetSignatureCount, FPDF_GetSignatureObject und die FPDFSignatureObj_*-Zugriffsfunktionen für /SubFilter, /ByteRange, /Contents, /Reason sowie die Signaturzeit auf. Nur-Lese-Zugriff ist hier wörtlich gemeint: PDFium kann Signaturen auflisten und lesen, besitzt aber keine API, um sie zu erstellen oder zu schreiben. Daher wird der schreibende Part dieser Artikelreihe von der Pascal-Bibliothek selbst und nicht von PDFium implementiert

var
  Pdf: TPdf;
  i: Integer;
  Sig: TPdfSignature;
begin
  Book := TXLSWorkbook.Create;
  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);            // signer date string, e.g. D:20260708120000+02'00'
      Writeln('Reason    : ', Sig.Reason);
      Writeln('CMS length: ', Length(Sig.Content)); // raw DER SignedData taken from /Contents
      Writeln('DocMDP    : ', Sig.Permission);      // 0 = no certification, 1..3 = MDP level
    end;
  finally
    Pdf.Free;
  end;
end;

Jeder TPdfSignature-Record bildet das Verzeichnis direkt ab. Encoding ist der Wert von /SubFilter, das aussagekräftigste Feld, da es das genutzte Signatur-Verfahren benennt und sofort eine moderne ETSI.CAdES.detached-Signatur von veralteten oder unzulässigen Formaten trennt. Time ist die vom Unterzeichner angegebene Signaturzeit als PDF-Datumszeichenfolge — eine bloße Behauptung des Unterzeichners und keine vertrauenswürdige Zeitangabe. Content enthält die rohen CMS-SignedData-Bytes, und Permission zeigt die DocMDP-Zertifizierungsebene an (0 für eine normale Genehmigungssignatur, 1 bis 3 für eine Zertifizierungssignatur, die spätere Änderungen einschränkt). Das einzige Feld, das der Record nicht direkt anbietet, ist das ByteRange-Array. Dieses Auslassen ist Absicht, da die Methode ValidatePades die Abdeckungberechnungen für Sie übernimmt

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

Die vier PAdES-Baseline-Ebenen bilden eine Kette von einer minimal gültigen Signatur bis hin zu einem Archivformat, das für Jahrzehnte ausgelegt ist. Jedes Niveau umfasst dabei das jeweils darunter liegende. ETSI EN 319 142-1 definiert sie als B-B, B-T, B-LT und B-LTA. B-B (Basic) besteht aus der Signatur und den zwingend erforderlichen signierten Attributen, aber nicht mehr. B-T (Timestamp) fügt einen vertrauenswürdigen Zeitstempel nach RFC 3161 über die Signatur hinzu, sodass der Zeitpunkt der Unterzeichnung von einer Zeitstempelbehörde (TSA) bezeugt wird und nicht von der Uhr des Unterzeichners abhängt. B-LT (Long-Term) bettet die Validierungsdaten — die Zertifikatskette und optional OCSP- oder CRL-Widerrufsantworten — direkt in die Datei ein, sodass die Signatur auch Jahre später validiert werden kann, wenn die ausstellende Infrastruktur nicht mehr existiert. B-LTA (Long-Term with Archive timestamp) umschließt diese Daten mit einem Archiv-Zeitstempel, schützt die Validierungsdaten selbst und ermöglicht eine spätere Erneuerung der Zeitstempel, bevor die genutzten kryptografischen Algorithmen veralten

In der Praxis geht es um den Zeithorizont: Eine B-B-Signatur beantwortet die Frage „Hat jemand dieses Dokument unterzeichnet?“. B-T beantwortet „Und wann, nachweisbar?“. B-LT beantwortet „Und kann ich das auch nach Ablauf der Zertifikate noch prüfen?“. B-LTA beantwortet „Und bleibt diese Prüfung auch in zwanzig Jahren noch gültig?“. Regulatorische Richtlinien fordern oft mindestens B-T, während Archivierungsvorschriften B-LT oder B-LTA verlangen. Zu wissen, welches Niveau ein Dokument erreicht, bevor Sie es annehmen oder ablehnen, ist Sinn und Zweck der Prüfung

Erkennen des Baseline-Niveaus mit TPdf.ValidatePades

Die PDFium-Komponente reduziert die Einstufung auf einen einzigen Aufruf. TPdf.ValidatePades liefert ein TPadesValidationResult-Record zurück, dessen Feld Level den Typ TPadesLevel trägt (plNone, plUnknown, plB_B, plB_T, plB_LT oder plB_LTA) — zusammen mit einer Liste von strukturellen Mängeln, der Anzahl der Signaturen und der Anzahl der Dokumenten-Zeitstempel. Das Niveau wird aufbauend ermittelt: Der Validator stellt zuerst B-B fest und stuft das Dokument auf B-T hoch, wenn ein Signatur-Zeitstempel oder ein Dokumenten-Zeitstempel vorhanden ist. Es folgt die Einstufung auf B-LT, wenn der Katalog einen /DSS-Eintrag mit Zertifikaten und den Extensions-Marker /ESIC Level 1 enthält, und schließlich auf B-LTA, wenn ein Dokumenten-Zeitstempel und der Marker ESIC Level 2 vorhanden sind. Zwei Hilfsfunktionen machen das Ergebnis nutzbar: IsCompliant ist nur True, wenn das Niveau mindestens B-B erreicht und keine Mängel vorliegen, und IsCompliantAt erlaubt es, ein minimal gefordertes Richtlinien-Niveau wie plB_T abzufragen

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 unzulässiger SubFilter?

Weil SHA-1 veraltet ist und das Verfahren adbe.pkcs7.sha1 diesen Algorithmus fest vorschreibt. Dieser SubFilter hasht das Dokument mit SHA-1, bevor er es in PKCS#7 verpackt. Da SHA-1 seit Jahren als kollisionsanfällig gilt, verbietet EN 319 142-1 Klausel 6.3 dieses Format für Baseline-Signaturen. ValidatePades meldet den Fehler ppeiForbiddenSubFilter, wenn es auf adbe.pkcs7.sha1 oder adbe.x509.rsa_sha1 stößt, und meldet ppeiBadDigestAlgorithm, wenn das CMS selbst MD5 oder SHA-1 als Nachrichten-Digest nutzt (Klausel 6.2.1). Dies sind zwei getrennte Prüfungen, die dieselbe Art von Schwachstelle auf verschiedenen Ebenen abfangen

Die Mängelliste umfasst insgesamt 26 Einträge. Die in der Praxis häufigsten betreffen die Struktur und die Abdeckung: ppeiByteRangeNotCoveringFile ist die oben beschriebene Abdeckungsprüfung. ppeiForbiddenCertKey schlägt an, wenn das Signaturverzeichnis einen eigenen /Cert-Eintrag enthält, was PAdES verbietet, da die Zertifikatskette ausschließlich in den CMS-Daten unter SignedData.certificates liegen darf. ppeiMissingSigningCertificate, ppeiMissingContentType und ppeiMissingMessageDigest melden das Fehlen zwingend vorgeschriebener signierter Attribute, und ppeiDetachedContentViolation meldet Signaturen, die den signierten Inhalt fälschlicherweise einbetten, statt ihn abzuspalten. Das Auswerten dieser Liste macht aus einer Ablehnung eine konkrete 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 prüft die Struktur, nicht die Vertrauenswürdigkeit. Diese beiden Dinge zu verwechseln, ist ein gefährlicher Fehler. Ein Ergebnis von plB_LTA bedeutet, dass das Dokument eine formal korrekte B-LTA-Signatur mit allen vorgeschriebenen Attributen und Zeitstempeln an den richtigen Stellen enthält — es bedeutet nicht, dass die Signatur kryptografisch gültig ist, dass die Zertifikatskette auf eine von Ihnen als vertrauenswürdig eingestufte Stammzertifizierungsstelle zurückgeht oder dass kein Zertifikat der Kette widerrufen wurde. Der Validator nimmt bewusst keine kryptografische Prüfung vor: Er berechnet die Signatur nicht über den ByteRange neu, baut keine Vertrauenskette auf und prüft keine OCSP- oder CRL-Widerrufsdienste. Diese Aufteilung ist sinnvoll, da die strukturelle Prüfung schnell und vollständig deterministisch ist und weder Schlüssel noch Netzwerkzugriff oder Betriebssystem-Kryptografie benötigt. So läuft ValidatePades auf Windows, Linux und macOS identisch als reiner Pascal-Code über die Datei-Bytes. Die Validierung der Vertrauenskette hingegen hängt stark von Ihren Richtlinien ab (welchen Stammzertifikaten Sie vertrauen, wie Sie Widerrufsdaten abrufen, wie tolerant Sie bei Zeitstempeln sind) und gehört in einen späteren Arbeitsschritt, der den Zertifikatsspeicher des Betriebssystems einbezieht. Betrachten Sie ValidatePades als den notwendigen ersten Filter, der die formale Korrektheit der Signatur bestätigt, bevor Sie die Signatur der eigentlichen kryptografischen Prüfung übergeben

Dieser strukturelle Durchlauf ist der richtige Einstiegspunkt. Er lässt sich ideal mit den Sicherheitsprüfungen aus dem Artikel über das Auditieren von PDF-Sicherheitsrisiken mit der PDFium-Komponente und mit Konformitätsprüfungen wie dem Prüfen druckfertiger PDF/X-Dokumente kombinieren. Wenn Sie eine Signatur lesen und einordnen können, besteht der nächste Schritt im Erstellen einer solchen: Der zweite Artikel dieser Reihe behandelt das Signieren von PDFs mit PAdES B-B, und der dritte erweitert dies um vertrauenswürdige Zeitstempel und Langzeit-Signaturen vom Typ B-LT und B-LTA. Die hier gezeigte Signatur-Prüfung und die ValidatePades-Einstufung sind Teil der PDFium-Komponente für Delphi, C++Builder und Lazarus