Skip to main content

Security model

OID4AC's threat model assumes a hostile agent, a hostile merchant, a hostile network, and a leaked token. None of these failure modes should let a charge complete without principal consent. The protocol's defences are layered so each individual leak still requires another to escalate.

Token theft is not enough

A leaked JWT-AT cannot be used by an attacker because the token is bound to the agent's DPoP keypair via the cnf.jkt claim. Every protected endpoint requires a fresh DPoP proof signed by the matching private key. See DPoP nonces and replay.

typ pinning defeats token confusion

JWT-ATs carry typ=at+jwt in the header (RFC 9068 §2.1). ID tokens, mandate VCs, and DPoP proofs all use distinct typ values. A verifier that accepts a wrong-typed token by mistake will fail the header check before reaching signature verification. See the JWT-AT claim set.

Algorithm whitelist defeats algorithm confusion

Every JWT and JWS verifier in the system rejects alg=none and enforces an explicit allow-list per surface. Mandatory tests (test_jwt_alg_none_rejected, test_jwt_rs256_rejected_outside_legacy, test_dpop_proof_hmac_rejected) gate every release. See the algorithm whitelist.

Replay defence

LayerMechanismStorage
DPoP proof replay(jkt, jti) tuple; 60-second windowRedis set oid4ac:dpop_jti:{jkt}
DPoP nonce pre-generationServer-issued DPoP-Nonce; rotated on every error and randomly on successRedis oid4ac:nonce:{id} TTL 90 s
Authorization code replaySingle-use with cascade revoke on second useRedis oid4ac:authz_code:{code} TTL 60 s
Refresh token replayRotation on every exchange; family-wide revoke on second use of a rotated RTRedis oid4ac:rt:{rt_id} and family set
KB-JWT presentation replayPer-merchant single-use nonce of the form sha256(merchant_nonce || offer_digest)Merchant-side nonce log
client_assertion replayjti single-use within the assertion's expRedis oid4ac_client_assertion_jti_seen

Audience binding

Every JWT-AT carries aud=<merchant origin> per RFC 8707. A merchant rejects any token whose aud does not match its own origin. The SD-JWT VC mandate carries the same aud; the KB-JWT binds the presentation to the same origin. Cross-merchant token reuse fails at three layers. See resource indicators and audience.

Issuer pinning

Every authorization response carries iss=https://as.oid4pay.com per RFC 9207. Mix-up attacks where an attacker substitutes a hostile AS are detected by the agent / wallet on the redirect leg. The Wallet Portal and every SDK enforce this.

Separation of duties

ConcernKey material
Agent DPoP proofsDistinct private key, registered via DCR as dpop_jwk
Agent client assertionsDistinct private key, registered via DCR as private_key_jwt_jwk
AS JWT-AT signingEd25519, rotated 90 days; kid in JWKS history
AS mandate signingSame Ed25519 key as JWT-AT in version 1; separating into a distinct kid is on the roadmap
Audit chain head signingDistinct Ed25519, never used elsewhere

Key rotation

Ed25519 signing keys rotate on a 90-day cadence; the JWKS publishes both the current and previous kid for the overlap window. JWT verifiers MUST honour kid lookup, not just current-key check. A documented key-rotation runbook describes the staged rollout.

Mandate revocation

Every mandate VC carries a credentialStatus claim pointing at a W3C VC Status List 2021 endpoint at /oauth/status-list. The AS publishes the list every 60 seconds. Merchants fetch the bitmap once per 5 minutes and look up the index locally. See the W3C VC Status List.

Audit chain

Every SSF event is wrapped in the audit-chain entry envelope, sequence-numbered per tenant, and prev-hash chained. The chain head is signed every hour; rewriting a past entry breaks the chain at the next signing. WORM exports land in S3 Object Lock for SOC2 (7y) and eIDAS (10y) retention classes. See the audit chain entry envelope.

What an attacker still cannot do

  1. Steal a JWT-AT and replay it: rejected by DPoP cnf.jkt check.
  2. Steal a JWT-AT and use it cross-merchant: rejected by aud check.
  3. Forge a mandate: requires the AS signing key.
  4. Forge an Offer: requires the merchant's signing key.
  5. Replay a charge: rejected by KB-JWT nonce single-use.
  6. Rewrite an audit entry: detected by prev-hash break at the next chain-head signing.
  7. Strip the four-signature pack: a dispute fails closed; the merchant is liable.