Skip to content
Series 7 · The cryptography of signatures · 7.7

What exactly gets signed.

CMS, signed attributes and the byte range of a PDF: the anatomy of a signature inside a document.

October 20269 min read

A digital signature does not sign a document. It signs an exact sequence of bytes. Between those bytes and what a person sees on screen lie formats, structures and programs that interpret them, and that gap is where most practical attacks on document signatures over the past two decades took place. The cryptography held; what failed was the answer to a deceptively simple question: what, exactly, was signed?

CMS: the signature envelope

Almost all document signatures, in PDF and in many other formats, are packaged in a structure called CMS (Cryptographic Message Syntax), the successor to RSA Laboratories’ PKCS #7 standard and specified today in RFC 5652. Its signing variant is called SignedData, and its structure is worth looking at from the inside.

SignedDatadigest algorithms used, content (or a reference to it, if the signature is detached), certificates and revocation lists
SignerInfoone per signer: who signs (issuer and serial number of their certificate), digest and signature algorithms
Signed attributescontentType · messageDigest = H(content) · signingCertificateV2 = H(signer’s certificate) · signingTime (claimed, not proven)
Signaturecomputed over the DER encoding of the signed attributes, not over the content
Unsigned attributesfor example, the signature timestamp: an RFC 3161 token over the signature value, added afterward

The crux lies in the signed attributes. When they are present, and in practice they always are, the signature is not computed over the document’s hash but over a small set of DER encoded attributes, one of which, messageDigest, holds that hash. It is a two level digest: the document is bound to the attribute, and the attribute to the signature. This makes it possible to sign, alongside the content, data about the signature itself.

md = H(content)
A = DER( SET { contentType, messageDigest = md, signingCertificateV2, … } )
signature = Sign(sk, A)    the verifier recomputes md, compares it with the attribute and verifies the signature over A

The signature inside a PDF

A signed PDF carries the CMS structure inside the file itself. That raises a circular problem: the signature must cover the file, but the file contains the signature. The ISO 32000 standard solves it by leaving a gap. The signature dictionary reserves a fixed size slot, the /Contents entry, which is filled with the CMS structure in hexadecimal, and declares in /ByteRange which bytes of the file were hashed: everything except the gap.

/ByteRange [ 0 a b c ]
The first a bytes are hashed, followed by c bytes starting at offset b. Between a and b lies the gap holding the signature. A strict verifier checks that the gap contains only the /Contents value and that the range covers the entire file up to that revision.

The signature’s filter indicates the format of its contents. In European advanced signatures, the PAdES profile uses ETSI.CAdES.detached: a detached CMS signature, with the attributes required by the advanced signature standards, over the bytes in the range.

Incremental updates: the document that keeps changing

PDF allows a file to be modified without rewriting it: changes are appended at the end, as a new revision that redefines the altered objects. This is what lets several people sign the same document, one after another, without invalidating earlier signatures: each signature covers its own revision and all previous ones, and whatever comes afterward falls outside its range.

That same flexibility is the attack surface. If a later revision can change what is displayed, a document can show new content while the earlier signature still verifies, because the bytes it covers have not changed. The standards provide safeguards. A certification signature can declare, through DocMDP permissions, which changes are allowed afterward: none, filling in forms only, or annotations as well. And the validator must compare revisions and flag any disallowed change.

AttackIdea
Universal forgeryTamper with the signature dictionary (null values, nonsensical ranges) so that the viewer shows the signature as valid without verifying anything.
Incremental savingAppend a revision that replaces the visible content, and disguise it so the viewer raises no warning.
Signature wrappingRelocate the signed bytes within the file and redefine the range, so that what is verified and what is displayed are different things.
Shadow attacksPrepare the document before it is signed with hidden content, which an update allowed by the permissions later makes visible.

In 2019, a team from Ruhr University Bochum tested the first three techniques against 22 desktop viewers and 7 online validation services: 21 viewers and 5 services were vulnerable to at least one. In 2021 the same group presented shadow attacks, which break no rule of the format but abuse the changes it permits. Vendors fixed most of the cases, but the lesson endures: the security of a signature on a document depends as much on the validator as on the algorithm.

What gets signed is bytes. What gets read is an interpretation of those bytes. The distance between the two is the attacker’s playground.

XML and other formats

XML signatures, the foundation of the XAdES profile, face the same problem in a different guise. Because the same XML document can be written in many equivalent ways, the signature is computed after canonicalization, and it covers references to parts of the document selected through transforms. If the verifier checks the signature over one element and the application processes another element with the same name elsewhere, the result is a signature wrapping attack, a family that plagued single sign on systems for years. The defensive principle is the same: the application must use exactly what the verifier verified.

What to demand from a validator
  • That it state whether the signature covers the entire document or only an earlier revision.
  • That it show what changed after each signature and whether those changes were permitted.
  • That it verify the signer certificate attribute and reject substituted certificates.
  • That it distinguish the time claimed by the signer from the time proven by a timestamp.
  • That it let you view the exact version that was signed, not just the latest one.
Key takeaways

In CMS, the signature covers signed attributes that contain the hash of the content and that of the signer’s certificate; the claimed time is not proof of time. In a PDF, the signature covers a byte range with a gap for itself, and later revisions fall outside it. Most real world attacks exploited the distance between what was verified and what was displayed, and they are countered by strict validators that report exactly what was signed.

Sources

  1. IETF, RFC 5652, Cryptographic Message Syntax (CMS), 2009
  2. IETF, RFC 5035, Enhanced Security Services Update: Adding CertID Algorithm Agility, 2007
  3. ISO 32000 2, Portable Document Format 2.0
  4. ETSI EN 319 142, PDF Advanced Electronic Signatures (PAdES)
  5. Mladenov, Mainka, Meyer zu Selhausen, Grothe and Schwenk, “1 Trillion Dollar Refund: How To Spoof PDF Signatures”, ACM CCS 2019
  6. Mainka, Mladenov and Rohlmann, “Shadow Attacks: Hiding and Replacing Content in Signed PDFs”, NDSS 2021

← All notes

Trust Notes

Articles for those who decide how an organization signs: principles, practices and criteria that hold up as rules and technologies change.

A publication of Signing Flows, by Interfase.

Series