Articolo tecnico

Firma PDF post-quantum e EdDSA con HotPDF in Delphi

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

HotPDF subordina la verifica PDF ML-DSA a una dichiarazione esplicita di estensione del Catalog scritta da EnsureMLDSAExtensions prima del salvataggio del documento, e rifiuta gli algoritmi a reticolo su file non dichiarati anche quando AllowMLDSA è impostato
HotPDF ammette ML-DSA solo quando il Catalog dichiara ancora l'estensione, il che tiene riproducibile un verdetto post-quantum, e chiamare EnsureMLDSAExtensions prima di EndDoc colloca la dichiarazione dentro l'intervallo di byte firmato anziché dopo
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:

  • THPDFCallbackSignatureProvider avvolge metodi anonimi per verify, sign e cancel, che è la strada più corta da una routine di firma interna esistente a una firma PDF
  • THPDFRemoteSignatureProvider avvolge 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 CMS
  • THPDFPKCS11SignatureProvider prende 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 chiamate C_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 ECDSA r||s grezzo nella forma DER che il CMS si aspetta
  • THPDFWindowsKeyStorageSignatureProvider firma 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

| Provider di firma collegabili HotPDF che scambiano un solo record di richiesta semplice e restituiscono uno stato a sei valori, mostrato per le implementazioni callback, remota e PKCS#11 mentre la chiave privata non entra mai nel processo Delphi
I provider callback, remoto e PKCS#11 parlano lo stesso record di richiesta e rispondono con lo stesso stato a sei valori, il che lascia token hardware, servizi HSM remoti e carte eID nazionali tenere le loro chiavi invece di esportarle
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

| Due punti d'ingresso di firma con provider HotPDF, HPDFCMSBuildSignedDataWithProvider per un digest calcolato esternamente e HPDFCMSSignPDFStreamWithProvider per un segnaposto disposto, convergono su un unico layout CMS che preserva i range /ByteRange che i validatori controllano per primi
L'uno o l'altro punto d'ingresso scrive la stessa forma firmata di due range coperti attorno alla finestra esadecimale, così un documento firmato con provider ha lo stesso layout di byte di uno firmato da un file PFX

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