Verifying answers

Using an SDK?

The SDKs verify every answer before you see it, and throw away any that fail. This page is for writing your own client.

Every answer Anis sends — success or refusal — is signed. A client throws away any answer it cannot verify: it does not log it and carry on, it does not act on it. An order answer that cannot be verified is treated like one that never arrived — the outcome is unknown and the order is resumed with the same operation id.

What Anis signs

Answers are signed with ECDSA P-256 and SHA-256, label sig1, with parameters created, keyid and alg — and no expires. The signed parts, in this order:

# Part Present when
1 "@status" always
2 "content-digest" always
3 "x-request-id" always
4 "signature-input";req you signed the request (absent on enrolment answers) — its value is your own Signature-Input
5 "location" the answer carries Location
6 "retry-after" the answer carries Retry-After
7 "idempotency-replayed" the answer carries Idempotency-Replayed: true
8 "cache-control" the answer carries Cache-Control: no-store

The base is built like a request’s: one line per part, then "@signature-params" with no newline after it.

"@status": 200
"content-digest": sha-256=:epGQsVay09pY/GXv6MpAjMbsZ7VvobF1nAdylVo/mMk=:
"x-request-id": 01J9R2K8T4V6XQ0M3B7C5D9E1F
"signature-input";req: sig1=("@method" "@authority" "@path" "@query" "x-anis-date");created=1789804800;expires=1789804860;keyid="…";alg="ecdsa-p256-sha256"
"@signature-params": ("@status" "content-digest" "x-request-id" "signature-input";req);created=1789804800;keyid="partner-response-signing/v1-active";alg="ecdsa-p256-sha256"

;req sits outside the quotes, identically in its line and in @signature-params.

The checks, in order

  1. Both Signature and Signature-Input are present; the label is sig1; alg is ecdsa-p256-sha256.
  2. The body matches its Content-Digest — checked before the signature. The signature covers the digest header, not the body, so a client that checks the signature first accepts a swapped body whenever the digest was rewritten too.
  3. Rebuild the list of signed parts from what the answer carries (the table above) and require the answer’s Signature-Input to name exactly that list. Never trust the list it advertises: an answer validly signed over a shorter list leaves its body unprotected.
  4. created is within 60 seconds of your clock — an answer has no expires, so without this bound a captured answer could be replayed to you forever.
  5. The keyid names a key in Anis’s published keys (below).
  6. The signature is 64 bytes and verifies over the rebuilt base.

Anything else: throw the answer away. The answer test vectors cover every one of these checks.

Anis’s public keys

GET /.well-known/partner-signing-keys.json — the one route that needs no signature — publishes every key that may sign an answer, including the next one before a rotation and the previous one after it. Each has a kid, which is what an answer’s keyid names. Treat kid as an opaque name.

  • Cache the document. When an answer names a kid you do not have, fetch it again once — that is the ordinary sign of a rotation. Do not refetch without limit on unknown keys.
  • Refuse the whole document if any key carries a private member (d) — not just that key. A document that leaks a private key is not to be trusted at all.
  • Each coordinate is exactly 32 bytes. Refuse a key of any other shape.

The one unsigned answer

If Anis’s own signing is unavailable, it answers 503 without a signature. Treat it like any answer you cannot verify: retry a read, resume an order with the same operation id.