{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://travel.agenthaven.dev/contracts/schemas/key-attestation.schema.json",
  "title": "Key attestation (payload of a key-attestation+jwt)",
  "description": "How the buyer's stable signing key is tied to an attested instance. The buyer's hourly X509-SVID (SPIRE) signs this document; the certificate chain travels in the JWS header as x5c (leaf first) and must chain to a use: x509-svid CA in /.well-known/spiffe-bundle.json on the buyer's domain (DNSSEC + DANE anchored); the leaf must carry iss in a URI SAN and be inside its validity window. In the cut-order fallback the signer is instead a use: jwt-svid key from the same bundle, named in kid by its thumbprint. The stable key inside cnf.jwk is what signs request envelopes and ledger entries; sub is its RFC 7638 thumbprint, which is the kid of every envelope and the mandate's cnf.jkt; the SVID itself signs no traffic. Published in /.well-known/agent-keys.json (schemas/agent-keys.schema.json), refreshed at every SVID rotation, so a mandate valid for minutes never races a one-hour rotation (F-13). This is the ONE artifact where x5c is allowed by the JWS policy. The stable key's 30-day cap counts from the key's generation; a handover (previous, below) starts a new window under the same SPIFFE ID. Status: the seller VERIFIES both forms (since 2026-09-07, mandate.test.mjs); agent-buyer PUBLISHES it at https://buyer.onbehalf.dev/.well-known/agent-keys.json since 2026-09-09, in the JWT-SVID form; in the X509-SVID form since 2026-09-12 (SPIRE on the host); handover written and verified since 2026-09-17.",
  "type": "object",
  "required": ["iss", "sub", "iat", "exp", "cnf", "spiffe_id"],
  "properties": {
    "iss": { "type": "string", "pattern": "^spiffe://", "description": "The SPIFFE ID of the X509-SVID that signs the attestation." },
    "sub": { "type": "string", "description": "The thumbprint (RFC 7638) of the stable key attested (cnf.jwk). Equals the kid every envelope and ledger signature carries; the seller recomputes it and refuses a mismatch." },
    "iat": { "type": "integer" },
    "exp": { "type": "integer", "description": "Hard cap 30 days after the key was generated; the attestation is re-signed at every SVID rotation before then." },
    "cnf": { "type": "object", "required": ["jwk"], "properties": { "jwk": { "type": "object", "required": ["kty", "crv", "x", "y"], "properties": { "kty": { "const": "EC" }, "crv": { "const": "P-256" }, "x": { "type": "string" }, "y": { "type": "string" }, "kid": { "type": "string" } } } } },
    "spiffe_id": { "type": "string", "pattern": "^spiffe://", "description": "The instance the key belongs to." },
    "previous": { "type": "object", "description": "Handover (F-10, written since 2026-09-17): the previous stable key's thumbprint, the last chain head it signed, and its public JWK. Carried by every attestation of the key that took over, under the SAME iss and spiffe_id: an instance keeps its SPIFFE ID across key changes (the last path segment names the instance from the first handover on, whatever it was derived from before). The same three values are the facts.previous of the instance.started entry the new key writes first, chained onto chain_head; a verifier accepts the new key only from there, and verifies what the retired key signed under previous.jwk once the retired key's last attestation (listed for at most an hour after the handover) has left the document.", "required": ["kid", "chain_head"], "properties": { "kid": { "type": "string", "description": "RFC 7638 thumbprint of the retired key." }, "chain_head": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "The hash of the last ledger entry the retired key signed; the handover entry's previous_hash." }, "jwk": { "type": "object", "required": ["kty", "crv", "x", "y"], "properties": { "kty": { "const": "EC" }, "crv": { "const": "P-256" }, "x": { "type": "string" }, "y": { "type": "string" } }, "description": "The retired key's public JWK, thumbprint equal to kid." } } }
  }
}
