skip to content

Signed Requests

Instead of sending the secret, the caller proves it holds one by signing a canonical view of the request. Interviewers ask what stops a replay once the signature itself is captured in a log.

on this pageshow

questions

3

Bench instruments sign each result upload with a signed timestamp — how do you size the skew window, and what replay does it still allow?

level: seniorimportance: must knowfreq 50%

answer

  1. freshness, not authenticity
  2. the timestamp must be signed too
  3. measured clock spread sets the width
  4. a captured signature works briefly
  5. seen-value lifetime equals the window

basics

~20 s

Size the window from measured client clock spread plus the longest legitimate retry delay, then accept that it bounds rather than prevents replay: a captured signature works until the signed timestamp ages out. Narrowing it further needs a shared seen-value store with the same lifetime.

solid answer

~50 s

A signature proves the bytes came from a holder of the secret; it says nothing about *when*. Freshness comes from a timestamp that is itself inside the canonical string, plus a verifier rule rejecting anything outside a window. Size the window from measurement, not taste: the p99 clock offset across the fleet, plus the longest delay a legitimate retry can sit in a queue, plus margin — for a fleet of unattended instruments that is minutes, not seconds. Be honest about the bound: anyone who captures the signed request from a proxy log or a crash dump can replay it byte-for-byte until the timestamp ages out. Closing that gap means remembering every accepted signature for exactly the window length, in a store shared by every verifier replica — which is real capacity and a new availability dependency, so decide deliberately whether you need it.

code

http · 10 lines
http
PUT /runs/2026-09-19/instrument-214 HTTP/1.1
X-Amz-Date: 20260919T101500Z
Authorization: <scheme> Credential=<keyId>/..., SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=3f2b...

HTTP/1.1 401 Unauthorized
WWW-Authenticate: <scheme> error="request_time_skewed"
Date: Sat, 19 Sep 2026 10:22:47 GMT
Content-Type: application/json

{"error":"request_time_skewed","server_time":"20260919T102247Z","window_seconds":300}

go deeper

for a junior

Hold on to the distinction: a signature says who made the bytes, a signed timestamp says when. Without the second, a copied request stays valid indefinitely.

for a middle

Be able to derive the window rather than quote one — client clock spread plus the longest legitimate delay plus margin — and explain why the timestamp has to sit inside the signed bytes.

for a senior

Own the residual risk out loud: state that a captured signature replays inside the window, price the seen-value store that would close it, and say what your verifiers do when that store is unreachable.

for a principal

Frame it as two budgets moving together. The window width sets both the replay exposure and the lag between revoking a secret and the last accepted request, and a fleet you cannot upgrade turns both into multi-year commitments.

## What the window is actually bounding A valid tag proves two things: the bytes in the canonical string are unmodified, and whoever produced them held the secret. It proves nothing about time. Without a freshness rule, the first correctly signed upload a bench instrument ever sends is a permanently valid credential for that exact request — anyone who obtains a copy can send it again a year later. The standard fix is a **signed timestamp**: the caller puts the current time in a header, that header is inside the canonical string, and the verifier rejects a request whose timestamp is further from its own clock than a published window. AWS Signature Version 4 works this way with `X-Amz-Date` and a small fixed window — five minutes in the common case — rather than tracking per-request unique values. The timestamp must be **inside** the signed bytes. A timestamp read from a header that is not in the canonical string is attacker-controlled: a replayer simply rewrites it forward and the tag still verifies. ## Sizing it against your own fleet The window is a budget with a cost on both sides: too narrow and legitimate uploads fail, too wide and a captured signature lives longer. Derive it: 1. **Measure the clock spread.** Log the signed timestamp against server receive time for every accepted request and look at the distribution. A fleet of instruments whose clocks were set at installation and never synchronised will show offsets of minutes to hours, and a long tail that no window can rescue. 2. **Measure the legitimate delay.** How long can a correctly signed request sit before it arrives — a store-and-forward buffer while a site's uplink is down, a retry with backoff, a slow multi-megabyte upload whose timestamp was computed before the first byte went out? 3. **Add the two, plus margin**, and publish the number. 4. **Publish the clock requirement too**, and give devices a way to meet it: an endpoint returning server time so an instrument can learn its offset and correct. Treat that response as a hint rather than authority — someone able to tamper with it can push a device out of the window, which is a denial of service, not a forgery. The long tail is the real decision. If five per cent of instruments cannot hold a five-minute window, you do not widen to an hour; you fix or quarantine those instruments, because the window applies to every caller including the compromised one. ## What a window does not stop Inside the window, a captured request replays cleanly. The realistic capture points are all mundane: a request logged in full by an intermediary, a crash dump, a support bundle, an error tracker that recorded the headers. Narrowing the window narrows the opportunity; it never removes it. It also does not stop — and must not stop — the caller's own retries. An instrument that does not receive an acknowledgement will legitimately re-send the identical signed bytes. **That makes at-least-once delivery a property of the scheme**, so the handler needs a business-level idempotency rule: dedupe on the run identifier, not on the signature. Idempotent handling blunts what a replayed *write* achieves; it does nothing for a non-idempotent action or for replaying a request that returns data. ## The alternative, and what it costs | Approach | What it stops | What it costs | |---|---|---| | Signed timestamp, window only | Replay after the window expires | Nothing beyond a clock requirement on callers | | Window plus a seen-value store | Replay of an exact request within the window | A store shared by every verifier replica, sized peak-rps times window, with a defined failure behaviour | | Per-request unique value with no window | Replay indefinitely, in principle | Unbounded state — there is no safe point at which to forget a value | The middle row is the one people under-cost. The key is the tag itself or a caller-supplied unique value inside the canonical string; the lifetime is **exactly the window**, because a record cannot be useful longer than the request it guards. Size it at peak requests per second times the window. It has to be shared across replicas or a replayer just retries until it lands on a different one. And you must decide what happens when that store is unavailable: reject everything, or accept and log. For an upload endpoint whose handler is already idempotent, accepting and logging is defensible; for a state-changing endpoint it is not. ## Rejecting well A stale timestamp and a bad tag are both "I cannot authenticate this request", so both are `401` with `WWW-Authenticate`, not `403`. But a remote integrator with a drifting clock cannot diagnose a bare `401`. Distinguish the reasons in a machine-readable body and return your own server time, so a device can see it is ninety seconds ahead. That leaks nothing: the timestamp came from the caller and your clock is public.

  • An instrument uploads a 400 MB result over a slow site uplink and is rejected for a stale timestamp. What is wrong?
    The timestamp is stamped when the request starts but checked when it is fully received, so the upload duration eats the whole window. Either check the timestamp against the time the headers arrived rather than the last byte, or have the caller sign a body digest header so the signature is verified at header time and the body is validated as it streams.
  • Why key a seen-value store on the signature rather than on the body?
    Because the tag is a fixed-size value that already covers the method, path, headers, timestamp and body digest, so two identical tags mean the same request within the same window. Keying on the body would collapse two legitimate uploads of the same measurement to different runs, and would mean storing or hashing the payload a second time.
  • Does widening the window to accommodate the worst instruments carry a hidden cost beyond replay?
    Yes — it also widens the period in which a request signed with a secret you have just revoked is still accepted, because the window admits requests created before the revocation. The two budgets move together, so a deliberately wide window has to be stated alongside the revocation lag it implies.

A cinema ticket printed with a showing time. It proves someone paid, and the usher only checks the time against the clock behind him — so anyone holding that stub during the showing walks in. Stopping that needs the usher to keep a list of stubs already torn, and only for the length of the showing.

saying these in an interview costs you the question

  • Believing a valid signature proves the request is fresh
  • Saying a timestamp window prevents replay rather than bounding it
  • Picking the window by taste instead of measured clock spread
  • Reading the timestamp from a header outside the canonical string
  • Keeping every accepted signature forever to detect duplicates
  • Returning a bare 401 that a remote integrator cannot diagnose
open as a page

Your service publishes a request-signing scheme for bench instruments uploading results — what belongs in the canonical string, and what does an unsigned header mean?

level: middleimportance: should knowfreq 45%

basics

~20 s

A canonical string fixes which bytes both sides sign: method, path, query in a defined order, a closed named set of headers, and a digest of the body as received. Anything outside it is unsigned and an intermediary may rewrite it undetected.

open as a page

What does a published request-signing contract owe several hundred instrument sites you cannot upgrade in one night?

level: principalimportance: should knowfreq 28%

basics

~20 s

Versioned coverage rules, two signing secrets accepted at once so rotation is rolling rather than a cutover, a secret scoped per purpose, an explicit at-least-once delivery promise with an idempotency key, diagnosable rejections, and published test vectors.

open as a page