AttestoClient.WalletAttestation (AttestoClient v2.9.0)

Copy Markdown View Source

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 (typ oauth-client-attestation+jwt), issued by the Wallet Provider (Client Attester) and signed by its key. It binds the wallet instance's public key into cnf and names the instance's client_id in sub. It is long-lived and reused across many requests; an :x5c header lets the server chain the signer to a configured trust anchor.

  • pop/2 - the Client Attestation PoP JWT (typ oauth-client-attestation-pop+jwt), minted fresh per request and signed by the instance key (the private half of the attestation's cnf key), proving possession to one aud (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

attestation_opt()

@type attestation_opt() ::
  {:client_id, String.t()}
  | {:issuer, String.t()}
  | {:instance_key, jwk()}
  | {:x5c, [String.t()]}
  | {:lifetime, pos_integer()}
  | {:alg, String.t()}
  | {:kid, String.t()}
  | {:now, integer()}

error()

@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()}

jwk()

@type jwk() :: JOSE.JWK.t() | map()

pop_opt()

@type pop_opt() ::
  {:client_id, String.t()}
  | {:audience, String.t()}
  | {:challenge, String.t()}
  | {:lifetime, pos_integer()}
  | {:jti, String.t()}
  | {:alg, String.t()}
  | {:kid, String.t()}
  | {:now, integer()}

Functions

attestation(provider_key, opts)

@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 (becomes sub).
  • :instance_key - the wallet instance's key; its public half is embedded as the cnf confirmation 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.

fetch_challenge(endpoint, opts \\ [])

@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.

pop(instance_key, opts)

@spec pop(jwk(), [pop_opt()]) :: {:ok, String.t()} | {:error, error()}

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 (becomes iss).
  • :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.