The Signature Was Valid. The Message Was Forged: A Field Guide to Serialization and Canonicalization Attacks
Why the bytes you sign matter more than the algorithm: hash length extension, ASN.1/DER forgery, JWT alg confusion, and XML signature wrapping -- and the fix.
Permalink1. Two Lines of Code, Fifteen Years Apart
In September 2009, two researchers forged a perfectly valid signature on a Flickr API request without ever learning Flickr's secret key, and the attack exploited no weakness in MD5's compression function at all [1]. Fifteen years later, an attacker holding any single assertion an identity provider had ever signed could walk into a GitLab instance as any user they chose, because the signature that verified covered one identity while the application trusted another [2] [3]. In both cases the mathematics was flawless. The bytes were the wound.
That pairing is the whole subject in miniature. A cryptographic signature, a message authentication code, and a hash all make the same narrow promise: they bind one exact string of bytes. They say nothing about what those bytes mean, and nothing about whether the bytes the verifier checked are the bytes the application will act on. Break either of those unspoken assumptions and you get a valid signature over a forged message, with the algorithm computing exactly what it was told to.
An attacker with any document signed by the identity provider can "forge a SAML Response/Assertion with arbitrary contents" and "log in as arbitrary user" -- with a signature that verifies. [2]
Once you see it this way, the family stops looking like seven unrelated tricks. Every break enters through one of exactly two doors. Call them the two-move lens, and you will spend the rest of this article placing new attacks into one bucket or the other on sight.
Diagram source
flowchart TD
A["A signature, MAC, or hash binds one exact byte string"] --> B{"Where does the attacker's wedge enter?"}
B -->|Move 1| C["Ambiguous encoding: many byte strings mean one thing, or one byte string parses two ways"]
B -->|Move 2| D["Attacker-influenced binding: the authenticated span, or the verify rule, is under the adversary's control"]
C --> E["The bytes the verifier checked are not the bytes the application acts on"]
D --> E Move one is an ambiguous encoding: many different byte strings decode to the same meaning, or one byte string parses two different ways. Move two is an attacker-influenced binding: the span that actually gets authenticated, or the rule used to verify it, is something the adversary can steer. Flickr was move two: hash length extension extends the authenticated span to swallow the attacker's appended bytes. Bleichenbacher's 2006 forgery was the clean move one, a lax encoding that a lenient verifier read as a valid one [4]. The GitLab break was move two, riding on move one. A third break we will meet shortly, the JWT alg:none bypass, is move two in its purest form: the message tells the verifier how to verify it [5].
This is Part 3 of a field guide for protocol designers. Part 1 built the vocabulary of security definitions, showing that a provably fine primitive still sinks a protocol when the deployment hands an adversary a capability the proof never modeled (Secure Against Whom?). This part is that thesis pointed at one specific seam: the serialization. We assume you are comfortable with the idea that "secure" means "secure against a named adversary," and we build from there.
If neither MD5's compression function, nor RSA's modular exponentiation, nor SAML's signature algorithm was ever broken, then where exactly did the wedge go in? To answer that, we have to go back to two inventions that created the gap, one from 1989 and one whose canonical form was standardized in 1994, both engineered for entirely good reasons.
2. Where the Gap Comes From
Kerckhoffs said it in 1883 and Shannon formalized it in 1949: put all the secrecy in the key, and assume the adversary knows everything else [6] [7]. That principle governs where a system's security must live -- in the key, never in the obscurity of the algorithm. Historically framed for secrecy systems, it says nothing about the moment you have to serialize a structured message you must authenticate: an API request, a certificate, an identity assertion.
The instant a structure has to become bytes on a wire, the rule that turns structure into bytes becomes attack surface. This family of breaks is not new. It is as old as the sentence "we need to sign this serialized data."
Two inventions, one from 1989 and one whose canonical form was standardized in 1994, built the gap into the foundations. Neither was a mistake. Each solved a real problem and quietly left a door ajar.
The first is the way almost every hash function you have used is built.
An iterated hash design, introduced independently by Ralph Merkle and Ivan Damgard at CRYPTO 1989. The padded message is split into fixed-size blocks, and a compression function folds them one at a time into a running chaining value. The final chaining value is published as the digest. MD5, SHA-1, and the SHA-2 family are all Merkle-Damgard hashes.
Write it as a recurrence and the shape of every later attack is already visible. Starting from a fixed initial value , the hash computes for each padded block , and emits the last state as the digest, [8] [9] [10]. Merkle and Damgard proved something valuable: if the compression function resists collisions, so does the whole iterated hash. To make the proof work, they appended the message length inside the final padding, a trick called Merkle-Damgard strengthening.
Here is the door. The digest is the internal state. Nothing about is secret or scrambled beyond what the public compression function did to it. If you know a digest and how many bytes preceded it, you can set your own machine's state to that digest and keep hashing from where the original left off. The appended length field was added to stop collision ambiguities between messages that pad to the same blocks. It does nothing to conceal the internal state. That is precisely why a length-extension attacker needs to know the length of the secret: only to reconstruct the "glue" padding, never to recover the state itself [11].
The second invention is how we write down the things we sign so two machines agree byte-for-byte.
An encoding rule under which every abstract value has exactly one legal byte representation. That makes "is this the encoding of X?" a single yes-or-no question. The Distinguished Encoding Rules (DER) of ASN.1, standardized by the ITU-T as X.690, are the archetype, and they carry the signed structures inside X.509 certificates and PKCS#1 signatures [12] [13].
ASN.1 dates to the 1980s, and its canonical DER form was standardized as ITU-T X.690 in 1994. DER exists for a genuine reason: if you are going to hash and sign a structured object like a certificate, every implementation must serialize it to identical bytes or no two of them will ever agree on a signature. DER is the "exactly one valid byte string" discipline done right. Its siblings, the more permissive Basic Encoding Rules (BER), allow many byte strings for the same value, which is convenient for streaming and a loaded gun for signature verification, as we will see [12].
Diagram source
timeline
title The byte-vs-meaning gap, 1989 to 2025
1989 : Merkle-Damgard hashing
1994 : X.690 DER canonical encoding
1997 : HMAC standardized (RFC 2104)
2005 : XML signature wrapping named
2006 : Bleichenbacher e-3 RSA forgery
2009 : Flickr length-extension forgery
2015 : JWT alg confusion and alg none
2025 : ruby-saml parser differential The third piece of history is younger, and it is the moment the industry first wrote the problem down as a class rather than a one-off bug. In 2005, Michael McIntosh and Paula Austel at IBM published the first formal treatment of XML signature element wrapping: a genuinely signed element could be relocated inside a document so that verification found it in one place while the application consumed a forged element somewhere else [14]. They named the attacker-influenced-binding move for documents two decades before it produced an unauthenticated GitLab administrator takeover in 2025.
Both roots were built for good reasons -- a collision-resistance proof and an interoperable, unambiguous encoding -- and both left the same gap between bytes and meaning. So what happens when a working engineer reaches for the obvious way to authenticate a message with these tools? They build a trap. Three of them, one for each obvious shortcut.
3. Three Obvious Designs, Three Traps
Give an engineer a hash, a signature, and a deadline, and each of the two foundations above suggests an obvious shortcut. All three shortcuts authenticate something near the thing they should have, and the small gap between "near" and "exactly" is where every attack in this article lives.
Trap one: prove you know a secret by hashing it in front of the message. Before HMAC existed, the natural way to build a keyed authentication tag was to concatenate the secret with the message and hash the result: MAC = H(secret || message). It feels airtight. An attacker who has never seen the secret cannot reproduce the hash, so a matching tag looks like proof of knowledge. But recall that in a Merkle-Damgard hash the digest is the internal state after absorbing secret || message || padding. Publishing the tag hands the attacker the machine mid-computation.
Given only the tag and the length of the secret -- but not the secret itself -- an attacker can compute a valid tag for a longer message for attacker-chosen bytes . The published digest is loaded back in as the hash's internal state, and hashing simply continues from there. It works against any plain "output equals full state" Merkle-Damgard hash [11].
Trap two: verify an RSA signature by looking for the right structure inside it. A PKCS#1 v1.5 signature, once you undo the RSA math, is a byte string that is supposed to be a specific padded encoding wrapping a small ASN.1 structure called DigestInfo, which names the hash algorithm and carries the hash [13]. The correct check is exact: re-encode the expected structure to its canonical DER bytes and compare the whole thing, byte for byte. The tempting shortcut is to parse: scan the decrypted block, find a DigestInfo somewhere inside it, pull out the hash, and compare only that. A lenient verifier silently changes the question from "is this the canonical encoding?" to "does a valid DigestInfo appear somewhere in here?" [4].
Trap three: sign an XML element and let the verifier find it later by name. XML Signature, standardized by the W3C in 2002, does not sign "the document." It signs whatever elements a <Reference> points at, typically by an Id attribute, after running them through a canonicalization transform [15] [16]. Verification asks "is there a correctly signed element with this Id?" The application, separately, asks "what does the assertion say?" Nothing in the design forces those two questions to resolve to the same element. That gap is the seed of every signature-wrapping attack [14].
A situation in which two implementations, or two passes of one implementation, derive different structure or meaning from the same input bytes. When one parser is the verifier and another is the consumer, the object that got checked and the object that gets acted on are no longer guaranteed to be the same object [17].
Each of these designs shipped, and each was trusted for years, because each works perfectly on honest input. Honest input is the trap. The verifier and the consumer agree on every well-formed message anyone generates by accident, and diverge only on the adversarial message nobody tested.
In all three traps, verification confirmed a valid reading of the bytes, not the single canonical one, and it authenticated a span the attacker could later move, extend, or reinterpret. That one sentence is the whole failure catalog. Everything that follows is the same wound in five different bodies.
These leaks did not announce themselves. They surfaced one at a time, over fifteen years, and each new break forced a specific defense that the next generation promptly outgrew. Line the breaks up in order and they beat like a metronome: an obvious design, a named forgery, a fix, then a more ambitious design that reopens the identical gap. Here is that sequence, generation by generation.
4. Five Generations, One Shape
The genealogy of this subject is easy to misread as "each fix replaced the previous primitive." It did not. Hashes did not replace signatures, and JSON did not replace XML because XML was cryptographically weaker. What actually happened is a steady rise in ambition.
Generation 1 -- The keyed hash done wrong
The simplest thing to authenticate is a raw string, and the simplest keyed tag is H(secret || message). In September 2009, Thai Duong and Juliano Rizzo showed that Flickr's API used exactly this over MD5, and that an attacker could append arbitrary parameters to a signed request and produce a tag that verified, without ever knowing the secret [1]. MD5's compression function was not the casualty. The construction was.
Diagram source
flowchart TD
S["Sender computes the tag as H of secret then message"] --> T["Published tag equals the final Merkle-Damgard state"]
T --> A["Attacker reads the tag and guesses the secret length"]
A --> R["Reload the tag as the hash internal state"]
R --> X["Keep hashing: glue padding, then attacker bytes m-prime"]
X --> F["Valid tag for secret, message, padding, m-prime, with the secret never known"] The mechanism is worth feeling in code, not just reading. A "resumable" hash publishes its whole state as the digest, so the tag is a saved game the attacker loads and continues.
// A toy "output equals full internal state" hash, in the Merkle-Damgard spirit.
// The tag IS the state, so anyone can resume hashing from it. (Padding elided.)
function toyCompress(state, block) {
let s = state >>> 0;
for (const ch of block) s = (Math.imul(s, 31) + ch.charCodeAt(0)) >>> 0;
return s; // final state == published digest
}
function toyHash(msg) { return toyCompress(0x9e3779b1, msg); }
const SECRET = "hunter2"; // attacker never sees this
const tag = toyHash(SECRET + "&role=guest");
console.log("server tag:", tag);
// Attacker knows ONLY the tag and the length of (secret + message).
// They reload the tag as state and keep hashing their own bytes:
const forged = toyCompress(tag, "&role=admin");
// What the server computes over the extended message, WITH the secret:
const serverRecompute = toyCompress(toyHash(SECRET + "&role=guest"), "&role=admin");
console.log("forged == server:", forged === serverRecompute); // true, no secret needed Press Run to execute.
The fix was to stop signing with a construction whose output is a resumable state. Bellare, Canetti, and Krawczyk had already shown how in 1996 [18].
The Hash-based Message Authentication Code, analyzed by Bellare, Canetti, and Krawczyk in 1996 and standardized in RFC 2104 in 1997. HMAC nests two keyed hash passes, , where is the secret key fitted to one full hash block. It is length-extension-immune not because it has no resumable state, but because its outer input is a fixed one-key-block-plus-one-inner-digest structure, and forging a new inner digest requires the secret [19] [18] [20].
Two details of that definition carry the whole argument, and both are routinely mangled. First, : FIPS 198-1 defines it as the key preprocessed to exactly one -byte hash block -- the key itself when it is already bytes, its hash padded with zeros when it is longer than a block, and the key padded with zeros when it is shorter [20]. The exclusive-or is always against that block , never against a raw key of arbitrary length.
Second, the reason HMAC resists length extension. It is tempting, and wrong, to say HMAC "has no internal state to resume." It does; an HMAC tag is an ordinary hash output, so you can load it back as a Merkle-Damgard state and keep hashing. The immunity is structural. A genuine HMAC tag is the outer hash of exactly two things: one -byte key block followed by exactly one inner digest. Resume from a tag, append your own bytes, and what you have computed is a valid HMAC of no well-formed message at all, because a real HMAC's outer input is never longer than one key block plus one inner digest. This is the subtlest true statement in the subject. An HMAC tag can be resumed as a Merkle-Damgard state, so the folk explanation "there is no state to resume" is false. What resuming actually yields is a valid tag for no message, because the outer hash's input is pinned at one key block plus one inner digest. Immunity is a property of that fixed structure, not of missing state [20]. To forge a tag for a genuinely new message you would have to produce that message's inner digest, and the inner hash begins with the secret-derived block . So you need , which means you need the secret [19] [20].
The single most-botched fact in this whole subject is the boundary between what is extendable and what is not, so here it is drawn exactly [11].
| Construction | Length-extendable? | Why |
|---|---|---|
| MD5, SHA-1, SHA-256, SHA-512 over secret then message | Yes | The digest is the full final Merkle-Damgard state |
| HMAC with any of those hashes | No | Outer input is fixed at one key block plus one inner digest; resuming a tag yields a tag for no message, and a new inner digest needs the secret |
| SHA-3, SHAKE, KMAC | No | Sponge construction; the capacity is never output |
| SHA-512/256, SHA-384 (truncated SHA-2) | No | Part of the final state is withheld from the output |
Here the reader's model should shift. Hashing secret || message felt like proof of knowledge; it was nothing of the sort, because the tag is the primitive's own continuation, offered to the attacker for free. Flickr shipped MD5(secret || params) in 2009, twelve years after RFC 2104 (1997) standardized HMAC, the exact fix for this exact bug. Folklore about "just hash the secret with it" outlived its own refutation by more than a decade [1] [19]. The compression function was never touched. The construction leaked.
Try it against a real hash
Ron Bowes published hash_extender, which automates the glue-padding arithmetic for MD5, SHA-1, the SHA-256 and SHA-512 families, and more. Feed it a captured H(secret || m) tag, a guess for the secret length, and the bytes you want to append, and it prints both the forged tag and the exact padded message that produces it [11].
Generation 2 -- Signing a cryptographic structure
Raise the ambition: now you are signing not a raw string but a structured object, an RSA signature carrying an ASN.1 DigestInfo. The lenient verifier from trap two turns this into a forgery machine. In 2006, Daniel Bleichenbacher demonstrated at the CRYPTO rump session that if a verifier parses the decrypted PKCS#1 v1.5 block for a DigestInfo instead of checking the exact padded encoding, and if the RSA public exponent is 3, an attacker can craft a value whose cube looks like a valid signature with garbage in the space the lenient parser ignores [4] [21]. No private key, no broken RSA, just a parser that accepted a structure instead of the encoding. Bleichenbacher bridges Part 1 and Part 3 of this guide. His 1998 padding oracle (Part 1) and his 2006 signature forgery are the same lesson told twice: the RSA math was always fine, and the wound was in how an implementation handled the bytes around it [21].
The lesson should have stuck. It did not. Eight years later, in September 2014, Antoine Delignat-Lavaud of Inria's Prosecco team and, independently, Intel Security's Advanced Threat Research team found the identical flaw shipping in Mozilla's NSS library, the code behind Firefox, Thunderbird, and Chrome on several platforms [22] [23]. NVD records it as a failure to "properly parse ASN.1 values," a "signature malleability" issue [23]. The break was nicknamed "BERserk" because the verifier accepted lax BER encodings where the specification demanded the single canonical DER form. The name is the diagnosis: the gap between "a valid encoding" and "the canonical encoding" is the whole bug [22].
A signature scheme is malleable if, given a valid signature or the structure inside it, an attacker can derive a different signature the verifier still accepts, without the private key. NVD logs BERserk (CVE-2014-1568) verbatim as a "signature malleability" issue. A non-malleable scheme -- RSA-PSS, or any strongly unforgeable (SUF-CMA) signature -- forecloses this, which is exactly why it is the durable fix for the low-exponent forgeries [23] [13].
The fix is the discipline from trap two, stated positively: never parse a signature to find its structure. Re-encode the expected DigestInfo to canonical DER, then compare the full block byte for byte, and prefer RSA-PSS, whose verification has no lenient-parse foothold [13]. Windows code signing gets this right in production by signing a DER-encoded PKCS#7 structure (Authenticode and catalog files), and the X.509 certificates every TLS stack validates ride on the same DER discipline (SChannel and the twenty-year algorithm).
Generation 3 -- Signing a whole document
Raise the ambition again: sign an entire XML document, flexibly, so a signature can cover part of a message and travel with it. This is XML Signature, and its reference-by-Id model from trap three is the most durable failure in the subject.
An attack in which a genuinely signed XML element is relocated, duplicated, or wrapped inside a document so that signature verification still finds and validates the original element by its reference, while the application's business logic reads a different, attacker-authored element. The signed node and the consumed node are not the same node. McIntosh and Austel formalized it in 2005 [14].
Diagram source
flowchart TD
D["Signed SAML response from the identity provider"] --> W["Attacker keeps the signed assertion and injects a forged assertion"]
W --> V{"Verifier: is there a valid signature for this Id?"}
V -->|Finds the genuine signed assertion| OK["Signature check passes"]
OK --> C["Application reads identity from the forged assertion instead"]
C --> R["Logged in as an arbitrary user"] In 2012, Juraj Somorovsky and colleagues tested this against real SAML frameworks in "On Breaking SAML: Be Whoever You Want to Be." Of the 14 major frameworks they examined, 11 fell to at least one wrapping variant, and only two -- Microsoft SharePoint 2010 and SimpleSAMLphp -- resisted every variant they threw at them [24]. Eleven of fourteen is the number that matters: this was a systemic property of the sign-a-tree-by-reference design, not one vendor's slip.
Then in 2018, Kelby Ludwig at Duo Security found a subtler variant. Because canonicalization handles XML comments inconsistently, inserting a comment inside a signed field could make the verifier and the application read different text from the same signed element. As Ludwig put it, "multiple different-but-similar XML documents can have the same exact signature," a cluster catalogued as CVE-2017-11427 and its siblings [25].
XML Signature can be patched case by case, but each patch closes one wrapping shape and leaves the model intact. That is why, as we are about to see, the same attack was still taking over logins in 2025. For new designs, this generation is a road that leads nowhere good.
Generation 4 -- Signing a self-describing token
Raise the ambition once more: a token that carries its own metadata, including which algorithm secured it. This is JOSE and the JSON Web Token. In 2015, Tim McLean showed what happens when you let a message choose how it is checked [5] [26].
A class of JWT and JOSE attack in which the token declares, in its unauthenticated header, which algorithm the verifier should use. Setting the header algorithm to none asks the verifier to accept an unsigned token. Switching it from RS256 (an RSA signature) to HS256 (an HMAC) can trick a verifier into using the public RSA key as an HMAC secret, a value the attacker also holds. The message chooses its own verification rule [5].
Diagram source
sequenceDiagram
participant A as Attacker
participant V as Verifier
Note over V: Holds the RSA public key, expects RS256
A->>V: Take a real token, change the header alg to HS256
Note over A: Sign the token using the RSA public key bytes as the HMAC secret
A->>V: Send the forged HS256 token
V->>V: Reads alg from the token, selects HMAC-SHA256
V->>V: Uses the public key as the HMAC key and recomputes the tag
V-->>A: Tag matches, token accepted as genuine The RS256/HS256 confusion is the archetype of the type-confusion move: the attacker does not break either algorithm, but reinterprets a public value (the RSA key) as a secret value (the HMAC key), because the verifier let the token decide which one it was. The none algorithm is not a bug in the JOSE specification. RFC 7518 defines it as the algorithm for an Unsecured JWS, a legitimate option when integrity is guaranteed by some other layer. It becomes a bypass only when a verifier in a secured context fails to forbid it [27].
The fix arrived formally in 2020 as RFC 8725, the JSON Web Token Best Current Practices, written after, in its own words, "there have been several widely published attacks on implementations and deployments" [28]. Its discipline: pin an explicit algorithm allow-list, bind each key to exactly one algorithm, reject none in secured contexts, and never trust the algorithm the token names for itself. Windows leans on JWS-style tokens in production, such as the Primary Refresh Token (Inside the Primary Refresh Token); the rules a service applies before it trusts such a token are a discipline of their own (Who Decided This Token Is Good?).
Generation 5 -- Signing a binary message
The newest ambition is signing binary remote-procedure-call payloads, and it runs straight into a wall the earlier generations only grazed: some formats have no canonical byte form at all. Protocol Buffers is the clearest case. Its own documentation warns against assuming stable output.
"Deterministic serialization only guarantees the same byte output for a particular binary. The byte output may change across different versions of the binary." -- Protocol Buffers documentation [29]
If two versions of your service can serialize the same logical message to different bytes, then "sign the bytes and compare" is not even well defined: the sender and receiver may disagree on what the bytes are before any attacker shows up [29]. The fix is to refuse the premise. Sign the exact bytes you received and never re-serialize them; or, if you must sign structure, move to a format with a specified deterministic form, such as deterministic CBOR (RFC 8949, Section 4.2) or the JSON Canonicalization Scheme (RFC 8785); or sign a hash over explicit, typed fields rather than over a serializer's mood [30] [31].
Five generations, five fixes, one shape. Every fix was a variation on "make the authenticated bytes be exactly, and unambiguously, the bytes you act on." By the time the field had patched the fifth, it could finally state out loud what the first four had only whispered.
| Generation | Named break (year) | Which move | The fix | Status |
|---|---|---|---|---|
| 1 Keyed hash | Flickr length extension (2009) | Attacker-influenced binding: extends the span | HMAC, SHA-3, truncated SHA-2 | Solved |
| 2 Crypto structure | Bleichenbacher e=3 (2006), BERserk (2014) | Ambiguous encoding: BER versus DER | Re-encode to DER and byte-compare; RSA-PSS | Solved if disciplined |
| 3 Document | On Breaking SAML (2012), Duo (2018) | Attacker-influenced binding plus ambiguous canonicalization | Sign the exact bytes; avoid for new designs | Recurring |
| 4 Token | alg none, RS256/HS256 confusion (2015) | Attacker-influenced binding: message picks the rule | RFC 8725 allow-list, key-to-algorithm binding | Solved if disciplined |
| 5 Binary or RPC | protobuf non-determinism | Ambiguous encoding: no canonical form | dCBOR, JCS, or sign the received bytes | Design guidance |
5. Authenticate Bytes, Not Parses
Here is the sentence the five generations were spelling out. Cryptography authenticates bytes, so make the bytes you authenticate be exactly the bytes the consumer parses, make those bytes mean only one thing, and bind the type, context, and algorithm into what you sign so a valid signature can never be replayed into a second meaning. That is not three separate ideas competing for your allegiance. It is one idea with a strict order of preference.
The cure for every break in this family is not a stronger algorithm. It is a change in what and how you sign: authenticate the exact bytes the consumer parses, in an encoding that can mean only one thing, with type, context, and algorithm bound in. The algorithm was never the problem, so a better algorithm was never going to be the answer.
Diagram source
flowchart TD
Start["You must authenticate structured data"] --> Q{"Can you sign the exact bytes the consumer parses?"}
Q -->|Yes, preferred| P1["Pattern 1: sign-the-encoding, as in JWS, COSE, TLS 1.3"]
Q -->|No, structure is detached from the wire| P2["Pattern 2: canonical encoding such as DER, deterministic CBOR, JCS, then byte-compare"]
P1 --> P3["Pattern 3, always on top: bind type, context, and algorithm, i.e. domain separation"]
P2 --> P3 Pattern one, and the one to reach for first, is to sign the encoding.
The discipline of authenticating the exact serialized bytes the consumer will parse, rather than an abstract structure that must be re-serialized to check. Because the signed bytes and the parsed bytes are byte-identical, no re-serialization or parser-differential gap can open between verification and use. JWS signs the base64url header.payload string it transmits; COSE signs a CBOR Sig_structure; TLS 1.3 signs its transcript directly [32] [33] [34].
When the bytes that were signed and the bytes that get parsed are literally the same bytes, moves one and two have nowhere to stand. There is no second parse to differ, and no separate span to relocate. JWS makes this concrete: the signature covers the exact base64url-encoded header.payload, so the verifier and the application see identical input [32]. base64url is the URL-safe base64 variant: it swaps the plus and slash characters for hyphen and underscore and drops the trailing padding, so a token survives being placed in a URL or header without further escaping. COSE does the same for CBOR in constrained and IoT settings via its Sig_structure [33].
Pattern two is the disciplined fallback for when you genuinely cannot sign the wire bytes -- when a structure must be signed once and survive re-serialization by intermediaries. Then, and only then, use an encoding with exactly one legal form and verify by re-encoding and comparing bytes: DER for ASN.1, deterministic CBOR (RFC 8949, Section 4.2), or JCS for JSON (RFC 8785) [12] [30] [31]. This is strictly weaker than pattern one, because it is only as safe as your canonicalizer, a caveat that becomes a hard limit in Section 8.
Pattern three sits on top of whichever of the first two you chose, always.
Binding a signature to its intended type, context, and algorithm by folding those into the signed input, so a signature produced for one purpose cannot be replayed as if produced for another. TLS 1.3 prefixes its signed transcript with a context string (RFC 8446, Section 4.4.3); Ethereum's EIP-712 prepends a domain separator; RFC 8725 binds each JWT key to a single algorithm [34] [35] [28].
EIP-712 shows the pattern in one line: the value actually signed is , so a signature for one contract, chain, or message type is meaningless anywhere else [35]. This is what stops the Generation 4 move: if the key is bound to one algorithm and the context is bound into the bytes, the token can no longer talk the verifier into a different rule.
The gap between the abstract object and the bytes is not a detail; it is the entire subject. A worked example makes it visceral.
// Two "equal" objects serialize to different bytes -> different tags.
const a = JSON.stringify({ user: "root", exp: 1710000000 });
const b = JSON.stringify({ exp: 1710000000, user: "root" }); // keys reordered
console.log("a:", a);
console.log("b:", b);
console.log("byte-equal:", a === b); // false: a signature over 'a' will not verify 'b'
// A canonical pass (here, just sorted keys) makes these two agree. A real
// canonicalizer (JCS, RFC 8785) must also pin number and string forms -- see section 8.
function canonical(obj) {
const keys = Object.keys(obj).sort();
const parts = keys.map(function (k) { return JSON.stringify(k) + ":" + JSON.stringify(obj[k]); });
return "{" + parts.join(",") + "}";
}
console.log("canonical a:", canonical(JSON.parse(a)));
console.log("canonical b:", canonical(JSON.parse(b)));
console.log("now equal:", canonical(JSON.parse(a)) === canonical(JSON.parse(b))); // true Press Run to execute.
That is the discipline in principle. But principles ship as code, and code ships on deadlines. What does the state of the art actually look like from 2024 to 2026, and, two decades after signature wrapping was first named, is this class of attack finally dead?
6. What Is Shipping, and What Is Still Breaking
It is not dead. In 2025, two decades after McIntosh and Austel named signature wrapping, the attack produced an unauthenticated administrator takeover of GitLab instances [36] [37]. The defenders have converged on the right patterns, and the attackers have responded by moving the wedge somewhere the patterns do not yet reach.
Start with the offense, because the 2024 to 2025 SAML cluster is the two-move lens at its sharpest. In September 2024, ruby-saml failed to verify the signature of a SAML response strictly enough, and NVD records the result plainly: an attacker "can forge a SAML Response/Assertion with arbitrary contents" and "log in as arbitrary user," which is exactly what happened to GitLab's SAML single sign-on [2] [3]. Weeks earlier, GitHub Enterprise Server had shipped a fix for a signature-wrapping bug in its handling of publicly exposed signed federation metadata that granted "site administrator privileges" to an unauthenticated attacker [38].
Then, in March 2025, the ruby-saml story got worse and more instructive. CVE-2025-25291 and CVE-2025-25292 are a parser differential: the library used two different XML parsers, REXML and Nokogiri, and, as NVD states, "the parsers can generate entirely different document structures from the same XML input," which "allows an attacker to execute a Signature Wrapping attack" [39] [40]. GitHub's Security Lab, which found it, summarized the payoff: an attacker holding a single valid signature can construct assertions themselves and "log in as any user," with an exploitable instance in GitLab [36].
The same shape recurred across the wider SAML space in 2025: samlify allowed an attacker to "forge a SAML Response to authenticate as any user," rated CVSS 9.9 [41], and passport-wsfed-saml2 allowed impersonation of "any user during SAML authentication by crafting a SAMLResponse" using a valid IdP-signed object [42].
| CVE (year) | Component | Move | Outcome |
|---|---|---|---|
| CVE-2024-45409 (2024) | ruby-saml with omniauth-saml | Improper signature verification | Forge any user; GitLab SSO bypass |
| CVE-2024-6800 (2024) | GitHub Enterprise Server | Signature wrapping | Unauthenticated site administrator |
| CVE-2025-25291 and CVE-2025-25292 (2025) | ruby-saml, REXML versus Nokogiri | Parser differential enabling wrapping | Unauthenticated GitLab admin |
| CVE-2025-47949 (2025) | samlify | Wrapping via injected assertion | Authenticate as any user (CVSS 9.9) |
| CVE-2025-46572 (2025) | passport-wsfed-saml2 | Signature wrapping | Impersonate any user |
Now the defense, which is genuinely encouraging. The right patterns are not just written down; they are shipping. RFC 8725 codified the JWT discipline in 2020, and its allow-list-and-bind rules are the baseline new JOSE code is measured against [28].
The deterministic and canonical formats matured into real standards: deterministic CBOR (RFC 8949) and the JSON Canonicalization Scheme (RFC 8785) give designers a specified single form when they truly need one [30] [31]. And "sign-the-encoding" now anchors an entire generation of software supply-chain provenance: C2PA content credentials sign their manifests as COSE structures [43] [33], Sigstore signs with ephemeral keys and records every signing event in a tamper-resistant transparency log [44] [45], and the in-toto and SLSA frameworks carry signed, typed provenance through a build pipeline [46] [47].
So the score in 2026 is split. On the tokens-and-provenance side, the field adopted sign-the-encoding and the class is receding. On the legacy-XML side, the attackers simply relocated the wedge to the seam between two parsers, where no single canonicalizer can see it. That split sharpens the real design question. When you must sign structured data, which pattern do you pick, and what is the genuine, still-live tension between them?
7. Sign the Encoding, or Canonicalize First?
This is the one genuinely live design tension in the whole subject, and the honest framing is that it is a hierarchy, not a contradiction. Three approaches compete, and history has already ranked them.
At the top sits sign-the-encoding: JWS, COSE, and the TLS 1.3 transcript all authenticate the exact bytes that get consumed [32] [33] [34]. The middle option is canonicalize then sign: reduce a structure to a single legal byte form (DER, deterministic CBOR, JCS) and sign that [12] [30] [31]. At the bottom, for legacy reasons only, sits XML-DSig with canonicalization, which signs a mutable tree by reference and canonicalizes it at verify time [16].
Score them on what actually matters: which of the two moves each one forecloses, what verification costs, and what the empirical record says.
| Approach | Move 1: ambiguous encoding | Move 2: attacker-influenced binding | Verify cost | Empirical record |
|---|---|---|---|---|
| Sign-the-encoding (JWS, COSE, TLS 1.3) | Closed: signed bytes are the parsed bytes | Closed when paired with domain separation | Low: hash the exact bytes | Clean where applied |
| Canonicalize then sign (DER, dCBOR, JCS) | Reduced: one form if the canonicalizer is correct | Needs separate binding | Medium: re-encode and byte-compare | BERserk when done leniently |
| XML-DSig with C14N (reference by Id) | Open: canonicalization ambiguity | Open: reference and wrapping | High: transforms plus canonicalize | Worst: unbroken 2005 to 2025 |
The ranking falls out of the table. Sign-the-encoding structurally removes move one, because there is no second serialization to disagree with the first. Canonicalization only reduces move one, because it is exactly as trustworthy as the canonicalizer, and it does nothing about move two on its own. XML-DSig leaves both moves open, which is why its CVE record runs, unbroken, from 2005 to 2025.
Two distinctions sharpen the choice. First, detached versus enveloped signatures: a signature can wrap the data (enveloping), sit inside it (enveloped), or travel beside it (detached). The more the signature and the data can be rearranged relative to each other, the more room move two has to operate, which is precisely XML-DSig's problem.
Second, and more often confused, message-layer versus transport-layer integrity. TLS protects the channel; it says nothing about a forged assertion carried inside that channel. Every SAML break above happened over perfectly good TLS. The twenty-year SChannel story is about transport-layer integrity (Rotating Every Cipher), a different and non-substitutable guarantee from a signed message that must survive being forwarded.
History has largely adjudicated this tension in favor of the top of the hierarchy. But "largely" is carrying real weight in that sentence, because sometimes you inherit a rich format you cannot replace and a canonicalizer you cannot fully trust. Which raises the uncomfortable question the next section has to face: is there anything in this subject we cannot escape, no matter how much discipline we bring?
8. The Wall You Cannot Out-Discipline
Most of this article is about discipline: check the canonical form, sign the exact bytes, bind the context. But some of the subject is not a habit you can perfect. It is a wall, and it helps to know exactly where the wall stands.
The first stone is provable. Length extension is not a bug in MD5 or SHA-256; it is a structural property of any hash whose output is its full internal state. You cannot fix it by validating harder, adding checks, or trying again more carefully. You can only fix it by changing the construction: nest the hash (HMAC), switch to a sponge that never outputs its capacity (SHA-3), or withhold part of the state (truncated SHA-2) [19] [11]. No amount of care rescues H(secret || m), because the leak is in the shape, not the effort. Length extension is not a collision attack. Collision resistance -- the difficulty of finding two inputs with the same digest -- is a separate primitive property, treated in a sibling part of this guide. A hash can be flawlessly collision-resistant and still be trivially length-extendable, because the two properties constrain entirely different things [11].
The second stone is deeper, and it comes from language-theoretic security. When you canonicalize a rich format, you are asking a program to fully recognize a complex input language, and that is not always a solvable problem.
For complex input languages, "the problem of full recognition of valid or expected inputs may be UNDECIDABLE, in which case no amount of input-checking code or testing will suffice to secure the program." -- LangSec [48]
Read that carefully, because it is the ceiling on the entire canonicalization strategy [48]. If deciding whether an input is well-formed can be undecidable for expressive grammars, then two independent parsers of such a grammar are not guaranteed to agree, ever, and no test suite can prove they do. Parser differentials like the 2025 REXML-versus-Nokogiri break are therefore not fully eliminable for rich formats [39]. Canonicalization can only ever be as trustworthy as the weakest recognizer that touches the document, and for a format like XML that recognizer is very complex indeed.
The third stone follows from the second: for genuinely rich formats, a universal canonical form may simply not exist. The JSON Canonicalization Scheme is instructive here, because even for the comparatively tame grammar of JSON it has to special-case string and number subtypes, spelling out in its Appendix E how stream- and schema-based parsers must treat numeric and date-like values so they do not diverge [31]. Protocol Buffers refuses the goal outright: its documentation states the byte output is not stable across versions [29]. If the tamest formats need footnotes and the binary ones decline entirely, "just canonicalize everything" is not a general escape.
The fourth stone is the one this whole guide rests on. A signature can never carry meaning that the encoding does not pin down. The primitive authenticates a string of bytes; if those bytes admit two readings, the signature endorses both, and no cryptographic strength changes that. The gap between bytes and meaning is intrinsic to "authenticate bytes," not an implementation defect waiting for a patch.
So the reader who concluded, at the end of Section 5, "fine, we will just canonicalize everything and sign the exact bytes, and be safe" has to take one step back. Where you control the format and can sign the wire bytes, you really are safe from this class. Where you inherit a rich legacy format and must canonicalize a mutable tree, you are working under a proven ceiling, and the best you can do is shrink the attack surface, never close it. If that wall is real, the honest question is where this class is still failing today, and what, if anything, could move the wall even slightly.
9. Where It Is Still Failing
The settled parts of this subject are settled engineering. What follows is the honestly contested frontier, the places where a thoughtful designer in 2026 still has to make a judgment call.
Universal canonicalization for rich formats. JCS gives JSON a specified single form, but adoption is thin and its own Appendix E shows how many Unicode and numeric edge cases survive even for JSON [31]. A related canonicalization cousin bites one layer up, at identifiers: Unicode normalization and homoglyph folding decide whether two usernames that look identical on screen -- a Latin letter and its Cyrillic look-alike, or a name carrying an invisible combining mark -- resolve to the same account. Get it wrong and one byte string quietly impersonates another before any signature is even checked. For richer grammars there is no comparable standard, and Section 8 explains why there may never be one.
Is XML-DSig salvageable? The unbroken chain of wrapping and canonicalization CVEs from 2005 to 2025 is strong evidence, though not a proof, that the reference-by-Id model cannot be made safe by patching, and that new systems should not adopt it [24] [39]. Reasonable engineers still disagree about whether hardened SAML libraries can be trusted or must be phased out.
Parser-differential defense across trust boundaries. This is the deepest of the open problems, and the root of the 2025 GitLab bypass: any time two recognizers see the same bytes -- a proxy and an origin, a web application firewall and a backend, REXML and Nokogiri, an identity provider and a service provider -- they can disagree, and an attacker lives in the disagreement [36]. The same shape shows up one layer up as HTTP request smuggling [49] and JSON interoperability bugs from duplicate keys [17]. It is the systems-level restatement of the LangSec ceiling, and no one has a general defense.
The next three problems are less famous but more tractable, and each has real, partial progress worth understanding in detail.
Signing streamed, partial, or very large messages. Sign-the-encoding wants the exact transmitted bytes, but a streaming consumer must act on a prefix before the whole message, and its signature, even exist, and you cannot buffer a multi-gigabyte asset in memory just to canonicalize it. The state of the art is Merkle-tree or hash-chain signing: hash each chunk, build a tree over the chunk hashes, and sign only the root, so every chunk carries a logarithmic-size inclusion proof and verifies on its own.
C2PA deploys exactly this as its c2pa.hash.bmff.v3 assertion, a Merkle-tree hash over the ISO-BMFF mdat box, so fragmented or streamed media segments (fMP4, DASH, HLS) each verify independently against one signed manifest, itself a detached COSE Sign1 structure [43] [33]. It is strong and shipping for content-addressed, chunkable data. What stays unsettled is a low-latency, exact-byte guarantee for arbitrary, non-chunkable streams: the guarantee is per chunk, not per message, and the chunk boundaries themselves become security-relevant.
Making typed and context binding the default rather than an add-on. Domain separation is almost always advisory. Most formats let a signature omit its type, context, or algorithm, so binding is something a careful designer remembers rather than something the format enforces. A minority get it right structurally: EIP-712 folds a type hash and a domain separator inside the signed bytes, , and TLS 1.3's CertificateVerify signs 64 fixed octets, a role-specific context string, a separator, and the transcript hash, so a signature made in one role cannot be replayed in another [35] [34].
JOSE is the cautionary opposite: it carries alg in-band, so the token still names its own verification rule, and RFC 8725 can only make binding a discipline -- enumerate algorithms, bind each key to one, reject none -- not a structural property [28]. That is why alg:none keeps returning as a zombie default: RFC 7518 legalizes the none algorithm, and verification APIs historically read the algorithm from the token, so the insecure path is reachable out of the box on every fresh implementation [27].
Formally verified parsers, serializers, and canonicalizers. For a fixed grammar, the only route past the LangSec ceiling is a machine-checked proof that a parser recognizes exactly its grammar, round-trips with its serializer, and is non-malleable, meaning each value has one accepted encoding. EverParse, presented at USENIX Security 2019, generates such parsers in the F* proof assistant and demonstrated them for TLS 1.0 through 1.3 in miTLS and for the ASN.1 DER payload of PKCS#1 RSA signatures [50]. The same line of work has since produced verified parsers for other network protocols, including QUIC [51], and has shipped inside the Windows kernel network stack, both steps beyond that original paper [52].
ASN1*, at CPP 2023, built a verified non-malleable DER parser on top of EverParse, giving the X.509 and PKI format its unique-encoding guarantee by proof rather than by convention [53]. The catch matches the theory exactly: verified non-malleable parsers ship for constrained binary grammars, but a verified canonicalizer for a rich text format -- XML C14N, or JSON with the JCS Appendix E edge cases -- does not exist at industry scale, precisely as the no-universal-canonical-form limit predicts [31].
| Open problem | Why it matters | What has been tried | Best partial result |
|---|---|---|---|
| Signing streamed / very large messages | Sign-the-encoding needs the exact bytes, but you cannot buffer a multi-gigabyte stream to canonicalize it | Merkle-tree / chunk signing; C2PA Merkle-over-mdat with a detached COSE Sign1 manifest | Solved for chunkable, content-addressed data; open for low-latency non-chunkable streams |
| Typed / context binding by default | Most formats leave domain separation optional, so designers forget it | EIP-712 and TLS 1.3 bind it structurally; RFC 8725 binds it by discipline | Structural in a few designs; still opt-in and zombie-prone in JOSE |
| Formally verified parsers and canonicalizers | Only a machine-checked proof escapes the LangSec ceiling for a fixed grammar | EverParse (F*) and ASN1* verified non-malleable DER parsers | Ships for binary grammars; no industry-scale verified canonicalizer for rich text |
And then the callback that reframes everything. Suppose every system above had already migrated to post-quantum signatures, swapping RSA and ECDSA for ML-DSA (FIPS 204, module-lattice) or SLH-DSA (FIPS 205, stateless hash-based). How many of these breaks would that have prevented? Almost none. Flickr's length extension, alg:none, the RS256-to-HS256 confusion, and every SAML wrapping from 2005 to 2025 are all orthogonal to the signature algorithm: swap the primitive and the attack computes the same correct answer over the same wrong bytes [54] [55].
The two exceptions prove the rule. Bleichenbacher's 2006 forgery and BERserk are RSA e=3 malleability forgeries, and a non-malleable scheme -- RSA-PSS, or any strongly unforgeable (SUF-CMA) post-quantum signature -- already stops them, which is exactly why this article prescribed RSA-PSS as the BERserk fix; simply raising the public exponent to 65537 also denies the low-exponent construction its foothold [13] [21]. Everything that remains is a bytes-not-meaning failure no algorithm transition can touch. That is the most important thing to carry out of this article, and it collapses the whole subject into one operational question, plus a checklist you can apply on Monday morning.
10. The Discipline, Made Operational
Everything above collapses into one decision procedure and one checklist of anti-patterns. Read the tree top to bottom and stop at the first rule that matches your situation.
Diagram source
flowchart TD
Root["Choose the first rule that matches"] --> Start{"What are you authenticating?"}
Start -->|Shared-secret integrity| M["Use HMAC or KMAC, never H of secret then message"]
Start -->|You control the serialization| SE["Sign-the-encoding: JWS, COSE, TLS 1.3"]
Start -->|Structure detached from the wire| CE["Canonical encoding: DER, dCBOR, JCS, then byte-compare"]
Start -->|Binary or RPC such as protobuf| PB["Sign the exact received bytes or a hash of typed fields"]
Start -->|XML or SAML| XM["Prefer not, if forced use one parser and verify the consumed element"]
M --> Bind["Then always bind type, context, and algorithm"]
SE --> Bind
CE --> Bind
PB --> Bind
XM --> Bind 2. Do you control the serialization, and does the consumer parse exactly what you signed? Then sign the encoding: JWS, COSE, or the TLS 1.3 transcript, and you have closed move one by construction [32] [33] [34].
3. Must you sign a structure detached from its wire bytes? Use a canonical encoding -- DER, deterministic CBOR (RFC 8949), or JCS (RFC 8785) -- and verify by re-encoding and byte-comparing, never by lenient parsing [12] [30] [31].
4. Binary or RPC payloads such as protobuf? Never sign freshly re-serialized bytes, because there is no canonical form. Sign the exact bytes you received, or sign a hash over explicit, typed fields with a domain separator [29].
5. Always, on top of rules 2 through 4, bind type, context, and algorithm. For JWT specifically: pin an explicit alg allow-list (prefer EdDSA or ES256), bind each key to exactly one algorithm, reject none in secured contexts, validate iss, aud, and exp, and treat header fields like kid, jku, and x5u as attacker-controlled, since they invite path traversal and server-side request forgery [28].
6. XML or SAML? Prefer not, for new designs. If you are stuck with it, use a vetted and patched library, parse the document exactly once and verify the same element your application will consume, pin the schema, enforce Id uniqueness, disable DTD and external-entity processing, reject comments and stray whitespace inside security-relevant nodes, and never let two different parsers touch one signed document [24] [25] [39].
7. RSA signatures? Prefer RSA-PSS. If you must verify PKCS#1 v1.5, re-encode the expected DigestInfo to strict DER and compare bytes, and never combine a public exponent of 3 with a lenient parser [13] [4].
Rule 5 is worth seeing as code, because the RFC 8725 discipline fits in a short function and every line closes one historical break. Note one easy-to-miss detail: a JWT segment is base64url, not standard base64, so decoding it with a raw atob on the segment is itself a latent bug.
// The header's "alg" is attacker-controlled. Never trust it -- pin your own set.
const ALLOWED = new Set(["ES256", "EdDSA"]); // no RSA-or-HMAC ambiguity in the mix
// JWT segments are base64url, NOT standard base64: translate -_ to +/ and restore '=' padding.
function decodeSegment(seg) {
let b64 = seg.replace(/-/g, "+").replace(/_/g, "/");
while (b64.length % 4) b64 += "="; // base64url drops padding; put it back
return JSON.parse(atob(b64)); // now standard base64 -> bytes -> JSON
}
function verifyJwt(token, expected, keyForAlg, verifySignature) {
const parts = token.split(".");
const h64 = parts[0], p64 = parts[1], sig = parts[2];
const header = decodeSegment(h64);
if (header.alg === "none") throw new Error("alg none rejected in secured context");
if (!ALLOWED.has(header.alg)) throw new Error("algorithm not on the allow-list");
// Bind key to algorithm: one key, one alg. Never reuse a public key as an HMAC secret.
const key = keyForAlg(header.alg);
if (!key) throw new Error("no key bound to this algorithm");
if (!verifySignature(header.alg, key, h64 + "." + p64, sig)) throw new Error("bad signature");
const claims = decodeSegment(p64);
const now = Math.floor(Date.now() / 1000);
if (claims.iss !== expected.iss) throw new Error("wrong issuer");
if (claims.aud !== expected.aud) throw new Error("wrong audience");
if (typeof claims.exp !== "number" || claims.exp < now) throw new Error("expired or missing exp");
return claims;
} Press Run to execute.
The other side of the guide is the mirror: every anti-pattern you will meet in real code maps one-to-one to a named break from the failure catalog. If you recognize the left column in a code review, you already know the fix.
| Misuse pattern in real code | Maps to break | Fix |
|---|---|---|
| A MAC built as SHA-256 of secret then message | Flickr, 2009 | HMAC or KMAC |
| Verifying an RSA signature by scanning for a DigestInfo | Bleichenbacher 2006, BERserk 2014 | Re-encode to DER and byte-compare; RSA-PSS |
| Trusting the algorithm named in the JWT header | alg none, RS256/HS256, 2015 | Allow-list; bind each key to one algorithm |
| Verifying one XML element and consuming another | On Breaking SAML 2012; ruby-saml 2025 | One parser; verify the consumed element |
| Signing freshly re-serialized protobuf bytes | protobuf non-determinism | Sign received bytes or a typed-field hash |
| Two parsers touching one signed document | ruby-saml 2025, REXML versus Nokogiri | A single recognizer across the trust boundary |
On Windows, the HMAC, hashing, and signing calls this guide keeps invoking are not abstractions; they live in a specific place in the platform (the CNG architecture behind BCrypt and NCrypt), and choosing the right one is the first line of every rule above.
Notice that every entry in both tables is the same rule wearing different clothes: make the bytes you authenticate be exactly the bytes you act on, and make them mean only one thing. Which is the single question this entire article has been walking toward, and the one worth pinning above your desk. Before that, a handful of misconceptions deserve to be named and corrected, because they are the folklore that keeps regenerating these bugs.
11. Misconceptions, Named and Corrected
The folklore around this subject is dense with half-truths, and each one causes real bugs. Here are the seven that do the most damage, every one of them dissolving into the two-move lens.
Frequently asked questions
Isn't this just an implementation bug, not cryptography?
It is exactly the boundary where cryptography meets parsing, which is why it belongs in a cryptography field guide. The algorithm is always fine in these breaks; the attack surface is the bytes it was handed. Treating "the crypto" and "the parsing" as separate concerns is what lets the authenticated bytes drift from the consumed bytes in the first place.
Is SHA-256, or HMAC-SHA256, broken by hash length extension?
No to both. The vulnerable thing is the construction SHA-256(secret || message) used as a MAC, not the hash itself: SHA-256 is not broken, and HMAC-SHA256 is not length-extendable. Section 4 carries the full reason its fixed outer structure makes HMAC immune even though the tag can be resumed, and why truncated SHA-2 and the SHA-3 family are not susceptible either [11] [19].
alg:none was patched a decade ago, so why does it still matter?
Because it keeps coming back as a zombie default in new libraries, and because its more dangerous cousin, RS256-to-HS256 key confusion, is very much alive. Both are the same move: the message chooses its own verification rule. It recurs by default because RFC 7518 legalizes the none algorithm and verify APIs read the algorithm from the token, so the insecure path ships reachable [27]. The durable fix is RFC 8725's discipline of an explicit algorithm allow-list and one key bound to one algorithm [5] [28].
Are DER and BER interchangeable for verifying signatures?
No. DER is the canonical form with exactly one legal encoding per value; BER is the permissive form that allows many [12]. Verifying a signature by accepting lax BER where the specification requires DER is not a shortcut, it is the BERserk vulnerability, which forged RSA signatures in shipping browsers in 2014 [22].
Does deterministic protobuf make it safe to sign the bytes?
No. Protocol Buffers' deterministic mode only guarantees stable output for a particular binary; the byte output can change across versions of your own software [29]. That is not a canonical form, so signing the serialized bytes remains undefined across versions. Sign the exact received bytes, or a hash over explicit typed fields, instead.
Would switching to post-quantum signatures fix any of this?
Almost none of it. The only exceptions are Bleichenbacher's 2006 forgery and BERserk, RSA e=3 malleability forgeries that any non-malleable scheme such as RSA-PSS already stops [13]; every other break here is orthogonal to the algorithm, so a post-quantum swap computes the same correct answer over the same wrong bytes [54] [55]. Section 9 works through why, and the conclusion returns to it as the point the whole article turns on.
Is this the same as insecure deserialization or pickle remote code execution?
No, and the distinction matters. Insecure deserialization is a code-execution topic: an attacker ships a serialized object that runs code when it is loaded. This family is about data whose bytes were authenticated ambiguously or bound loosely. Same word, "serialization," entirely different failure and fix.
Every one of these dissolves into a single sentence, and delivering that sentence with the full weight of the evidence behind it is the last thing left to do.
12. The Bytes Were Always the Wound
Line up the whole catalog. Flickr's forged API signature in 2009. Bleichenbacher's cube in 2006 and its rerun as BERserk in 2014. The alg:none bypass and the RS256-to-HS256 confusion in 2015. Every XML signature-wrapping forgery from McIntosh and Austel in 2005 through the ruby-saml parser differential that took over GitLab logins in 2025. Protocol Buffers used as a signing input it was never fit to be. In almost every one of those breaks, the algorithm computed exactly what it was asked to compute. The hash hashed correctly. The RSA verification did precisely what the math prescribes. The signature was valid. The message was forged.
A post-quantum signature would have prevented almost none of these breaks. The two exceptions -- Bleichenbacher's 2006 forgery and BERserk -- are RSA
e=3malleability forgeries that a non-malleable scheme like RSA-PSS already stops, which only sharpens the point: everything else rode on the gap between the bytes that were authenticated and the bytes that were consumed, and a stronger, or quantum-resistant, algorithm computes the same correct answer over the same wrong bytes. The encoding gap is orthogonal to the algorithm [54] [13].
Part 1 of this guide opened on the same note from the confidentiality side: Bleichenbacher did not break RSA in 1998, and the RSA math was always fine (Secure Against Whom?). It is fitting that the same researcher bridges both parts, because it is the same lesson pointed at two different seams. Confidentiality failed when the environment handed an adversary an oracle the proof never modeled. Integrity fails when the serialization hands an adversary a second reading the signature never pinned down. The primitive is not the variable. The bytes are.
So carry one question out of all of this, and apply it the next time you sign, verify, or review anything: are the bytes I authenticated exactly the bytes I will act on, and can they mean only one thing? If yes, this entire family of attacks has nowhere to stand. If you are not sure, you have just found the wedge before an attacker did.
That question survives every algorithm transition the field will ever make. When today's signatures are replaced by their post-quantum successors, and those by whatever comes after, the lens will still read the breaks, because it was never about the algorithm. That is why serialization and canonicalization deserve a full part of a field guide for protocol designers, and not a footnote at the end of a chapter on hashing.
Study guide
Key terms
- Merkle-Damgard construction
- An iterated hash design where a compression function folds padded blocks into a chaining value and the final chaining value is published as the digest
- Hash length extension
- Given H(secret then message) and the secret length, forging a valid tag for a longer message without knowing the secret, by resuming from the published state
- HMAC
- A message authentication code nesting two keyed hashes; length-extension-immune because its outer input is fixed at one key block plus one inner digest, and forging a new inner digest needs the secret
- Canonical (distinguished) encoding
- An encoding rule with exactly one legal byte string per value; DER is the archetype
- Parser differential
- When two implementations derive different structure from the same bytes, so the checked object and the consumed object diverge
- XML Signature Wrapping
- Relocating a genuinely signed element so verification finds it while the application consumes an attacker-authored element instead
- Algorithm and key confusion
- A JOSE attack where the token's own header chooses the verification rule, enabling alg none or reuse of a public key as an HMAC secret
- Sign-the-encoding
- Authenticating the exact serialized bytes the consumer parses, so no re-serialization or parser-differential gap can open
- Domain separation
- Binding type, context, and algorithm into the signed input so a signature cannot be replayed into a different meaning
- Signature malleability
- When an attacker can derive another accepted signature from a valid one without the key; a non-malleable scheme such as RSA-PSS forecloses it
References
- (2009). Flickr's API Signature Forgery Vulnerability. https://web.archive.org/web/20091007023800/http://netifera.com/research/flickr_api_signature_forgery.pdf ↩
- (2024). CVE-2024-45409. https://nvd.nist.gov/vuln/detail/CVE-2024-45409 ↩
- (2024). GitLab Patch Release 17.3.3. https://docs.gitlab.com/releases/patches/patch-release-gitlab-17-3-3-released/ ↩
- (2006). Bleichenbacher's RSA signature forgery based on implementation error. https://web.archive.org/web/20171212214934id_/https://www.ietf.org/mail-archive/web/openpgp/current/msg00999.html ↩
- (2015). Critical vulnerabilities in JSON Web Token libraries. https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ ↩
- (1883). La cryptographie militaire. https://www.petitcolas.net/kerckhoffs/crypto_militaire_1.pdf - Journal des sciences militaires, 1883; the origin of Kerckhoffs's principle ↩
- (1949). Communication Theory of Secrecy Systems. https://archive.org/details/bstj28-4-656 - Bell System Technical Journal 28(4), pp. 656-715; formalizes the key-not-algorithm principle ↩
- (1989). One Way Hash Functions and DES. https://doi.org/10.1007/0-387-34805-0_32 - CRYPTO 1989; the iterated-compression construction and length-strengthening ↩
- (1989). A Design Principle for Hash Functions. https://doi.org/10.1007/0-387-34805-0_31 - CRYPTO 1989; independent companion to Merkle ↩
- (2010). Cryptography Engineering: Design Principles and Practical Applications. ISBN 978-0-470-47424-2. - Chapter 5, Merkle-Damgard and length-strengthening ↩
- (2012). Everything you need to know about hash length extension attacks. https://www.skullsecurity.org/2012/everything-you-need-to-know-about-hash-length-extension-attacks ↩
- (2021). Recommendation X.690: ASN.1 encoding rules (BER, CER and DER). https://www.itu.int/rec/T-REC-X.690 ↩
- (2016). PKCS #1: RSA Cryptography Specifications Version 2.2. https://datatracker.ietf.org/doc/html/rfc8017 ↩
- (2005). XML signature element wrapping attacks and countermeasures. https://dl.acm.org/doi/10.1145/1103022.1103026 ↩
- (2002). XML-Signature Syntax and Processing (First Recommendation). https://www.w3.org/TR/2002/REC-xmldsig-core-20020212/ ↩
- XML Signature Syntax and Processing. https://www.w3.org/TR/xmldsig-core/ ↩
- (2020). An Exploration and Remediation of JSON Interoperability Vulnerabilities. https://bishopfox.com/blog/json-interoperability-vulnerabilities ↩
- (1996). Keying Hash Functions for Message Authentication. https://doi.org/10.1007/3-540-68697-5_1 - CRYPTO 1996; the HMAC construction and its security analysis ↩
- (1997). HMAC: Keyed-Hashing for Message Authentication. https://datatracker.ietf.org/doc/html/rfc2104 ↩
- (2008). FIPS 198-1: The Keyed-Hash Message Authentication Code (HMAC). https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.198-1.pdf ↩
- Bleichenbacher'06 signature forgery in python-rsa. https://words.filippo.io/bleichenbacher-06-signature-forgery-in-python-rsa/ ↩
- (2014). Mozilla Foundation Security Advisory 2014-73: RSA Signature Forgery in NSS. https://www.mozilla.org/en-US/security/advisories/mfsa2014-73/ ↩
- (2014). CVE-2014-1568. https://nvd.nist.gov/vuln/detail/CVE-2014-1568 ↩
- (2012). On Breaking SAML: Be Whoever You Want to Be. https://www.usenix.org/conference/usenixsecurity12/technical-sessions/presentation/somorovsky ↩
- (2018). Duo Finds SAML Vulnerabilities Affecting Multiple Implementations. https://web.archive.org/web/20181116121859/https://duo.com/blog/duo-finds-saml-vulnerabilities-affecting-multiple-implementations ↩
- (2015). CVE-2015-9235. https://nvd.nist.gov/vuln/detail/CVE-2015-9235 ↩
- (2015). JSON Web Algorithms (JWA). https://datatracker.ietf.org/doc/html/rfc7518 ↩
- (2020). JSON Web Token Best Current Practices. https://datatracker.ietf.org/doc/html/rfc8725 ↩
- Protocol Buffers: Encoding. https://protobuf.dev/programming-guides/encoding/ ↩
- (2020). Concise Binary Object Representation (CBOR). https://datatracker.ietf.org/doc/html/rfc8949 ↩
- (2020). JSON Canonicalization Scheme (JCS). https://datatracker.ietf.org/doc/html/rfc8785 ↩
- (2015). JSON Web Signature (JWS). https://datatracker.ietf.org/doc/html/rfc7515 ↩
- (2022). CBOR Object Signing and Encryption (COSE): Structures and Process. https://datatracker.ietf.org/doc/html/rfc9052 ↩
- (2018). The Transport Layer Security (TLS) Protocol Version 1.3. https://datatracker.ietf.org/doc/html/rfc8446 ↩
- (2017). EIP-712: Typed structured data hashing and signing. https://eips.ethereum.org/EIPS/eip-712 ↩
- (2025). Sign in as anyone: Bypassing SAML SSO authentication with parser differentials. https://github.blog/security/sign-in-as-anyone-bypassing-saml-sso-authentication-with-parser-differentials/ ↩
- (2025). SAML roulette: the hacker always wins. https://portswigger.net/research/saml-roulette-the-hacker-always-wins ↩
- (2024). CVE-2024-6800. https://nvd.nist.gov/vuln/detail/CVE-2024-6800 ↩
- (2025). CVE-2025-25291. https://nvd.nist.gov/vuln/detail/CVE-2025-25291 ↩
- (2025). CVE-2025-25292. https://nvd.nist.gov/vuln/detail/CVE-2025-25292 ↩
- (2025). CVE-2025-47949. https://nvd.nist.gov/vuln/detail/CVE-2025-47949 ↩
- (2025). CVE-2025-46572. https://nvd.nist.gov/vuln/detail/CVE-2025-46572 ↩
- (2024). C2PA Specifications, Version 2.1. https://spec.c2pa.org/specifications/specifications/2.1/index.html ↩
- Sigstore. https://www.sigstore.dev/ ↩
- Sigstore Documentation. https://docs.sigstore.dev/ ↩
- (2019). in-toto: Providing farm-to-table guarantees for bits and bytes. https://www.usenix.org/conference/usenixsecurity19/presentation/torres-arias ↩
- SLSA: Supply-chain Levels for Software Artifacts. https://slsa.dev/ ↩
- LangSec: Language-theoretic Security. http://langsec.org/ ↩
- (2019). HTTP Desync Attacks: Request Smuggling Reborn. https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn ↩
- (2019). EverParse: Verified Secure Zero-Copy Parsers for Authenticated Message Formats. https://www.usenix.org/conference/usenixsecurity19/presentation/delignat-lavaud ↩
- (2025). EverParse: verified secure parser and serializer generator. https://github.com/project-everest/everparse - Project Everest repository; the QuackyDucky frontend has generated message-processing code for networking protocols including TLS and QUIC ↩
- (2022). Hardening Attack Surfaces with Formally Proven Binary Format Parsers. https://doi.org/10.1145/3519939.3523708 - PLDI 2022, pp. 31-45; EverParse3D deployed in the Windows kernel network stack (Hyper-V network virtualization) ↩
- (2023). ASN1*: Provably Correct, Non-malleable Parsing for ASN.1 DER. https://haobin.cx/publications/asn1star.pdf ↩
- (2024). FIPS 204: Module-Lattice-Based Digital Signature Standard (ML-DSA). https://csrc.nist.gov/pubs/fips/204/final ↩
- (2024). FIPS 205: Stateless Hash-Based Digital Signature Standard (SLH-DSA). https://csrc.nist.gov/pubs/fips/205/final ↩