Skip to content
KEDBYTE
How Identity Works
Chapter
32

The Digital Signature

Part III · The Certificate and the Signature|13,467 words|about 59 min read|Volume 3

32.0 What this chapter gives you#

  1. You will be able to state, in one sentence and without hedging, the only thing a verified digital signature proves, and name the three assumptions that every larger claim quietly rests on.
  2. You will be able to explain why the message is hashed before it is signed, and give two separate reasons that have nothing to do with speed.
  3. You will be able to read a real CMS SignedData structure field by field, find the message-digest attribute inside it, and check by hand that it matches the file.
  4. You will be able to explain why the signature does not cover the document at all in most real formats, and say what it covers instead.
  5. You will be able to choose correctly between a detached, an enveloping and an enveloped signature for a given job, and say what each costs you.
  6. You will be able to describe an XML signature wrapping attack precisely enough to test for one, and explain why canonicalisation makes the attack possible rather than preventing it.
  7. You will be able to open a signed PDF, find the ByteRange and Contents entries, and work out exactly which octets were signed and which were not.
  8. You will be able to write out the full verification code path in order, and name the five checks that real implementations routinely skip.
  9. You will be able to explain why non-repudiation is a legal outcome and not a cryptographic property, and use the word correctly in a design document.
  10. You will be able to reason about multiple signatures, countersignatures and signing order, and say what a signing order does and does not prove.

A digital signature is the smallest useful thing in this book and the most over-claimed. The mathematics is narrow and honest. It takes a private key, some bytes, and produces a value that anyone holding the matching public key can test. If the test passes, the holder of that private key put that value on those bytes. That is the whole statement. It is exact, it is checkable, and it is far less than what people say when they say “digitally signed”.

Everything else attached to the phrase is inference. That the key belongs to a named person comes from a certificate, a different mechanism with its own failure modes. That the person meant to agree with the document comes from the user interface, usually the weakest part of the system. That the person cannot later deny it is not inference at all but a legal conclusion reached by a court weighing evidence, and no key length affects it. That the bytes signed are the bytes the signer saw on screen is an assumption that has been broken repeatedly, in public, with published research and assigned vulnerability numbers.

This chapter separates the narrow true thing from the wide assumed things, and then goes through the machinery in detail. We will build a real signature on a laboratory key, take it apart byte by byte, verify it by hand without trusting any library, and then break it in three ways that all leave the mathematics intact. We will build a signed PDF from nothing, compute its byte ranges, and see precisely which parts of the file the signature misses.

Three boundaries, stated once. What a signature means in front of a judge, including the three legal tiers of electronic signature and which of them software usually implements, is chapter 33, “What a Signature Means in Law”. How a signature is kept verifiable after the signing certificate has expired, which is the business of timestamps and archive formats, is chapter 34, “Time, and Signatures That Must Outlive Their Keys”. And the public logs that record every certificate a public authority issues are chapter 31, “Certificate Transparency”.

The plain version#

A signature at the bottom of a page proves less than you think#

Picture a shipping agent’s office. Orders arrive on paper, three or four pages at a time, and at the bottom of the last page the customer writes her name by hand. The clerk files the pages. Months later there is an argument about what was ordered, the file is pulled out, and everybody looks at the handwriting.

The handwriting has two jobs. It is supposed to say who agreed, and it is supposed to say what they agreed to. It does the first job moderately well. It does the second job hardly at all.

Here is why. The mark is on page four. Pages one, two and three are held on with a staple. Anyone with a typewriter and a stapler can retype page two, change the quantity, put the old page four back, and the mark at the bottom is untouched and genuine. It was a real signature. It is still a real signature. It simply never said anything about the pages above it.

Businesses have patched this for centuries: initial every page, write the total in words as well as figures, rule a line through empty space so nothing can be added. Every one of those habits exists because a mark at the bottom of a page does not protect the page. What we are about to build fixes exactly this, completely, and then a new set of problems arrives to take its place.

A machine that turns any pile of paper into one short number#

Now imagine a machine in the corner of the office. You feed it anything at all: one page, four hundred pages, a photograph, a recording. It reads every mark and prints a single code, sixty-four characters long, made of the digits nought to nine and the letters a to f.

The machine has four properties, and they are the whole foundation. It is faithful: the same pages give the same code, always, on any machine of the same make anywhere in the world, with no randomness in it. It is jumpy: change one comma on page three, or even the invisible difference between one space and two, and the code that comes out is not slightly different but completely different, with no visible relation to the old one. It only runs one way: from pages you get a code in a fraction of a second, but from a code you cannot get the pages back, because the code is short and the pages are long and most of the information is simply gone. And, the important one, it is supposed to be beyond anybody’s reach to find two different piles of paper that produce the same code. Not merely difficult. Beyond the reach of every computer that exists, working together, for longer than anyone would wait.

Give this code a name. Call it the fingerprint of the document. The machine the world mostly uses is called SHA-256, and its fingerprints are those sixty-four characters. Nothing else about it matters here.

Take a real document. It is two lines of text, one hundred and eleven characters long including the two invisible marks that end the lines.

Invoice 2026-0417: KedByte Technologies Private Limited.
Amount payable: INR 84,500.00. Due 15 September 2026.

Its fingerprint, printed in full:

90b7b789 1426229c 5907deef ab8ad94a
a0f4aae7 331de317 4894880d 29d6467d

The spaces are only there so it fits on the page. Now change one character. Make the amount 84,500.99 instead of 84,500.00, and change nothing else.

e04a23a1 04610eed 7bb371a7 fe822c16
ae6a7a9b 714fd71d 6dcb9580 cb787155

Two characters different in the document, sixty-four different in the fingerprint. That jumpiness is what makes the fingerprint a usable stand-in for the document. If you know the fingerprint, and you are handed a document, you can tell in a fraction of a second whether it is the document you were told about.

A stamp only one person can make and everybody can check#

The fingerprint solves the “what was agreed” problem and does nothing about “who agreed”, because anybody can run the machine. Reshma can compute the fingerprint of the invoice, and so can a thief, and the two will be identical.

So we need a second device. Imagine a matched pair of metal blocks, cut together so that they fit each other and nothing else. One is the stamping block: it lives in a locked drawer, exactly one person has the key, and pressed onto a number it produces a mark. The other is the checking block: copies are given away freely, and held against a mark and a number it says yes or no.

What makes this work is that the two are not interchangeable. Having the checking block tells you nothing useful about the shape of the stamping block. You can check marks all day and never learn to make one. That is not a property of real metal, obviously; it is a mathematical one, and the pair together is called a key pair, which is chapter 21, “The Key Pair”. Here only the behaviour matters: one side makes marks, the other checks them, and the checking side gives nothing away.

Stamp the fingerprint, not the document#

Put the two devices together, and be careful about the order, because the order is the whole design. You do not stamp the document. You run the document through the fingerprint machine, and then you stamp the fingerprint.

There are two reasons, and the obvious one is the less important one. The obvious reason is size: the stamping block works on one short number, it cannot take four hundred pages, and a version that could would be slow in proportion to the pages. A fingerprint is always the same small size no matter what went in.

The less obvious reason, and the one that actually keeps you safe, is that stamping raw text turns out to have hidden regularities. The stamping operation applied directly to plain numbers has patterns in it that let a clever attacker assemble a valid-looking mark for a message nobody ever stamped, by combining marks from messages that were. The fingerprint machine destroys every one of those patterns, because its output looks like noise. Putting the fingerprint machine in front of the stamp is not a convenience. It is a wall.

So the finished procedure is four steps.

  1. Take the document exactly as it is, every byte of it.
  2. Run it through the fingerprint machine and get the code.
  3. Press the stamping block onto that code, producing the mark.
  4. Send the document, the mark, and a note saying which fingerprint machine you used.

Checking it is four steps that mirror them: take the document as it arrived, run the same fingerprint machine yourself, hold the checking block against the mark to recover the code that was stamped, and compare the two codes character for character.

What the check actually tells you, said carefully#

Suppose the check passes. Write down, in the plainest possible language, what you now know. You know that whoever had the stamping block in their hands pressed it onto the fingerprint of these exact bytes. That is the sentence.

Read it again and notice everything it does not say. It does not say who that person was; it says the block was used, and the block is an object. It does not say when: the mark carries no clock, and a date written on the document is just more document, worth exactly as much as the stamper’s honesty. It does not say the stamper read the document, agreed with it, or even knew what it said, because a stamping block in a machine that stamps whatever is put in front of it will stamp a confession as readily as an invoice. And it does not say the document is true, lawful or complete, because a correctly stamped lie is a correctly stamped lie.

Nearly every disappointment people have with digital signatures comes from quietly adding one of those four things to the sentence and not noticing.

The three assumptions, in plain words#

Between “this block stamped these bytes” and “Reshma agreed to pay 84,500” sit exactly three assumptions. Each is a real-world fact about people and objects, not about mathematics, and each can be false.

The first is that the block was Reshma’s. Somebody, at some point, has to have written down that this checking block belongs to a person named Reshma. That written-down statement is a certificate, which is chapter 26, “The Certificate”. If nobody wrote it down, or whoever did was careless or was tricked, the block belongs to whoever the paperwork says, and paperwork can be wrong.

The second is that nobody else could have used the block. It has to have stayed in the locked drawer, and the drawer key has to have stayed with Reshma, from the day the block was cut until now. A block that was copied, or borrowed for an afternoon, still makes perfectly valid marks.

The third is that the bytes that went into the fingerprint machine are the same as what Reshma saw and understood. She read a document on a screen; some software then chose some bytes and hashed them. Whether those two match is a question about that software, and there are whole classes of documents where they demonstrably do not.

Hold these three. The rest of the chapter is largely about how each of them fails, in real deployed systems, with names and dates.

Where the mark can sit: three arrangements#

You have a document and you have a mark. There are three sensible ways to keep them together, and every real signature format offers a version of all three.

The first is to keep them apart. The document stays exactly as it is, not one byte changed, and the mark travels in a separate small file alongside it. This is a detached signature. Choose it when the document must stay usable by software that knows nothing about signatures. The cost is that two things must be kept together forever, and people lose one of them.

The second is to put the document inside the signature. The signature file becomes a small box, the document is placed in it, and the box is sealed. This is an enveloping signature. One file, nothing to lose. The cost is that anything wanting to read the document must know how to open the box.

The third is to put the mark inside the document, in a place set aside for it. This is an enveloped signature. It is what a signed PDF does, and it is why a signed PDF is still a PDF that opens normally. The cost is a genuine puzzle: the mark is inside the thing being fingerprinted, so the fingerprint would change the moment you wrote the mark in. Every format that does this solves it the same way, by leaving a hole in the document and agreeing in advance that the hole itself is not fingerprinted. That hole is where a family of real attacks lives, and we will measure one later.

Two people signing the same paper#

Two managers must both approve the invoice. There are two genuinely different things they might mean, and the difference matters.

They might each be approving the invoice, separately. Two people, two stamps, both on the same fingerprint of the same document. Neither looked at the other’s mark. Remove either and the other is still perfectly valid. Their order is irrelevant, because neither depended on the other. This is a parallel signature, and it is the arrangement for “both of us agree”.

Or the second manager might be approving the first manager’s approval. She is not stamping the invoice; she is stamping the mark the first manager made. Now the order is baked in. The second mark could not have existed before the first, because it is a mark on the first. Remove the first and the second becomes meaningless. This is a countersignature, and it is the arrangement for “I confirm that she signed this”, for “this was witnessed”, and for “a trusted clock saw this mark at this time”.

A great deal of confused system design comes from wanting the second thing and building the first. Two parallel marks carry no order at all, even though one of them was physically made after the other. If your process requires that the clerk signed before the manager, parallel signatures will not record it and will not prove it.

Where the plain version stops being true#

The fingerprint machine has been beaten, in public, twice#

The fourth property of the fingerprint machine was that nobody can find two different documents with the same fingerprint. That property is not a theorem. It is a bet on the state of the art, and the state of the art moves.

The honest version: for the two fingerprint machines the industry used before the current one, the bet has already been lost, and both losses were turned into working forgeries.

The first machine was MD5, published in 1992. On 30 December 2008, at the 25th Chaos Communication Congress in Berlin, a team including Alexander Sotirov, Marc Stevens, Jacob Appelbaum, Arjen Lenstra, David Molnar, Dag Arne Osvik and Benne de Weger showed a certificate authority certificate they had forged by finding two documents with the same MD5 fingerprint, using a cluster of about two hundred PlayStation 3 consoles. In May 2012 the Flame malware was found in the wild using a previously unknown MD5 collision technique to forge a Microsoft code-signing certificate, which let it install itself through Windows Update. That was a weapon, not a paper.

The second machine was SHA-1, standardized in 1995. On 23 February 2017, researchers from CWI Amsterdam and Google, Marc Stevens, Pierre Karpman, Elie Bursztein, Ange Albertini and Yarik Markov, published two PDF files with different content and the same SHA-1 fingerprint. They called it SHAttered. It took roughly nine quintillion SHA-1 computations, about 6,500 CPU-years and 110 GPU-years. In January 2020, Gaetan Leurent and Thomas Peyrin published “SHA-1 is a Shambles”, a chosen-prefix collision, which is the stronger and far more dangerous kind, for a computation they said had cost about 75,000 US dollars of rented graphics processors.

SHA-256 has not been broken, as of August 2026. But the pattern is clear enough to design around, and there is a second consequence. If an attacker can find two documents with the same fingerprint, they do not need to steal anybody’s key. They get the victim to sign the harmless document, and the resulting mark is equally valid on the harmful one. The signer did everything right. That is why a format that lets the signer choose the fingerprint machine, and does not protect that choice, is a badly designed format.

You never sign the document#

The plain version said: fingerprint the document, stamp the fingerprint. Almost no real format does that.

The honest version: in CMS, in CAdES, in XAdES, in PAdES and in JAdES, the signature is computed over a small structured block of statements about the document, and the fingerprint of the document is only one field inside it. In JSON Web Signature the signature is computed over the header and the payload joined by a full stop, in their text-encoded form, not over the payload’s meaning.

This is a good design, not a flaw. If the mark covered nothing but the fingerprint, everything else about the signature would be unprotected: which fingerprint machine was used, which certificate the signer meant, what kind of content it was, when they claim to have signed it. An attacker who could change any of those without invalidating the mark has a lever.

The practical consequence is that “verify the signature” and “check the document” are two separate operations, and a program can do the first and forget the second. Whether the block’s message-digest field matches the file you are holding is a comparison the verifier must perform separately, and it is one of the most commonly skipped checks in the field.

The mark belongs to a key, and a key is not a person#

Here is the laboratory demonstration, run on 18 August 2026 with OpenSSL 3.0.13. Two keys were generated on the P-256 curve, and two self-signed certificates were made with byte-for-byte identical subject names: country IN, organization KedByte Technologies Private Limited, common name Shikhar Singh. The same invoice was signed with each.

signer   certificate serial 65F801AE...CA41DF15
impostor certificate serial 5AA75A57...107AB1A2

$ openssl cms -verify -in impostor.p7s -inform DER \
    -content invoice.txt -binary -noverify
CMS Verification successful

That is not a bug. The -noverify flag tells OpenSSL to check the mathematics and not the trust chain, and the mathematics is impeccable. A real key signed those real bytes, and the certificate attached to it says, in perfectly well-formed fields, that the signer is Shikhar Singh. Point the same command at a real trust anchor and it collapses.

$ openssl cms -verify -in impostor.p7s -inform DER \
    -content invoice.txt -binary -CAfile signer.crt
CMS Verification failure
certificate verify error: self-signed certificate

The honest version: a signature check is two independent tests wearing one name. Test one asks whether a key signed these bytes; it is pure arithmetic. Test two asks whether that key belongs to anyone you should listen to; it is a question about a certificate chain, a trust store, an expiry date and a revocation status. Software that reports “signature valid” after test one is answering a question nobody asked. Chapter 27, “The Chain”, works the second test through properly.

“Only one person has the block” is the assumption that fails most#

The historical record on private keys staying private is not encouraging, and the failures are usually not thefts.

On 13 May 2008 the Debian project published advisory DSA-1571-1. A maintainer had removed two lines from the OpenSSL random number generator because a memory-checking tool complained about them, and for about twenty months every key generated on affected Debian and Ubuntu systems came from a pool of roughly 32,768 possibilities. The keys were not stolen. They were guessable, and they had been in use for nearly two years.

On 29 December 2010, at the 27th Chaos Communication Congress, the group fail0verflow showed that Sony had used the same random value twice when signing PlayStation 3 code with ECDSA. Reusing that value in that algorithm allows the private key to be recovered by simple algebra, and the console’s master signing key fell out of the arithmetic.

Then there is the ordinary case, in which the key is not in a locked drawer at all. It is in a file on a build server, or in a cloud key service where a dozen engineers hold roles that can call the sign operation, or in a document-signing platform that releases a signature when someone clicks a button after a code arrives by text message. Each is a defensible engineering choice. None supports the sentence “only Reshma could have produced this mark”.

The honest version: what a valid signature proves about people is bounded by the weakest control on the key, and that control is almost never cryptographic. It is a password policy, an access review, a hardware module’s authentication rules, or a text message.

The date inside a signature is just more of the signer’s writing#

CMS has a signing-time attribute. Our laboratory signature contains one, and it reads 260818004459Z, meaning 18 August 2026 at 00:44:59 UTC. It is inside the signed block, so it cannot be altered afterwards by anybody except the signer. That last clause is the problem: it is protected against everyone except the one party with a motive to lie about it. The signer’s own computer wrote it from the signer’s own clock, and a signer who wants a document to appear to have been signed last March sets the clock to last March.

The honest version: signing-time is a claim by the signer, of the same evidential weight as the date written by hand at the top of a letter. Trusted time comes from a third party who signs a statement that they saw this particular mark at this particular moment, which is a timestamp under RFC 3161, and that is chapter 34’s material.

What you saw is not necessarily what you signed#

A person reads a document on a screen and clicks a button. Some software then selects a sequence of bytes and hashes them. The assumption that these two things correspond is called “what you see is what you sign”, and it is an assumption about software, not about cryptography.

It fails in ordinary, non-malicious ways. A document that renders differently depending on which fonts are installed shows different words to different readers from identical bytes. A spreadsheet with a formula reading the current date shows a different figure tomorrow. A PDF containing JavaScript, or a form field whose appearance stream disagrees with its value, can display one thing and contain another.

It also fails in engineered ways, and the research is thorough. In November 2019, at the ACM Conference on Computer and Communications Security, Vladislav Mladenov, Christian Mainka, Karsten Meyer zu Selhausen, Martin Grothe and Joerg Schwenk published “1 Trillion Dollar Refund: How To Spoof PDF Signatures”, finding 21 of 22 desktop PDF viewers and 5 of 7 online validation services vulnerable to at least one of three attack classes. The same group’s “Shadow Attacks”, at the Network and Distributed System Security Symposium in 2021, had a signer sign a document that already contained the attacker’s hidden alternative content, and affected 15 of 28 viewers. “Breaking the Specification: PDF Certification”, at the IEEE Symposium on Security and Privacy in May 2021, broke 24 of 26 applications.

The honest version: the sentence “she signed this document” is really “her key signed a byte range, and some software then rendered that byte range in a way she is presumed to have understood”. The second half is where almost all of the real-world failures live.

Non-repudiation is not something arithmetic can give you#

The word turns up in every product page. It means, roughly, that the signer cannot later deny having signed.

The honest version: no mathematical operation can prevent a human being from denying something. Denial is speech. What the arithmetic can do is make certain denials expensive to sustain, by producing evidence that a court can weigh. Whether the court finds it sufficient depends on the jurisdiction, the legal tier of the signature, how the key was held, what the signer was shown, and who bears the burden of proof. Experts still disagree about how far a legal presumption should run: the European approach attaches strong presumptions to the highest tier of signature, the United States approach deliberately attaches none to any particular technology, and both positions are defensible. Chapter 33, “What a Signature Means in Law”, sets them out. The technical version below returns to why the standards bodies themselves retreated from the word.

Two documents that mean the same thing are different bytes#

The plain version said “fingerprint the document exactly as it is”. For a photograph that is straightforward. For structured formats it is a trap.

Consider two XML fragments that any parser will treat as identical: the same elements and attributes with the same values, differing only in attribute order, in whether a namespace is declared on the parent or the child, in quoting style, or in a line ending. They mean the same thing. Their fingerprints are wildly different, because their bytes are different. So XML signatures introduced a normalization step called canonicalisation: before hashing, rewrite the fragment into one agreed byte form, so that equivalent documents hash equally.

The honest version: canonicalisation is not a security feature, it is a compatibility feature, and it weakens the signature by design. It creates a deliberate gap between “these bytes” and “what was signed”. The signature no longer covers a byte range in a file; it covers the output of a transformation applied to a part of a document that the signature itself selects by reference. If an attacker can change which part gets selected, the signature stays valid over something the verifier is no longer looking at. That is the entire family of XML signature wrapping attacks, first described by Michael McIntosh and Paula Austel of IBM Research in 2005, and treated in full later in this chapter. JSON Web Signature took the opposite decision: it canonicalises nothing, signs the exact text-encoded header and payload, and requires the verifier to work from those same characters. That is why JWS has never had a wrapping attack of the XML kind.

“The signature is valid” is at least six questions#

When a library returns true, ask which of these it checked: whether the mathematics holds for the signed block; whether the document fingerprint inside that block matches the document you hold; whether the certificate the block points to is the one whose key was used; whether that certificate chains to something you trust; whether it was valid, unexpired and unrevoked at the relevant moment; and whether the algorithms it names are ones you still accept. The honest version: many libraries check the first and, if asked nicely, the fourth. The rest are your job.

The technical version#

Hash then sign, and why the hash is inside the envelope#

The construction has a standard name, hash-then-sign, and a standard shape. Let M be the message octets, H a collision-resistant hash function and S the signing primitive keyed by the private key d.

  M (any length)
      |
      v
  +---------+
  |  H(M)   |   fixed width: 32 octets for SHA-256
  +---------+
      |
      v
  +--------------------------+
  | pad / encode to modulus  |  scheme-specific
  +--------------------------+
      |
      v
  +--------------------------+
  | S with private key d     |
  +--------------------------+
      |
      v
   signature value

The padding step is not decoration. For RSA it is the whole security argument. RSASSA-PKCS1-v1_5, specified in RFC 8017, which is PKCS #1 version 2.2 of November 2016, builds an encoded message EM of exactly the modulus length. For a 2048-bit key and SHA-256 the layout is fixed and easy to verify by hand:

EM = 00 01 <202 octets of FF> 00 <DigestInfo, 51 octets>

DigestInfo for SHA-256, the fixed 19-octet prefix:
  30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01
  05 00 04 20
followed by the 32-octet digest.

Those numbers were checked by signing our invoice with a fresh 2048-bit key and recovering the encoded message with the public exponent: 202 octets of FF, one 00 separator, then a 51-octet DigestInfo whose last 32 octets are the SHA-256 of the file. The identifier of the hash function is carried inside the signed encoding. That matters, and we will come back to why in a moment.

RSASSA-PSS, in the same RFC, does the same job with a randomized salt and a mask generation function, and has a security proof that RSASSA-PKCS1-v1_5 lacks. ECDSA, standardized in FIPS 186-5 of 3 February 2023, takes the leftmost bits of the hash as an integer and uses a per-signature random value whose reuse leaks the private key outright. EdDSA, RFC 8032 of January 2017, is the odd one out: pure Ed25519 hashes the whole message internally, twice, as part of the algorithm, so an application must not pre-hash it and hand it a digest. There is a separate pre-hash variant, Ed25519ph, for when you genuinely need to sign a digest.

Scheme Input to primitive Randomized
RSA PKCS#1 v1.5 Padded DigestInfo No
RSA PSS Padded, salted hash Yes
ECDSA Leftmost bits of hash Yes, per signature
Ed25519 (pure) Whole message No, deterministic

Now the question the chapter title implies: why is the hash inside the signature operation rather than outside it, and why is that a security decision rather than an efficiency one.

The first reason is domain size. RSA operates on integers smaller than the modulus, so there is no way to feed it four megabytes. Any scheme that signs long messages must reduce them first.

The second reason is that textbook RSA is multiplicatively homomorphic. If s1 is a raw signature on m1 and s2 on m2, then s1 times s2 modulo n is a valid raw signature on m1 times m2, so an attacker who obtains signatures on two chosen values gets a third for free. Hashing first destroys the relation, because the attacker would need messages whose hashes multiply correctly, and finding those is the preimage problem the hash is built to resist.

The third reason is existential forgery. Without a hash, an attacker picks any value s, computes s to the power e modulo n, and has a valid signature on whatever garbage that turns out to be. With a hash and a structured padding, the result would have to be a well-formed EM whose DigestInfo contains the hash of a real message, which is again a preimage attack.

The fourth reason is algorithm binding, and it is the one that has caused real breaches. Because the hash identifier sits inside the signed encoding, an attacker cannot swap SHA-256 for a broken function without breaking the signature. Implementations that parse the recovered EM loosely have been exploited: Daniel Bleichenbacher showed in 2006 that an implementation which did not check that the FF padding runs all the way to the DigestInfo can be fooled into accepting forged signatures for exponent 3 keys, and the same class of bug reappeared as CVE-2014-1568, published 25 September 2014, affecting Mozilla NSS 3.16 and 3.17 and shipping in Firefox before 32.0.3 and Chrome before 37.0.2062.124. The mathematics was fine both times. The parser was not.

The last reason is that because the primitive sees only a digest, a system can migrate hash functions without changing the signature algorithm, naming the hash in metadata so that a verifier knows which to run. That is what CMS does with its digestAlgorithm field, and it is why moving the world off SHA-1 was possible at all.

The exact statement, and the three assumptions written formally#

Let (d, Q) be a key pair, m the octets that were hashed, and sigma the signature value. A successful verification establishes one proposition:

There exists an event in which the holder of d applied the
signing operation to H(m), producing sigma.

Everything else requires added premises. There are three, and it is worth naming them because a design review is largely a matter of asking which of the three your system actually enforces.

Assumption Enforced by Fails when
Q belongs to a party Certificate and chain Misissuance, weak vetting
Only that party uses d Key storage, access rules Key copied or guessable
m is what they saw The signing application Ambiguous rendering

The first is the business of chapters 26 to 31: certificates, chains, authorities, revocation and the transparency logs that expose misissuance. The second is an operational property with no cryptographic component whatsoever. The third is the one nearly every deployment gets wrong, because it is invisible until somebody attacks it.

Note carefully what is absent from the proposition. There is no time in it. There is no intent in it. There is no statement that the holder of d read anything. A signing service that signs whatever arrives on a queue satisfies the proposition perfectly and supports no human claim at all.

CMS SignedData, taken apart with a real signature#

Cryptographic Message Syntax is the workhorse. It is RFC 5652 of September 2009, which is STD 70, and it descends directly from RSA Security’s PKCS #7 version 1.5, first published as an RSA Laboratories Technical Note in November 1993 and republished as RFC 2315 in March 1998. When somebody says “a PKCS#7 signature” in 2026 they almost always mean a CMS SignedData. It is the payload inside S/MIME mail, inside signed PDFs, inside Windows Authenticode, and inside most enterprise document signing.

Here is the structure, reduced to the fields that matter for signing.

ContentInfo
  contentType = id-signedData
  content = SignedData
    version
    digestAlgorithms  SET OF AlgorithmIdentifier
    encapContentInfo
      eContentType   what kind of thing was signed
      eContent       the content, OPTIONAL
    certificates      [0] IMPLICIT, optional bag
    crls              [1] IMPLICIT, optional bag
    signerInfos       SET OF SignerInfo
      SignerInfo
        version
        sid            issuer + serial, or subjectKeyIdentifier
        digestAlgorithm
        signedAttrs    [0] IMPLICIT, optional
        signatureAlgorithm
        signature      the actual bytes
        unsignedAttrs  [1] IMPLICIT, optional

The single most important fact about this structure is stated in RFC 5652 section 5.4. If signedAttrs is absent, the signature is over the content itself. If signedAttrs is present, the digest that gets signed is “the message digest of the complete DER encoding of the SignedAttrs value”, and the content is not signed directly at all. Instead, section 5.3 requires that signedAttrs, when present, contains at minimum a content-type attribute and a message-digest attribute “having as its value the message digest of the content”. The document is bound in through that one field.

There is a detail in the encoding that trips up everyone who implements this from scratch. In the SignerInfo the attributes are tagged [0] IMPLICIT, which on the wire is the octet A0. But the bytes that are hashed and signed must have the SET OF tag, 31. So the signer takes the encoded signedAttrs, replaces the leading A0 with 31, leaves the length octets alone, and signs that. RFC 5652 section 5.4 says so explicitly, and a great many hours have been lost to it.

Now the laboratory. On 18 August 2026, with OpenSSL 3.0.13, a P-256 key and a self-signed certificate were generated and the invoice from earlier was signed in detached form.

SUBJ="/C=IN/O=KedByte Technologies Private Limited"
SUBJ="$SUBJ/CN=Shikhar Singh"
openssl ecparam -name prime256v1 -genkey -noout \
  -out signer.key
openssl req -new -x509 -key signer.key -sha256 -days 365 \
  -subj "$SUBJ" -out signer.crt
openssl cms -sign -in invoice.txt -signer signer.crt \
  -inkey signer.key -binary -outform DER -md sha256 \
  -out detached.p7s

The result is 1,021 octets. Parsing it and stripping the certificate, here is the SignerInfo, with offsets from the start of the file.

 571 SET OF SignerInfo
 575  SEQUENCE (SignerInfo, 442 octets)
 579   INTEGER 1                     version
 582   SEQUENCE                      sid
 584     issuer  C=IN, O=KedByte..., CN=Shikhar Singh
 670     serial  65F801AE...CA41DF15
 692   SEQUENCE OBJECT sha256        digestAlgorithm
 705   [0] signedAttrs, 228 octets
 708     SEQUENCE
 710       OBJECT contentType
 723         OBJECT pkcs7-data
 734     SEQUENCE
 736       OBJECT signingTime
 749         UTCTIME 260818004459Z
 764     SEQUENCE
 766       OBJECT messageDigest
 779         OCTET STRING, 32 octets:
             90b7b789 1426229c 5907deef ab8ad94a
             a0f4aae7 331de317 4894880d 29d6467d
 813     SEQUENCE
 815       OBJECT smimeCapabilities
 936   SEQUENCE OBJECT ecdsa-with-SHA256
 948   OCTET STRING signature, 71 octets

The message-digest attribute at offset 779 is character for character the SHA-256 of invoice.txt computed at the start of this chapter. That is the only place the document appears in the signature at all. Everything else in this structure is metadata.

Now verify it without trusting any library, to make the construction concrete. The signedAttrs header at offset 705 is A0 81 E4: implicit tag zero, long form length, 228 octets of content. Replace A0 with 31 and hash the 231 octets that result.

der  = open('detached.p7s','rb').read()
hdr  = der[705:708]                 # A0 81 E4
body = der[708:708+228]
signed_bytes = b'\x31' + hdr[1:] + body
 # 3181E4 3018 06 ...  231 octets total

That produces a re-tagged block starting 31 81 E4 30 18 06, 231 octets long, whose SHA-256 is

ccd9680a 7b755eb0 7f24539c 91641659
483a3409 85613268 a3baac06 aa1b6840

Handing those 231 octets, the ECDSA value from offset 948 and the public key from the certificate to a bare verification routine returns success, with no CMS library involved. The construction is exactly as described: the hash of a re-tagged attribute block, one attribute of which is the hash of the file. Tamper with the file and it collapses in the right place.

$ sed 's/84,500.00/84,500.99/' invoice.txt > tampered.txt
$ openssl cms -verify -in detached.p7s -inform DER \
    -content tampered.txt -binary -CAfile signer.crt
CMS Verification failure
CMS_SignerInfo_verify_content: verification failure

The signature over signedAttrs is still perfectly valid. What failed is the comparison between the message-digest attribute and the recomputed hash of the content, and OpenSSL is careful enough to do it. Not every implementation is.

Two more attributes deserve mention because the ETSI profiles require them. The signing-certificate-v2 attribute, from RFC 5035 of August 2007, puts a hash of the signer’s certificate inside the signed block, closing a substitution attack in which an attacker confuses the verifier about which certificate to use; RFC 2634 of June 1999 defined the original SHA-1-only version. The cms-algorithm-protection attribute, RFC 6211 of April 2011, puts copies of the digest and signature algorithm identifiers inside the signed block, so an attacker cannot rewrite the unprotected copies to point at a weaker algorithm. Both are cheap. Both are omitted constantly.

Detached, enveloping and enveloped, in each format#

Three arrangements, and every format expresses them differently.

Arrangement Where content lives Typical use
Detached Separate file or stream Big files, archives
Enveloping Inside the signature Small payloads, transport
Enveloped Signature inside content PDF, signed XML docs

In CMS the switch is the eContent field of EncapsulatedContentInfo. Present means enveloping: the content sits inside the SignedData and the whole thing is one file, conventionally with the extension .p7m. Absent means detached: the SignedData carries only the metadata and the digest, conventionally .p7s, and the verifier must be handed the content separately. Our two laboratory signatures differ by exactly this: 1,021 octets detached, 1,135 octets with the 111-octet invoice enveloped inside, plus the encoding overhead.

 # enveloping: the content is carried inside
openssl cms -sign -in invoice.txt -signer signer.crt \
  -inkey signer.key -binary -nodetach -outform DER \
  -out attached.p7m

In XML Signature the three names come from where the ds:Signature element sits relative to what it covers. Enveloped means it is a descendant of the element being signed, which requires the enveloped-signature transform to remove the signature element before digesting, or the digest would be self-referential. Enveloping means the signed data sits inside a ds:Object element within the signature. Detached means the ds:Reference URI points somewhere else entirely, possibly to an external file.

In JWS the compact serialization is enveloping by default: header, payload and signature separated by full stops. Detached content is described in RFC 7515 appendix F, and the mechanism is blunt: transmit the compact form with the payload segment left empty and reinsert it at verification time. With our laboratory key that produces this, wrapped here to fit the page.

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJr
Yi0yMDI2LTA4In0..lKqwGKsKXykTlF90t9128SdxlMMh5Q9
QdMQEdnVrEaN1v6hs98mjvXc_0f4ybLtGXWEh6mgnbI0v56G
CcSWAyg

The two consecutive full stops are the empty payload. RFC 7797, of February 2016, adds a related option: the b64 header parameter, which when false means the payload is signed without base64url encoding, useful for very large or binary detached payloads. That RFC also forbids the option in JSON Web Tokens, which are chapter 39’s subject.

In PDF the signature is always enveloped and always detached at the same time, which sounds contradictory until you look at the mechanism. The CMS blob sits physically inside the file, so the file is self-contained, but the CMS blob is a detached signature whose content is a pair of byte ranges of the file around the blob’s own position. That construction gets its own subsection below.

The format family: what each one actually wraps#

The confusion in this area is almost entirely historical. Each format is a layer on top of the one before it, and the layers were added for reasons that were valid at the time.

Format Wraps First published
PKCS #7 v1.5 ASN.1 SignedData 1993, RFC 2315 in 1998
CMS PKCS #7, cleaned up RFC 5652, Sept 2009
CAdES CMS plus attributes ETSI, EN in 2016
XAdES XML Signature plus ETSI, EN in 2016
PAdES PDF plus CAdES ETSI, EN in 2016
JAdES JWS plus attributes ETSI TS, 2021

CMS is the IETF’s cleaned-up successor to PKCS #7, and section 5.2.1 of RFC 5652 documents the compatibility rules for anybody who has to read old files.

The AdES formats, standing for Advanced Electronic Signatures, come from ETSI and exist because the European legal framework needed a signature that could still be validated years later. They do not replace the base format. They add signed and unsigned attributes to it, and they define “baseline” profiles that pin down exactly which attributes are mandatory so that two implementations can interoperate. As of August 2026 the current published versions are ETSI EN 319 122-1 V1.3.1, dated June 2023, for CAdES; ETSI EN 319 132-1 V1.3.1, dated July 2024, for XAdES; ETSI EN 319 142-1 V1.2.1, dated January 2024, for PAdES; and ETSI TS 119 182-1 V1.2.1, dated July 2024, for JAdES. A draft V1.3.0 of the PAdES part was in the ETSI repository in August 2026 but had not been published as a final version.

Every one of them defines the same four baseline levels, and knowing the letters saves a great deal of confusion in procurement documents.

Level Adds
B-B The required signed attributes
B-T A trusted time token
B-LT Certificates and revocation
B-LTA Archive timestamps as well

The CAdES baseline at B-B, per clause 6.2.2 of EN 319 122-1 V1.3.1, requires content-type, message-digest, signing-time, either signing-certificate or signing-certificate-v2, and cms-algorithm-protection. The last two of those are the ones that a plain OpenSSL signature omits, which is why an OpenSSL CMS signature is a valid CMS signature and not a valid CAdES baseline signature. The levels above B-B are the material of chapter 34.

JSON Web Signature, RFC 7515 of May 2015, sits slightly apart. It was designed for tokens on the web rather than documents in an archive, and it shows. The signing input is defined in section 2 as

ASCII( BASE64URL(UTF8(JWS Protected Header)) || '.'
       || BASE64URL(JWS Payload) )

and the compact serialization in section 7.1 appends the base64url signature after a second full stop. Signing our invoice with the same P-256 key under alg ES256 gives a protected header of

{"alg":"ES256","typ":"JOSE","kid":"kb-2026-08"}

a signing input of 212 octets whose SHA-256 is 2e5c6a8b..., and a 64-octet raw signature, for a compact form of 299 characters in total. Note that the ECDSA value in JWS is the raw pair of coordinates, 32 octets each for P-256, not the DER SEQUENCE that CMS uses. Feeding one to a parser expecting the other is a common integration bug.

Canonicalisation, and why XML signature wrapping exists#

XML has a property that is a gift to document authors and a disaster for signatures: many different byte sequences are the same document. Attribute order is not significant, namespace declarations may sit on any ancestor, quotes may be single or double, empty elements may be written closed or as a pair, and whitespace between attributes is free. Two files that every parser agrees are identical will have different hashes.

So XML Signature defined a normalization step. Canonical XML 1.0 became a W3C Recommendation on 15 March 2001, and Canonical XML 1.1, which fixed the handling of inherited xml: attributes, followed on 2 May 2008. Exclusive XML Canonicalization 1.0, a Recommendation of 18 July 2002, went further: it deliberately strips the ancestor namespace context so that a signed fragment can be lifted out of one document, dropped into another, and still verify. A Canonical XML 2.0 effort was published as a Working Group Note on 11 April 2013 with the statement that the working group had “agreed not to progress this Canonical XML 2.0 specification further as a Recommendation track document”. It is not a standard and you will not meet it in production.

XML Signature Syntax and Processing Version 1.1 became a W3C Recommendation on the same day, 11 April 2013; the original version 1.0 dates from 12 February 2002. Its processing model is where the trouble starts. A signature does not cover a file. It contains a SignedInfo element holding one or more Reference elements. Each Reference names a URI, applies a list of transforms, digests the result, and stores the digest. The signature value covers the canonicalised SignedInfo, and therefore covers the digests, and therefore covers whatever the references happened to resolve to at signing time.

The word “happened” is doing a great deal of work. A reference of the form URI="#body" means: search the document for an element whose ID attribute is body. Not “the element at this position”. Not “the child of the envelope”. Search the whole document.

Here is a SOAP message with a signed body, cut down to essentials.

<soap:Envelope>
 <soap:Header>
  <wsse:Security>
   <ds:Signature>
    <ds:SignedInfo>
     <ds:Reference URI="#body">
      <ds:DigestValue>k3fB2...=</ds:DigestValue>
     </ds:Reference>
    </ds:SignedInfo>
    <ds:SignatureValue>MEQCIBQ...=</ds:SignatureValue>
   </ds:Signature>
  </wsse:Security>
 </soap:Header>
 <soap:Body wsu:Id="body">
  <pay to="Reshma" amount="100"/>
 </soap:Body>
</soap:Envelope>

Now the attack, in the form Michael McIntosh and Paula Austel of IBM Research described in “XML signature element wrapping attacks and countermeasures”, pages 20 to 27 of the proceedings of the 2005 ACM Workshop on Secure Web Services. The attacker does not touch the signature at all. He moves the signed element somewhere harmless and puts his own in its place.

<soap:Envelope>
 <soap:Header>
  <wsse:Security>
   <ds:Signature> ...unchanged... </ds:Signature>
  </wsse:Security>
  <Wrapper>
   <soap:Body wsu:Id="body">
    <pay to="Reshma" amount="100"/>
   </soap:Body>
  </Wrapper>
 </soap:Header>
 <soap:Body>
  <pay to="Attacker" amount="500000"/>
 </soap:Body>
</soap:Envelope>

The verification module looks up ID body, finds it inside Wrapper, digests it, gets the same digest as before, checks the signature over SignedInfo, and reports valid. The business logic then asks for the SOAP body, gets the second one, and pays the attacker. Every cryptographic operation succeeded. Nothing was forged.

Three properties combine to make this possible, and it is worth naming them separately because the defences attack different ones.

The first is indirection. The signature references content by identifier rather than by position, so what is verified and what is consumed are resolved by two different pieces of code using two different rules.

The second is that canonicalisation, particularly the exclusive variety, deliberately makes a subtree’s digest independent of where it sits. That feature exists so that signed fragments survive being embedded in different envelopes. It is also precisely what lets the attacker relocate the signed element without changing its digest.

The third is that most XML toolkits verify a document and then re-parse it, or hand the original DOM tree to code that navigates by element name. Any gap between “the node that was verified” and “the node that is used” is the whole vulnerability.

This is not theoretical. In 2011, at the third ACM Cloud Computing Security Workshop, Juraj Somorovsky, Mario Heiderich, Meiko Jensen, Joerg Schwenk, Nils Gruschka and Luigi Lo Iacono published "All Your Clouds are Belong to us

  • Security Analysis of Cloud Management Interfaces", which used signature wrapping against the SOAP control interface of a major public cloud. In 2012, at the 21st USENIX Security Symposium, Somorovsky, Andreas Mayer, Schwenk, Marco Kampmann and Jensen published “On Breaking SAML: Be Whoever You Want to Be”, reporting that of 14 major SAML frameworks analysed, 11 had critical signature wrapping vulnerabilities, including Salesforce, Shibboleth and IBM XS40. SAML itself is chapter 42, “SAML and the Enterprise”.

The defences, in rough order of effectiveness:

  1. Verify and consume the same node. Extract the subtree that the signature actually covered, and pass that object to the application. Never re-query the document by name afterwards.
  2. Constrain the reference. Require that each Reference resolves to an element at a fixed, schema-defined position, checked by an absolute path rather than by an ID search.
  3. Reject duplicate and ambiguous identifiers before verification, and reject any element with the target ID that appears outside the permitted place.
  4. Restrict transforms. Allow only enveloped-signature and a fixed canonicalisation method, and reject XSLT and XPath transforms outright, since an attacker-chosen transform can rewrite what gets digested.
  5. Validate against a strict schema that forbids arbitrary extension elements, so a Wrapper element cannot legally exist.

JSON Web Signature avoids all of this by refusing the premise. It references nothing by identifier and canonicalises nothing. It signs the literal characters of the base64url-encoded header and payload, and the verifier must operate on those same characters, so there is no gap between what was verified and what is used. If you do need canonical JSON for another purpose, RFC 8785 of June 2020 defines the JSON Canonicalization Scheme. JWS has its own family of failures, but they are algorithm-confusion failures rather than wrapping failures, and they are the topic of chapter 39, “JSON Web Tokens”.

A signed PDF, byte by byte#

A signed PDF is the format most people actually meet, and its construction is unusual enough to deserve a full worked example. The rules live in clause 12.8 of ISO 32000-2:2020, the PDF 2.0 specification, second edition, published in December 2020 and confirmed as current in 2026. It was the only core PDF specification published for purchase only, which held back adoption between 2017 and 2023; the PDF Association has made it freely available since 5 April 2023, with an errata collection updated on 18 June 2026. A signature lives in a signature dictionary, and the two entries that matter are ByteRange and Contents.

5 0 obj
<< /Type /Sig /Filter /Adobe.PPKLite
   /SubFilter /ETSI.CAdES.detached
   /ByteRange [0 609 3611 61]
   /Contents <308203 ... 000000> >>
endobj

Contents holds the CMS SignedData as a hexadecimal string. ByteRange is a list of offset and length pairs saying which parts of the file were signed. The pairs always skip exactly the region occupied by the Contents string, including its angle brackets, because the signature cannot cover itself.

Here is a real file, constructed on 18 August 2026 in order to have every number checkable. A minimal PDF 2.0 file of 468 octets was written, with a catalogue, a page tree, one page and one content stream. A signature was then added as an incremental update: the signature dictionary appended to the end, with 3,000 hexadecimal digits reserved inside Contents, followed by a new trailer pointing back at the original cross-reference table. The finished file is 3,672 octets.

total file                       3672 octets
range 1: offset 0, length        609  octets
gap    : the /Contents string    3002 octets
range 2: offset 3611, length      61 octets
signed by the CMS blob            670 octets
not signed                       3002 octets

The gap of 3,002 is the 3,000 reserved hexadecimal digits plus the two angle brackets. The 670 signed octets are the two ranges concatenated, and their SHA-256 is

4391f13d 968dd55e 8660c966 6a073450
89c296df c53033e7 f2946661 eeb4e50b

Those 670 octets were then signed with the same P-256 key, producing an 897-octet CMS SignedData, which is 1,794 hexadecimal digits, leaving 1,206 zeros of unused padding inside the reserved space. Parsing that CMS blob shows the message-digest attribute:

765  OBJECT messageDigest
776    SET
778      OCTET STRING, 32 octets:
         4391F13D 968DD55E 8660C966 6A073450
         89C296DF C53033E7 F2946661 EEB4E50B

Identical to the hash of the byte ranges, as it must be. Writing the blob into the reserved space, reading the ByteRange back out of the finished file, recomputing the hash and running the CMS verification returns “CMS Verification successful”. That is the entire mechanism, and there is nothing else to it.

Now the parts people get wrong.

The ByteRange is written by the signer, and read by the verifier, and nothing in the mathematics forces it to cover the whole file. ETSI EN 319 142-1 V1.2.1 of January 2024, clause 6.3, requirement k, is explicit: “The ByteRange shall cover the entire file, including the Signature Dictionary but excluding the PDF Signature itself (the entry with key Contents).” Requirement l requires SubFilter to be ETSI.CAdES.detached, and requirement h requires Contents to hold “a DER-encoded SignedData object as specified in CMS (IETF RFC 5652)”. A verifier that does not enforce requirement k will happily validate a file where range 2 stops early and several kilobytes of attacker content sit unsigned at the end. That is the Signature Wrapping attack from the PDF work, and its cousin the Universal Signature Forgery attack manipulates ByteRange and Contents to make viewers skip verification altogether.

The second problem is incremental updates. PDF is designed to be appended to: adding annotations, filling a form, or adding a second signature is done by writing more objects at the end and a new cross-reference table. A signed PDF that has been appended to is still a file whose signed ranges verify. Whether the appended material is legitimate is a policy question the viewer must answer, and the Incremental Saving Attack family exists because viewers answer it inconsistently.

PDF has a mechanism for the policy: a transform method called DocMDP, carried in the signature’s Reference entry, which declares what later changes are permitted. Its P parameter takes value 1 for no changes at all, 2 for form filling and signing only, and 3 for those plus annotations. That is the difference between an author signature, of which a document may have only one, and an ordinary approval signature, of which it may have many. Where viewers disagree about what counts as an annotation, or render an annotation over the top of the signed text, you get the Evil Annotation Attack from the 2021 certification work.

The third problem is that the CMS blob inside Contents is a CMS blob like any other, so every check in the verification path below applies to it, including verifying the certificate chain rather than merely the arithmetic.

SubFilter value Meaning Status in PDF 2.0
adbe.pkcs7.detached Detached CMS, PDF 1.3 Still permitted
adbe.pkcs7.sha1 SHA-1 bound, PDF 1.4 Deprecated
adbe.x509.rsa_sha1 Raw value, cert in /Cert Deprecated
ETSI.CAdES.detached CAdES, added in PDF 2.0 Current, PAdES

Those values are listed in table 255 of ISO 32000-2:2020, which notes that adbe.x509.rsa_sha1 and adbe.pkcs7.sha1 “have been deprecated with PDF 2.0”. A fifth value, ETSI.RFC3161, marks a document timestamp rather than a signature, and belongs to chapter 34.

The verification code path, step by step#

This is the part that separates a working implementation from a decorative one. Assume a detached CMS signature over a file. Here is the whole path, in order. Steps marked as commonly skipped are the ones that field experience says get dropped.

  1. Parse the outer ContentInfo and confirm its contentType is id-signedData. Reject anything else; a SignedData wrapper is not optional.
  2. Parse SignedData and pick the SignerInfo you intend to check. If there are several, decide deliberately which ones must pass. Commonly skipped: implementations that accept “at least one SignerInfo verified” will accept a file where an attacker appended their own SignerInfo.
  3. Locate the signer’s certificate using the sid field, which is either an issuer name plus serial number or a subject key identifier. Do not simply take the first certificate in the bag.
  4. Confirm the signing-certificate-v2 attribute, if present, hashes to the certificate you selected. Commonly skipped, and its absence entirely is commoner still.
  5. Check the digestAlgorithm and signatureAlgorithm against your policy. Refuse MD5 and SHA-1 for new signatures. Commonly skipped.
  6. If a cms-algorithm-protection attribute is present, confirm it matches the unprotected algorithm identifiers. Commonly skipped.
  7. Recompute the hash of the content with the stated digest algorithm.
  8. Compare it to the message-digest attribute. RFC 5652 section 5.6 is blunt about this: “The recipient MUST NOT rely on any message digest values computed by the originator.” This is the single most important comparison in the whole path and the one most often forgotten by code that calls a low-level verify function directly.
  9. Confirm the content-type attribute matches encapContentInfo’s eContentType, which section 5.6 also requires.
  10. Take the signedAttrs octets, replace the leading A0 tag with 31, and verify the signature over that. This is the arithmetic step, and it is the only step most people think of as “verification”.
  11. Build a certification path from the signer’s certificate to a trust anchor you configured. Not to any root in the file. Not to a root the document supplied.
  12. Check validity dates. Check basic constraints, key usage and extended key usage on every certificate in the path. A signer certificate whose key usage lacks digitalSignature or contentCommitment should be refused for document signing.
  13. Check revocation for every certificate in the path, at the correct moment, which for a document signature is the signing time established by a trusted timestamp rather than by the signer’s own claim. Chapter 30, “Revocation”, covers the mechanisms and their limits.
  14. Check any timestamp token, verify its own signature and chain, and confirm that the value it timestamps is the signature you are checking. Commonly skipped: an unverified timestamp is worse than none, because it looks like evidence.
  15. Apply your own policy: which signers are acceptable, which roles, which algorithms, which document types, how old a signature may be.
  16. Report a result that distinguishes the failure modes. “Signature invalid”, “signer untrusted” and “certificate expired” are three different facts, and an interface that merges them teaches users to ignore all three.

The five that go missing most often: the message-digest comparison at step 8, the algorithm policy at step 5, the certificate binding at step 4, the revocation check at step 13, and the distinction at step 11 between a trust anchor you chose and a root the document brought with it. That last one deserves emphasis. A CMS file carries a bag of certificates, and it is convenient to build the path from that bag; it is correct to build the path from that bag and then require it to terminate at an anchor in your own store. Code that stops at “the chain is internally consistent” will validate a signature from a chain the attacker generated in full, root and all.

Multiple signatures, countersignatures and signing order#

Three arrangements exist, they mean different things, and choosing the wrong one silently destroys the property you wanted.

Parallel signatures. In CMS, SignedData carries signerInfos as a SET OF SignerInfo. Two signers each compute their own signedAttrs, each including the same message-digest of the same content, and each signs their own block. Removing one leaves the other valid; adding a third disturbs neither. A SET has no order in DER beyond the sorting rules of the encoding, and even if it did, the order would be an encoding artefact rather than evidence. The XML equivalent is several ds:Signature elements over the same reference; the JWS equivalent is the JSON serialization’s signatures array, which exists because compact serialization cannot express more than one signer.

Countersignatures. RFC 5652 section 11.4 defines this precisely, and the precision matters. The countersignature attribute is identified by the object identifier 1.2.840.113549.1.9.6, and the specification says it “specifies one or more signatures on the contents octets of the signature OCTET STRING in a SignerInfo value of the signed-data. That is, the message digest is computed over the octets comprising the value of the OCTET STRING, neither the tag nor length octets are included. Thus, the countersignature attribute type countersigns (signs in serial) another signature.”

Three rules follow, all stated in the RFC. The countersignature attribute MUST be an unsigned attribute, never a signed one, because it did not exist when the original signature was made. Its signedAttributes MUST NOT contain a content-type attribute, because a countersignature has no content type. And because it is itself a SignerInfo, it can carry its own countersignature attribute, so an arbitrarily long chain can be built.

The consequences matter. A countersignature proves order, because it digests the value of the signature it covers and so could not have been made first. It depends entirely on that signature: strip the inner one and the outer has no meaning. And it is carried in an unsigned attribute, so nobody’s signature protects it, and anybody can strip it off without invalidating anything. Adding is proof; removing is undetectable unless something else records that it was supposed to be there.

Sequential document signatures. In PDF, a second signature is added by appending an incremental update, so its ByteRange covers the whole file as it stood after the first signature, including that first signature. That is a countersignature in effect if not in name, and it records order.

Arrangement Order proved Removal detectable
Parallel CMS signers No No
CMS countersignature Yes No
PDF incremental signing Yes Yes, by the later one
Timestamp token Yes, against a clock No

The fourth row is the special case that matters most in practice. A trusted timestamp is technically a countersignature made by a third party over the signature value, and its purpose is to bind the signature to a moment that the signer did not choose. That is chapter 34’s subject and the reason the B-T baseline level exists.

A design note that catches teams repeatedly. If your workflow is “the clerk prepares, the manager approves, the director authorizes”, and you implement it as three parallel signatures, you have recorded that three people signed and nothing about sequence, which is what auditors will ask for. The correct implementations are nested countersignatures, three PDF incremental signatures, or three signatures each over a document that includes the previous signature as content. The wrong one is three signatures over the same bytes with three signing-time attributes, because those times are self-asserted and prove nothing.

The word has a technical pedigree. It appears in the OSI security architecture and in the ISO/IEC 13888 series on non-repudiation mechanisms, where it is defined carefully as a service that generates, collects and maintains evidence about a claimed event so that a dispute can be resolved. Read that again: generates evidence. Not prevents denial.

The industry then used the word loosely for two decades and the standards bodies quietly retreated. The X.509 key usage bit that had been named nonRepudiation was renamed contentCommitment, and RFC 5280 section 4.2.1.3 records the change in one sentence. It is bit 1 of the KeyUsage extension, between digitalSignature at bit 0 and keyEncipherment at bit 2, and what it now says is narrow and true: this key is for committing to content rather than authenticating a session. It says nothing about what a court will do.

Adrian McCullagh and William Caelli, in First Monday volume 5, number 8, of 7 August 2000, put the objection sharply. Early digital signature statutes had built a near-irrebuttable presumption that whatever the private key signed was the act of the registered key holder. That reverses the ordinary commercial position, in which the party asserting a transaction has to prove it, and it places on individuals a standard of key custody that no ordinary person can meet.

For a designer, the practical translation is a list of the evidence you are actually producing. A signature value proves key use. A certificate proves somebody vouched for the key. A timestamp proves the signature existed by a certain moment. An audit log proves what the signer was shown. A device attestation proves where the key lived. A record of authentication proves who was present when the key was used. Each has to be created deliberately, and the phrase “non-repudiation” on a feature list generally means that none of them were.

Which of these a legal system actually requires, the three tiers of electronic signature under European law and the deliberately technology-neutral approach of the United States, is chapter 33, “What a Signature Means in Law”.

Choosing a format, and checking your own work#

A short decision guide, which is a convention rather than a standard, and which reflects what interoperates as of August 2026.

If you must sign Use Because
A document people read PAdES over PDF Renders and self-carries
An API request or token JWS Small, no canonicalisation
A file of any type CAdES detached Format-neutral, archival
Existing XML plumbing XAdES Only if XML is required

The general advice, which is opinion and marked as such, is to avoid XML signatures for new systems. The wrapping attack class has been known since 2005, has broken major implementations repeatedly, and requires the application to maintain a discipline that is easy to lose in a refactor. If the choice is open, sign the bytes and not the tree.

Finally, some commands to check things you already have. These are real, and the output shapes are from OpenSSL 3.0.13.

 # What is inside a detached CMS signature
openssl cms -cmsout -in detached.p7s -inform DER -print

 # Just the structure, offsets included
openssl asn1parse -inform DER -in detached.p7s -i

 # The signer certificate, extracted
openssl pkcs7 -inform DER -in detached.p7s -print_certs

 # Verify properly: content, chain, and purpose
openssl cms -verify -in detached.p7s -inform DER \
  -content invoice.txt -binary -CAfile trusted-root.pem \
  -purpose smimesign -out /dev/null

For a PDF, the first thing to read is always the ByteRange, and the first question is always whether the two ranges plus the gap add up to the exact size of the file on disk. If they do not, the file has content that no signature covers.

import re
d = open('doc-signed.pdf','rb').read()
m = re.search(rb'/ByteRange \[(\d+) (\d+) (\d+) (\d+)\]', d)
a1, l1, a2, l2 = map(int, m.groups())
print(a1 + l1, a2, a2 + l2, len(d))
 # 609 3611 3672 3672   <- ranges end exactly at end of file
 # gap is a2 - (a1 + l1) = 3002 octets

If the third number and the fourth number differ, stop. Somebody has appended unsigned content. If there is more than one ByteRange match in the file, count the signatures and check each one, because a second signature after an incremental update is normal and a second ByteRange inside a single signature dictionary is not.

32.98 Common wrong ideas#

Wrong: A digital signature proves that a particular person signed a document. Right: It proves that whoever held a particular private key applied it to a particular sequence of octets. Attaching a person to that key requires a certificate and a chain to a trust anchor you chose; attaching intent requires evidence about what the signer was shown and how the key was protected, none of which the signature contains.

Wrong: The signature covers the document. Right: In CMS, CAdES, PAdES, XAdES and JAdES the signature covers a block of signed attributes, one of which is the hash of the content. RFC 5652 section 5.4 makes that explicit, and section 5.6 requires the verifier to recompute the content hash and compare it, because “the recipient MUST NOT rely on any message digest values computed by the originator”. Skipping that comparison is a common and complete failure.

Wrong: Hashing before signing is an optimization for large files. Right: It is a security requirement. Raw RSA is multiplicatively homomorphic, so signatures on chosen messages can be combined into a signature on a message never signed, and without a hash an attacker can pick a signature value first and derive whatever message it happens to sign. The hash also carries its own identifier inside the signed encoding, which is what prevents a digest-algorithm downgrade.

Wrong: Non-repudiation means the signer cannot deny signing. Right: No mathematical operation can stop a person from denying anything. Signatures generate evidence that a dispute-resolution process can weigh, which is exactly how ISO/IEC 13888 defines the service. The X.509 key usage bit once called nonRepudiation was renamed contentCommitment for this reason, as RFC 5280 section 4.2.1.3 records.

Wrong: The signing time inside a signature tells you when it was signed. Right: The CMS signing-time attribute is inside the signed block, so nobody except the signer can alter it, and the signer is the one party with a motive to. Trusted time requires a third party to sign a statement about the signature value, which is an RFC 3161 timestamp token, and without one the date is an assertion of the same weight as a date written by hand.

Wrong: Canonicalisation makes XML signatures safer. Right: It makes them possible, by letting logically equivalent documents hash equally, and in doing so it separates “what was verified” from “where it sits”. That separation is the root of the wrapping attack class described by McIntosh and Austel in 2005, which later broke 11 of 14 SAML frameworks in the 2012 USENIX Security study and a cloud provider’s SOAP interface in the 2011 CCSW study.

Wrong: If a PDF opens with a green tick, the whole file is signed. Right: Only the octets named in the ByteRange are signed, and the ByteRange is written by the signer. ETSI EN 319 142-1 requires it to cover the entire file except the Contents string, but many viewers never check, which is why 21 of 22 desktop viewers and 5 of 7 online services failed in the 2019 study at ACM CCS. Read the ByteRange yourself and confirm it reaches the last octet.

Wrong: Two signatures on a file record who signed first. Right: Parallel signatures in CMS live in a SET OF SignerInfo, which carries no order, and their signing-time attributes are self-asserted. Order is proved only by a countersignature digesting the earlier signature’s value under RFC 5652 section 11.4, by a PDF incremental signature whose range covers the earlier one, or by a timestamp from a third party.

Wrong: A valid signature means the certificate was valid. Right: Signature verification and path validation are independent steps. OpenSSL reports “CMS Verification successful” for a key whose self-signed certificate carries any name the attacker liked, if you pass the flag that skips the chain. The chain, the validity dates, the key usage bits and the revocation status are all separate, and dropping any of them leaves a check that proves arithmetic and nothing else.

Wrong: SHA-1 signatures are old but still fine for documents. Right: SHA-1 collisions were demonstrated publicly on 23 February 2017 with the SHAttered result, and chosen-prefix collisions, the kind that let an attacker prepare two meaningful documents in advance, were demonstrated in January 2020 for tens of thousands of US dollars. A signature over a SHA-1 digest can be transplanted onto a second document the attacker prepared, and the signer will have done nothing wrong.

32.99 Chapter summary in 20 lines#

  1. A verified digital signature proves exactly one thing: the holder of a specific private key applied it to a specific sequence of octets.
  2. Every larger claim rests on three assumptions, about who owns the key, who could use it, and whether the signed octets are what the signer saw.
  3. The construction is hash-then-sign, and the hash is inside the signature operation for security reasons, not for speed.
  4. Raw RSA is multiplicatively homomorphic and allows existential forgery, so a structured padding around a digest is what makes the scheme sound.
  5. RSASSA-PKCS1-v1_5 in RFC 8017 encodes a 51-octet DigestInfo for SHA-256 inside a full-modulus block; PSS, ECDSA and Ed25519 differ in shape.
  6. CMS, defined in RFC 5652 of September 2009 and descended from PKCS #7 version 1.5 of November 1993, is the format almost everything else wraps.
  7. When signedAttrs is present the signature covers the DER of that attribute block with its tag rewritten from A0 to 31, not the document.
  8. The document is bound in only through the message-digest attribute, and RFC 5652 section 5.6 forbids the verifier from trusting it without recomputing.
  9. Detached, enveloping and enveloped are the three arrangements, and every format expresses all three by different means.
  10. CAdES, XAdES, PAdES and JAdES are ETSI profiles that add attributes to CMS, XML Signature, PDF and JWS respectively, with four baseline levels.
  11. As of August 2026 the current versions are EN 319 122-1 V1.3.1 of June 2023, EN 319 132-1 V1.3.1 and TS 119 182-1 V1.2.1 of July 2024, and EN 319 142-1 V1.2.1 of January 2024.
  12. XML must be canonicalised before hashing because equivalent documents differ in bytes, and that normalization is what enables wrapping attacks.
  13. A wrapping attack moves the signed element elsewhere in the document and substitutes new content in the place the application reads.
  14. The defence is to verify and consume the same node, constrain references by position, and forbid arbitrary transforms.
  15. JWS signs the literal base64url text of header and payload, which is why it has no wrapping attack class of the XML kind.
  16. A signed PDF holds a detached CMS blob inside the file, covering two byte ranges around the hexadecimal string that holds the blob itself.
  17. If the two ranges plus the gap do not add up to the size of the file, unsigned content is present and the signature does not cover it.
  18. The verification path has sixteen steps, and the five most often skipped are the message-digest comparison, algorithm policy, certificate binding, revocation, and using a trust anchor you chose rather than one supplied.
  19. Parallel signatures carry no order; countersignatures under RFC 5652 section 11.4 do, because they digest the earlier signature’s value.
  20. Non-repudiation is a legal outcome reached by weighing evidence, which is why X.509 renamed the nonRepudiation key usage bit to contentCommitment.

Chapter sources: RFC 5652 of September 2009, Cryptographic Message Syntax, STD 70, sections 5.1 to 5.6 and 11.1 to 11.4, with RFC 2315 of March 1998 recording PKCS #7 version 1.5, first published as an RSA Laboratories Technical Note in November 1993; RFC 8017 of November 2016, PKCS #1 version 2.2; FIPS 186-5, Digital Signature Standard, 3 February 2023; RFC 8032 of January 2017 for EdDSA; RFC 2634 of June 1999 and RFC 5035 of August 2007 for the signing-certificate attributes; RFC 6211 of April 2011 for CMS algorithm protection; RFC 5280 section 4.2.1.3 for contentCommitment; RFC 7515 of May 2015 for JSON Web Signature, sections 2, 4.1.1, 7.1, 10.6 and appendix F, with RFC 7518 section 3.6 and RFC 7797 of February 2016; RFC 8785 of June 2020 for the JSON Canonicalization Scheme; RFC 3161 for timestamp tokens; the W3C Recommendations Canonical XML Version 1.0 of 15 March 2001, Exclusive XML Canonicalization Version 1.0 of 18 July 2002, Canonical XML Version 1.1 of 2 May 2008, XML-Signature Syntax and Processing of 12 February 2002 and XML Signature Syntax and Processing Version 1.1 of 11 April 2013, and the Canonical XML Version 2.0 Working Group Note of the same day, which the working group declined to progress; ETSI EN 319 122-1 V1.3.1 of June 2023 clause 6.2.2, EN 319 132-1 V1.3.1 of July 2024, EN 319 142-1 V1.2.1 of January 2024 clause 6.3 requirements h, k and l, and TS 119 182-1 V1.2.1 of July 2024; ISO 32000-2:2020, PDF 2.0, second edition of December 2020, clause 12.8, freely available from the PDF Association since 5 April 2023 with errata collection 3 of 18 June 2026; Michael McIntosh and Paula Austel, “XML signature element wrapping attacks and countermeasures”, 2005 ACM Workshop on Secure Web Services, pages 20 to 27; Somorovsky, Heiderich, Jensen, Schwenk, Gruschka and Lo Iacono, “All Your Clouds are Belong to us”, third ACM Cloud Computing Security Workshop, 2011; Somorovsky, Mayer, Schwenk, Kampmann and Jensen, “On Breaking SAML: Be Whoever You Want to Be”, 21st USENIX Security Symposium, 2012; Mladenov, Mainka, Meyer zu Selhausen, Grothe and Schwenk, “1 Trillion Dollar Refund: How To Spoof PDF Signatures”, ACM CCS, November 2019, with “Shadow Attacks” at NDSS 2021 and “Breaking the Specification: PDF Certification” at IEEE Symposium on Security and Privacy, May 2021; the SHAttered SHA-1 collision of 23 February 2017 by Stevens, Karpman, Bursztein, Albertini and Markov; “SHA-1 is a Shambles” by Leurent and Peyrin, January 2020; the MD5 rogue certificate authority demonstration of 30 December 2008; Debian advisory DSA-1571-1 of 13 May 2008; the fail0verflow PlayStation 3 ECDSA disclosure of 29 December 2010; CVE-2014-1568, published 25 September 2014; Adrian McCullagh and William Caelli, “Non-repudiation in the digital environment”, First Monday volume 5 number 8, 7 August 2000; and the ISO/IEC 13888 series. Laboratory values were generated with OpenSSL 3.0.13 on 18 August 2026 using a P-256 key and a self-signed certificate trusted by nothing.