# Protected resource conformance

A protected resource is the thing that actually refuses. This states what one
must do to enforce a 2AYE grant, and what it must not do.

It is written for somebody implementing against the platform without reading
its source, in a language nobody here has used. Where this document and the
reference implementation disagree, this document is what an implementation is
judged against; where it is silent, it is silent, and the silence is a gap to
report rather than a licence.

Vectors are in [`vectors.v1.json`](vectors.v1.json), generated from the
reference implementation. Each carries a canonical form and a signature; a
conforming verifier reproduces the **canonical form** and **verifies** the
signature, never the reverse. **The rejection vectors are as normative as the
acceptance ones.** A permissive implementation is not a conforming one.

## The load-bearing rule

> A protected resource MUST verify a grant without calling the issuing platform.

This is the whole point and the easiest thing to give away. A resource that
asks "is this token okay?" over the network has not been given portable
authority; it has been given a remote authorization server with extra steps,
and it inherits that server's availability, its latency, and its ability to
change its mind after the fact.

Verification needs the published key set and nothing else. A resource MAY call
the platform for redemption — see §5, where the reason is different — but it
MUST NOT need to call it to decide whether a grant is genuine.

## 1. What a grant is

A grant is an ES256 signature over a canonical JSON object. The canonical form
is hand-built and fixed:

```json
{"action_hash":"…","agent_id":"…","expires_at":"…","grant_id":"…","intent_id":"…","intent_version":"1","single_use":"true","tenant_id":"…"}
```

Keys in Unicode code-point order. Every value is a string — including
`intent_version` and `single_use`, which are numbers and booleans in every
natural representation and strings here. `expires_at` is RFC 3339, UTC, `Z`
suffix, millisecond precision.

**Signatures are not reproducible.** ECDSA as used here is randomised: signing
the same bytes twice produces two different, equally valid signatures. A
verifier MUST verify, and MUST NOT expect to reproduce a signature it was
given. An implementation that compares signature bytes against an expected
value will pass its own tests and fail against the platform.

**Do not reserialize.** A serializer chooses its own escaping: one widely used
runtime rewrites `+` inside a string as a six-character escape, which produced
canonical bytes no other language could reproduce. Build the string.

## 2. Verify, in this order

Each step MUST refuse, and refusing MUST NOT reveal which step failed to the
caller (§7).

1. **Signature.** ES256 over the canonical bytes, against a key from the
   published set. Signatures are raw 64-byte R||S, Base64Url. A provider
   returning DER MUST convert; DER is valid and every conforming verifier
   rejects it.
2. **Key identity.** The grant names `signingKeyId`; use that key. Accepting
   any key in the set is weaker and hides a rotation error.
3. **Expiry.** `expires_at` against the resource's own clock. Clock skew: see
   §6.
4. **Audience.** The resource MUST check that it is the intended one. A grant
   accepted by whichever resource it is presented to is a bearer token.
5. **Action binding.** Recompute the parameter hash from the request the
   resource actually received and compare with `action_hash` in constant time.
   Not from a body the caller also sent — from the parameters that will be
   used.
6. **Agent.** The caller MUST be the agent the grant names.
7. **Admission.** Where receiver admission applies, the admission evidence MUST
   name this resource and this grant.
8. **Authority epoch.** A grant issued at or before a principal's pause instant
   MUST be refused, permanently, including after that authority resumed.
9. **Single use.** §5.

## 3. Recomputing the parameter hash

The hash is over the canonical predicate envelope. Its rules are in
[`../schemas/predicate-envelope/`](../schemas/predicate-envelope/) with their
own vectors, and two of them cause most interoperability failures:

**Amounts are text.** `13800` and `13800.00` are the same number and different
strings. A resource that parses an amount to a decimal and re-renders it
computes a different hash and refuses every legitimate request. Carry the
string the caller sent.

**Enums cross the wire as names.** An ordinal is unreadable in an audit trail
and changes meaning silently if a member is inserted.

## 4. Key discovery and rotation

Keys are published at `/.well-known/verid-grant-keys` as a JWK set: EC,
P-256, ES256, with `kid`.

- A resource MUST accept any key in the current set, selected by `kid`.
- A resource SHOULD cache the set and MUST refresh on an unknown `kid`, with a
  floor between refreshes: an unknown `kid` is otherwise a way to make the
  resource fetch on demand.
- Retired keys remain published while grants signed under them can still be
  valid. A resource MUST NOT treat retirement as revocation — a grant signed
  by a retired key and still within its expiry is valid.
- A resource MUST NOT accept a key delivered with the grant. A key travelling
  with the thing it authenticates authenticates nothing.

## 5. Single use, and why redemption is the exception

Single use cannot be enforced by verification alone: two resources verifying
the same grant independently both see a valid one. Exactly one execution
requires shared state.

A conforming resource MUST do one of:

- **Redeem against the platform.** One atomic compare-and-swap decides the
  winner. This is a network call, and it is the one call the design accepts,
  because the alternative is not a weaker guarantee but no guarantee.
- **Redeem against its own store,** when it is the sole executor for that
  grant's tool. The swap MUST be atomic against every other writer, not merely
  against other threads in the process.

**Verification is local; redemption is where the state is.** A resource that
skips redemption because verification passed has built a replayable credential.

The concurrency requirement is testable and MUST be tested: N simultaneous
redemptions of one grant produce exactly one execution and N−1 refusals.

## 6. Clock skew

Two minutes, and grants are short-lived — around sixty seconds — so skew is a
material fraction of the lifetime.

- Expiry: a resource MUST refuse at or after `expires_at`, and MAY allow up to
  two minutes of skew in the resource's favour only for `not-before`-style
  checks, never for expiry.
- A resource whose clock is more than two minutes out will refuse valid grants,
  and that is the intended failure. Widening the window to compensate for a
  broken clock widens the replay window by the same amount.

## 7. Refusing

The caller learns that it was refused. It does not learn which check failed.

A resource that reports `expired` separately from `bad-signature` separately
from `wrong-audience` is a tool for finding the check that is weakest. The
reason belongs in the resource's own log and its receipt, not in the response.

Error codes on the wire, and no others:

| Code | Meaning |
|---|---|
| `unauthorized` | The grant was not accepted. No further detail. |
| `conflict` | The grant was valid and has already been spent. |
| `malformed` | The request could not be parsed at all. |

`conflict` is separate deliberately: a replay is the one refusal a
well-behaved caller needs to distinguish, because retrying is the wrong
response and it would otherwise retry.

## 8. The receipt

A resource MUST emit a receipt for every execution, and SHOULD emit one for
every refusal after signature verification.

A receipt states what ran against what was authorized. `Matched`, `Mismatched`
with the fields that differ, or `NotProvided`. **A resource that describes
nothing MUST report `NotProvided` and MUST NOT report `Matched`** — describing
nothing has not established that nothing changed.

A receipt MUST be independently verifiable: signed, or on a hash-linked chain
that can be exported and checked without the platform.

**Verify over the compact form, never over a reserialized payload.** The
reference receiver emits `header.payload.signature`, Base64Url, ES256, raw
64-byte R||S - and its own verifier re-serializes the payload to compare, which
is an internal consistency check between its in-memory record and what it
signed. An external verifier copying that approach would have to reproduce one
runtime's JSON byte for byte. Split on the dots, verify over the first two
parts as bytes, then read the payload.

**Receipts chain.** Each names the SHA-256 of the previous compact evidence in
uppercase hex; the first names `GENESIS`. A deleted receipt is then as
detectable as an altered one, which matters because a ledger is usually edited
by deletion. Balances are recomputable from the amounts, so a verifier checks
the arithmetic rather than trusting it.

Vectors: [`receipt-vectors.v1.json`](receipt-vectors.v1.json) - real receipts
from a real execution, with one payload edited after signing and the signature
left alone, which is what somebody altering a kept file actually does.

## 9. What a conforming resource must never do

- Accept a grant whose signature it did not check.
- Take the verification key from the message.
- Execute before redeeming.
- Return a reusable "authorized" boolean to application code that then executes
  separately — the gap between the two is where a grant gets spent twice.
- Report which check failed.
- Normalise an amount before hashing.
- Treat a missing receipt as a success.

## 10. The hostile suite

[`threat-cases.v1.json`](threat-cases.v1.json) is the list a conforming
resource must survive: thirteen cases covering a parameter changed after
issue, a scale normalised, a changed counterparty, currency, action, tool or
environment, a grant presented by another agent, presented twice, presented
late, presented after its authority was withdrawn, presented across tenants -
and one hundred redemptions at once, of which exactly one may win.

Each case names the test that asserts it against the reference platform.
Those tests pass, and they prove the platform refuses; they do not prove a
resource refuses. Running them against a deployed resource, across a real
network and process boundary, is the milestone that changes what any of this
is worth.

## 11. Gate order

Where a resource composes several checks, the order is normative. See
[`GATE_ORDER_2026-09-24.md`](../../../GATE_ORDER_2026-09-24.md).

## What this document does not cover

**No deployed resource has been conformance-tested against it.** The vectors
are generated from the reference implementation, which means they prove that
an implementation agrees with this one — not that this one is right. An
independent implementation finding a disagreement is the point of publishing
them, and until one exists this is an untested specification.

**Receipt signing is not specified here.** The platform seals an evidence
package with a content hash rather than signing the manifest, so a recipient
can prove it has not changed and cannot prove to a third party who produced
it. That is an open gap, recorded in `EVIDENCE_PACKAGE_2026-09-24.md`, and a
resource implementing its own receipt signature should not assume this
document will later match what it chose.
