AttestoClient.Wallet (AttestoClient v2.3.0)

Copy Markdown View Source

OID4VCI Wallet (Holder) issuance flow (draft-ietf-oauth-openid4vci).

request_credential/3 drives the pre-authorized_code issuance flow end to end:

  1. exchange the offer's pre-authorized code for an access token (AttestoClient.Token.exchange_pre_authorized_code/2) - skipped when an :access_token is already supplied, e.g. obtained separately through the authorization_code flow (AttestoClient.AuthorizationCode);
  2. fetch a fresh c_nonce when the issuer advertises a nonce endpoint;
  3. build the holder key proof (AttestoClient.Wallet.Proof);
  4. POST the Credential Request and parse the Credential Response (Attesto.CredentialResponse shape) - a transaction_id response is returned as a :pending marker (deferred issuance is not polled here); and
  5. verify each returned credential with the attesto verifier matching :format (Attesto.SdJwtVc, Attesto.JwtVc, or Attesto.Mdoc).

All HTTP goes through AttestoClient.OAuthHTTP, so the flow is mockable the same way as the rest of this library (req_options: [plug: ...]).

Summary

Functions

Run the wallet-holder issuance flow for offer and return the verified, held credential(s) (or a pending marker for deferred issuance).

Types

held_credential()

@type held_credential() :: %{
  :format => String.t(),
  :credential => String.t(),
  :claims => map(),
  :holder_binding => map() | nil,
  optional(:doc_type) => String.t()
}

opt()

@type opt() ::
  {:credential_configuration_id, String.t()}
  | {:credential_endpoint, String.t()}
  | {:token_endpoint, String.t()}
  | {:nonce_endpoint, String.t()}
  | {:notification_endpoint, String.t()}
  | {:notification_event, String.t()}
  | {:access_token, String.t()}
  | {:tx_code, String.t()}
  | {:client_id, String.t()}
  | {:client_auth, term()}
  | {:format, String.t()}
  | {:trusted, term()}
  | {:verify_opts, keyword()}
  | {:proof_alg, String.t()}
  | {:proof_kid, String.t()}
  | {:key_attestation,
     String.t()
     | ([map()], String.t() | nil -> {:ok, String.t()} | {:error, term()})}
  | {:dpop, AttestoClient.Wallet.Proof.jwk()}
  | {:now, integer()}
  | {:req_options, keyword()}
  | {:timeout, pos_integer()}

pending_credential()

@type pending_credential() :: %{
  status: :pending,
  transaction_id: String.t(),
  notification_id: String.t() | nil
}

result()

@type result() :: %{
  credentials: [held_credential() | pending_credential()],
  c_nonce: String.t() | nil
}

Functions

request_credential(offer, holder_key, opts)

@spec request_credential(
  AttestoClient.Wallet.CredentialOffer.t(),
  AttestoClient.Wallet.Proof.jwk() | [AttestoClient.Wallet.Proof.jwk()],
  [opt()]
) :: {:ok, result()} | {:error, term()}

Run the wallet-holder issuance flow for offer and return the verified, held credential(s) (or a pending marker for deferred issuance).

Required options: :credential_endpoint, :format (one of "vc+sd-jwt", "dc+sd-jwt", "jwt_vc_json", or "mso_mdoc"), and :trusted (the Credential Issuer's verification key material, in the shape the matching attesto verifier expects - a JWKS/JWK list for Attesto.SdJwtVc and Attesto.JwtVc, a single JWK/PEM for Attesto.Mdoc). :verify_opts are forwarded to that verifier (e.g. :accepted_algs, :now).

Unless :access_token is supplied, the flow performs the pre-authorized_code token exchange itself and requires :token_endpoint and, when the offer's grant carries one, :tx_code. :client_id and :client_auth authenticate that token request exactly as in AttestoClient.Token, and, when present, :client_id is also carried as the proof's iss.

:nonce_endpoint, when supplied, is used to fetch a fresh c_nonce before building the proof. :credential_configuration_id selects which of the offer's configuration ids to request; it defaults to the offer's only id when there is exactly one.

:dpop (a JOSE.JWK or JWK map) sender-constrains the flow with RFC 9449 DPoP proofs. The one key is used at the token endpoint (binding the access token to its jkt) and at the Credential Endpoint (where the proof carries the ath binding to that token), and a use_dpop_nonce challenge is retried once - see AttestoClient.DPoP.

:key_attestation (a compact JWT from AttestoClient.KeyAttestation.build/2) is carried in the holder proof's key_attestation header, vouching to the issuer that the holder key is held in secure storage (a HAIP requirement).

:notification_endpoint, when supplied, makes the wallet POST an OID4VCI §10 Notification acknowledging the credential once issuance succeeds and the response carried a notification_id; :notification_event overrides the default "credential_accepted". The returned result always includes the issuer's notification_id (or nil).