💡 Exemple complet disponible sur GitHub :
sign-docx-with-mldsa-certificates-python

Introduction

Signez un contrat cet après‑midi avec RSA‑2048 et vous avez fait une promesse qui doit tenir tant que le contrat a de l’importance. Que ce soit vingt ou trente ans – et pour les actes, les formulaires de consentement et les validations d’ingénierie c’est souvent le cas – la promesse doit dépasser l’algorithme. L’attaque n’a pas besoin d’exister aujourd’hui. Elle doit exister avant que le document ne perde son intérêt, et alors quiconque possède la clé publique pourra dériver la clé privée et signer en votre nom.

La signature de documents post‑quantique est la fonctionnalité GroupDocs.Signature pour Python qui remplace cette promesse par une basée sur ML‑DSA, l’algorithme de signature standardisé par le NIST sous le nom FIPS 204 en 2024. La prise en charge du format Word est arrivée dans GroupDocs.Signature 26.9, et elle réutilise l’API que vous avez déjà : une clé ML‑DSA vit dans un PFX et est passée à DigitalSignOptions exactement comme une clé RSA.

Ce guide signe un DOCX en quatre étapes, compare les trois niveaux de sécurité sur la sortie mesurée, vérifie la signature uniquement avec un certificat public, et se termine par les deux limites à connaître avant de s’engager.

Pourquoi cela importe plus que la migration habituelle

La migration de signatures diffère de la migration de chiffrement en un point qui la rend plus facile à reporter et plus difficile à corriger.

Avec le chiffrement, le problème « récolter maintenant / décrypter plus tard » est immédiat : tout ce qui est intercepté aujourd’hui peut être stocké et ouvert plus tard. Avec les signatures, rien de ce que vous avez déjà signé ne devient falsifiable rétroactivement – mais rien de ce que vous avez signé ne reste non plus prouvablement le vôtre, une fois que la clé peut être dérivée du certificat dont tout le monde possède une copie. Resigner une décennie de documents archivés avec de nouvelles clés est possible et personne ne veut être la personne qui planifie cela.

C’est pourquoi le conseil pratique est étroit plutôt que généralisé : migrez les documents dont la rétention est longue, laissez le reste. Certains profils ont déjà fixé la barre – CNSA 2.0 exige ML‑DSA‑87 pour les systèmes de sécurité nationale – et pour tout le monde le facteur décisif est la durée pendant laquelle le fichier doit rester défendable.

Prérequis

  • Python 3.9 ou ultérieur sur un interpréteur 64 bits – le paquet fournit un runtime .NET intégré et n’a pas de roue 32 bits
  • GroupDocs.Signature pour Python via .NET 26.10.0, avec une licence temporaire gratuite pour supprimer les limites d’évaluation
  • Un certificat ML‑DSA sous forme de PFX protégé par mot de passe, et un document Word à signer

Installation

pip install groupdocs-signature-net

Étape 1 – Signer avec un certificat ML‑DSA

Le certificat fait le travail. L’appel est celui que vous écririez pour RSA :

with signature.Signature(source_path) as sign:
    options = DigitalSignOptions(pfx_path)
    options.password = CERTIFICATE_PASSWORD

    result = sign.sign(output_path, options)

C’est toute l’histoire d’adoption pour du code qui signe déjà : pointez DigitalSignOptions vers un autre PFX. Aucun nouvel option, aucun paramètre d’algorithme séparé, aucune branche pour le post‑quantique.

Lire le signataire à nouveau nécessite une étape supplémentaire, et contient le seul piège propre à Python dans cet exercice :

for created in result.succeeded:
    certificate = getattr(created, "certificate", None)
    subject = getattr(certificate, "subject", None)
    if subject:
        return str(subject)

Le certificat sur un DigitalSignature est un objet de pont qui résout les attributs dynamiquement. certificate.subject renvoie CN=GroupDocs.Signature MLDSA65 test, tandis que dir() sur le même objet ne liste rien du tout. Je l’ai inspecté avec dir() d’abord, j’ai conclu que le sujet n’était pas exposé, et j’avais simplement tort – donc si vous introspectez avant de lire, vous sauterez une valeur qui est bien là.

Étape 2 – Comparer les trois niveaux de sécurité

ML‑DSA propose trois jeux de paramètres, sélectionnés en fournissant un certificat différent :

levels = (
    ("ML-DSA-44", MLDSA44_PFX),
    ("ML-DSA-65", MLDSA65_PFX),
    ("ML-DSA-87", MLDSA87_PFX),
)

for level, pfx_path in levels:
    with signature.Signature(source_path) as sign:
        options = DigitalSignOptions(pfx_path)
        options.password = CERTIFICATE_PASSWORD
        sign.sign(output_path, options)

    sizes[f"{level} -> {file_name}"] = os.path.getsize(output_path)

C’est l’étape qui vaut la peine d’être réellement exécutée, car le compromis est généralement décrit mais rarement mesuré. À partir d’un contrat source de 132 KB :

Niveau Catégorie de sécurité NIST Fichier signé Par rapport au plus petit
ML-DSA-44 2 138 202 octets -
ML-DSA-65 3 140 650 octets +2 448 octets
ML-DSA-87 5 143 971 octets +5 769 octets

Moins de 6 KB séparent le niveau le plus faible du plus fort. Sur un contrat, cela ne change rien, ce qui simplifie la décision : utilisez ML‑DSA‑65 par défaut, ML‑DSA‑87 lorsqu’un profil exige la catégorie 5 ou lorsque la taille est sans importance, et ML‑DSA‑44 uniquement si vous signez tant de fichiers que les kilooctets s’agrègent en quelque chose de réel.

Étape 3 – Vérifier avec un certificat public

Un destinataire n’a besoin que du certificat public du signataire et de rien de secret :

with signature.Signature(signed_path) as sign:
    options = DigitalVerifyOptions(certificate_path)
    if password is not None:
        options.password = password

    return sign.verify(options).is_valid

L’exemple appelle cela deux fois sur le même fichier : une fois avec mldsa65.cer, la moitié publique de la clé de signature, et une fois avec le PFX d’un autre signataire. Le premier renvoie True, le second False. Notez que le mauvais certificat renvoie False plutôt que de lever une exception – « signé par quelqu’un d’autre » est une réponse que votre code doit gérer, pas une exception. La vérification couvre le contenu du document ainsi que le numéro de série et l’empreinte du certificat, donc un fichier modifié après la signature échoue également.

Étape 4 – Lire les signatures d’un document

Lorsqu’un document signé arrive et que vous ne savez pas quel certificat attendre :

with signature.Signature(signed_path) as sign:
    found = sign.search(SignatureType.DIGITAL)
    for item in found:
        print(item.sign_time, item.is_valid)

search avec SignatureType.DIGITAL renvoie des objets DigitalSignature contenant le certificat, l’heure de signature et un drapeau de validité. Un document Word peut contenir plusieurs signatures, y compris un mélange de RSA et de ML‑DSA, et chacune est rapportée avec son propre certificat et sa propre validité.

Cela change‑t‑il la façon dont les destinataires vérifient ?

Pas du tout, ils ne le remarqueront pas. Un destinataire a toujours besoin uniquement du certificat public du signataire, le passe toujours au même DigitalVerifyOptions, et reçoit toujours un booléen en retour. Rien dans le chemin de vérification n’est spécifique à ML‑DSA. Le seul endroit où l’algorithme apparaît, c’est l’indicateur de signature propre à Microsoft Word, qui peut ne pas reconnaître encore ML‑DSA parce que le format ne possède aucun identifiant standard pour celui‑ci.

Applications concrètes

Contrats à rétention longue

Le cas le plus évident. Un document qui doit rester vérifiable pendant des décennies est signé une fois, maintenant, avec ML‑DSA‑65 ou ML‑DSA‑87, et n’a jamais besoin d’être re‑signé car son algorithme ne vieillit plus.

Environnements réglementés avec un profil nommé

Lorsque CNSA 2.0 ou un profil similaire s’applique, le niveau n’est pas une décision à la discretion – ML‑DSA‑87 est l’exigence, et la seule question d’ingénierie est de savoir si le format est supporté.

Pipelines mixtes pendant la migration

Signer les nouveaux documents post‑quantique tout en laissant les archives intactes est un état intermédiaire parfaitement raisonnable, et le reporting séparé de chaque signature via search rend cela gérable.

Bonnes pratiques et conseils

  • Migrer selon la durée de rétention, pas selon le volume. Les documents qui en ont besoin sont ceux qui vivent longtemps ; un reçu valable 90 jours n’en a pas besoin.
  • Utiliser ML‑DSA‑65 par défaut sauf si un profil spécifie un niveau, et ne pas s’inquiéter de la différence de taille – elle est inférieure à 6 KB par signature.
  • Conserver RSA lorsque le destinataire valide dans Word. Des signatures correctes que le lecteur signale comme problématiques sont pires qu’une migration plus lente.
  • Remplacer les certificats de test. Les fichiers PFX de l’exemple sont auto‑signés avec un mot de passe publié, donc tout ce qui est signé avec eux ne prouve rien.
  • Vérifier après la signature dans tout pipeline, en utilisant le certificat public que le destinataire aurait.

Dépannage des problèmes courants

Microsoft Word n’affiche pas la signature comme valide. C’est attendu pour l’instant : il n’existe aucun identifiant XML‑DSig standard pour ML‑DSA, donc Word peut ne pas le reconnaître même si la signature est correcte et que GroupDocs.Signature la valide. Vérifiez dans votre propre pipeline, et conservez RSA pour les documents dont les destinataires s’appuient sur l’indicateur de Word.

L’appel de signature rejette un PDF ou une feuille de calcul. La signature ML‑DSA couvre les formats Word – DOCX, DOC, ODT et les autres. PDF, feuilles de calcul et présentations ne sont pas encore supportés, et continuent d’être signés avec RSA ou ECDSA comme auparavant.

Le sujet du certificat revient vide. Le piège dir() de l’Étape 1 se produit presque toujours : l’attribut se résout dynamiquement, il faut donc le lire plutôt que de tester son existence au préalable.

Conclusion

Le changement de code se résume à un changement de certificat, ce qui rend cette évolution intéressante avant qu’elle ne devienne urgente. Signez les documents Word à longue durée de vie avec ML‑DSA‑65, utilisez ML‑DSA‑87 lorsqu’un profil l’exige, vérifiez avec le certificat public, et conservez RSA là où le format ou le lecteur le demande.

Exécutez l’exemple sur l’un de vos propres contrats et les trois tailles vous indiqueront, en octets, exactement ce que coûte le niveau le plus fort disponible. Sur le fichier que j’ai testé, c’était 5 769 octets.

Ressources supplémentaires