Attesto.PresentationSession (Attesto v1.14.0)

Copy Markdown View Source

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

correlation()

@type correlation() :: {:state, String.t()} | {:id, String.t()}

create_attrs()

@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(),
  optional(:query_constraints) => map()
}

issuer_trust()

@type issuer_trust() ::
  {:issuer_jwks, map() | list()} | {:resolve_issuer, (String.t() -> term())}

Functions

attach_request_object(store, id, request_object)

@spec attach_request_object(module(), String.t(), String.t()) ::
  :ok | {:error, :unavailable}

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_response_encryption_jwk(store, id, jwk)

@spec attach_response_encryption_jwk(module(), String.t(), map()) ::
  :ok | {:error, :unavailable}

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(store, attrs, opts \\ [])

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

request_object(store, id)

@spec request_object(module(), String.t()) :: {:ok, String.t()} | :error

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.

response_encryption_jwk(store, id)

@spec response_encryption_jwk(module(), String.t()) :: {:ok, map()} | :error

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.

result(store, id)

@spec result(module(), String.t()) :: {:ok, map()} | :error

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.

verify_response(store, correlation, vp_token, opts \\ [])

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