5 August 2026
Testing a signed webhook before the sender exists
The counterparty holds the private key, so their payloads cannot be produced locally. Standing in for them turns a blocked integration into an ordinary one.
An integration that receives signed payloads has an awkward property: the half under test cannot produce its own input. The signature is made with the sender's private key, and that key is theirs. Until they are ready, there is nothing realistic to send.
The usual responses are all worse than they look.
Waiting. The handler is written, reviewed and merged without ever having run against a real artifact. Integration begins on the day the counterparty is ready, which is also the day every remaining question surfaces at once.
A stub that skips verification. The handler is exercised with a flag that turns the signature check off. What is tested is the parsing and the business logic; what is not tested is the part that decides whether to trust the message at all.
Signing with a key generated locally. Closer, and the shape of the artifact is right. It works until the moment the real key set is introduced, at which point the parts that were quietly hardcoded - a key id, an issuer, an algorithm - all surface together.
The third is the right instinct. What it lacks is a second party: something that holds a key set, publishes the public half where a verifier can fetch it, and produces artifacts on request.
Standing in for the sender
The pieces are ordinary. A key set, kept somewhere stable. A published JWKS at a URL the verifier can be pointed at. And a way to ask for a payload, signed with that key, containing claims chosen for the case being tested.
With those, the handler is exercised end to end before the counterparty exists:
it fetches a key set over HTTP, resolves a kid, checks a signature it did not
make, and reaches a verdict. The only thing that changes on the day the
counterparty is ready is which URL the key set comes from.
That last point is what makes the exercise worth doing. An integration that has only ever verified against a key generated in the same process has not tested key resolution, and key resolution is where rotation failures live.
The cases worth having
A valid payload proves the handler works when nothing is wrong, which is the least interesting thing about it. A receiver's job is to refuse, and refusal is what goes untested.
- An expired payload. Confirms the timestamp is checked at all. A handler that accepts an expired message accepts a replayed one.
- A payload signed with a key that is not published. Confirms the handler cares which key signed, rather than that something did.
- A payload whose body was changed after signing. The signature is genuine and no longer matches. This is precisely what signing exists to catch, and a handler that parses before verifying will often not catch it.
- A payload with the signature removed. Some handlers treat an absent signature as nothing to check.
- A payload declaring no algorithm. If it is accepted, anyone can send anything.
Each of these has an outcome the handler should reach, and asserting against that outcome is what makes the test meaningful. A suite that checks "an exception was thrown" passes for the wrong reasons about as often as the right ones - a malformed test fixture throws too.
Where clock skew comes in
Two of the cases above are about time, and time is the one input a test environment usually gets wrong by removing it. A payload with a not-yet-valid timestamp is rarely an attack; it is two machines a few seconds apart. A handler with no tolerance will reject perfectly good messages under load, and one with several minutes of tolerance will accept messages that expired some time ago. Both are worth deciding deliberately rather than inheriting from a library default.
What this buys
Nothing about release day is a first. The handler has verified real signatures, resolved keys over the network, rejected the cases it should reject, and done so in CI on every commit since. The counterparty's arrival becomes a configuration change rather than the start of integration.
The bench produces these artifacts for JOSE and OpenPGP, including the malformed variants no library will generate. The test tokens list what each case is and the outcome a service ought to reach, with the weakness class each one covers. Test keys only - nothing here is a statement about any particular service, and "it passed" is not a security guarantee.