Aller au contenu
ecdsa.com
← Tous les articles

Signatures PDF expliquées : ByteRange, CMS, PAdES et LTV

Une signature PDF signe chaque octet du fichier, à l’exception d’un trou qui contient la signature elle-même. La plupart des verdicts déroutants viennent de ce qui se trouve en dehors de ce trou : révisions ultérieures, racines de confiance manquantes, horodatages absents. Chaque cas ci-dessous a été signé et vérifié avec pyHanko et OpenSSL.

En bref. Une signature PDF est une signature CMS (PKCS #7) stockée dans l’entrée /Contents d’un dictionnaire de signature. Elle couvre les octets énumérés dans /ByteRange, c’est-à-dire tout le fichier tel qu’il est au moment de la signature, à l’exception de la valeur de /Contents elle-même. Les validateurs répondent à trois questions distinctes. Intégrité : les octets signés correspondent-ils toujours ? Confiance : la chaîne de certificats du signataire aboutit-elle à une racine à laquelle le validateur fait confiance ? Modifications : qu’a-t-on ajouté après la signature, et est-ce autorisé ? « Valide mais non approuvé », le point d’interrogation jaune d’Acrobat, signifie presque toujours que les octets sont intacts, mais que l’AC du signataire ne figure pas dans la liste de confiance du lecteur PDF. PAdES (ETSI EN 319 142-1) définit des niveaux allant de B-B à B-LTA. Un horodatage RFC 3161 (B-T) prouve à quel moment la signature existait, et les données de validation intégrées, complétées par des horodatages de document (B-LT, B-LTA), la maintiennent vérifiable après l’expiration des certificats.

Ce que contient un PDF signé

Le test ci-dessous signe une facture d’une page avec pyHanko 0.38 (CLI 0.6.0), à l’aide d’une clé P-256 certifiée par une petite AC de test et d’un horodatage de la TSA publique de DigiCert :

pyhanko sign addsig (pyHanko 0.38.0, 20 octobre 2026)
$ pyhanko sign addsig --field Sig1 --use-pades --timestamp-url http://timestamp.digicert.com --reason Approved \
    pemder --key signer.key --cert signer.pem --chain root.pem --no-pass invoice.pdf invoice-signed.pdf

L’outil de signature ne réécrit pas le fichier. Il ajoute une mise à jour incrémentale : de nouveaux objets, une nouvelle section de références croisées et un nouveau trailer après le %%EOF d’origine. Le fichier d’origine de 627 octets est toujours là, octet pour octet, au début du fichier signé. La mise à jour ajoute un champ de signature et un dictionnaire de signature. Celui que voici provient d’une autre exécution de la même commande, si bien que ses valeurs diffèrent de celles du fichier examiné plus bas (la valeur hexadécimale de /Contents est raccourcie) :

Dictionnaire de signature écrit par pyHanko (autre exécution)
9 0 obj
<<
/Contents <30821CFC06092A864886F70D010702A0821CED3...(zeros)...0000000>
/ByteRange [0 1331 23601 1118]
/Type /Sig
/Filter /Adobe.PPKLite
/SubFilter /ETSI.CAdES.detached
/M (D\07220261020140517Z)
/Reason (Approved)
/Prop_Build <<
/App <<
/Name /pyHanko#20CLI
/REx (0\05638\0560)
>>
>>
>>
endobj

La valeur de /M est la date PDF D:20261020140517Z. \072 est un deux-points échappé. Un petit script qui lit /ByteRange et la taille de /Contents donne les valeurs du fichier utilisé dans la suite de cette page :

Structure de la signature dans invoice-signed.pdf
file size: 24869 bytes, revisions (%%EOF markers): 2
/ByteRange [0 1331 23751 1118]
   signed: bytes 0-1330 and 23751-24868; the 22420-byte gap is the /Contents value
   /Type /Sig  /SubFilter /ETSI.CAdES.detached
   /Contents: 11209 bytes reserved, 7472 bytes used by the CMS object
   range ends at end of file: True

Comment les éléments s’articulent :

  • /ByteRange [a b c d] énumère deux plages d’octets : b octets à partir de a, puis d octets à partir de c. L’intervalle qui les sépare correspond exactement à la chaîne hexadécimale de /Contents, délimiteurs compris. Tout le reste du fichier, tel qu’il est au moment de la signature, est haché. Comme la taille du fichier n’est connue qu’une fois la signature en place, les outils de signature écrivent un contenu provisoire, renseignent /ByteRange, hachent, signent, puis écrivent la signature dans l’espace réservé.
  • /Contents contient le SignedData CMS encodé en DER, écrit en hexadécimal et complété par des zéros. pyHanko a réservé 11 209 octets et en a utilisé 7472. Le reste est du remplissage, présent parce que l’espace doit être réservé avant que la taille de la signature soit connue.
  • /SubFilter indique le format. ETSI.CAdES.detached est le format PAdES. adbe.pkcs7.detached est l’ancien format défini par ISO 32000-1. Tous deux sont des signatures CMS détachées portant sur les octets du ByteRange.
  • /M est l’heure de signature déclarée par le signataire. C’est là que PAdES la place : le profil baseline indique que l’attribut CMS signing-time « ne doit pas être présent », quel que soit le niveau. Ce n’est qu’une déclaration, puisque quiconque détient la clé peut y écrire n’importe quelle heure.

L’objet CMS n’a rien de particulier, et OpenSSL le vérifie par rapport aux plages extraites :

openssl cms -verify sur /Contents (OpenSSL 3.5.7)
CMS Verification successful
      1 object: contentType (1.2.840.113549.1.9.3)
      1 object: id-smime-aa-signingCertificateV2 (1.2.840.113549.1.9.16.2.47)
      1 object: id-smime-aa-timeStampToken (1.2.840.113549.1.9.16.2.14)
      1 object: messageDigest (1.2.840.113549.1.9.4)

Les attributs signés sont le type de contenu, le messageDigest des octets du ByteRange et signingCertificateV2, une empreinte du certificat du signataire qui empêche de lui substituer un autre certificat portant la même clé. L’attribut non signé timeStampToken est l’horodatage RFC 3161 portant sur la valeur de la signature. Le vérificateur de signatures PDF affiche cette décomposition pour n’importe quel PDF, y compris les octets couverts par chaque signature. L’inspecteur de jetons d’horodatage décode le jeton intégré.

Les trois questions auxquelles répond un validateur

Le rapport de pyHanko les distingue nettement, ce qui en fait un bon outil pédagogique. Avec la racine de l’AC de test fournie comme racine de confiance :

pyhanko sign validate --pretty-print --trust root.pem invoice-signed.pdf (extrait)
Trust anchor: "Organization: Example GmbH, Common Name: Example Signing Root CA"
The signer's certificate is trusted.

Integrity
---------
The signature is cryptographically sound.

Signing time
------------
Signing time as reported by signer: 2026-10-20T14:04:36+00:00

Signature timestamp token: 2026-10-20T14:04:37+00:00
The token is guaranteed to be newer than the signature.
This timestamp is backed by a time stamping authority.
The timestamp token is cryptographically sound.
TSA certificate subject: "Common Name: DigiCert SHA256 RSA4096 Timestamp Responder 2026 1; Organization: DigiCert, Inc.; Country: US"
The TSA certificate is trusted.

Modifications
-------------
The signature covers the entire file.

Bottom line
-----------
The signature is judged VALID.

Valide mais non approuvé

Le même fichier, validé sans fournir la racine de test :

Même fichier, sans racine de confiance pour le signataire (extrait)
Trust anchor: "No path to trust anchor found."
The signer's certificate is untrusted.

Integrity
---------
The signature is cryptographically sound.
...
Bottom line
-----------
The signature is judged INVALID.

L’intégrité est préservée, la confiance fait défaut. C’est ce qu’Acrobat signale par « Signature validity is UNKNOWN », en précisant que l’identité du signataire est inconnue. C’est la plainte la plus fréquente au sujet des PDF signés, et le document est généralement en bon état. Les lecteurs PDF accordent leur confiance aux certificats de signature au moyen de leurs propres listes. Acrobat utilise l’Adobe Approved Trust List (AATL) et, en option, les listes de confiance de l’UE (EU Trusted Lists). Les certificats émis par l’AC d’une entreprise, ceux d’une carte d’identité nationale que le lecteur PDF ne connaît pas et les certificats auto-signés aboutissent à ce verdict. Les solutions se trouvent côté lecteur (faire confiance à l’AC émettrice ou activer les listes de confiance de l’UE) ou côté signataire (signer avec un certificat émis par une AC à laquelle les destinataires font confiance). Notez le verdict distinct du certificat de la TSA : la chaîne de la TSA de DigiCert aboutit à une racine du magasin de certificats du système, si bien que l’horodatage est approuvé même lorsque le signataire ne l’est pas.

Octets modifiés dans la plage signée

Remplacement de EUR 1,250.00 par EUR 9,250.00 directement dans le fichier :

Un montant modifié sur place (extrait)
Integrity
---------
The signature is cryptographically unsound.
...
Bottom line
-----------
The signature is judged INVALID.

Ce cas-là ne laisse aucune échappatoire. Chaque octet des plages est haché, si bien que toute modification sur place casse la signature.

Modifications ajoutées après la signature

Les attaques intéressantes ne touchent pas aux octets signés. Elles ajoutent une nouvelle révision. Le format PDF autorise les mises à jour incrémentales, qui sont légitimes pour remplir des formulaires, ajouter des annotations ou ajouter d’autres signatures. Le test ajoute une révision qui remplace le contenu de la page par le nouveau montant :

Contenu remplacé par une mise à jour incrémentale après la signature
file size: 26337 bytes, revisions (%%EOF markers): 3
/ByteRange [0 1331 23751 1118]
   range ends at end of file: False
...
The signature is cryptographically sound.
...
Modifications
-------------
The signature does not cover the entire file.
Some modifications may be illegitimate, and they appear to be incompatible with the current document modification policy.

Bottom line
-----------
The signature is judged INVALID.

La signature reste cryptographiquement correcte, car la révision signée n’a pas été touchée. Ce que le lecteur voit, c’est la nouvelle révision. Seule l’analyse des différences le détecte : elle compare ce qui a changé après la signature à ce qui est autorisé. Une visionneuse qui vérifie seulement « signature correcte, signataire approuvé » affiche 9250 EUR sous une signature valide. Les travaux de recherche menés sur les visionneuses PDF depuis 2019 ont mis au jour exactement ces lacunes dans de nombreuses applications (attaques par sauvegarde incrémentale et attaques « shadow »). L’ajout d’une seconde signature, en revanche, est une modification autorisée :

Seconde signature ajoutée après la première : verdict de Sig1
Modifications
-------------
The signature does not cover the entire file.
All modifications relate to signing and form filling operations, and they appear to be compatible with the current document modification policy.

Bottom line
-----------
The signature is judged VALID.

Une signature de certification (--certify dans pyHanko, « certify » dans Acrobat) enregistre dans ses autorisations DocMDP la politique de l’auteur pour les modifications ultérieures : aucune modification, remplissage de formulaires uniquement, ou remplissage de formulaires et annotations. Les validateurs la font respecter lors de l’analyse des différences.

Le temps : horodatages RFC 3161

Une signature est faite avec un certificat qui expire et peut être révoqué. Sans preuve indépendante du moment où la signature a été faite, un validateur ne peut contrôler le certificat qu’à la date d’aujourd’hui. Une fois le certificat expiré, ou révoqué pour quelque raison que ce soit, la signature paraît aussi suspecte qu’une signature faite hier avec une clé volée. L’entrée /M n’y peut rien, car c’est le signataire qui la choisit.

Un horodatage RFC 3161 émis par une autorité d’horodatage (TSA) de confiance règle ce problème. La TSA signe un jeton qui contient le condensat de la valeur de la signature et sa propre heure. Dans le test, la TSA de DigiCert a renvoyé un jeton daté de 14:04:37 UTC, une seconde après l’heure de signature déclarée. Cela prouve que la signature existait à ce moment-là, alors que le certificat était valide. C’est le niveau PAdES B-T. Les horodatages peuvent aussi être ajoutés sous la forme d’un horodatage de document distinct, un dictionnaire de signature avec /Type /DocTimeStamp et /SubFilter /ETSI.RFC3161 qui horodate le document entier dans son état actuel.

Validation à long terme : B-LT et B-LTA

Pour valider une signature dans dix ans, un horodatage ne suffit pas. Les CRL ou les répondeurs OCSP de l’AC peuvent avoir disparu, et les algorithmes peuvent s’être affaiblis. ETSI EN 319 142-1 (V1.2.1, janvier 2024) définit les niveaux suivants :

NiveauAjouteRôle
B-BLa signature CMS avec les attributs signés requisSignature de base
B-TUn horodatage de confianceProuve que la signature existait à un instant donné
B-LTTous les éléments de validation : certificats, CRL et réponses OCSP dans le DSS du documentValidation sans rien récupérer
B-LTADes horodatages de document portant sur le document, ces éléments compris, renouvelés au fil du tempsMaintient la fiabilité des éléments et des horodatages à mesure que les certificats expirent et que les algorithmes vieillissent

Les éléments de validation se trouvent dans le Document Security Store (/DSS dans le catalogue du document), ajouté par une mise à jour incrémentale après la signature. Signature de la même facture au niveau B-LTA, avec une CRL servie par l’AC de test :

pyhanko sign addsig --use-pades-lta --with-validation-info (extrait)
file size: 54674 bytes, revisions (%%EOF markers): 4
/ByteRange [0 1331 23745 1099]
   /Type /Sig  /SubFilter /ETSI.CAdES.detached
   range ends at end of file: False
/ByteRange [0 34471 52463 668]
   /Type /DocTimeStamp  /SubFilter /ETSI.RFC3161
   range ends at end of file: False
catalog /DSS: True
   /Certs present: True
   /CRLs present: True
   /OCSPs present: True
   /VRI present: True
...
Modifications
-------------
The signature does not cover the entire file.
All modifications relate to signature maintenance, and they appear to be compatible with the current document modification policy.

Bottom line
-----------
The signature is judged VALID.

Quatre révisions : l’original ; la signature ; une mise à jour qui ajoute le DSS avec les données de validation et se termine par l’horodatage de document, dont la plage d’octets s’arrête à l’octet 53 131 ; et une dernière mise à jour de 1543 octets, ajoutée après l’horodatage et que le validateur considère comme de la maintenance de signature. Lister le DSS d’un fichier signé de la même façon montre ce que signifie concrètement « tous les éléments de validation ». Il contient la chaîne du signataire et la CRL de l’AC de test, mais aussi la chaîne du répondeur d’horodatage de DigiCert avec deux réponses OCSP, car l’horodatage doit lui aussi rester vérifiable :

Document Security Store d’un fichier B-LTA (lu avec pyHanko et asn1crypto)
certs:
   Organization: Example GmbH, Common Name: Example Signing Root CA
   Organization: Example GmbH, Common Name: Alice Example
   Common Name: DigiCert Trusted Root G4, Organizational Unit: www.digicert.com, Organization: DigiCert Inc, Country: US
   Common Name: DigiCert Trusted G4 TimeStamping RSA4096 SHA256 2025 CA1; Organization: DigiCert, Inc.; Country: US
   Common Name: DigiCert SHA256 RSA4096 Timestamp Responder 2026 1; Organization: DigiCert, Inc.; Country: US
CRLs:
   Organization: Example GmbH, Common Name: Example Signing Root CA
OCSP responses:
   ef6f534ae9e4067c7acae29056f62fd449eccb4e
   ecd7e382d2715d644cdf2e673fe7ba98ae1c0f4f

Les répondeurs OCSP sont identifiés par l’empreinte de leur clé. L’horodatage de document doit à son tour être renouvelé avant que le certificat de sa TSA n’expire, et pyhanko sign ltaupdate en ajoute un nouveau. Acrobat qualifie de « LTV enabled » les signatures dont les éléments de validation sont intégrés.

Quel niveau produire

  • Horodatez toujours. B-B ne suffit que pour les documents lus et classés pendant la durée de vie du certificat, et même dans ce cas, un horodatage ne coûte qu’une requête HTTP adressée à une TSA. Visez au minimum B-T.
  • Utilisez B-LTA pour tout ce qui est archivé. Les contrats, les documents réglementés et les factures qui doivent rester vérifiables des années plus tard ont besoin de données de validation intégrées et d’horodatages de document. Planifiez les exécutions de ltaupdate bien avant l’expiration du certificat de TSA du dernier horodatage de document, et avant que ses algorithmes ne soient jugés faibles.
  • L’effet juridique est une autre question. Selon le règlement européen eIDAS (UE) n° 910/2014, une signature électronique qualifiée exige un certificat qualifié délivré par un prestataire de services de confiance inscrit sur les listes de confiance de l’UE, ainsi qu’un dispositif de création de signature qualifié. PAdES décrit le format du fichier, pas ce niveau juridique. Les options --eutl-territories et --require-qualified de pyHanko contrôlent le volet des listes de confiance.
  • Signez avec une AC à laquelle vos lecteurs font confiance. Pour des destinataires externes, cela signifie une AC inscrite sur l’AATL ou sur les listes de confiance de l’UE. Une AC interne convient lorsque chaque lecteur peut être configuré pour lui faire confiance.

Comment vérifier un PDF signé

  1. Examinez toutes les signatures et toutes les révisions, pas seulement le bandeau vert. Combien de révisions suivent la dernière signature, et que modifient-elles ?
  2. Distinguez les verdicts. Échec de l’intégrité → les octets signés ont changé. Échec de la confiance → une question de confiance côté lecteur. Modifications signalées → du contenu a été ajouté après la signature, et il faut voir lequel.
  3. Vérifiez les preuves temporelles. Y a-t-il un horodatage d’une TSA à laquelle vous faites confiance, ou seulement le /M du signataire ?
  4. Pour les archives, vérifiez la LTV. Les éléments de validation sont-ils intégrés, et le dernier horodatage de document se trouve-t-il encore dans la période de validité du certificat de sa TSA ?
  5. Utilisez un validateur qui rend compte de tout cela. Dans les scripts, pyhanko sign validate --pretty-print avec un --trust explicite, ou pyhanko sign adesverify, de style AdES. pyHanko 0.38 avertit que le repli sur le magasin de confiance du système d’exploitation « is deprecated and will be removed in a future release » (est obsolète et sera supprimé dans une version future) : passez donc vos racines de confiance explicitement. Dans le navigateur, le vérificateur de signatures PDF fait de même sans envoyer le document.

Erreurs fréquentes

  • Se fier à l’image de signature visible. Le tampon sur la page n’est qu’une décoration. La signature, c’est l’objet CMS, et une image ne prouve rien.
  • Confondre « signature valide » et « document inchangé ». Seul le verdict « covers the entire file » ou une analyse des différences sans anomalie a ce sens.
  • Ajouter le certificat du signataire lui-même comme approuvé pour faire disparaître un avertissement. Accordez votre confiance à l’AC émettrice, après l’avoir vérifiée, ou ne l’accordez pas du tout.
  • Pas d’horodatage. Sans horodatage d’une TSA, la valeur probante de la signature prend fin à l’expiration du certificat.
  • Réenregistrer un PDF signé dans un outil qui réécrit le fichier. De nombreux éditeurs effectuent un enregistrement complet plutôt qu’incrémental, ce qui casse toutes les signatures existantes.

Sources

  • ISO 32000-2 (PDF 2.0), §12.8, signatures numériques. ISO 32000-1 définit adbe.pkcs7.detached et le dictionnaire de signature.
  • ETSI EN 319 142-1 V1.2.1 (2024-01) : éléments constitutifs de PAdES et niveaux baseline, horodatages de document.
  • RFC 5652 (CMS), RFC 3161 (horodatages), RFC 5035 (signingCertificateV2).
  • Documentation et CLI de pyHanko 0.38.0.
  • PDF Insecurity : travaux de recherche sur les attaques par sauvegarde incrémentale, par encapsulation de signature (signature wrapping) et « shadow ».

Tous les fichiers de cette page ont été créés le 20 octobre 2026 dans node:24.22.0-trixie avec pyHanko 0.38.0 (pyhanko-cli 0.6.0, pyhanko-certvalidator 0.33.0) dans un virtualenv et OpenSSL 3.5.7, avec des horodatages fournis par http://timestamp.digicert.com.