Ein PDF mit einer ML-DSA-65-Signatur kann die Prüfung der Toolchain passieren, die es erzeugt hat, und trotzdem von einem Verifizierer als nicht unterstützter Algorithmus zurückkommen, der darauf besteht, dass die Datei deklariert, welche Erweiterung seinen Algorithmus definiert. Der öffentliche Schlüssel ist 1.952 Bytes groß, exakt die FIPS-204-Größe für diesen Parametersatz. Die Signaturbytes sind intakt und die Bytebereiche decken die ganze Datei ab. Was ein solcher Verifizierer sucht, ist ein Eintrag im Dokument-Catalog, der diese Erweiterung benennt, denn ISO 32000-2 listet ML-DSA überhaupt nicht auf
Die Signierseite hat eine andere Wand. Ein Hardware-Token, ein Cloud-HSM oder eine nationale eID-Karte rechnet auf Anfrage eine Signatur, aber keiner von ihnen gibt den privaten Schlüssel an Ihre Anwendung heraus, also hat jeder Signiercode, der eine PFX-Datei laden und im Prozess signieren will, schlicht nichts zu laden. Beide Probleme drehen sich darum, wo eine Entscheidung liegt: Die Algorithmusdeklaration liegt in der Datei, und der Schlüssel liegt irgendwo, wo Ihr Delphi-Code ihn nicht erreicht
Warum kommt eine gültige ML-DSA-Signatur als unsupported zurück?
HotPDF verifiziert ML-DSA-44-, ML-DSA-65- und ML-DSA-87-CMS-Signaturen nur, wenn das geladene Dokument die HotPDF-Developer-Erweiterung in seinem Catalog deklariert. Ohne diese Deklaration liefert der Verifizierer svUnsupportedAlgorithm für eine Gittersignatur, so fundiert die Kryptografie auch ist. Die Begründung ist Reproduzierbarkeit: Eine Datei, die einen Algorithmus nutzt, den der Basisstandard nicht benennt, und nicht sagt, auf welche Erweiterung sie sich stützt, produziert ein Urteil, das eine andere Implementierung nicht nachvollziehen kann
Auf der Erzeugerseite schreibt THotPDF.EnsureMLDSAExtensions /Extensions << /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> >> in den Catalog und hebt das Dokument auf PDF 2.0 an, wenn es auf eine ältere Version zielte. Auf der Leseseite gibt LoadedDocumentDeclaresMLDSAExtension nur dann True zurück, wenn dieses Dictionary vorhanden ist, mit /BaseVersion gleich 2.0 und einem /ExtensionLevel von mindestens 1. VerifyLoadedSignatureWithOptions verknüpft beides: Es UND-verknüpft das AllowMLDSA-Flag des Aufrufers mit dem Deklarationstest, bevor irgendeine Verifikation läuft, also ändert das Setzen des Flags an einer undeklarierten Datei nichts
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'contract-pq.pdf';
Pdf.StrictVersionLock := False; // der Default; erlaubt dem Aufruf, die Datei auf PDF 2.0 anzuheben
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
Pdf.EnsureMLDSAExtensions; // deklarieren, bevor eine Signatur den Catalog abdeckt
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Zwei Details in diesem Snippet haben Gewicht. Der Aufruf muss passieren, bevor das Dokument gespeichert und signiert wird, denn der Catalog sitzt im signierten Bytebereich; ihn nachträglich einzupatchen ist entweder eine unsignierte Änderung an einer signierten Datei oder eine neue inkrementelle Revision, die jeder Validator als Modifikation listen wird. Das zweite Detail ist StrictVersionLock. Bei aktivierter Sperre und einem Dokument unter PDF 2.0 kehrt EnsureMLDSAExtensions zurück, ohne etwas zu schreiben, denn eine PDF-2.0-Erweiterung lässt sich in einer 1.7-Datei nicht deklarieren. Wenn Ihr Generator Versionen aus Archivgründen festnagelt, prüfen Sie Version = pdf20, bevor Sie sich auf die Deklaration verlassen
Ein Verifikationsaufruf für EdDSA und ML-DSA
VerifyLoadedSignatureWithOptions nimmt einen Signatur-Index, den Quellstream, einen THPDFCMSVerifyOptions-Record und einen out-THPDFSignatureInfo. Beginnen Sie den Record mit THPDFCMSVerifyOptions.Default, das AllowMLDSA aus lässt, einen CMS-Container auf 256 Signierer begrenzt und Counter-Signature-Verschachtelung auf 8 deckelt. Drei Felder zählen bei den neueren Algorithmen. SignatureProvider lässt Ihren eigenen Verifizierer zuerst antworten. OpenSSLLibraryPath nennt den OpenSSL-3-libcrypto, der die Pure-Mode-Verifikation von Ed25519 und Ed448 ausführt. AllowMLDSA opted in die Gitteralgorithmen ein, vorbehaltlich des Catalog-Tests oben. Der Record trägt außerdem Felder zum Pinnen der Signatur-Policy (RequireSignaturePolicy, ExpectedSignaturePolicyOID, ExpectedSignaturePolicyHash), die für jede Algorithmusfamilie gelten
Der eingebaute Pfad teilt sich nach Algorithmus. Ed25519 und Ed448 laufen über OpenSSL 3; ohne Pfadangabe probiert der Plattform-Provider libcrypto-3-x64.dll, libcrypto-3.dll und libcrypto.dll über die normale DLL-Suchreihenfolge auf Win64. ML-DSA läuft über den Windows-CNG-ML-DSA-Algorithmus-Provider, und auf einem Windows-Build, der ihn nicht anbietet, ist das Ergebnis unsupported. Setzen Sie OpenSSLLibraryPath auf eine Kopie, die Sie neben der Executable ausliefern. Leer gelassen, hängt das Urteil daran, welchen libcrypto ein anderer Installer zufällig auf den Suchpfad geworfen hat – und das ist eine harte Erklärung in einem Audit
Ihr eigener SignatureProvider wird zuerst gefragt. Gibt er spsUnsupported zurück, bekommt der eingebaute Plattform-Provider dieselbe Anfrage, also kann ein Provider, der nur ML-DSA kennt, vor die Default-EdDSA-Behandlung gesetzt werden, ohne sie neu zu implementieren
var
Pdf: THotPDF;
PQ: THPDFCallbackSignatureProvider;
Opts: THPDFCMSVerifyOptions;
Info: THPDFSignatureInfo;
Status: THPDFSignatureVerifyStatus;
Src: TFileStream;
begin
PQ := THPDFCallbackSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest): THPDFSignatureProviderStatus
begin
if not (Req.Algorithm in [hsaMLDSA44, hsaMLDSA65, hsaMLDSA87]) then
Exit(spsUnsupported); // EdDSA an den Plattform-Provider durchreichen
// MyFips204Verify ist Ihr eigener Wrapper um eine ML-DSA-Bibliothek;
// Req.Input enthält die DER-kodierten signed Attributes (Pure Mode)
Result := MyFips204Verify(Req.AlgorithmOID, Req.PublicKey,
Req.Input, Req.Signature);
end);
Pdf := THotPDF.Create(nil);
Src := TFileStream.Create('contract-pq.pdf', fmOpenRead or fmShareDenyNone);
try
Pdf.LoadFromFile('contract-pq.pdf');
Opts := THPDFCMSVerifyOptions.Default;
Opts.SignatureProvider := PQ;
Opts.OpenSSLLibraryPath := ExtractFilePath(ParamStr(0)) + 'libcrypto-3-x64.dll';
Opts.AllowMLDSA := True; // wird weiterhin mit der Catalog-Deklaration UND-verknüpft
Status := Pdf.VerifyLoadedSignatureWithOptions(0, Src, Opts, Info);
if Status = svValid then
Memo1.Lines.Add(string(Info.SignatureAlgorithm) + ' / ' +
string(Info.SignatureAlgorithmOID));
finally
Src.Free;
Pdf.Free;
PQ.Free; // der Options-Record besitzt ihn nicht
end;
end;
Protokollieren Sie Info.SignatureAlgorithmOID statt dessen, was Ihre Konfiguration erwartet hat. Für ML-DSA-65 ist das 2.16.840.1.101.3.4.3.18, und ein Audit-Trail, der die in der Datei gefundene OID aufzeichnet, beantwortet später Fragen, die ein Konfigurationswert nicht kann. Wenn Sie überhaupt keinen eigenen Provider brauchen, baut der kürzere VerifyLoadedSignature(SigIndex, Source, Info)-Overload Default-Optionen und setzt AllowMLDSA für Sie aus der Catalog-Deklaration
Kodierungsregeln, an denen fremd erzeugte ML-DSA- und EdDSA-CMS stolpern
Beide Algorithmusfamilien werden im Pure Mode über die vollständigen DER-kodierten signed Attributes verifiziert, und ein SignerInfo ohne signed Attributes wird als unsupported gemeldet. Bei EdDSA müssen die AlgorithmIdentifier-Parameter fehlen. Ein verbreiteter Fehler in handgestricktem CMS ist, die signed Attributes zuerst mit SHA-512 oder SHAKE256 zu hashen und diesen Digest zu signieren, was stillschweigend PureEdDSA in eine Prehash-Variante verwandelt, die nichts im CMS deklariert. Bei ML-DSA prüft der Verifizierer zusätzlich das Paaring zwischen Parametersatz und Message Digest: ML-DSA-44 akzeptiert SHA-256, SHA-384, SHA-512 oder SHAKE256, ML-DSA-65 braucht SHA-384 oder stärker, und ML-DSA-87 braucht SHA-512 oder SHAKE256. Ein falsches Paaring zeigt sich in Info.DigestConsistencyIssue, dem ersten Feld, das man liest, wenn eine Datei aus einer anderen Toolchain scheitert
EdDSA hat seine eigene Registrierungsgeschichte. ISO/TS 32002 ist die Erweiterung, die Ed25519 und Ed448 für PDF-2.0-Signaturen registriert, und EnsureISO32002Extensions ergänzt den passenden ISO_-Eintrag mit ExtensionLevel 32002 im Catalog. Die Verifikation akzeptiert EdDSA auch ohne diesen Eintrag, aber eine Datei, die Sie erzeugen, sollte ihn trotzdem tragen – aus demselben Reproduzierbarkeitsgrund wie die ML-DSA-Deklaration
Was ein Signatur-Provider gefragt wird zu tun
Ein Signatur-Provider empfängt einen THPDFSignatureProviderRequest und liefert einen THPDFSignatureProviderStatus plus, bei Erfolg, die Signaturbytes. Die Anfrage trägt den Algorithmus und seine OID, die Digest-Algorithmus-OID, die PSS-Salt-Länge, ob Input eine Nachricht (sikMessage) oder ein vorberechneter Digest (sikDigest) ist, die Eingabebytes, das Signaturzertifikat, einen Schlüsselbezeichner und einen aufrufspezifischen Vorgangsbezeichner. Das ist das Vokabular, das ein Token-Treiber oder ein Signaturdienst ohnehin spricht, und keines davon verlangt, dass der Schlüssel in Ihren Prozess hinüberwechselt
Jeder Provider kündigt zudem eine Reihe von Capabilities an, etwa spcSign, spcCancel, spcHardwareKey und spcRemoteKey. Mehrere Implementierungen kommen mit der Bibliothek:
THPDFCallbackSignatureProviderhüllt anonyme Methoden für Verifizieren, Signieren und Abbrechen ein – der kürzeste Weg von einer bestehenden hauseigenen Signatur-Routine zu einer PDF-SignaturTHPDFRemoteSignatureProviderhüllt eine Transportfunktion mit Wiederholungslimit, einer Abbruch-Registry und Schranken für Eingabe- und Signaturgröße ein, sodass ein sich schlecht benehmender Remote-Dienst keine überdimensionierte Antwort in den CMS-Encoder drücken kannTHPDFPKCS11SignatureProviderleiht eine Sitzung und ein Private-Key-Handle, die Ihr Code bereits geöffnet und eingeloggt hat. HotPDF sieht die PIN nie und verwaltet die Token-Sitzung nicht; es serialisiertC_Sign-Aufrufe, stellt den DigestInfo für RSA PKCS#1 v1.5 voran, reicht die echte Salt-Länge über Mechanismus-Parameter an RSA-PSS durch und wandelt ein rohes ECDSA-r||s-Ergebnis in die DER-Form um, die CMS erwartetTHPDFWindowsKeyStorageSignatureProvidersigniert über den Windows-CNG-Key-Speicher, was Smartcards und TPM-gestützte Schlüssel abdeckt, und kann einen hardwaregestützten Schlüssel verlangen
Der PKCS#11-Provider nimmt Digest-Eingabe, also bedient er RSA PKCS#1 v1.5, RSA-PSS und ECDSA. Ed25519 und Ed448 signieren die vollständige Signed-Attributes-Nachricht, und die laufen über den Callback- oder Remote-Provider, wo Ihr Code Req.Input als Nachricht an den Token oder Dienst reicht
var
Provider: THPDFRemoteSignatureProvider;
Opts: THPDFCMSSignOptions;
CertDER: TBytes;
InStream, OutStream: TFileStream;
begin
CertDER := TFile.ReadAllBytes('signer.cer'); // System.IOUtils; DER-Zertifikat des Remote-Schlüssels
Provider := THPDFRemoteSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
out Signature: TBytes): THPDFSignatureProviderStatus
begin
// PostToSigningService ist Ihr eigener HTTP-Client: Timeouts und 5xx auf
// spsProviderError mappen, eine abgelehnte Freigabe auf spsCancelled, einen unbekannten Schlüssel auf spsUnsupported
Result := PostToSigningService(Req.KeyIdentifier, Req.AlgorithmOID,
Req.Input, Signature);
end,
3, // RetryLimit: insgesamt bis zu vier Versuche
1048576, // MaxInputBytes
65536); // MaxSignatureBytes
Opts := HPDFCMSDefaultOptions(palBaseline_B_B);
Opts.SignatureScheme := cmsRSAPSS;
Opts.DigestAlgorithm := cmsdaSHA256;
Opts.PSSSaltLength := 32;
// contract-unsigned.pdf wurde mit einem Platzhalter aus AddSignedSignatureField gespeichert
InStream := TFileStream.Create('contract-unsigned.pdf', fmOpenRead or fmShareDenyWrite);
OutStream := TFileStream.Create('contract-signed.pdf', fmCreate);
try
HPDFCMSSignPDFStreamWithProvider(InStream, OutStream, CertDER,
Provider, 'contracts-2026', Opts);
finally
OutStream.Free;
InStream.Free;
Provider.Free;
end;
end;
Sechs Statuswerte und was jeden auslösen sollte
THPDFSignatureProviderStatus hat sechs Werte, weil die richtige Reaktion je nach Wert differiert. spsInvalid ist ein kryptografischer Fehlschlag und gehört in ein Sicherheitsprotokoll. spsUnsupported heißt, der Provider implementiert den Algorithmus nicht – ein Deployment-Problem. spsMalformed heißt, Anfrage oder Antwort hatten die falsche Form. spsProviderError ist ein Transport- oder Gerätefehler, der bei erneutem Versuch gelingen kann. spsCancelled heißt, eine Person hat den Token-Prompt abgelehnt, und ihn zu wiederholen ärgert sie nur
Der Remote-Provider baut diese Policy ein. Er wiederholt nur bei spsProviderError, prüft die Abbruch-Registry vor jedem Versuch, weist leere oder überdimensionierte Eingabe schon vor dem Transportaufruf als spsMalformed zurück und stuft eine „gültige“ Antwort mit leerer oder überdimensionierter Signatur auf spsMalformed herab. Auf der Verifikationsseite mappen die Antworten eines Providers auf den Verifizier-Status: spsInvalid wird svSignatureInvalid, spsMalformed wird svMalformed, spsProviderError und spsCancelled werden beide svCryptoProviderError, und spsUnsupported wird svUnsupportedAlgorithm, sobald auch der Plattform-Provider die Anfrage abgelehnt hat
Wer einen Provider schreibt, sollte jeder Versuchung widerstehen, jede unerwartete Bedingung auf spsInvalid zu mappen. Das verwandelt eine fehlende DLL oder einen abgelaufenen Dienst-Token in einen Bericht, die Signatur des Kunden sei gefälscht, und irgendjemand muss das dem Kunden erklären. spsInvalid nur dann zurückgeben, wenn die Mathematik tatsächlich gescheitert ist. Beim Signieren spsValid nur zusammen mit einer nicht-leeren Signatur zurückgeben; beide Einsprungpunkte werfen EHPDFCMSError, wenn ein Provider Erfolg ohne Bytes meldet
Wo die Signatur des Providers in der Datei landet
Zwei Funktionen verbinden einen Provider mit echten PDF-Bytes. HPDFCMSBuildSignedDataWithProvider baut detached CMS aus einem Dokument-Digest, den Sie woanders berechnet haben; die Digest-Länge muss zu Options.DigestAlgorithm passen, also erwartet eine SHA-384-Konfiguration 48 Bytes und weist einen 32-Byte-SHA-256-Digest mit einer Exception ab. HPDFCMSSignPDFStreamWithProvider signiert den Platzhalter, den AddSignedSignatureField ausgelegt hat, hasht beide /ByteRange-Fenster, fragt den Provider nach der Signatur über die signed Attributes und patcht das Hex-Ergebnis in /Contents. Wächst das CMS über den Platzhalter hinaus, wirft der Aufruf eine Exception und stellt den unsignierten Platzhalter im Ausgabe-Stream wieder her, statt irgendetwas abzuschneiden
Das Layout ist dasselbe, das der PFX-basierte Signierpfad schreibt: zwei Bereiche, die das Hex-Signaturfenster auslassen. Der Verifizierer hält jede Datei an diese Form, prüft, dass die Lücke zwischen den Bereichen genau den /Contents-String dieser Signatur enthält und sonst nichts, und meldet sonst svInvalidByteRange. Ein über ein PKCS#11-Token signiertes Dokument verifiziert daher mit demselben Signatur-Verifikations-Code wie eines aus einer PFX-Datei. Die Profilregeln oberhalb der Algorithmuswahl behandelt PAdES-Baseline-Signaturen in Delphi, und die ECDSA-Kodierungsfallen, um die sich die PKCS#11-Konvertierung kümmert, stehen in ECDSA-CMS-Verifikation und P1363-Signaturformaten
Post-Quantum-Signaturen ausrollen, ohne Leser zu stranden
Ein PDF-Reader ohne ML-DSA-Implementierung meldet so eine Signatur als nicht verifizierbar, und die Person vor dem Dokument sieht eine Warnung statt eines grünen Häkchens. Das macht die Migration zu einem Terminplanungsproblem. Behalten Sie RSA oder ECDSA als die Signatur, die die heutigen Validatoren beurteilen. Wo eine Policy quantenresistente Nachweise verlangt, deklarieren Sie die Erweiterung mit EnsureMLDSAExtensions und ergänzen eine zweite Signatur, erzeugt von einem ML-DSA-fähigen Signierer, neben der klassischen. Diese Signatur kommt aus Ihrem eigenen FIPS-204-Signierer, denn der HotPDF-CMS-Builder erzeugt keine ML-DSA-Signaturen; der Teil der Bibliothek ist, sie zu verifizieren. Die primäre Signatur erst dann verschieben, wenn die Systeme, die Ihre Dokumente verarbeiten, sie verifizieren können
In diesem Plan deckt HotPDF beide Enden ab, die Sie aus Delphi kontrollieren. Seine CMS-Builder signieren mit RSA PKCS#1 v1.5, RSA-PSS, ECDSA, Ed25519 und Ed448 über jeden der Provider oben, und sein Verifizierer prüft ML-DSA, EdDSA und die klassischen Algorithmen aus einem Aufruf, mit der tatsächlich gefundenen OID im Ergebnis verzeichnet. HotPDF ist eine native VCL-PDF-Komponente für Delphi und C++Builder ohne externe PDF-Laufzeit, also kompiliert der Signatur- und Verifikationscode in Ihre Executable. Editionen, der Signatur-Funktionsumfang und ein Trial-Download stehen auf der HotPDF-Delphi-PDF-Komponenten-Seite