skip to content

Why should a test's callback receiver verify a signature over the exact bytes it received rather than a re-serialized body?

level: middleimportance: nice to knowfreq 25%

answer

  1. Arrival alone proves nothing about the sender
  2. The proof is over bytes, not fields
  3. Parsing changes the shape
  4. Keep what arrived, verify, then parse
  5. Both sides must hold the same secret

basics

~20 s

A signature covers the byte sequence that was sent. Parsing the body and encoding it again changes whitespace, key order or number formatting, so the recomputed value no longer matches and the check fails on a perfectly genuine callback.

solid answer

~40 s

A signed callback carries a value computed over the request body together with a secret both sides hold, which lets the receiver prove the request came from the system under test and arrived unaltered. That proof is only valid over the exact octets that were signed. If the receiver deserializes the body and encodes it again before recomputing, any difference the encoder introduces - reordered keys, dropped insignificant whitespace, a different escape or number rendering - produces a different value, and the check fails on a good callback. So capture the raw bytes at the moment of receipt, verify against those, and parse only afterwards for field assertions. Verifying is also what makes the payload assertion mean anything: without it the case has proved that *something* posted a body, not that the system did.

code

pseudocode · 12 lines
pseudocode
on_request(raw_bytes, headers):
    store(raw = raw_bytes, headers = headers)   # verbatim, before any parsing
    respond(SUCCESS)

# in the case
received = receiver.requests_at(case_path)[0]
expected = keyed_hash(secret = case_secret, data = received.raw)
assert expected == received.headers["signature"]

body = parse(received.raw)                      # parse only after verifying
assert body.event == "order.settled"
assert body.reference == case_reference

go deeper

for a junior

Know that a callback can carry a value computed from its body and a shared secret, and that recomputing it is how a receiver tells a genuine callback from anything else that can reach the same address.

for a middle

Explain why verification runs over the bytes as received: parsing and encoding again change the sequence, so the recomputed value differs even though nothing is wrong. Be ready to say where the secret comes from.

for a senior

Show the assertion ladder - authenticity from the signature, shape from the contract, meaning from the fields, causation from a value your case minted - and what each rung does and does not prove alone.

for a principal

Argue for the product surface that makes this cheap: per-subscription secrets a caller can set and rotate, a documented signing scope, and a stable representation, so every consumer is not solving the same verification puzzle independently.

A callback arrives at an address that, by construction, anything on the network can reach. That is uncomfortable for an automated case, because the case is about to treat whatever landed there as evidence of what the system under test did. A **signature** closes that gap: the sender computes a value over the request body together with a secret shared with whoever registered the destination, and puts it in a header. The receiver recomputes the same value and compares. Equality asserts two things at once - the body was produced by a party holding the secret, and not one byte of it changed in transit. ## Why "the exact bytes" is the whole point Serialization is not a single-valued function. The same structured content can be rendered many ways, and every renderer makes its own choices: - **key order** - insertion order, alphabetical, or whatever a map iterator yields; - **whitespace** - indented for humans, compact for the wire, a trailing newline or not; - **number formatting** - `1.0` versus `1`, exponent notation, precision of a decimal; - **string escaping** - which non-ascii characters get escaped and in what case; - **absent versus null** - a field dropped on re-encode that was present as empty. None of those changes the meaning, and every one of them changes the bytes. A signature is deliberately sensitive to bytes, because that sensitivity is what detects tampering. Recomputing over a re-encoded body therefore fails on content that is completely correct, and the failure looks maddeningly like an intermittent product defect. | What the receiver verifies over | Result on a genuine callback | | --- | --- | | The bytes exactly as received | Matches, every time | | The body parsed and encoded again by the receiver | Fails whenever any encoder choice differs | | A pretty-printed rendering used in the failure message | Fails, and the message that was meant to help caused it | ## Build the receiver so the check is possible The order of operations is the design, and it has to be fixed before any convenience layer touches the request: 1. Accept the request and take the **body as an opaque byte sequence**. 2. Store those bytes verbatim, alongside every header. 3. Recompute the signature over the stored bytes with the secret the case configured, and compare. 4. Only now parse the stored bytes into a structure, and assert on fields. Step one is where most receivers go wrong, because a convenience layer that hands you an already-parsed body has thrown the original away and you cannot get it back. If the receiver is going to be shared, keeping the raw bytes is the single design decision that makes it reusable. ## Where the secret comes from The secret has to be one the case knows. The clean arrangement is that the case registers its own destination through the product's configuration surface, generates the secret at that moment, hands it over with the address, and keeps it for verification. A secret hard-coded in the receiver ties every case to one deployment's configuration and quietly stops proving anything the day someone rotates it - and because an unverifiable callback still *arrives*, the case usually keeps passing while the check has silently become decorative. ## The assertion ladder: what each step actually proves A verified signature is one rung, not the whole ladder. A case working through received evidence answers four different questions with four different mechanisms: | Question | What answers it | | --- | --- | | Did a holder of the secret produce exactly this body? | The signature, over the raw bytes | | Is this well-formed and shaped like the contract? | Parsing, plus a check on required fields and the declared type | | Do the values describe the state the action produced? | Assertions on the fields that carry meaning | | Was it caused by *this* case's action? | A value the case minted and the payload echoes | Collapsing those is the common error. A valid signature does not tell you the callback is about your record; a matching reference does not tell you the sender was genuine. ## Choosing which fields to assert Assert the fields that carry the contract's meaning - the event type, the identifier of the entity, the state values the action was supposed to produce - and be deliberate about the ones that cannot be predicted. A generated identifier, an issued-at value or a delivery counter should be asserted for **presence and shape**, not for an exact value, or the case fails on nothing. Do assert the declared content type and any schema-version field, because a change there is a contract change that ought to break something loudly. ## Failure modes - **Verifying after re-encoding**, then raising the suite's tolerance for "flaky signature failures" instead of fixing the capture. - **Skipping verification because the body looks right** - which accepts a callback from anything that can reach the address. - **Storing only the parsed form**, making verification impossible later and diagnosis guesswork. - **Treating a valid signature as proof of causation**, and asserting on someone else's authentic callback.

  • The receiver verifies the signature and the case passes. What has that not proved?
    Only that a party holding the shared secret produced that exact body. It says nothing about whether the fields are semantically right, whether the callback describes the entity this case created, or whether it was sent because of this case's action. Authenticity and causation are separate claims, and the second one needs a value the case minted and the payload echoes.
  • Where should the secret the receiver verifies with come from?
    From the case. When the destination is registered through the product's own configuration surface, the case generates the secret, hands it over with the address, and keeps it for the verification step. A secret hard-coded in the receiver binds every case to one deployment's configuration and stops proving anything the day it is rotated - silently, because the callback still arrives.
  • Which fields in a callback body should a case not assert on by exact value?
    Anything the system generates that the case cannot predict: identifiers it did not supply, issued-at values, sequence or attempt counters, and rendered display text. Assert those for presence, type and plausible shape instead. Reserve exact-value assertions for the event type, the reference the case supplied, and the state values the action was supposed to produce.

saying these in an interview costs you the question

  • Recomputing the signature over a re-encoded body and blaming a flaky sender
  • Skipping the signature check because the body already looks correct
  • Storing only the parsed body and discarding the bytes that were signed
  • Treating a valid signature as proof the callback belongs to this case
  • Hard-coding a shared secret the case never configures or rotates