Attesto.WalletAttestation (Attesto v1.8.0)

Copy Markdown View Source

OAuth 2.0 Attestation-Based Client Authentication (draft-ietf-oauth-attestation-based-client-auth-10, 2026-07-06), the "Wallet Attestation" client authentication method OID4VCI recommends for native-app Wallets in place of private_key_jwt/mTLS.

A Wallet Provider (the Client Attester) issues its Wallet a Client Attestation JWT (typ oauth-client-attestation+jwt) binding the Wallet/Client Instance's public key into its cnf claim. To authenticate a request, the Client Instance additionally presents a Client Attestation PoP JWT (typ oauth-client-attestation-pop+jwt) signed by that instance key, proving possession of it to this Authorization or Resource Server.

verify/3 verifies both JWTs together per the draft's §7 "Verification and Processing" rules and returns the proven Client Instance key. The host owns the trusted Wallet Provider key material and any Challenge issuance/tracking; this module only performs the checks the draft prescribes. Conn-free and fail-closed.

Client Attestation JWT (typ=oauth-client-attestation+jwt, draft §4)

  • sub - REQUIRED. The OAuth client_id of the Wallet instance.
  • exp - REQUIRED. Expiration time; rejected once passed.
  • cnf - REQUIRED. A {"jwk" => <public JWK>} (RFC 7800) confirmation key - the Client Instance Key used to sign the PoP JWT.
  • iat - OPTIONAL.

The draft version implemented here carries no aud on the Client Attestation JWT itself (earlier drafts did; it was removed) - the receiving server's identity is asserted by the PoP JWT's aud instead. Trust in the signer (the Wallet Provider / Client Attester) is out-of-band, per host-supplied trusted keys; the draft leaves the discovery mechanism (PKI, kid+jku, pre-shared metadata) unspecified.

Client Attestation PoP JWT (typ=oauth-client-attestation-pop+jwt, draft §5.1)

  • aud - REQUIRED. This Authorization Server's issuer identifier URL (RFC 8414) or, for a Resource Server, its resource identifier URL (RFC 9728). Single-valued; a Client Attestation PoP JWT targets one audience only.
  • jti - REQUIRED. A unique identifier the caller may use for its own replay tracking (see :replay_check).
  • iat - REQUIRED. Freshness is checked against :max_age_seconds.
  • challenge - OPTIONAL. Echoes a server-issued Challenge (draft §6); checked when the caller supplies :expected_challenge.

It MUST be signed by the private half of the Client Attestation's cnf key - this module verifies exactly that.

Summary

Functions

Verify a Client Attestation JWT together with its Client Attestation PoP JWT and return the proven Client Instance key.

Types

instance_key()

@type instance_key() :: %{jwk: map(), jkt: String.t()}

replay_check_fun()

@type replay_check_fun() :: (String.t(), pos_integer() -> :ok | {:error, :replay})

verified()

@type verified() :: %{
  instance_key: instance_key(),
  attestation_claims: map(),
  pop_claims: map(),
  replay_key: String.t(),
  replay_ttl: pos_integer()
}

verify_error()

@type verify_error() ::
  :invalid_attestation
  | :invalid_typ
  | :invalid_alg
  | :unsupported_critical_header
  | :invalid_signature
  | :expired
  | :invalid_client_id
  | :missing_cnf
  | :invalid_cnf
  | :invalid_pop
  | :invalid_pop_typ
  | :invalid_pop_alg
  | :unsupported_pop_critical_header
  | :invalid_pop_signature
  | :invalid_pop_audience
  | :invalid_pop_challenge
  | :missing_pop_jti
  | :invalid_pop_jti
  | :missing_pop_iat
  | :invalid_pop_iat
  | :pop_expired
  | :replay

verify_opts()

@type verify_opts() :: [
  trusted_wallet_provider_jwks: map() | [map()],
  audience: String.t(),
  client_id: String.t(),
  expected_challenge: String.t(),
  now: DateTime.t() | non_neg_integer(),
  max_age_seconds: pos_integer(),
  accepted_algs: [Attesto.SigningAlg.alg()],
  enforce_fapi_alg_policy: boolean(),
  replay_check: replay_check_fun() | nil
]

Functions

verify(attestation, pop, opts \\ [])

@spec verify(String.t(), String.t(), verify_opts()) ::
  {:ok, verified()} | {:error, verify_error()}

Verify a Client Attestation JWT together with its Client Attestation PoP JWT and return the proven Client Instance key.

Required opts

  • :trusted_wallet_provider_jwks - an RFC 7517 JWK Set, a single public JWK map, or a list of public JWK maps the Client Attestation JWT's signature is checked against. Establishing which Wallet Provider(s) to trust is the host's responsibility (draft §7.1 leaves this out of scope).
  • :audience - this Authorization/Resource Server's own identifier; the PoP JWT's aud MUST equal it exactly.

Optional opts

  • :client_id - when set, the Client Attestation's sub MUST equal it.
  • :expected_challenge - a Challenge (draft §6) previously issued to the client; when set, the PoP's challenge claim MUST match it. The Challenge is a server-issued, non-secret freshness token (visible on the wire already), so this is a plain equality check, matching Attesto.CredentialProof's c_nonce check.
  • :now - clock reference (DateTime or unix seconds).
  • :max_age_seconds - how far in the past the PoP's iat may be. Default 300.
  • :accepted_algs - JWS algorithms accepted for both JWTs' signatures. Defaults to Attesto.SigningAlg.fapi_algs/0.
  • :enforce_fapi_alg_policy - additionally enforce the FAPI RSA modulus / Edwards curve restrictions on the Client Attestation signer's key. Defaults to true when :accepted_algs is omitted, false otherwise (matches Attesto.ClientAssertion.verify/5).
  • :replay_check - a 2-arity function (replay_key, ttl_seconds) -> :ok | {:error, :replay}, called after every other PoP check passes. replay_key is a fixed-length digest namespacing the jti by the Client Instance Key's thumbprint - do not assume it is the raw jti. Omitted, no replay check is performed inline; the caller may instead record the returned replay_key/replay_ttl itself after any later binding step (see Attesto.DPoP's "Replay protection" for why that order matters when there is one).

Returns {:ok, %{instance_key: %{jwk:, jkt:}, attestation_claims:, pop_claims:, replay_key:, replay_ttl:}}.