Verifier-side OID4VP presentation-session state machine.
create/3 persists the nonce, audience, requested DCQL IDs, and issuer trust
needed to verify a later direct_post response. The opaque session id is
also the OID4VP state value, so a wallet response can be correlated without
a second index or identifier. An optional :response_uri attr is also
stored and forwarded to Attesto.VpToken.verify/2; it is required only if
the session expects an mso_mdoc presentation (see Attesto.VpToken's
moduledoc) and may be omitted for SD-JWT-VC-only sessions exactly as before.
Verification delegates all SD-JWT VC / mdoc and holder-binding cryptography
to Attesto.VpToken.verify/2. A malformed or invalid presentation leaves the
session pending, allowing a valid response to arrive before expiry. Only a
successfully verified response attempts the store's atomic completion.
A {:resolve_issuer, fun} trust source contains an in-memory function and is
therefore suitable only for in-memory stores such as the bundled ETS store.
Persistent stores must use static {:issuer_jwks, jwks} trust material (or
define their own serializable trust-reference convention outside this core
primitive).
Summary
Functions
Attach the signed OID4VP request object to a pending session.
Attach the verifier's per-request response-encryption private JWK to a pending
session (its kid is the session id), so the direct-post endpoint can decrypt
a direct_post.jwt response with the ephemeral key it advertised.
Create and persist a short-lived OID4VP presentation session.
Read a pending session's stored request object (the signed OID4VP request
object the interface serves at its request_uri), if one was persisted at
create/3 via the optional :request_object attr. Returns :error for an
unknown, expired, or request-object-less session.
Read a pending session's per-request response-encryption private JWK, if one
was attached. Returns :error for an unknown, expired, or key-less session.
Read and consume a completed session's verified result. Single-use.
Verify a wallet response and atomically complete its pending session.
Types
@type create_attrs() :: %{ :audience => String.t(), :expected_query_ids => [String.t()], :issuer_trust => issuer_trust(), optional(:request_object) => String.t(), optional(:response_uri) => String.t() }
Functions
Attach the signed OID4VP request object to a pending session.
Called once at creation time (the request object needs the session's nonce
and its id/state, which create/3 generates). Atomic on the pending
status. Returns {:error, :unavailable} if the session is unknown, expired,
or already completed.
Attach the verifier's per-request response-encryption private JWK to a pending
session (its kid is the session id), so the direct-post endpoint can decrypt
a direct_post.jwt response with the ephemeral key it advertised.
@spec create(module(), create_attrs(), keyword()) :: {:ok, %{id: String.t(), nonce: String.t()}} | {:error, :invalid_attrs}
Create and persist a short-lived OID4VP presentation session.
The returned id is both the store key and the request's state; nonce
belongs in the presentation request. Options are :ttl (default
300 seconds) and :now (a clock override).
Read a pending session's stored request object (the signed OID4VP request
object the interface serves at its request_uri), if one was persisted at
create/3 via the optional :request_object attr. Returns :error for an
unknown, expired, or request-object-less session.
Read a pending session's per-request response-encryption private JWK, if one
was attached. Returns :error for an unknown, expired, or key-less session.
Read and consume a completed session's verified result. Single-use.
The result is returned at most once: the completed session is atomically
removed on read (via the store's take/1). This bounds exposure of the
presented — potentially PII — claims. The response_code a verifier hands the
browser to trigger this read is the session id, and it transits the browser
address bar, history, Referer, and logs; a non-consuming read would let
anyone who later captured that value replay it to re-read the claims for the
rest of the session TTL. Single-use closes that: the verifier front-end reads
the result once, on the completion redirect, and a captured response_code is
dead afterwards. A second read (or a read of a still-pending/expired session)
returns :error.
Returns the same shape as verify_response/4 — the VpToken results map
directly — so the live-return and read-back paths handle one shape, not two.
@spec verify_response(module(), correlation(), map(), keyword()) :: {:ok, map()} | {:error, :unknown_session | :expired | :already_completed | {:invalid_presentation, term()}}
Verify a wallet response and atomically complete its pending session.
Invalid presentations return {:invalid_presentation, reason} and do not
complete the session. When concurrent valid responses race, exactly one can
complete it; all losing calls return :already_completed.