Die HotPDF-Komponente verifiziert digitale Signaturen in geladenen PDF-Dokumenten über drei THotPDF-Methoden: GetLoadedSignatureInfo, VerifyLoadedSignature und VerifyLoadedSignatureEx, die in v2.259.0 eingeführt wurden. Die Komponente berechnet die Hashes der /ByteRange-Segmente der Originaldatei neu, überprüft das CMS-Attribut messageDigest und führt eine RSA-PKCS#1 v1.5-Verifizierung gegen das eingebettete Zertifikat des Unterzeichners durch. Sie gibt svValid zurück, wenn die Bytes des Dokuments unbeschädigt sind
Das Szenario ist alltäglich, aber die Tragweite hoch: Ein Vertragspartner sendet einen signierten Vertrag zurück. Ihr Workflow muss diesen archivieren, und jemand stellt die einzig entscheidende Frage: Handelt es sich Byte für Byte um exakt das Dokument, das wir gesendet haben, und wurde es mit dem angegebenen Zertifikat unterzeichnet? Die Beantwortung dieser Frage im Code ist die Verifizierungsseite des Signaturprozesses. Die Erstellung und Einbettung von PAdES-Signaturen wird im Begleitartikel über die Erstellung digitaler PAdES-Signaturen mit HotPDF behandelt. Dieser Artikel befasst sich mit dem umgekehrten Weg: Ein bereits signiertes PDF trifft ein, und Sie möchten ein programmgesteuertes Urteil erhalten, anstatt sich auf das grüne Häkchen von Acrobat zu verlassen
Wie beweist ein signiertes PDF, dass es nicht manipuliert wurde?
Eine PDF-Signatur schützt bestimmte Byte-Bereiche der Datei, nicht eine abstrakte Vorstellung des „Dokuments“. ISO 32000-1 §12.8 definiert den Mechanismus: Das Signaturformularfeld enthält ein Verzeichnis, dessen Eintrag /Contents einen CMS-SignedData-Container (RFC 5652) trägt und dessen Array /ByteRange die genauen Regionen der Datei angibt, die die Signatur abdeckt (gemäß §12.8.1). Das Array ist eine Liste von Offset- und Längenpaaren. In der Praxis handelt es sich um zwei Segmente: alles vor der Hex-Zeichenfolge /Contents und alles dahinter. Da der Signaturwert sich nicht selbst abdecken kann, wird die Datei um dieses Loch herum gehasht
Dieses Design hat eine Konsequenz, die die gesamte API prägt: Die Verifizierung muss die ursprünglichen serialisierten Bytes genau so hashen, wie sie auf dem Datenträger liegen. Ein geparstes Objektmodell ist hierfür nutzlos, da das erneute Serialisieren eines auch unveränderten Dokuments andere Bytes erzeugt. HotPDF verifiziert daher gegen die Quelldatei, aus der das Dokument geladen wurde, oder gegen einen von Ihnen bereitgestellten TStream mit Rohbytes — niemals gegen seine Darstellung im Arbeitsspeicher
Auslesen von Signatur-Metadaten vor der Verifizierung
GetLoadedSignatureInfo parst das Signatur-Verzeichnis und dessen CMS-Container, ohne ein einziges Byte des Dokuments zu prüfen. Dies macht die Methode zum idealen ersten Aufruf, wenn Sie lediglich anzeigen möchten, wer wann signiert hat. Signaturfelder sind in der Reihenfolge des Formularfelds ab 0 indiziert, und GetLoadedSignatureFieldCount gibt an, wie viele vorhanden sind. Der zurückgegebene Datensatz THPDFSignatureInfo enthält den Feldnamen, /SubFilter, den Common Name (CN) des Unterzeichnerzertifikats, die Distinguished Names (DN) von Inhaber und Aussteller, die Seriennummer, die Gültigkeitsdaten, den Signaturzeitpunkt (aus dem signierten Attribut, falls vorhanden, andernfalls aus dem Eintrag /M des Verzeichnisses), den Namen des Digest-Algorithmus sowie die Zeichenfolgen für /Reason, /Location und /ContactInfo. Das Element Status verbleibt auf dem ehrlichen Wert svNotVerified (gelesen, nicht verifiziert)
var
Pdf: THotPDF;
Info: THPDFSignatureInfo;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('signed-contract.pdf');
for I := 0 to Pdf.GetLoadedSignatureFieldCount - 1 do
begin
Info := Pdf.GetLoadedSignatureInfo(I);
Writeln('Feld: ', Info.FieldName);
Writeln('Signer: ', Info.SignerName);
Writeln('Aussteller: ', Info.IssuerDN);
Writeln('Algorithmus:', Info.HashAlgorithm);
Writeln('SubFilter: ', Info.SubFilter);
end;
finally
Pdf.Free;
end;
end;
Durchführung der kryptografischen Prüfung
VerifyLoadedSignatureEx führt die vollständige Verifizierung für ein aus einer Datei geladenes Dokument durch und liefert den ausgefüllten Info-Datensatz in einem einzigen Aufruf zurück: Es öffnet die Quelldatei erneut, hasht die /ByteRange-Segmente mit dem Digest-Algorithmus der SignerInfo, vergleicht das Ergebnis mit dem signierten Attribut messageDigest (RFC 5652 §5.4) und verifiziert anschließend die RSA-Signatur über der DER-SET-Neucodierung der signierten Attribute. Enthält eine Signatur keine signierten Attribute, wird die RSA-Prüfung stattdessen direkt über dem Dokumenten-Hash ausgeführt. Unterstützt werden RSA-Signaturen nach PKCS#1 v1.5 mit SHA-1, SHA-256, SHA-384 oder SHA-512 Digests, was die von gängigen Signaturwerkzeugen erzeugten Subfilter adbe.pkcs7.detached und ETSI.CAdES.detached abdeckt
var
Status: THPDFSignatureVerifyStatus;
Info: THPDFSignatureInfo;
begin
Status := Pdf.VerifyLoadedSignatureEx(0, Info);
case Status of
svValid:
if Info.CoversWholeDocument then
Writeln('Gültig; Signatur deckt die gesamte Datei ab')
else
Writeln('Gültig; Datei wurde nach Signierung erweitert');
svDigestMismatch:
Writeln('Dokumentenbytes wurden nach Signierung geändert');
svSignatureInvalid:
Writeln('RSA-Prüfung der signierten Attribute fehlgeschlagen');
svUnsupportedAlgorithm:
Writeln('Nicht-RSA-Schlüssel oder unbekannter Digest-Algorithmus');
svMalformed:
Writeln('CMS-Container konnte nicht geparst werden');
svSourceUnavailable:
Writeln('Keine Quellbytes vorhanden. Nutzen Sie die TStream-Überladung');
end;
end;
Zwei Details der Implementierung sind wissenswert, da sie Fehler erklären, die von außen betrachtet mysteriös wirken. Erstens ist die Prüfung der signierten Attribute streng bezüglich der Codierung: In der Datei sind die Attribute als [0] IMPLICIT gekennzeichnet, aber die Signatur wurde über ihrer DER-codierten SET OF-Form berechnet. Daher führt der Verifizierer vor dem Hashen eine entsprechende Kennzeichnung durch, genau wie es RFC 5652 §5.4 vorschreibt. Ein selbst erstellter Verifizierer, der die Bytes so hasht, wie sie in der Datei erscheinen, wird jedes ordnungsgemäß signierte Dokument ablehnen. Zweitens wird /Contents üblicherweise mit Nullen auf eine reservierte Byte-Größe aufgefüllt. Der Verifizierer schneidet daher das DER-Blob vor dem Parsen auf die tatsächliche Länge seiner äußeren SEQUENCE ab; unbrauchbar wirkende nachfolgende Nullen sind normal und keine Beschädigung. Die gleiche Familie von ASN.1-Parsing-Risiken auf der Seite des Zertifikatsimports wird im Artikel über PKCS#12- und ASN.1-Sicherheits-Härtung in HotPDF behandelt
Was garantiert eine gültige Signatur tatsächlich?
svValid bedeutet präzise Folgendes: Die durch /ByteRange definierten Bytes liefern den Hashwert, den der Unterzeichner signiert hat, und die Signatur ist unter dem öffentlichen Schlüssel des im CMS-Container eingebetteten Zertifikats gültig. Das ist die Integrität der Bytes und die Bindung an den Schlüssel, mehr nicht. Die Validierung der Zertifikatskette und des Vertrauens (Trust Validation) liegt explizit außerhalb des Aufgabenbereichs des Verifizierers von HotPDF: Er prüft die Kette nicht bis zu einem Stammzertifikat (Root), prüft keine Sperrlisten (Revocation) und konsultiert keinen Trust Store. Ein selbstsigniertes Zertifikat eines Angreifers, der ein modifiziertes Dokument neu signiert hat, wird als svValid verifiziert, da die Mathematik in sich konsistent ist. Ob der Unterzeichner derjenige ist, der er angibt zu sein, und ob man ihm vertrauen sollte, ist eine Richtlinienentscheidung, die einer separaten Schicht gehört — sei es der Zertifikats-Whitelist Ihrer Organisation, dem Windows-Zertifikatsspeicher oder einer Validierungsstelle
Das Flag CoversWholeDocument sichert eine subtilere Lücke ab. Eine Signatur deckt immer nur ihren /ByteRange ab, und der inkrementelle Aktualisierungsmechanismus von PDF erlaubt das Anhängen von Inhalten nach einer Signatur, ohne diese ungültig zu machen. Dies ist so entworfen und ermöglicht Workflows mit mehreren Unterschriften. Das Flag wird während der Verifizierung berechnet und ist nur dann wahr, wenn die beiden Segmente zuzüglich der Lücke für /Contents die gesamte Datei umfassen. Wenn svValid zurückgegeben wird, CoversWholeDocument jedoch falsch ist, ist die signierte Revision zwar intakt, aber die Datei enthält spätere Ergänzungen. Was diese Änderungen bewirkt haben, muss Ihr Workflow entscheiden
Über einen Stream geladene und verschlüsselte Dokumente benötigen ihre eigenen Quellbytes
Die parameterlosen Methoden VerifyLoadedSignature und VerifyLoadedSignatureEx hängen davon ab, dass die Komponente sich merkt, aus welcher Datei das Dokument stammt. Lädt man das Dokument aus einem Stream, gibt es keinen Dateinamen, der erneut geöffnet werden könnte. Dasselbe gilt nach dem Passwort-Ladevorgang für verschlüsselte Dokumente (der Workflow, der im Artikel über AES-256 PDF-Verschlüsselung mit HotPDF beschrieben wird). In beiden Fällen geben die dateibasierten Überladungen svSourceUnavailable zurück. Die Lösung ist die TStream-Überladung, mit der Sie die originalen Rohbytes übergeben können — sei es aus einer Datei, einem Speicherpuffer oder einem Datenbank-Blob
var
Src: TFileStream;
Status: THPDFSignatureVerifyStatus;
Info: THPDFSignatureInfo;
begin
// Über einen Stream geladenes Dokument: Die Komponente hält keinen
// Quelldatei-Namen. Stellen Sie die Original-Bytes selbst bereit.
Src := TFileStream.Create('signed-contract.pdf',
fmOpenRead or fmShareDenyWrite);
try
Status := Pdf.VerifyLoadedSignature(0, Src, Info);
if Status <> svValid then
Writeln('Verifizierung fehlgeschlagen: ', Ord(Status));
finally
Src.Free;
end;
end;
Fehlermeldung für nicht verifizierbare Dokumente
Ein Verifizierer, der nur „gültig“ und „ungültig“ kennt, würde Dokumente falsch einstufen, die er schlicht nicht versteht. Daher unterscheidet die Status-Enumeration die Fälle, die Ihre Benutzeroberfläche differenzieren sollte. svDigestMismatch bedeutet, dass die Bytes des Dokuments nach dem Signieren geändert wurden — das klassische Zeichen für eine Manipulation. svSignatureInvalid bedeutet, dass die Bytes korrekt gehasht wurden, die RSA-Prüfung jedoch fehlgeschlagen ist, was auf einen beschädigten oder gefälschten Signaturwert hinweist. svUnsupportedAlgorithm ist die ehrliche Antwort bei ECDSA-Schlüsseln und unbekannten Digests: Die Signatur kann völlig in Ordnung sein, HotPDF kann sie nur nicht prüfen. Dies als „ungültig“ zu melden, würde ein intaktes Dokument fälschlicherweise diskreditieren. svMalformed kennzeichnet einen CMS-Container, der überhaupt nicht geparst werden konnte. Für Gate-Prüfungen gibt VerifyAllLoadedSignatures nur dann true zurück, wenn mindestens ein Signaturfeld existiert und jedes davon als svValid verifiziert wird — ein praktischer einzelner Boolean für eine Archiv-Pipeline, die nichts Geringeres akzeptiert
Signaturverifizierung, PAdES-Signierung, AES-256-Verschlüsselung und die Bearbeitungs-API für geladene Dokumente sind alle in derselben nativen VCL-Bibliothek für Delphi und C++Builder enthalten, ohne externe DLL-Abhängigkeiten. Die vollständige Funktionsliste und die unterstützten IDE-Versionen finden Sie auf der Produktseite von HotPDF Component