Build the two JWTs of OAuth 2.0 Attestation-Based Client Authentication
(draft-ietf-oauth-attestation-based-client-auth-11), the client-side mirror
of Attesto.WalletAttestation.verify/3 and the client-auth method OID4VCI
recommends for native-app wallets over private_key_jwt/mTLS.
Two artifacts, signed by two different keys:
attestation/2- the Client Attestation JWT (typoauth-client-attestation+jwt), issued by the Wallet Provider (Client Attester) and signed by its key. It binds the wallet instance's public key intocnfand names the instance'sclient_idinsub. It is long-lived and reused across many requests; an:x5cheader lets the server chain the signer to a configured trust anchor.pop/2- the Client Attestation PoP JWT (typoauth-client-attestation-pop+jwt), minted fresh per request and signed by the instance key (the private half of the attestation'scnfkey), proving possession to oneaud(the server's identifier).
The client presents them in the OAuth-Client-Attestation and
OAuth-Client-Attestation-PoP headers; AttestoClient.OAuthHTTP's
{:client_attestation, ...} client-auth attaches both. Signing and key-bound
:alg/:kid validation behave as in AttestoClient.Wallet.Proof (shared
AttestoClient.Builder internals).
OAuthHTTP retries use_attestation_challenge once with a fresh PoP and
the response's OAuth-Client-Attestation-Challenge. Set :challenge in
the client-auth options to use a Challenge obtained proactively. Its
:attestation_challenge_received callback receives Challenge response
headers, including successful responses, so a host can retain the newest
Challenge for its next request. The callback receives one string.
Independent DPoP can accompany this authentication method using a separate
proof and key. The optional attest_jwt_client_auth_dpop combined mode is
not implemented; both attestation headers remain required.
Summary
Functions
Build a Client Attestation JWT, returning {:ok, compact_jws} or
{:error, reason}. Fails fast on invalid input.
Obtain a Client Attestation Challenge from an advertised challenge endpoint.
Build a Client Attestation PoP JWT, returning {:ok, compact_jws} or
{:error, reason}. Fails fast on invalid input.
Types
@type error() :: :invalid_key | :invalid_issuer | :invalid_client_id | :invalid_audience | :invalid_challenge | :invalid_instance_key | :invalid_x5c | :invalid_kid | :invalid_lifetime | :invalid_time | :invalid_jti | :unsupported_alg | :unsupported_key | {:signing_failed, String.t()}
@type jwk() :: JOSE.JWK.t() | map()
Functions
@spec attestation(jwk(), [attestation_opt()]) :: {:ok, String.t()} | {:error, error()}
Build a Client Attestation JWT, returning {:ok, compact_jws} or
{:error, reason}. Fails fast on invalid input.
provider_key is the Wallet Provider (Client Attester) private key that signs
the attestation. Required options:
:client_id- the wallet instance's client identifier (becomessub).:instance_key- the wallet instance's key; its public half is embedded as thecnfconfirmation JWK the PoP must be signed by.
Optional: :issuer names the Client Attester in the iss claim where the
ecosystem uses that claim; draft 11 does not require it. :x5c is optional
for the generic builder, but HAIP's Appendix E format requires the provider
certificate and any intermediate certificates, excluding the trust anchor,
in this base64 DER certificate list. The authorization server validates
signature, certificate trust and attestation validity. Renewals may retain
the same client subject and instance key without retaining the same JWT.
Other options include :lifetime (seconds to
exp, default 3600), and :alg,
:kid, :now as in AttestoClient.Wallet.Proof.build/2.
@spec fetch_challenge( String.t(), keyword() ) :: {:ok, %{ challenge: String.t(), dpop_nonce: String.t() | nil, expires_in: pos_integer() | nil }} | {:error, term()}
Obtain a Client Attestation Challenge from an advertised challenge endpoint.
Sends an empty, unauthenticated POST using the same endpoint screening and
timeout protections as the OAuth HTTP layer. Requires HTTP 200, JSON, and a
nonempty attestation_challenge; rejects duplicate JSON members. Response
bytes are limited to 16 KiB and challenge/nonce values to 4 KiB.
Returns %{challenge: challenge, dpop_nonce: nonce_or_nil, expires_in: seconds_or_nil}.
Supply challenge in the client-auth tuple's options, and dpop_nonce as
the :dpop_nonce request option when using DPoP. The most recently received
challenge and DPoP nonce supersede older values. expires_in is an optional
positive-integer extension, not a field required by the attestation draft.
Options include :timeout (milliseconds), :resolver, and :req_options.
Select the endpoint from trusted server metadata; this call performs no
issuer discovery and stores no challenge globally.
Build a Client Attestation PoP JWT, returning {:ok, compact_jws} or
{:error, reason}. Fails fast on invalid input.
instance_key is the wallet instance private key - the private half of the
attestation's cnf key. Required options:
:client_id- the wallet instance's client identifier (becomesiss).:audience- the server's identifier the PoP is presented to (aud); an AS issuer URL or a resource identifier, single-valued.
Optional: :challenge (echo a server-issued Challenge), :jti (default a
fresh random value), :lifetime (seconds to exp, default
120), and :alg, :kid, :now.