💡 Exemple complet fonctionnel disponible sur GitHub :
pdf-signing-certificate-checks-python

Introduction

Un service signe les PDF téléchargés chaque nuit. Un matin, le certificat qu’il utilise dépasse sa date d’expiration, et rien ne semble changer : le travail s’exécute, les fichiers sont écrits, le journal paraît normal. Quelques semaines plus tard, quelqu’un ouvre l’un de ces documents dans Acrobat et voit une bannière d’avertissement, car une signature réalisée avec un certificat expiré n’est pas une signature plus faible — c’est une signature que les validateurs signalent comme invalide. Les documents qui semblent approuvés valent moins que ceux qui ne sont pas signés, parce que les gens les ont crus.

Ce refus porte un nom. La vérification de la validité du certificat est un comportement de GroupDocs.Signature pour Python qui refuse de signer une fois la période de validité du certificat écoulée, ou avant qu’elle ne commence. Il est arrivé dans la version 26.9 avec deux changements de même forme : SHA‑256 est devenu le condensat par défaut pour les signatures PDF, et SignatureSettings.log_level a commencé à filtrer au lieu d’être silencieusement ignoré. Chacun transforme un résultat qui se produisait auparavant en silence en un résultat présenté devant vous.

Cet article compare ces trois contrôles tels qu’ils se comportent depuis Python via .NET — ce que chaque contrôle modifie dans la sortie, quand l’utiliser, et quels deux détails de la liaison ont coûté une après‑midi aux développeurs. Chaque résultat cité provient de l’exécution de l’exemple sur un PDF d’une page.

Pourquoi cela importe plus qu’une simple note de version

Les trois changements partagent une propriété qui mérite d’être nommée : tous convertissent un échec que vous découvririez plus tard en un échec que vous découvrez immédiatement.

  • Certificats expirés : l’appel de signature échoue lorsqu’on peut renouveler le certificat, au lieu de produire des documents qui échouent à la validation après distribution.
  • Valeurs de condensat par défaut : les nouvelles signatures utilisent SHA‑256 sans que personne n’ait à le demander, ainsi l’option faible nécessite une décision plutôt qu’une inattention.
  • Niveaux de journalisation : un service qui ne configure que les avertissements ne reçoit désormais que les avertissements, ce qui les rend lisibles, ce qui signifie qu’ils sont effectivement lus.

Ce dernier point est moins cosmétique qu’il n’y paraît. Toute la valeur de l’avertissement de certificat expiré réside dans le fait que quelqu’un le voit, et un avertissement enfoui parmi dix messages de trace par exécution de signature est un avertissement que personne ne voit.

Prérequis

Avant de commencer, assurez‑vous d’avoir :

  • 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 si vous voulez supprimer les limites d’évaluation.
  • Un PDF à signer, et le paquet cryptography si vous voulez créer des certificats de test jetables comme le fait l’exemple.

Installation

pip install groupdocs-signature-net cryptography

Contrôle 1 - Le condensat écrit dans la signature

hash_algorithm sur DigitalSignOptions choisit le condensat. La valeur par défaut depuis la 26.9 est SHA‑256, dans le format adbe.pkcs7.detached que les validateurs actuels attendent ; avant cela, les nouvelles signatures étaient SHA‑1.

with signature.Signature(source_path) as sign:
    options = DigitalSignOptions()
    options.certificate_stream = io.BytesIO(pfx)
    options.password = PASSWORD
    options.hash_algorithm = HashAlgorithm.SHA512
    options.reason = "Approved"

    result = sign.sign(output_path, options)
    return len(result.succeeded)

Deux détails méritent d’être soulignés. Le certificat arrive via certificate_stream sous forme d’io.BytesIO plutôt que d’un chemin de fichier, ce qui permet à un PKCS#12 construit en mémoire d’atteindre la bibliothèque sans jamais être écrit sur le disque — l’exemple s’appuie sur cela afin de ne jamais fournir de clé privée. Et HashAlgorithm propose AUTO, SHA1, SHA256, SHA384 et SHA512, où un horodatage, si vous en ajoutez un, utilise le même condensat que la signature.

En pratique, c’est le contrôle que vous manipulez le moins. La valeur par défaut est déjà la bonne réponse, SHA384 et SHA512 existent pour les politiques de signature qui les nomment, et SHA1 est un réglage de compatibilité pour les validateurs que vous ne pouvez pas modifier.

Contrôle 2 - Un certificat périmé vous bloque‑t‑il ?

Sans aucune surcharge, signer avec un certificat dont la période de validité est terminée — ou qui n’a pas encore commencé — lève GroupDocsSignatureException et n’écrit rien du tout.

try:
    sign.sign(output_path, options)
    return True
except signature.GroupDocsSignatureException as error:
    print(f"Rejected: {str(error).splitlines()[0]}")
    return False

Le message indique le certificat, la date d’expiration, son empreinte et la propriété qui le permettrait, ce qui suffit à une application pour indiquer à l’opérateur ce qu’il faut renouveler. Ne retenir que la première ligne est important en Python : le texte de l’exception continue avec la trace .NET provenant de la liaison, et ce n’est pas quelque chose à présenter à l’utilisateur.

Lorsque vous devez réellement signer malgré tout — un test avec un certificat archivé, ou un lot qui doit s’exécuter ce soir alors que le renouvellement est en cours — la surcharge s’applique à l’appel :

settings = signature.SignatureSettings(ConsoleLogger())
settings.log_level = LogLevel.WARNING | LogLevel.ERROR

with signature.Signature(source_path, settings=settings) as sign:
    options.allow_expired = True
    result = sign.sign(output_path, options)

allow_not_yet_valid a la même forme pour un certificat délivré à une date ultérieure, et les deux indicateurs sont indépendants : autoriser un certificat expiré n’autorise pas un certificat prématuré. Un certificat prématuré signifie généralement que l’horloge de la machine est incorrecte plutôt que que le certificat soit anormal, et une horloge erronée rend chaque signature produite par cette machine douteuse, il faut donc vérifier cela avant de surcharger quoi que ce soit.

Les deux surcharges émettent un avertissement plutôt que de passer silencieusement, ce qui constitue le lien avec le troisième contrôle.

Contrôle 3 - Quelqu’un découvre‑t‑il le problème ?

SignatureSettings.log_level est une valeur de drapeaux. L’exemple signe le même document trois fois, sous LogLevel.NONE, LogLevel.WARNING | LogLevel.ERROR et LogLevel.ALL, en comptant ce qui arrive :

logger = CollectingLogger()
settings = signature.SignatureSettings(logger)
settings.log_level = level

with signature.Signature(source_path, settings=settings) as sign:
    options.allow_expired = True
    sign.sign(output_path, options)

Les comptes donnent d’abord rien du tout, puis un avertissement, puis cet avertissement plus dix traces. Avant la 26.9, les trois lignes auraient été identiques, parce que le niveau était accepté puis ignoré — ce qui vaut la peine de le savoir si vous avez déjà défini un niveau, n’avez vu aucun changement et avez conclu que vous aviez mal lu votre propre code.

Deux détails de la liaison m’ont coûté une après‑midi, il faut donc les exposer clairement. SignatureSettings.logger est en lecture seule, donc le logger doit être passé au constructeur et toute assignation lève AttributeError ; log_level se règle normalement après. De plus, un logger personnalisé ne doit pas sous‑classer groupdocs.signature.logging.ILogger — cette classe de base encapsule un objet natif dont le constructeur nécessite un handle détenu par la bibliothèque, donc la sous‑classe lève TypeError. La liaison accepte tout objet simple qui fournit les trois méthodes :

class StdlibLogger:
    def error(self, message, exception=None):
        logging.getLogger("groupdocs").error(message, exc_info=exception)

    def warning(self, message, exception=None):
        logging.getLogger("groupdocs").warning(message)

    def trace(self, message):
        logging.getLogger("groupdocs").debug(message)

Donnez à error et warning un paramètre optionnel exception. La bibliothèque ne le transmet pas toujours, et un logger qui l’exige échoue sur les messages qui ne le contiennent pas.

Comparaison des trois : Quand utiliser chacun

Contrôle Idéal pour Principaux avantages Limitations
hash_algorithm répondre à une politique qui nomme un condensat une seule affectation ; même taille de sortie inutile si le certificat lui‑même n’est pas fiable
vérification de validité et surcharges toute signature pour d’autres personnes l’échec apparaît là où il peut être corrigé une surcharge produit un fichier, mais pas un fichier fiable
log_level services dont les journaux sont déjà chargés onze messages deviennent un seul ne filtre que la journalisation, jamais les exceptions

Ce ne sont pas des alternatives — un appel de signature unique utilise les trois. L’ordre de réflexion doit suivre l’ordre des conséquences : la vérification de validité décide si un fichier existe, le condensat décide ce qu’il contient, et le niveau de journalisation décide qui en prend connaissance.

Le niveau de journalisation change‑t‑il les exceptions que je reçois ?

Non. Il détermine quels messages atteignent votre logger et rien d’autre. Un certificat expiré lève toujours GroupDocsSignatureException sous LogLevel.NONE, et allow_expired signe toujours sous LogLevel.ALL ; les valeurs de retour et les exceptions sont identiques quel que soit le niveau. Ce qui change, c’est si l’avertissement qui explique une signature douteuse est jamais lu par une personne.

Vérification déplacée dans la même direction

À mentionner car c’est l’autre moitié de la même version. verify avec un DigitalVerifyOptions vide vérifie maintenant chaque signature numérique PDF cryptographiquement, de sorte qu’un document modifié après signature redevient invalide plutôt que simplement inexpliqué :

with signature.Signature(signed_path) as sign:
    result = sign.verify(DigitalVerifyOptions())
    return result.is_valid

Deux lignes, et cela vaut la peine de l’ajouter à tout pipeline qui signe puis stocke. Notez ce que True ne promet pas : il indique que la signature correspond au document, pas que l’émetteur est de confiance. Les certificats auto‑signés de l’exemple sont vérifiés ici et restent refusés par un lecteur PDF, ce qui répond séparément à la question de la confiance.

Bonnes pratiques et astuces

  • Conservez le refus comme valeur par défaut dans tout ce qui signe au nom des utilisateurs, et surchargez par appel plutôt que globalement. L’exception est peu coûteuse ; un lot de signatures invalides ne l’est pas.
  • Journalisez le texte de l’avertissement, pas seulement un compteur. Il indique le certificat et la date, la seule partie sur laquelle un opérateur peut agir.
  • Vérifiez l’horloge avant d’autoriser un certificat pas encore valide. Le certificat est généralement correct et la machine est généralement erronée, ce qui affecte plus d’un appel de signature.
  • Évitez les traces en production. Environ dix par exécution de signature s’accumulent rapidement ; activez‑les lors du diagnostic et désactivez‑les ensuite.
  • Vérifiez après la signature dans tout pipeline, maintenant que la vérification est cryptographique, afin qu’une sortie corrompue soit détectée avant que le destinataire ne la trouve.

Conclusion

Trois contrôles, un appel de signature, et la même idée de conception derrière tous : le résultat risqué nécessite maintenant une décision, et le résultat sûr ne nécessite rien. Conservez la vérification de validité, traitez allow_expired comme une exception par appel que vous journalisez, laissez le condensat tel quel sauf si une politique indique le contraire, et choisissez un niveau de journalisation qui rend les avertissements lisibles.

Exécuter l’exemple sur l’un de vos propres PDF prend une minute et affiche exactement ce que chaque contrôle a modifié — six fichiers signés, un refus délibéré, et trois lignes de comptes de messages qui ne se ressemblent plus.

Ressources supplémentaires