PDF niosący podpis ML-DSA-65 może przejść sprawdzenie toolchaina, który go wyprodukował, i wciąż wrócić jako nieobsługiwany algorytm od weryfikatora, który nalega, żeby plik deklarował, które rozszerzenie definiuje jego algorytm. Klucz publiczny ma 1 952 bajty — dokładnie rozmiar FIPS 204 dla tego zestawu parametrów. Bajty podpisu są nietknięte, a zakresy bajtów pokrywają cały plik. Taki weryfikator szuka jednej pozycji w Catalog dokumentu nazywającej to rozszerzenie, bo ISO 32000-2 w ogóle nie wymienia ML-DSA
Strona podpisująca ma inną ścianę. Sprzętowy token, chmurowy HSM albo narodowa karta eID policzą podpis na życzenie, ale żaden nie odda klucza prywatnego twojej aplikacji, więc każdy kod podpisujący, który oczekuje załadować plik PFX i podpisać in-process, nie ma po prostu czego ładować. Oba problemy dotyczą tego, gdzie mieszka decyzja: deklaracja algorytmu mieszka w pliku, a klucz mieszka gdzieś, gdzie twój kod Delphi nie sięga
Czemu poprawny podpis ML-DSA wraca jako nieobsługiwany?
HotPDF weryfikuje podpisy CMS ML-DSA-44, ML-DSA-65 i ML-DSA-87 tylko wtedy, gdy załadowany dokument deklaruje w swoim Catalog rozszerzenie deweloperskie HotPDF. Bez tej deklaracji weryfikator zwraca svUnsupportedAlgorithm dla podpisu kratowego, jakkolwiek zdrowa jest kryptografia. Rozumowanie to powtarzalność: plik, który używa algorytmu, którego standard bazowy nie wymienia, i nie mówi, na jakim rozszerzeniu się opiera, produkuje werdykt, którego inna implementacja nie ma na czym powtórzyć
Po stronie produkującej THotPDF.EnsureMLDSAExtensions zapisuje /Extensions << /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> >> do Catalog i podnosi dokument do PDF 2.0, jeśli celował w starszą wersję. Po stronie czytającej LoadedDocumentDeclaresMLDSAExtension zwraca True tylko wtedy, gdy ten słownik jest obecny z /BaseVersion równym 2.0 i /ExtensionLevel co najmniej 1. VerifyLoadedSignatureWithOptions składa jedno i drugie: bierze flagę AllowMLDSA wywołującego i koniunkuje ją z testem deklaracji, zanim jakakolwiek weryfikacja ruszy, więc ustawienie flagi na niezadeklarowanym pliku nie zmienia niczego
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'contract-pq.pdf';
Pdf.StrictVersionLock := False; // domyślne; pozwala wywołaniu podnieść plik do PDF 2.0
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
Pdf.EnsureMLDSAExtensions; // zadeklaruj, zanim jakikolwiek podpis obejmie Catalog
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Dwa szczegóły w tym fragmencie niosą ciężar. Wywołanie musi się wydarzyć, zanim dokument zostanie zapisany i podpisany, bo Catalog siedzi wewnątrz podpisanego zakresu bajtów; wstawienie go po fakcie to albo niepodpisana zmiana podpisanego pliku, albo nowa rewizja przyrostowa, którą każdy walidator wypisze jako modyfikację. Drugim szczegółem jest StrictVersionLock. Z włączoną blokadą i dokumentem przypiętym poniżej PDF 2.0 EnsureMLDSAExtensions wraca, niczego nie zapisując, bo rozszerzenia PDF 2.0 nie da się zadeklarować w pliku 1.7. Jeśli twój generator przypina wersje względami archiwizacji, sprawdź Version = pdf20, zanim zaczniesz polegać na deklaracji
Jedno wywołanie weryfikacji dla EdDSA i ML-DSA
VerifyLoadedSignatureWithOptions bierze indeks podpisu, strumień źródłowy, rekord THPDFCMSVerifyOptions i out THPDFSignatureInfo. Zacznij rekord od THPDFCMSVerifyOptions.Default, które zostawia AllowMLDSA wyłączone, capuje kontener CMS na 256 signerów i ogranicza zagnieżdżenie kontrpodpisów do 8. Trzy pola mają znaczenie dla nowszych algorytmów. SignatureProvider pozwala twojemu własnemu weryfikatorowi odpowiedzieć pierwszemu. OpenSSLLibraryPath nazywa OpenSSL 3 libcrypto, który wykonuje weryfikację Ed25519 i Ed448 w trybie pure. AllowMLDSA włącza algorytmy kratowe, z zastrzeżeniem testu Catalog powyżej. Rekord niesie też pola przypinania polityki podpisu (RequireSignaturePolicy, ExpectedSignaturePolicyOID, ExpectedSignaturePolicyHash), które dotyczą każdej rodziny algorytmów
Wbudowana ścieżka rozdziela się według algorytmu. Ed25519 i Ed448 idą przez OpenSSL 3; gdy ścieżka nie jest podana, provider platformowy próbuje libcrypto-3-x64.dll, libcrypto-3.dll i libcrypto.dll przez normalną kolejność szukania DLL na Win64. ML-DSA idzie przez CNG-owego providera algorytmu ML-DSA z Windows, a na buildzie Windows, który go nie oferuje, wynik brzmi unsupported. Ustaw OpenSSLLibraryPath na kopię, którą wysyłasz obok pliku wykonywalnego. Zostawienie go pustego znaczy, że werdykt zależy od tego, jaki libcrypto cudzy instalator rzucił na ścieżkę szukania, a to trudna rzecz do wyjaśnienia w audycie
Twój własny SignatureProvider jest pytany pierwszy. Jeśli zwróci spsUnsupported, wbudowany provider platformowy dostaje to samo żądanie, więc provider znający wyłącznie ML-DSA może siedzieć przed domyślną obsługą EdDSA, nie reimplementując jej
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); // przekaż EdDSA providerowi platformowemu
// MyFips204Verify to twoja własna obudowa wokół biblioteki ML-DSA;
// Req.Input trzyma DER-owane podpisane atrybuty (tryb pure)
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; // wciąż koniunkcja z deklaracją Catalog
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; // rekord opcji go nie posiada
end;
end;
Loguj Info.SignatureAlgorithmOID, a nie to, jakiego algorytmu oczekiwała twoja konfiguracja. Dla ML-DSA-65 to 2.16.840.1.101.3.4.3.18, a ślad audytowy rejestrujący OID znaleziony w pliku odpowiada później na pytania, na które wartość konfiguracji nie umie. Jeśli w ogóle nie potrzebujesz własnego providera, krótsze przeciążenie VerifyLoadedSignature(SigIndex, Source, Info) buduje opcje domyślne i ustawia AllowMLDSA z deklaracji Catalog za ciebie
Reguły kodowania, na których wywracają się zewnętrznie wyprodukowane ML-DSA i EdDSA CMS
Obie rodziny algorytmów są weryfikowane w trybie pure po kompletnych DER-owanych podpisanych atrybutach, a SignerInfo bez podpisanych atrybutów jest raportowany jako nieobsługiwany. Dla EdDSA parametry AlgorithmIdentifier muszą być nieobecne. Częsty błąd w ręcznie składanym CMS to najpierw zhashowanie podpisanych atrybutów SHA-512 albo SHAKE256 i podpisanie tego digesta, co po cichu zamienia PureEdDSA w wariant prehash, którego nic w CMS nie deklaruje. Dla ML-DSA weryfikator sprawdza też parowanie między zestawem parametrów a digestem wiadomości: ML-DSA-44 przyjmuje SHA-256, SHA-384, SHA-512 albo SHAKE256, ML-DSA-65 potrzebuje SHA-384 lub mocniejszego, a ML-DSA-87 potrzebuje SHA-512 albo SHAKE256. Niespójne parowanie pokazuje się w Info.DigestConsistencyIssue — to pierwsze pole do przeczytania, gdy plik z obcego toolchaina pada
EdDSA ma swoją własną historię rejestracji. ISO/TS 32002 to rozszerzenie rejestrujące Ed25519 i Ed448 dla podpisów PDF 2.0, a EnsureISO32002Extensions dodaje pasującą pozycję ISO_ z ExtensionLevel 32002 do Catalog. Weryfikacja przyjmuje EdDSA bez tej pozycji, ale plik, który produkujesz, powinien ją nieść — z tego samego powodu powtarzalności co deklaracja ML-DSA
O co provider podpisujący jest proszony
Provider podpisujący dostaje jeden THPDFSignatureProviderRequest i zwraca THPDFSignatureProviderStatus plus, przy sukcesie, bajty podpisu. Żądanie niesie algorytm i jego OID, OID algorytmu digesta, długość soli PSS, czy Input to wiadomość (sikMessage) czy wcześniej policzony digest (sikDigest), bajty wejścia, certyfikat signera, identyfikator klucza i identyfikator operacji unikalny dla wywołania. To słownik, którym driver tokena albo usługa podpisująca już mówią, i nic z tego nie wymaga, żeby klucz przekroczył granicę twojego procesu
Każdy provider reklamuje też zestaw zdolności jak spcSign, spcCancel, spcHardwareKey i spcRemoteKey. Kilka implementacji przychodzi z biblioteką:
THPDFCallbackSignatureProvideropakowuje metody anonimowe dla weryfikacji, podpisu i anulowania — najkrótsza droga od istniejącej firmowej rutyny podpisującej do podpisu PDFTHPDFRemoteSignatureProvideropakowuje funkcję transportową limitem ponowień, rejestrem anulowań i ograniczeniami rozmiaru wejścia i podpisu, więc źle zachowująca się zdalna usługa nie wpychnie przesadnej odpowiedzi do enkodera CMSTHPDFPKCS11SignatureProviderpożycza sesję i uchwyt klucza prywatnego, które twój kod już otworzył i na które już się zalogował. HotPDF nigdy nie widzi PIN-u i nie zarządza sesją tokena; serializuje wywołaniaC_Sign, dokleja DigestInfo dla RSA PKCS#1 v1.5, podaje prawdziwą długość soli do RSA-PSS przez parametry mechanizmu i zamienia surowy wynik ECDSAr||sw formę DER, jakiej CMS oczekujeTHPDFWindowsKeyStorageSignatureProviderpodpisuje przez Windows CNG key storage, co pokrywa karty smart i klucze oparte o TPM, i może wymagać klucza sprzętowego
Provider PKCS#11 bierze wejście digestowe, więc obsługuje RSA PKCS#1 v1.5, RSA-PSS i ECDSA. Ed25519 i Ed448 podpisują pełną wiadomość podpisanych atrybutów, a te idą przez providera callbackowego albo zdalnego, gdzie twój kod podaje Req.Input tokenowi albo usłudze jako wiadomość
var
Provider: THPDFRemoteSignatureProvider;
Opts: THPDFCMSSignOptions;
CertDER: TBytes;
InStream, OutStream: TFileStream;
begin
CertDER := TFile.ReadAllBytes('signer.cer'); // System.IOUtils; certyfikat DER zdalnego klucza
Provider := THPDFRemoteSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
out Signature: TBytes): THPDFSignatureProviderStatus
begin
// PostToSigningService to twój własny klient HTTP: mapuj timeouty i 5xx na
// spsProviderError, odmówioną aprobatę na spsCancelled, nieznany klucz na spsUnsupported
Result := PostToSigningService(Req.KeyIdentifier, Req.AlgorithmOID,
Req.Input, Signature);
end,
3, // RetryLimit: do czterech prób łącznie
1048576, // MaxInputBytes
65536); // MaxSignatureBytes
Opts := HPDFCMSDefaultOptions(palBaseline_B_B);
Opts.SignatureScheme := cmsRSAPSS;
Opts.DigestAlgorithm := cmsdaSHA256;
Opts.PSSSaltLength := 32;
// contract-unsigned.pdf był zapisany z placeholderem z AddSignedSignatureField
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;
Sześć statusów i co każdy powinien wyzwolić
THPDFSignatureProviderStatus ma sześć wartości, bo właściwa reakcja różni się dla każdej. spsInvalid to porażka kryptograficzna i należy do dziennika bezpieczeństwa. spsUnsupported znaczy, że provider nie implementuje algorytmu — to problem wdrożeniowy. spsMalformed znaczy, że żądanie albo odpowiedź miały zły kształt. spsProviderError to usterka transportu albo urządzenia, która może się udać przy ponowie. spsCancelled znaczy, że człowiek odmówił w okienku tokena, a ponawianie go tylko zirytuje
Provider zdalny wbudowuje tę politykę. Ponowi wyłącznie przy spsProviderError, sprawdza rejestr anulowań przed każdą próbą, odrzuca puste albo przesadne wejście jako spsMalformed przed wołaniem transportu i zdejmuje odpowiedź „poprawną” z pustym albo przesadnym podpisem do spsMalformed. Po stronie weryfikacji odpowiedź providera mapuje się na status weryfikacji: spsInvalid staje się svSignatureInvalid, spsMalformed staje się svMalformed, spsProviderError i spsCancelled stają się oba svCryptoProviderError, a spsUnsupported staje się svUnsupportedAlgorithm, gdy i provider platformowy odmówił żądania
Jeśli piszesz providera, oprzyj się mapowaniu każdego nieoczekiwanego stanu na spsInvalid. To zamienia brakujący DLL albo wygasły token usługi w raport, że podpis klienta jest podrobiony, a ktoś będzie musiał wyjaśnić to klientowi. Zwracaj spsInvalid tylko wtedy, gdy matematyka faktycznie padła. Do podpisu zwracaj spsValid wyłącznie razem z niepustym podpisem; oba punkty wejścia podnoszą EHPDFCMSError, gdy provider zwraca sukces bez bajtów
Gdzie podpis providera ląduje w pliku
Dwie funkcje spinają providera z prawdziwymi bajtami PDF. HPDFCMSBuildSignedDataWithProvider buduje odłączony CMS z digesta dokumentu policzonego gdzie indziej; długość digesta musi pasować do Options.DigestAlgorithm, więc konfiguracja SHA-384 oczekuje 48 bajtów i odrzuca 32-bajtowy digest SHA-256 wyjątkiem. HPDFCMSSignPDFStreamWithProvider podpisuje placeholder ułożony przez AddSignedSignatureField, hashuje oba okna /ByteRange, prosi providera o podpis nad podpisanymi atrybutami i wkleja szesnastkowy wynik do /Contents. Jeśli CMS urośnie ponad placeholder, wywołanie podnosi wyjątek i odtwarza niepodpisany placeholder w strumieniu wyjściowym, zamiast coś przycinać
Układ jest ten sam, jaki pisze ścieżka podpisywania z PFX: dwa zakresy omijające szesnastkowe okno podpisu. Weryfikator trzyma każdy plik do tego kształtu — sprawdza, że przerwa między zakresami zawiera dokładnie string /Contents tego podpisu i nic poza tym, a inaczej raportuje svInvalidByteRange. Dokument podpisany przez token PKCS#11 weryfikuje się więc tym samym kodem weryfikacji podpisów co podpisany z pliku PFX. Reguły profilu siedzące nad wyborem algorytmu są pokryte w podpisach bazowych PAdES w Delphi, a pułapki kodowania ECDSA, z którymi radzi sobie konwersja PKCS#11 — w weryfikacji CMS ECDSA i formatach podpisów P1363
Wdrażanie podpisów postkwantowych bez zostawiania czytelników na lodzie
Czytnik PDF, który nie implementuje ML-DSA, raportuje taki podpis jako niezweryfikowalny, a osoba patrząca na dokument widzi ostrzeżenie zamiast zielonego ptaszka. To czyni migrację problemem do zaplanowania. Trzymaj RSA albo ECDSA jako podpis, który dzisiejsze walidatory osądzą. Tam, gdzie polityka wymaga dowodu odpornego kwantowo, zadeklaruj rozszerzenie przez EnsureMLDSAExtensions i dodaj obok klasycznego drugi podpis wyprodukowany przez signera potrafiącego ML-DSA. Ten podpis pochodzi z twojego własnego signera FIPS 204, bo budowniczy CMS HotPDF nie tworzy podpisów ML-DSA; częścią biblioteki jest jego weryfikacja. Przesuń podpis podstawowy dopiero wtedy, gdy systemy konsumujące twoje dokumenty potrafią go zweryfikować
W tym planie HotPDF pokrywa oba końce, którymi sterujesz z Delphi. Jego budowniczowie CMS podpisują RSA PKCS#1 v1.5, RSA-PSS, ECDSA, Ed25519 i Ed448 przez dowolnego z providerów powyżej, a jego weryfikator sprawdza ML-DSA, EdDSA i algorytmy klasyczne z jednego wywołania, z OID-em faktycznie znalezionym zapisanym w wyniku. HotPDF to natywny komponent VCL PDF dla Delphi i C++Builder, który nie potrzebuje zewnętrznego runtime'u PDF, więc kod podpisywania i weryfikacji kompiluje się do twojego pliku wykonywalnego. Edycje, zestaw funkcji podpisu i trial do pobrania są na stronie komponentu PDF HotPDF dla Delphi