What gets signed
| Component | Required | What it pins |
|---|---|---|
@method | Always | The HTTP method — prevents replaying a GET as a POST |
@target-uri | Always | The full request URI — pins the signature to one service, one path, one query |
content-digest | When body present | SHA-256 of the request body, per RFC 9530 — body tampering breaks verification |
| Parameter | What it pins |
|---|---|
created | The signing timestamp |
expires | When the signature stops being valid (≤ 300s after created) |
nonce | A unique value preventing replay within the freshness window |
keyid | The account’s DID — the sole identity surface |
alg | Signature algorithm (ed25519 for v0.1) |
@authority and an AFAuth-Account header. Both were removed: @target-uri subsumes the authority, and keyid is the only identity field — avoiding the split-brain failure mode where two competing identity surfaces could disagree.
A signature on the wire
Signature-Inputdeclares what was signed. The verifier rebuilds the canonical input from these declarations and the request and verifies the signature against the agent’s public key (recovered fromkeyid).nonceis unique per request. The verifier maintains a seen-set of(keyid, nonce)tuples; a replay within the freshness window returnsreplayed_nonce.expires - created ≤ 300. The spec caps the signature lifetime at 5 minutes. Longer would extend the replay window without proportionate ergonomic benefit.
Verification, step by step
When the service receives this request, theVerifier:
- Parses
Signature-Inputand confirms all required covered components and parameters are present. - Reconstructs the canonical input string per RFC 9421.
- Resolves the public key from
keyidby decoding thedid:key— the identifier is the key, so there’s no network fetch or registry lookup. - Verifies the Ed25519 signature against the canonical input + public key.
- Checks the current time is between
createdandexpires, with ±60 seconds of skew tolerance. - Checks the
noncehasn’t been seen before for thiskeyidwithin the storage window. - If the body is non-empty, verifies
Content-Digestmatchessha-256(body).
401 Unauthorized with an error envelope.
Replay protection — scoped to keyid
The seen-nonce set is keyed by (keyid, nonce). For did:key the keyid is the account DID, so each agent key has its own nonce space; when an agent rotates (a new key, and therefore a new DID), the new keyid starts a fresh space — a nonce reused under the old key is not a replay against the new one.
What this means in practice
- Stop reusing nonces. The SDK generates a fresh random nonce per request by default. Don’t override unless you’re testing.
- Clock matters. If your agent’s clock drifts ±60s from the service’s, signatures will be rejected as future-dated or expired. NTP.
- Mind the body. Any byte you’d serve to the server has to be in the signed
Content-Digest. Stringify your JSON once, sign over that exact string, send that exact string. The SDK’sbuildOwnerInvitationand friends do this for you; if you’re usingsignRequestdirectly, watch for JSON pretty-printing in your transport stack mangling the body.
Further reading
- Spec §5 — full normative signing rules.
- Verify a request recipe — runnable verifier example.
- Error envelope — how signature failures surface.