skip to content

Signature schemes hash the message before signing it. Beyond "the message might be large", what does that hash contribute to the security argument, and what goes wrong when the hash is weak or when signer and verifier disagree about exactly which bytes were covered?

level: middleimportance: must knowfreq 50%

answer

  1. unforgeability reduces to collision resistance
  2. collision = attacker crafts both messages
  3. encoding binds the hash algorithm identity
  4. sign octets, never re-serialise
  5. verify returns the bytes you consume

basics

~20 s

The digest maps arbitrary input into the primitive's fixed domain, and unforgeability then rests on collision resistance: a collision means one signature is valid for two messages. Separately, a signature covers exact bytes, so any normalisation gap lets the verifier authenticate one thing and the application consume another.

solid answer

~60 s

**What the hash buys.** Signature primitives operate on fixed-size values in a bounded domain; hashing compresses any message into it. But the real contribution is the security reduction: the composition is unforgeable only if the hash is collision resistant. If an attacker can find two messages with the same digest, a signature over the benign one is *simultaneously* a valid signature over the malicious one — no key material is broken. That is why a deprecated hash destroys a signing system while the key stays perfectly sound, and why the dangerous workflows are those where the signer signs content the requester influenced. The standardised encoding around the digest matters too: it removes malleability and binds the hash algorithm's identity into what is signed, so a digest cannot be reinterpreted under a different algorithm. **The coverage gap.** A signature is over *bytes*. When signer and verifier normalise differently — a canonicalised node-set, a re-serialised object, an archive where only some entries are covered, whitespace or encoding normalisation — an attacker can make verification succeed on one interpretation while the application acts on another. Rule: sign exact octets, verify the same octets, and consume only what was verified.

code

text · 8 lines
text
BAD:
  ok = verify(pubkey, request.raw_bytes, sig)
  if ok: doc = parse(request.body)        # a second, possibly different parse

GOOD:
  authenticated_bytes = verify_and_return(pubkey, request.raw_bytes, sig)
                        # throws on failure; no other route to the payload
  doc = parse(authenticated_bytes)

go deeper

for a junior

Know that the message is hashed and the digest is signed, that a weak hash breaks the whole scheme, and that a signature covers exact bytes.

for a middle

Give the reduction to collision resistance, distinguish collision from second preimage and say why the collision case fits real signing workflows, and name the canonicalisation gap.

for a senior

Design against the gap: sign transmitted octets, have verification return the consumed bytes, cover trusted metadata, and refuse to weaken checks when normalisation breaks traffic.

for a principal

Own hash-algorithm lifecycle across the estate — deprecation deadlines, re-signing or re-timestamping of long-lived artefacts, and preventing designs where you sign fully attacker-controlled blobs.

## Part 1 — why hash, really The throwaway reason is size: a signature primitive consumes a fixed-length value in a bounded algebraic domain, and a message can be a gigabyte. Hashing solves that. But the interview answer is the **security reduction**. In the hash-then-sign construction, forging a signature on a new message means either breaking the underlying primitive or finding two messages that hash to the same digest. So the scheme's unforgeability *depends on the hash's collision resistance*. The consequences are concrete: - **A broken hash breaks the signature system without touching the key.** Key length, key storage and the signing algorithm can all be beyond reproach; if the digest function admits collisions, an attacker who gets one message signed obtains a valid signature on another. - **Collision, not second preimage, is the relevant property.** A second preimage attack (finding a second message matching the digest of a *given* message) is far harder and would be needed to attack an already-signed document. A collision attack lets the attacker craft *both* messages, which fits exactly the workflows where a signer signs something a requester supplied or influenced: certificate issuance from a submitted request, an approval workflow over a document someone else drafted, a build system signing an artefact from a contributed source. Chosen-prefix collisions strengthen this further by letting both messages start with attacker-chosen, meaningfully different content. - **The mitigation is structural, not vigilance.** Never sign a bit-for-bit attacker-supplied blob when you can help it: include signer-chosen, unpredictable content in what gets signed (a random serial, a nonce, a server-generated field) so the attacker cannot fully control both sides of a collision, and retire hashes before they are broken. The standardised **encoding** applied to the digest before the primitive runs also earns its keep. It removes the algebraic malleability that raw application of the primitive admits, and it embeds an identifier of the hash algorithm inside the signed value, providing domain separation — an attacker cannot take a digest and have it accepted as a digest under a different algorithm. ## Part 2 — the coverage gap: which bytes were signed? A signature is a statement about a specific octet string. Every real incident of the "valid signature, wrong content" shape comes from the signer and the verifier disagreeing about that string, or from the *verifier* and the *consumer* disagreeing. Four recurring shapes, from different stacks: 1. **Canonicalised tree structures.** A signature over a canonicalised node-set of a hierarchical document identifies content by reference. An attacker can restructure the document so the referenced, signed subtree still validates while the application's own lookup finds a different, unsigned subtree of the same name elsewhere in the tree. Verification says yes; the application acts on content that was never signed. This is the classic wrapping attack, and its lesson is that **verification must return the exact content to be consumed**, not merely a boolean. 2. **Object formats with no canonical serialisation.** "Sign the object" is undefined when key order, number formatting, string escaping and whitespace can all vary and the two ends use different libraries. The robust designs avoid the problem entirely by signing the exact transmitted octets — typically by carrying an encoded copy of the payload inside the signed structure so re-serialisation never happens. Attempting to define canonicalisation after the fact is a long tail of edge cases. 3. **Containers where the signature covers a subset.** Archive and package formats where a manifest lists digests of entries: anything not listed is unsigned, so an attacker may be able to add entries, or exploit duplicate-name handling so the verifier checks one entry and the loader uses another. 4. **Normalisation in transit.** Text that gets line-ending translated, re-encoded, trimmed or transcoded between signing and verifying. Verification then fails for honest traffic — which teaches operators to "fix" it by relaxing the check, the worst possible outcome. ## The governing rule **What you see is what you sign, and what you verify is what you consume.** In implementation terms: - Sign and verify over exact octets. Never re-serialise between the two. - Have the verify step *hand back* the authenticated bytes, and make the application incapable of reading the payload by any other route. If your parse happens before verification and your consume happens after, they must be the same parse of the same bytes. - Cover everything that matters, including metadata the consumer trusts — names, versions, types, targets — not just the body. If an unsigned field can change how signed content is interpreted, it is part of the message. - Fail closed and loudly on mismatch, and never let a normalisation quirk become a reason to weaken the check. ## Interview delivery "Hashing isn't just for size: the scheme's unforgeability reduces to collision resistance, so a broken hash forges signatures without touching the key — and the risky workflows are the ones where you sign content someone else supplied. Then, separately, a signature covers exact bytes, so my main implementation worry is the gap between what was verified and what the application actually parses."

  • Why does a collision matter more than a second preimage for a signing system?
    A second preimage attack targets an existing message with a fixed digest and is far beyond reach for sound hashes. A collision attack lets the attacker construct both messages before either is signed, so they get the benign one signed through a normal workflow and keep the malicious twin, whose signature is byte-identical. Any process where you sign content that a requester supplied or influenced is exactly the shape this attacks, which is why signers should mix in unpredictable content of their own.
  • How do you avoid the canonicalisation problem in a system you are designing now?
    Do not define a canonical form; sign the exact octets you transmit. Carry the payload in its encoded form inside the signed envelope so neither side ever re-serialises, have the verification step return the authenticated bytes, and make it impossible for the application to reach the payload without going through that step. Also sign the metadata the consumer trusts — content type, target, version — so unsigned fields cannot change the interpretation of signed content.
  • A signature check started failing for legitimate traffic after a proxy was introduced. What do you do?
    Find what changed the bytes — header rewriting, decompression and recompression, character-set transcoding, line-ending normalisation, whitespace trimming — and either preserve the original octets end to end or move the signing boundary so it covers a representation that survives the hop. What you must not do is relax the verification, strip fields from coverage, or add a fallback that accepts unverified content, because that converts an availability problem into an authentication bypass.

saying these in an interview costs you the question

  • "We hash first just for speed / because the key can't take big inputs."
  • Treating a deprecated hash as low risk because "our keys are strong".
  • "Sign the JSON object" without defining which bytes, then re-serialising on the verifying side.
  • Verifying a document and then re-parsing the raw input separately to extract content.
  • Loosening coverage or adding an unverified fallback to fix normalisation-induced failures.

context