HotPDF verifiziert ML-DSA-44-, ML-DSA-65-, ML-DSA-87-, Ed25519- und Ed448-CMS-Signaturen in geladenen PDF-Dokumenten und unterschreibt über steckbare Provider, sodass der private Schlüssel nie im Delphi-Prozess leben muss. Diese zweite Hälfte ist der Teil, den die meisten Teams zuerst brauchen. Ein Hardware-Token, ein Remote-Signaturdienst und eine nationale eID-Karte weigern sich sämtlich, einen Schlüssel herauszugeben, und solange die Signatur-Pipeline nicht vom Schlüsselspeicher getrennt ist, lässt sich keiner davon überhaupt verwenden
Diese Trennung ist der Sinn von THPDFSignatureProvider. HotPDF behält die Teile, die es ownen sollte — CMS parsen, SignedData aufbauen, die /ByteRange legen — und delegiert den einen Vorgang, den es nicht ownen kann: einen Digest mit einem Schlüssel, den es nicht sehen darf, in eine Signatur zu verwandeln. Alles im Folgenden ergibt sich aus dieser Aufteilung
Warum scheitert die Verifikation einer gültigen ML-DSA-Signatur?
Weil HotPDF ML-DSA bei einem geladenen Dokument ablehnt, das die Erweiterung dafür nicht deklariert. ML-DSA — das als FIPS 204 standardisierte Gitter-Signaturverfahren und der Grund, warum Leute „post-quantum PDF" sagen — hat noch keine Registrierung in ISO 32000-2. Ein PDF, das eine solche trägt, verwendet einen Algorithmus, den der Basis-Standard nicht benennt, und eine Datei, die stillschweigend einen unbenannten Algorithmus verwendet, ist eine Datei, deren Urteil niemand sonst reproduzieren kann
HotPDF macht die Behauptung also explizit. EnsureMLDSAExtensions hebt das Dokument, wo zulässig, auf PDF 2.0 an und schreibt /Extensions /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> in den Catalog. Auf der Leseseite meldet LoadedDocumentDeclaresMLDSAExtension, ob diese Deklaration überlebt hat, und VerifyLoadedSignatureWithOptions wendet dieselbe Prüfung an, bevor es Options.AllowMLDSA honoriert. Setzt man das Flag bei einem nicht deklarierten Dokument, bleibt es aus — die Option kann die Policy lockern, niemals aber die strukturelle Anforderung
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'contract-pq.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
Pdf.EnsureMLDSAExtensions; // declare before the signature is written
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Das vor dem Speichern aufrufen, nicht danach. Die Deklaration ist Teil des signierten Byte-Bereichs, und ein Catalog, der nachträglich gepatcht wird, ist entweder eine unsignierte Änderung an einer signierten Datei oder eine zweite Revision, die ein Validator als Modifikation meldet
Drei Algorithmen-Familien, ein Verifikations-Einstiegspunkt
Alle drei Familien laufen über VerifyLoadedSignatureWithOptions ein, das einen Signatur-Index, den Quell-Stream, einen THPDFCMSVerifyOptions-Datensatz und einen out-Parameter für die Signaturdetails entgegen nimmt. Der Datensatz hat exakt drei Felder, und jedes beantwortet eine Frage, die früher einen Rebuild erforderte
SignatureProvider ersetzt den eingebauten Plattform-Provider durch einen eigenen. OpenSSLLibraryPath wählt eine OpenSSL-3-Bibliothek, die die Pure-Mode-Ed25519- und Ed448-Verifikation liefert, die Windows CNG nicht überall bietet. AllowMLDSA opted in die Gitter-Algorithmen ein, vorbehaltlich der obigen Erweiterungs-Prüfung. Die exakte Algorithmen-OID, die erkannt wurde, kommt in THPDFSignatureInfo.SignatureAlgorithmOID zurück, sodass ein Audit-Log aufzeichnen kann, was verifiziert wurde, statt was angefordert wurde
var
Opts: THPDFCMSVerifyOptions;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
Src: TFileStream;
begin
Opts := THPDFCMSVerifyOptions.Default;
Opts.OpenSSLLibraryPath := 'C:\openssl3\libcrypto-3-x64.dll';
Opts.AllowMLDSA := Pdf.LoadedDocumentDeclaresMLDSAExtension;
Src := TFileStream.Create('contract-pq.pdf', fmOpenRead or fmShareDenyWrite);
try
Status := Pdf.VerifyLoadedSignatureWithOptions(0, Src, Opts, Info);
if Status = svValid then
Memo1.Lines.Add('signed with OID ' + string(Info.SignatureAlgorithmOID));
finally
Src.Free;
end;
end;
Ed25519 und Ed448 brauchen keine Erweiterungs-Deklaration, denn ISO 32000-2 lässt sie bereits zu. Sie benötigen jedoch einen Provider, der sie implementiert, was auf den meisten Windows-Deployments bedeutet, OpenSSLLibraryPath auf eine selbst ausgelieferte und kontrollierte Bibliothek zeigen zu lassen statt auf das, was zufällig auf der Maschine liegt
Was verspricht ein Signatur-Provider tatsächlich?
Ein Provider verspricht eines: gegeben einen Request, einen Status und beim Signieren Bytes zurückgeben. THPDFSignatureProviderRequest trägt den Algorithmus und seine OID, die Digest-OID, die PSS-Salt-Länge, ob die Eingabe eine Nachricht oder ein bereits berechneter Digest ist, die Eingabe selbst, den öffentlichen Schlüssel oder das Zertifikat, einen Schlüsselbezeichner und einen Vorgangs-Bezeichner. Nichts in diesem Datensatz ist HotPDF-spezifisch — es ist das Vokabular, das ein Token-Treiber oder ein Signaturdienst ohnehin spricht
Drei Implementierungen werden mit der Bibliothek ausgeliefert. THPDFCallbackSignatureProvider hüllt anonyme Methoden ein, was der kürzeste Weg von einer bestehenden hauseigenen Signatur-Routine zu einer funktionierenden PDF-Signatur ist. THPDFRemoteSignatureProvider hüllt einen Transport-Callback mit einer Wiederholungsgrenze, einer Abbruch-Registry und Schranken für Eingabe- und Signaturgröße ein, sodass ein hängendes HSM nicht zu einer hängenden Anwendung werden kann. THPDFPKCS11SignatureProvider serialisiert RSA-Vorgänge gegen eine vom Aufrufer geführte, bereits authentifizierte PKCS#11-Sitzung und Private-Key-Handle — HotPDF loggt sich nie ein, sieht nie eine PIN und schließt nie eine Sitzung, die es nicht geöffnet hat
var
Provider: THPDFRemoteSignatureProvider;
begin
Provider := THPDFRemoteSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
out Signature: TBytes): THPDFSignatureProviderStatus
begin
// POST Req.Input to the signing service; Req.KeyIdentifier selects the key
if PostToSigningService(Req.KeyIdentifier, Req.Input, Signature) then
Result := spsValid
else
Result := spsProviderError;
end,
3, // RetryLimit
1048576, // MaxInputBytes
65536); // MaxSignatureBytes
try
// hand Provider to the signing call
finally
Provider.Free;
end;
end;
Warum hat die Status-Enumeration sechs Werte statt eines Boolean?
THPDFSignatureProviderStatus unterscheidet spsValid, spsInvalid, spsUnsupported, spsMalformed, spsProviderError und spsCancelled, und sie zusammenzufalten kostet die Fähigkeit, korrekt zu reagieren. Eine kryptographisch falsche Signatur (spsInvalid) ist ein Sicherheits-Ereignis. Ein Algorithmus, den der Provider nicht implementiert (spsUnsupported), ist eine Deployment-Lücke. Ein Transport-Fehlschlag (spsProviderError) lohnt erneut zu versuchen, und ein vom Nutzer abgebrochenes Token-Prompt (spsCancelled) lohnt sich gar nicht erneut zu versuchen
Die Regel beim Signieren ist eng: Ein Signatur-Provider liefert spsValid nur mit einer nicht-leeren Signatur zurück. Verifikations-Provider liefern spsValid oder spsInvalid, und die anderen vier bleiben auf beiden Pfaden getrennt. Wer einen Provider schreibt, sollte der Versuchung widerstehen, alles Unerkannte auf spsInvalid abzubilden — das macht eine fehlende DLL zu einer Meldung, die Signatur des Kunden sei gefälscht
Wo die Signatur tatsächlich in der Datei landet
Zwei Funktionen verbinden Provider mit echten PDF-Bytes. HPDFCMSBuildSignedDataWithProvider baut abgetrennte CMS aus einem Dokument-SHA-256-Digest auf, was der richtige Einstiegspunkt ist, wenn der eigene Workflow den Digest woanders berechnet. HPDFCMSSignPDFStreamWithProvider signiert einen bestehenden Signatur-Platzhalter in einem PDF-Stream und bewahrt die Standard-/ByteRange-Pipeline, was der richtige Einstiegspunkt ist, wenn HotPDF den Platzhalter selbst gelegt hat
Diese Pipeline zu bewahren ist wichtiger, als es klingt. Die /ByteRange-Konvention — zwei Bereiche, die das Hex-Signatur-Fenster auslassen — ist das, was jeder Validator zuerst prüft, und ein Provider-basierter Pfad, der sie umschriebe, würde die PAdES-Konformität zerstören, egal wie fundiert die Kryptographie war. HotPDF hält das Layout identisch zum eingebauten Signatur-Pfad, sodass ein Dokument, das über ein PKCS#11-Token signiert wurde, mit demselben Signatur-Verifikations-Code verifiziert wird wie eines, das aus einer PFX-Datei signiert wurde. Für die Profil-Regeln oberhalb der Algorithmen-Wahl siehe die Erläuterung der PAdES-Baseline-Signaturen in Delphi, und für die ECDSA-spezifischen Kodierungs-Fallen, die diesem Provider-Modell vorausgingen, die Hinweise zur ECDSA-CMS-Verifikation und zu P1363-Signaturformaten
Eine Migrations-Reihenfolge, die die Dokumente nicht stranden lässt
Post-Quantum-Bereitschaft ist ein Terminproblem, kein Schalter. Nahezu kein ausgelieferter PDF-Viewer validiert heute ML-DSA, sodass ein Dokument, das ausschließlich damit signiert wurde, aus Sicht des Lesers ein Dokument mit nicht verifizierbarer Signatur ist. Die Reihenfolge, die den Kontakt mit realen Archiven übersteht, lautet: RSA oder ECDSA als die Signatur beibehalten, die ein Validator beurteilt, die Erweiterungs-Deklaration und eine zweite ML-DSA-Signatur hinzufügen, wo eine Policy quantenresistente Beweise fordert, und die primäre Signatur erst verschieben, wenn die verarbeitenden Systeme aufgeholt haben
Was HotPDF heute bietet, ist die Fähigkeit, beide zu schreiben und zu verifizieren, aus demselben Code, mit dem Algorithmus ehrlich in der Datei und im Verifikations-Ergebnis festgehalten. HotPDF ist eine native VCL-PDF-Komponente für Delphi und C++Builder ohne externe PDF-Laufzeit, sodass Signatur- und Verifikations-Pfade innerhalb der eigenen Executable ausgeliefert werden statt neben ihr — siehe die HotPDF-Delphi-PDF-Komponenten-Seite für die vollständige Funktionsliste und den Trial-Download