Un PDF portant une signature ML-DSA-65 peut passer le contrôle de la chaîne d'outils qui l'a produit et revenir quand même en algorithme non pris en charge auprès d'un vérificateur qui exige que le fichier déclare quelle extension définit son algorithme. La clé publique fait 1 952 octets, exactement la taille FIPS 204 pour ce jeu de paramètres. Les octets de signature sont intacts et les plages d'octets couvrent tout le fichier. Ce qu'un tel vérificateur cherche, c'est une entrée dans le Catalog du document nommant cette extension, parce qu'ISO 32000-2 ne liste pas du tout ML-DSA
Le côté signature a un autre mur. Un token matériel, un HSM en nuage ou une carte eID nationale calculera une signature sur demande, mais aucun ne remettra la clé privée à votre application, donc tout code de signature qui s'attend à charger un fichier PFX et signer en processus n'a tout simplement rien à charger. Les deux problèmes parlent de l'endroit où vit une décision : la déclaration d'algorithme vit dans le fichier, et la clé vit quelque part que votre code Delphi ne peut pas atteindre
Pourquoi une signature ML-DSA valide revient-elle non prise en charge ?
HotPDF vérifie les signatures CMS ML-DSA-44, ML-DSA-65 et ML-DSA-87 seulement quand le document chargé déclare l'extension développeur HotPDF dans son Catalog. Sans cette déclaration, le vérificateur renvoie svUnsupportedAlgorithm pour une signature à treillis, si saine que soit la cryptographie. Le raisonnement est la reproductibilité : un fichier qui utilise un algorithme que la norme de base ne nomme pas, et ne dit pas à quelle extension il se fie, produit un verdict qu'une autre implémentation n'a aucune base de répéter
Côté production, THotPDF.EnsureMLDSAExtensions écrit /Extensions << /HotPDF << /BaseVersion /2.0 /ExtensionLevel 1 >> >> dans le Catalog et hisse le document en PDF 2.0 s'il visait une version plus ancienne. Côté lecture, LoadedDocumentDeclaresMLDSAExtension renvoie True seulement quand ce dictionnaire est présent avec /BaseVersion égal à 2.0 et /ExtensionLevel d'au moins 1. VerifyLoadedSignatureWithOptions combine les deux : elle prend le drapeau AllowMLDSA de l'appelant et le combine en ET avec le test de déclaration avant toute vérification, donc positionner le drapeau sur un fichier non déclaré ne change rien
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'contract-pq.pdf';
Pdf.StrictVersionLock := False; // le défaut ; permet à l'appel de hisser le fichier en PDF 2.0
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, 'Supply agreement 2026-114');
Pdf.EnsureMLDSAExtensions; // déclarer avant qu'une signature ne couvre le Catalog
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Deux détails dans cet extrait portent du poids. L'appel doit se produire avant que le document ne soit sauvegardé et signé, parce que le Catalog siège dans la plage d'octets signée ; le retoucher ensuite est soit une modification non signée d'un fichier signé, soit une nouvelle révision incrémentale que tout validateur listera comme modification. Le second détail est StrictVersionLock. Avec le verrou actif et le document épinglé sous PDF 2.0, EnsureMLDSAExtensions revient sans rien écrire, puisqu'une extension PDF 2.0 ne peut pas être déclarée dans un fichier 1.7. Si votre générateur épingle les versions pour des raisons d'archivage, contrôlez Version = pdf20 avant de vous fier à la déclaration
Un seul appel de vérification pour EdDSA et ML-DSA
VerifyLoadedSignatureWithOptions prend un index de signature, le flux source, un enregistrement THPDFCMSVerifyOptions et un THPDFSignatureInfo en sortie. Partez l'enregistrement de THPDFCMSVerifyOptions.Default, qui laisse AllowMLDSA éteint, plafonne un conteneur CMS à 256 signataires et limite l'imbrication des contre-signatures à 8. Trois champs comptent pour les algorithmes plus récents. SignatureProvider laisse votre propre vérificateur répondre en premier. OpenSSLLibraryPath nomme le libcrypto OpenSSL 3 qui effectue la vérification en mode pur d'Ed25519 et Ed448. AllowMLDSA opte pour les algorithmes à treillis, sous réserve du test de Catalog ci-dessus. L'enregistrement porte aussi des champs d'épinglage de politique de signature (RequireSignaturePolicy, ExpectedSignaturePolicyOID, ExpectedSignaturePolicyHash), qui s'appliquent à chaque famille d'algorithmes
Le chemin intégré se divise par algorithme. Ed25519 et Ed448 passent par OpenSSL 3 ; quand aucun chemin n'est donné, le fournisseur de plateforme essaie libcrypto-3-x64.dll, libcrypto-3.dll et libcrypto.dll via l'ordre de recherche DLL normal sur Win64. ML-DSA passe par le fournisseur d'algorithme CNG ML-DSA de Windows, et sur un build Windows qui ne l'offre pas le résultat est non pris en charge. Positionnez OpenSSLLibraryPath vers une copie que vous livrez à côté de l'exécutable. Le laisser vide signifie que le verdict dépend du libcrypto que tel autre installateur a laissé tomber sur le chemin de recherche, et c'est une chose difficile à expliquer dans un audit
Votre propre SignatureProvider est sollicité en premier. S'il renvoie spsUnsupported, le fournisseur de plateforme intégré reçoit la même demande, donc un fournisseur qui ne connaît que ML-DSA peut se placer devant la gestion EdDSA par défaut sans la réimplémenter
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); // passe EdDSA au fournisseur de plateforme
// MyFips204Verify est votre propre enveloppe autour d'une bibliothèque ML-DSA ;
// Req.Input contient les attributs signés encodés en DER (mode pur)
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; // toujours combiné en ET avec la déclaration du 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; // l'enregistrement d'options ne le possède pas
end;
end;
Journalisez Info.SignatureAlgorithmOID plutôt que l'algorithme que votre configuration attendait. Pour ML-DSA-65 c'est 2.16.840.1.101.3.4.3.18, et une piste d'audit qui enregistre l'OID trouvé dans le fichier répond plus tard à des questions qu'une valeur de configuration ne peut pas. Si vous n'avez besoin d'aucun fournisseur personnalisé, la surcharge plus courte VerifyLoadedSignature(SigIndex, Source, Info) bâtit des options par défaut et positionne AllowMLDSA d'après la déclaration du Catalog pour vous
Règles d'encodage qui font trébucher les CMS ML-DSA et EdDSA produits à l'extérieur
Les deux familles d'algorithmes sont vérifiées en mode pur sur les attributs signés encodés en DER complets, et un SignerInfo sans attributs signés est rapporté comme non pris en charge. Pour EdDSA, les paramètres de l'AlgorithmIdentifier doivent être absents. Une erreur courante du CMS fait main est de condenser d'abord les attributs signés avec SHA-512 ou SHAKE256 puis de signer ce condensé, ce qui transforme silencieusement PureEdDSA en variante à pré-condensé que rien dans le CMS ne déclare. Pour ML-DSA, le vérificateur contrôle aussi l'appariement entre jeu de paramètres et condensé de message : ML-DSA-44 accepte SHA-256, SHA-384, SHA-512 ou SHAKE256, ML-DSA-65 exige SHA-384 ou plus fort, et ML-DSA-87 exige SHA-512 ou SHAKE256. Un appariement inadapté se manifeste dans Info.DigestConsistencyIssue, le premier champ à lire quand un fichier d'une autre chaîne d'outils échoue
EdDSA a sa propre histoire d'enregistrement. ISO/TS 32002 est l'extension qui enregistre Ed25519 et Ed448 pour les signatures PDF 2.0, et EnsureISO32002Extensions ajoute l'entrée ISO_ assortie avec ExtensionLevel 32002 au Catalog. La vérification accepte EdDSA sans cette entrée, mais un fichier que vous produisez devrait quand même la porter, pour la même raison de reproductibilité que la déclaration ML-DSA
Ce qu'on demande à un fournisseur de signature
Un fournisseur de signature reçoit une THPDFSignatureProviderRequest et renvoie un THPDFSignatureProviderStatus plus, en cas de succès, les octets de signature. La demande porte l'algorithme et son OID, l'OID de l'algorithme de condensé, la longueur du sel PSS, si Input est un message (sikMessage) ou un condensé précalculé (sikDigest), les octets d'entrée, le certificat du signataire, un identifiant de clé et un identifiant d'opération unique à l'appel. C'est le vocabulaire que parle déjà un pilote de token ou un service de signature, et rien de tout cela n'exige que la clé traverse vers votre processus
Chaque fournisseur annonce aussi un jeu de capacités telles que spcSign, spcCancel, spcHardwareKey et spcRemoteKey. Plusieurs implémentations sont livrées dans la bibliothèque :
THPDFCallbackSignatureProviderenveloppe des méthodes anonymes pour vérifier, signer et annuler, ce qui est le chemin le plus court d'une routine de signature interne existante à une signature PDFTHPDFRemoteSignatureProviderenveloppe une fonction de transport avec une limite de tentatives, un registre d'annulation et des bornes sur la taille d'entrée et de signature, donc un service distant mal comporté ne peut pas pousser une réponse surdimensionnée dans l'encodeur CMSTHPDFPKCS11SignatureProvideremprunte une session et un handle de clé privée que votre code a déjà ouverts et connectés. HotPDF ne voit jamais le PIN et ne gère pas la session du token ; il sérialise les appelsC_Sign, préfixe le DigestInfo pour RSA PKCS#1 v1.5, passe la vraie longueur de sel à RSA-PSS via les paramètres de mécanisme, et convertit un résultat ECDSA brutr||sen la forme DER que CMS attendTHPDFWindowsKeyStorageSignatureProvidersigne via le stockage de clés CNG de Windows, qui couvre les cartes à puce et les clés adossées à un TPM, et peut exiger une clé adossée au matériel
Le fournisseur PKCS#11 prend du condensé en entrée, donc il gère RSA PKCS#1 v1.5, RSA-PSS et ECDSA. Ed25519 et Ed448 signent le message complet des attributs signés, et ceux-là passent par le fournisseur callback ou distant, où votre code remet Req.Input au token ou au service comme message
var
Provider: THPDFRemoteSignatureProvider;
Opts: THPDFCMSSignOptions;
CertDER: TBytes;
InStream, OutStream: TFileStream;
begin
CertDER := TFile.ReadAllBytes('signer.cer'); // System.IOUtils ; certificat DER de la clé distante
Provider := THPDFRemoteSignatureProvider.Create(
function(const Req: THPDFSignatureProviderRequest; Attempt: Integer;
out Signature: TBytes): THPDFSignatureProviderStatus
begin
// PostToSigningService est votre propre client HTTP : mappez timeouts et 5xx vers
// spsProviderError, une approbation refusée vers spsCancelled, une clé inconnue vers spsUnsupported
Result := PostToSigningService(Req.KeyIdentifier, Req.AlgorithmOID,
Req.Input, Signature);
end,
3, // RetryLimit : jusqu'à quatre tentatives au total
1048576, // MaxInputBytes
65536); // MaxSignatureBytes
Opts := HPDFCMSDefaultOptions(palBaseline_B_B);
Opts.SignatureScheme := cmsRSAPSS;
Opts.DigestAlgorithm := cmsdaSHA256;
Opts.PSSSaltLength := 32;
// contract-unsigned.pdf a été sauvegardé avec un placeholder venant de 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;
Six statuts, et ce que chacun devrait déclencher
THPDFSignatureProviderStatus a six valeurs parce que la bonne réaction diffère pour chacune. spsInvalid est un échec cryptographique et appartient à un journal de sécurité. spsUnsupported signifie que le fournisseur n'implémente pas l'algorithme, ce qui est un problème de déploiement. spsMalformed signifie que la demande ou la réponse avait la mauvaise forme. spsProviderError est une faute de transport ou de matériel qui peut réussir à la retrée. spsCancelled signifie qu'une personne a refusé l'invite du token, et la relancer ne fait que l'agacer
Le fournisseur distant bâtit cette politique en soi. Il ne retente que sur spsProviderError, contrôle le registre d'annulation avant chaque tentative, rejette une entrée vide ou surdimensionnée comme spsMalformed avant d'appeler le transport, et rétrograde une réponse « valide » avec une signature vide ou surdimensionnée en spsMalformed. Côté vérification, la réponse d'un fournisseur se projette sur le statut de vérification : spsInvalid devient svSignatureInvalid, spsMalformed devient svMalformed, spsProviderError et spsCancelled deviennent tous deux svCryptoProviderError, et spsUnsupported devient svUnsupportedAlgorithm une fois que le fournisseur de plateforme a décliné la demande lui aussi
Si vous écrivez un fournisseur, résistez à projeter chaque condition inattendue sur spsInvalid. Cela transforme une DLL manquante ou un token de service expiré en rapport affirmant que la signature du client est falsifiée, et quelqu'un devra l'expliquer au client. Renvoyez spsInvalid seulement quand les maths ont réellement échoué. Pour la signature, renvoyez spsValid seulement accompagné d'une signature non vide ; les deux points d'entrée lèvent EHPDFCMSError quand un fournisseur renvoie le succès sans octets
Où la signature du fournisseur atterrit dans le fichier
Deux fonctions relient un fournisseur aux vrais octets PDF. HPDFCMSBuildSignedDataWithProvider bâtit un CMS détaché depuis un condensé de document que vous avez calculé ailleurs ; la longueur du condensé doit correspondre à Options.DigestAlgorithm, donc une configuration SHA-384 attend 48 octets et rejette un condensé SHA-256 de 32 octets par une exception. HPDFCMSSignPDFStreamWithProvider signe le placeholder que AddSignedSignatureField a disposé, condense les deux fenêtres /ByteRange, demande au fournisseur la signature sur les attributs signés, et recoud le résultat hexadécimal dans /Contents. Si le CMS grandit au-delà du placeholder, l'appel lève une exception et restaure le placeholder non signé dans le flux de sortie au lieu de tronquer quoi que ce soit
La disposition est la même que celle que le chemin de signature à base de PFX écrit : deux plages qui sautent la fenêtre hexadécimale de la signature. Le vérificateur tient chaque fichier à cette forme, contrôlant que l'écart entre les plages contient exactement la chaîne /Contents de cette signature et rien d'autre, et rapportant svInvalidByteRange sinon. Un document signé via un token PKCS#11 se vérifie donc avec le même code de vérification de signatures qu'un signé depuis un fichier PFX. Les règles de profil qui siègent au-dessus du choix d'algorithme sont couvertes dans les signatures baseline PAdES en Delphi, et les pièges d'encodage ECDSA que la conversion PKCS#11 traite sont dans la vérification CMS ECDSA et les formats de signature P1363
Déployer les signatures post-quantiques sans strander les lecteurs
Un lecteur PDF qui n'implémente pas ML-DSA rapporte une telle signature comme invérifiable, et la personne qui regarde le document voit un avertissement plutôt qu'une coche verte. Cela fait de la migration un problème de calendrier. Gardez RSA ou ECDSA comme signature que les validateurs d'aujourd'hui jugeront. Là où une politique exige des preuves résistantes au quantique, déclarez l'extension avec EnsureMLDSAExtensions et ajoutez une seconde signature produite par un signeur capable de ML-DSA à côté de la classique. Cette signature vient de votre propre signeur FIPS 204, puisque le bâtisseur CMS de HotPDF ne crée pas de signatures ML-DSA ; la part de la bibliothèque est de la vérifier. Ne bougez la signature primaire que quand les systèmes qui consomment vos documents peuvent la vérifier
Dans ce plan, HotPDF couvre les deux extrémités que vous contrôlez depuis Delphi. Ses bâtisseurs CMS signent avec RSA PKCS#1 v1.5, RSA-PSS, ECDSA, Ed25519 et Ed448 via n'importe lequel des fournisseurs ci-dessus, et son vérificateur contrôle ML-DSA, EdDSA et les algorithmes classiques depuis un seul appel, avec l'OID réellement trouvé enregistré dans le résultat. HotPDF est un composant PDF VCL natif pour Delphi et C++Builder qui n'exige aucun runtime PDF externe, donc le code de signature et de vérification se compile dans votre exécutable. Éditions, jeu de fonctionnalités de signature et essai téléchargeable sont sur la page du composant PDF Delphi HotPDF