Article technique

Signature PDF Post-Quantique et EdDSA avec HotPDF en Delphi

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

HotPDF verrouille la vérification PDF ML-DSA derrière une déclaration d'extension explicite du Catalog écrite par EnsureMLDSAExtensions avant la sauvegarde du document, et refuse les algorithmes à treillis sur les fichiers non déclarés même quand AllowMLDSA est positionné
HotPDF n'admet ML-DSA que quand le Catalog déclare toujours l'extension, ce qui garde un verdict post-quantique reproductible, et appeler EnsureMLDSAExtensions avant EndDoc place la déclaration dans la plage d'octets signée plutôt qu'après
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 :

  • THPDFCallbackSignatureProvider enveloppe 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 PDF
  • THPDFRemoteSignatureProvider enveloppe 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 CMS
  • THPDFPKCS11SignatureProvider emprunte 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 appels C_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 brut r||s en la forme DER que CMS attend
  • THPDFWindowsKeyStorageSignatureProvider signe 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

Les fournisseurs de signature enfichables HotPDF échangent un unique enregistrement de demande simple et renvoient un statut à six valeurs, montré pour les implémentations callback, distante et PKCS#11 pendant que la clé privée n'entre jamais dans le processus Delphi
Les fournisseurs callback, distant et PKCS#11 parlent le même enregistrement de demande et répondent avec le même statut à six valeurs, ce qui laisse les tokens matériels, les services HSM distants et les cartes eID nationales garder leurs clés au lieu de les exporter
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

Les deux points d'entrée de signature à fournisseur HotPDF, HPDFCMSBuildSignedDataWithProvider pour un condensé calculé à l'extérieur et HPDFCMSSignPDFStreamWithProvider pour un placeholder disposé, convergent vers une disposition CMS unique qui préserve les plages /ByteRange que les validateurs contrôlent en premier
Chacun des deux points d'entrée écrit la même forme signée de deux plages couvertes autour de la fenêtre hexadécimale, donc un document signé par fournisseur a la même disposition d'octets qu'un signé depuis un fichier PFX

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