Un PDF che porta una firma ML-DSA-65 può passare il controllo della toolchain che l'ha prodotto e tornare comunque come algoritmo non supportato da un verificatore che pretende che il file dichiari quale estensione definisce il suo algoritmo. La chiave pubblica è di 1.952 byte, esattamente la dimensione FIPS 204 per quel parameter set. I byte della firma sono intatti e i byte range coprono l'intero file. Ciò che un tale verificatore cerca è una voce nel Catalog del documento che nomini quell'estensione, perché ISO 32000-2 non elenca affatto ML-DSA
Il lato firma ha un muro diverso. Un token hardware, un HSM cloud o una carta eID nazionale calcolano una firma su richiesta, ma nessuno di loro consegna la chiave privata alla vostra applicazione, quindi qualunque codice di firma che si aspetta di caricare un file PFX e firmare in-process semplicemente non ha niente da caricare. Entrambi i problemi riguardano dove vive una decisione: la dichiarazione dell'algoritmo vive nel file, e la chiave vive da qualche parte che il vostro codice Delphi non può raggiungere
Perché una firma ML-DSA valida torna come non supportata?
HotPDF verifica firme CMS ML-DSA-44, ML-DSA-65 e ML-DSA-87 solo quando il documento caricato dichiara l'estensione sviluppatore HotPDF nel suo Catalog. Senza quella dichiarazione il verificatore restituisce svUnsupportedAlgorithm per una firma a reticolo (lattice), per quanto sana sia la crittografia. Il ragionamento è la riproducibilità: un file che usa un algoritmo che lo standard di base non nomina, e non dice su quale estensione conta, produce un verdetto che un'altra implementazione non ha base per ripetere
Sul lato produzione, THotPDF.EnsureMLDSAExtensions scrive /Extensions << /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> >> nel Catalog e porta il documento a PDF 2.0 se puntava a una versione più vecchia. Sul lato lettura, LoadedDocumentDeclaresMLDSAExtension restituisce True solo quando quel dizionario è presente con /BaseVersion uguale a 2.0 e /ExtensionLevel di almeno 1. VerifyLoadedSignatureWithOptions combina le due cose: prende il flag AllowMLDSA del chiamante e lo mette in AND con il test della dichiarazione prima che parta qualunque verifica, quindi impostare il flag su un file non dichiarato non cambia niente
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'contract-pq.pdf';
Pdf.StrictVersionLock := False; // il default; lascia alla chiamata di portare il file a PDF 2.0
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
Pdf.EnsureMLDSAExtensions; // dichiarare prima che qualunque firma copra il Catalog
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Due dettagli in quello snippet portano peso. La chiamata deve avvenire prima che il documento sia salvato e firmato, perché il Catalog sta dentro il byte range firmato; appiccicarlo dopo è o una modifica non firmata a un file firmato o una nuova revisione incrementale che qualunque validatore elencherà come modifica. Il secondo dettaglio è StrictVersionLock. Con il lock acceso e il documento bloccato sotto PDF 2.0, EnsureMLDSAExtensions ritorna senza scrivere niente, dato che un'estensione PDF 2.0 non può essere dichiarata in un file 1.7. Se il vostro generatore blocca le versioni per ragioni di archiviazione, controllate Version = pdf20 prima di contare sulla dichiarazione
Una sola chiamata di verifica per EdDSA e ML-DSA
VerifyLoadedSignatureWithOptions prende un indice di firma, lo stream sorgente, un record THPDFCMSVerifyOptions e un out THPDFSignatureInfo. Partite il record da THPDFCMSVerifyOptions.Default, che lascia AllowMLDSA spento, pone il tetto di un contenitore CMS a 256 firmatari e limita l'annidamento delle controfirmhe a 8. Tre campi contano per gli algoritmi più nuovi. SignatureProvider lascia che il vostro verificatore risponda per primo. OpenSSLLibraryPath nomina il libcrypto OpenSSL 3 che esegue la verifica Ed25519 e Ed448 in modalità pura. AllowMLDSA aderisce agli algoritmi a reticolo, subordinato al test del Catalog sopra. Il record porta anche campi di blocco della policy di firma (RequireSignaturePolicy, ExpectedSignaturePolicyOID, ExpectedSignaturePolicyHash), che valgono per ogni famiglia di algoritmi
Il percorso integrato si divide per algoritmo. Ed25519 e Ed448 passano per OpenSSL 3; quando nessun percorso è dato, il provider di piattaforma prova libcrypto-3-x64.dll, libcrypto-3.dll e libcrypto.dll attraverso il normale ordine di ricerca delle DLL su Win64. ML-DSA passa per il provider di algoritmo CNG ML-DSA di Windows, e su una build Windows che non lo offre il risultato è non supportato. Impostate OpenSSLLibraryPath a una copia che spedite accanto all'eseguibile. Lasciarlo vuoto significa che il verdetto dipende da quale libcrypto un altro installer è capitato che abbia scaricato sul percorso di ricerca, e quella è una cosa dura da spiegare in un audit
Il vostro SignatureProvider viene interpellato per primo. Se restituisce spsUnsupported, il provider di piattaforma integrato riceve la stessa richiesta, quindi un provider che conosce solo ML-DSA può sedersi davanti alla gestione EdDSA predefinita senza re-implementarla
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); // passa EdDSA al provider di piattaforma
// MyFips204Verify è un wrapper vostro attorno a una libreria ML-DSA;
// Req.Input contiene gli attributi firmati codificati DER (modalità pura)
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; // ancora in AND con la dichiarazione del 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; // il record opzioni non lo possiede
end;
end;
Loggate Info.SignatureAlgorithmOID invece di qualunque algoritmo la vostra configurazione si aspettava. Per ML-DSA-65 è 2.16.840.1.101.3.4.3.18, e un audit trail che registra l'OID trovato nel file risponde più avanti a domande che un valore di configurazione non può. Se non vi serve affatto un provider personalizzato, l'overload più corto VerifyLoadedSignature(SigIndex, Source, Info) costruisce le opzioni predefinite e imposta AllowMLDSA dalla dichiarazione del Catalog per voi
Regole di codifica che fanno inciampare ML-DSA e CMS EdDSA prodotti esternamente
Entrambe le famiglie di algoritmi vengono verificate in modalità pura sugli attributi firmati completi codificati DER, e un SignerInfo senza attributi firmati viene riportato come non supportato. Per EdDSA i parametri dell'AlgorithmIdentifier devono essere assenti. Un errore comune nei CMS scritti a mano è fare l'hash degli attributi firmati con SHA-512 o SHAKE256 prima e firmare quel digest, il che trasforma di nascosto PureEdDSA in una variante prehash che niente nel CMS dichiara. Per ML-DSA il verificatore controlla anche l'accoppiamento tra parameter set e message digest: ML-DSA-44 accetta SHA-256, SHA-384, SHA-512 o SHAKE256, ML-DSA-65 vuole SHA-384 o più forte, e ML-DSA-87 vuole SHA-512 o SHAKE256. Un accoppiamento sbagliato si mostra in Info.DigestConsistencyIssue, che è il primo campo da leggere quando un file da un'altra toolchain fallisce
EdDSA ha la sua storia di registrazione. ISO/TS 32002 è l'estensione che registra Ed25519 e Ed448 per le firme PDF 2.0, e EnsureISO32002Extensions aggiunge al Catalog la voce ISO_ corrispondente con ExtensionLevel 32002. La verifica accetta EdDSA senza quella voce, ma un file che producete dovrebbe portarla comunque, per la stessa ragione di riproducibilità della dichiarazione ML-DSA
A cosa viene chiesto di fare un provider di firma
Un provider di firma riceve una THPDFSignatureProviderRequest e restituisce uno THPDFSignatureProviderStatus più, in caso di successo, i byte della firma. La richiesta porta l'algoritmo e il suo OID, l'OID dell'algoritmo di digest, la lunghezza del sale PSS, se Input è un messaggio (sikMessage) o un digest precalcolato (sikDigest), i byte di input, il certificato del firmatario, un identificatore di chiave e un identificatore di operazione unico alla chiamata. Quel è il vocabolario che un driver di token o un servizio di firma parla già, e nessuna parte di esso richiede che la chiave attraversi nel vostro processo
Ogni provider pubblica anche un insieme di capability come spcSign, spcCancel, spcHardwareKey e spcRemoteKey. Diverse implementazioni viaggiano nella libreria:
THPDFCallbackSignatureProvideravvolge metodi anonimi per verify, sign e cancel, che è la strada più corta da una routine di firma interna esistente a una firma PDFTHPDFRemoteSignatureProvideravvolge una funzione di trasporto con un limite di retry, un registro di cancellazione e tetti su input e dimensione della firma, così un servizio remoto che si comporta male non può spingere una risposta sovradimensionata nell'encoder CMSTHPDFPKCS11SignatureProviderprende in prestito una sessione e un handle di chiave privata che il vostro codice ha già aperto e su cui ha fatto login. HotPDF non vede mai il PIN e non gestisce la sessione del token; serializza le chiamateC_Sign, antepone il DigestInfo per RSA PKCS#1 v1.5, passa la vera lunghezza del sale a RSA-PSS tramite mechanism parameters, e converte un risultato ECDSAr||sgrezzo nella forma DER che il CMS si aspettaTHPDFWindowsKeyStorageSignatureProviderfirma attraverso il key storage CNG di Windows, che copre smart card e chiavi supportate da TPM, e può richiedere una chiave supportata da hardware
Il provider PKCS#11 prende input di digest, quindi gestisce RSA PKCS#1 v1.5, RSA-PSS e ECDSA. Ed25519 e Ed448 firmano il messaggio completo degli attributi firmati, e quelli passano per il provider callback o remoto, dove il vostro codice passa Req.Input al token o al servizio come messaggio
var
Provider: THPDFRemoteSignatureProvider;
Opts: THPDFCMSSignOptions;
CertDER: TBytes;
InStream, OutStream: TFileStream;
begin
CertDER := TFile.ReadAllBytes('signer.cer'); // System.IOUtils; certificato DER della chiave remota
Provider := THPDFRemoteSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
out Signature: TBytes): THPDFSignatureProviderStatus
begin
// PostToSigningService è un client HTTP vostro: mappate timeout e 5xx a
// spsProviderError, un'approvazione rifiutata a spsCancelled, una chiave sconosciuta a spsUnsupported
Result := PostToSigningService(Req.KeyIdentifier, Req.AlgorithmOID,
Req.Input, Signature);
end,
3, // RetryLimit: fino a quattro tentativi in totale
1048576, // MaxInputBytes
65536); // MaxSignatureBytes
Opts := HPDFCMSDefaultOptions(palBaseline_B_B);
Opts.SignatureScheme := cmsRSAPSS;
Opts.DigestAlgorithm := cmsdaSHA256;
Opts.PSSSaltLength := 32;
// contract-unsigned.pdf è stato salvato con un segnaposto da 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;
Sei stati, e cosa ognuno dovrebbe innescare
THPDFSignatureProviderStatus ha sei valori perché la reazione giusta differisce per ognuno. spsInvalid è un fallimento crittografico e appartiene a un log di sicurezza. spsUnsupported significa che il provider non implementa l'algoritmo, il che è un problema di deployment. spsMalformed significa che la richiesta o la risposta aveva la forma sbagliata. spsProviderError è un guasto di trasporto o di dispositivo che può riuscire al retry. spsCancelled significa che una persona ha declinato il prompt del token, e riprovarci la annoia solo
Il provider remoto costruisce quella policy dentro. Fa retry solo su spsProviderError, controlla il registro di cancellazione prima di ogni tentativo, rifiuta input vuoto o sovradimensionato come spsMalformed prima di chiamare il trasporto, e degrada una risposta "valida" con firma vuota o sovradimensionata a spsMalformed. Sul lato verifica, la risposta di un provider si mappa sullo stato di verifica: spsInvalid diventa svSignatureInvalid, spsMalformed diventa svMalformed, spsProviderError e spsCancelled diventano entrambi svCryptoProviderError, e spsUnsupported diventa svUnsupportedAlgorithm una volta che anche il provider di piattaforma ha declinato la richiesta
Se scrivete un provider, resistete a mappare ogni condizione inattesa su spsInvalid. Questo trasforma una DLL mancante o un token di servizio scaduto in un rapporto che la firma del cliente è contraffatta, e qualcuno dovrà spiegare quello al cliente. Restituite spsInvalid solo quando la matematica ha davvero fallito. Per la firma, restituite spsValid solo insieme a una firma non vuota; entrambi i punti d'ingresso sollevano EHPDFCMSError quando un provider restituisce successo senza byte
Dove la firma del provider atterra nel file
Due funzioni collegano un provider a veri byte PDF. HPDFCMSBuildSignedDataWithProvider costruisce CMS detached da un digest del documento calcolato altrove; la lunghezza del digest deve combaciare con Options.DigestAlgorithm, quindi una configurazione SHA-384 si aspetta 48 byte e rifiuta un digest SHA-256 di 32 byte con un'eccezione. HPDFCMSSignPDFStreamWithProvider firma il segnaposto che AddSignedSignatureField ha disposto, fa l'hash di entrambe le finestre /ByteRange, chiede al provider la firma sugli attributi firmati, e appunta il risultato esadecimale in /Contents. Se il CMS cresce oltre il segnaposto, la chiamata solleva un'eccezione e ripristina il segnaposto non firmato nello stream di output invece di troncare qualcosa
Il layout è lo stesso che il percorso di firma da PFX scrive: due range che saltano la finestra esadecimale della firma. Il verificatore tiene ogni file a quella forma, controllando che il varco tra i range contenga esattamente la stringa /Contents di questa firma e nient'altro, e riportando svInvalidByteRange altrimenti. Un documento firmato attraverso un token PKCS#11 verifica quindi con lo stesso codice di verifica delle firme di uno firmato da un file PFX. Le regole di profilo che stanno sopra la scelta dell'algoritmo sono coperte in firme baseline PAdES in Delphi, e le trappole di codifica ECDSA di cui si occupa la conversione PKCS#11 stanno in verifica CMS ECDSA e formati di firma P1363
Diffondere firme post-quantum senza lasciare a terra i lettori
Un lettore PDF che non implementa ML-DSA riporta una firma simile come inverificabile, e la persona che guarda il documento vede un avviso invece di un segno di spunta verde. Questo rende la migrazione un problema di programmazione. Tenete RSA o ECDSA come la firma che i validatori di oggi giudicheranno. Dove una policy pretende evidenza resistente al quantum, dichiarate l'estensione con EnsureMLDSAExtensions e aggiungete una seconda firma prodotta da un firmatore capace di ML-DSA accanto a quella classica. Quella firma viene dal vostro firmatore FIPS 204, dato che il costruttore CMS di HotPDF non crea firme ML-DSA; la parte della libreria è verificarla. Spostate la firma primaria solo quando i sistemi che consumano i vostri documenti sanno verificarla
In quel piano HotPDF copre entrambe le estremità che controllate da Delphi. I suoi costruttori CMS firmano con RSA PKCS#1 v1.5, RSA-PSS, ECDSA, Ed25519 e Ed448 attraverso qualunque dei provider sopra, e il suo verificatore controlla ML-DSA, EdDSA e gli algoritmi classici da una chiamata sola, con l'OID davvero trovato registrato nel risultato. HotPDF è un componente PDF VCL nativo per Delphi e C++Builder che non vuole nessun runtime PDF esterno, così il codice di firma e verifica compila nel vostro eseguibile. Edizioni, il set di funzionalità di firma e un download di prova sono sulla pagina del componente PDF Delphi HotPDF