AttestoClient.Wallet (AttestoClient v2.9.0)

Copy Markdown View Source

OpenID4VCI 1.0 Wallet (Holder) issuance flow.

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), decrypting an encrypted response when negotiated with the issuer; a deferred response is polled when a Deferred Credential Endpoint is supplied; 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(),
  optional(:authority_key_identifiers) => [String.t()],
  optional(:issuer_certificate_chain) => [binary()]
}

opt()

@type opt() ::
  {:credential_configuration_id, String.t()}
  | {:credential_endpoint, String.t()}
  | {:credential_issuer_metadata, map()}
  | {:credential_encryption, :auto | :required | :disabled}
  | {:credential_response_encryption_key, AttestoClient.Wallet.Proof.jwk()}
  | {:deferred_credential_endpoint, String.t()}
  | {:deferred_poll, boolean()}
  | {:deferred_timeout, pos_integer()}
  | {:deferred_max_attempts, pos_integer()}
  | {:deferred_clock, (-> integer())}
  | {:deferred_sleep, (non_neg_integer() -> :ok | {:error, term()})}
  | {:token_endpoint, String.t()}
  | {:nonce_endpoint, String.t()}
  | {:notification_endpoint, String.t()}
  | {:notification_event, String.t()}
  | {:access_token, String.t() | AttestoClient.TokenSet.t()}
  | {:tx_code, String.t()}
  | {:client_id, String.t()}
  | {:client_auth, term()}
  | {:format, String.t()}
  | {:trusted, term()}
  | {:haip, boolean()}
  | {:trusted_certificates, [binary()]}
  | {:certificate_trust, ([binary()] -> :ok | {:error, 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()}
  | {:alg, String.t()}
  | {:kid, String.t()}
  | {: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() :: %{
  notification_id: String.t() | nil,
  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. :alg and :kid select the DPoP proof's signing algorithm and key identifier; :proof_alg and :proof_kid configure the separate OID4VCI credential proof.

HAIP requires DPoP and authenticated token acquisition. When supplying an existing token, pass access_token: tokens with the complete locally bound AttestoClient.TokenSet and the same DPoP private key. Bare token strings remain available for generic flows only. A retained HAIP profile cannot be disabled by passing haip: false to credential issuance.

: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 §11 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).

:credential_issuer_metadata accepts metadata already fetched and validated by the caller. Its credential_issuer must equal the offer's issuer. Endpoint options may be omitted when present in this metadata; conflicting endpoint overrides are rejected. Encryption uses ECDH-ES/P-256 and A128GCM or A256GCM. :credential_encryption defaults to :auto, which requests encryption when advertised. :required requires encrypted requests and responses; :disabled is allowed only when the issuer does not require encryption. Response encryption always also encrypts the request, as required by §8.2.

:deferred_credential_endpoint enables bounded polling of a deferred result. :deferred_poll can be set to false to retain a pending marker. Polling respects the issuer's positive interval, with at most 10 attempts and a 120-second overall deadline by default. :deferred_max_attempts (1–100) and :deferred_timeout (milliseconds, at most one hour) customize these bounds; :deferred_clock and :deferred_sleep provide injectable monotonic clock and wait functions. The response encryption key remains private to this flow.