Documentation
¶
Overview ¶
Package oid4vp implements the verifier (Relying Party) side of OpenID for Verifiable Presentations 1.0: signed Request Objects (RFC 9101 JAR) carrying the operator WRPAC, request_uri lifecycle, encrypted direct_post.jwt response processing with per-session ephemeral keys, Digital Credentials API shapes (Annex A), same-device response_code return (§8.2/§8.3), and OID4VP error responses.
The package is storage-free (SessionStore interface; in-memory reference implementation; contract suite in storetest) and HTTP-free (services own transport). All cryptography is delegated to github.com/gmb-eudi/go-eudi-crypto; algorithms come from configuration validated against the ECCG policy, never from tokens.
Error mapping guidance for services (docs/conventions.md taxonomy) is on each sentinel in errors.go; wallet-boundary serialization is (*Engine).ErrorResponse — OID4VP error shapes, not problem+json.
Index ¶
- Constants
- Variables
- func SessionTranscriptFor(p Presentation) (mdoc.SessionTranscript, error)
- func TransactionDataHashes(td [][]byte, alg string, policy crypto.Policy) ([]string, error)
- type Config
- type Engine
- func (e *Engine) AbsorbWalletMetadata(s *Session, raw []byte) error
- func (e *Engine) ClientID() string
- func (e *Engine) ConsumeResponseCode(s *Session, code ResponseCode) error
- func (e *Engine) DCAPIRequest(ctx context.Context, s *Session) ([]byte, error)
- func (e *Engine) DCAPIUnsignedRequest(s *Session) ([]byte, error)
- func (e *Engine) ErrorResponse(err error) (int, []byte)
- func (e *Engine) NewSession(ctx context.Context, spec RequestSpec) (*Session, WalletInvocation, error)
- func (e *Engine) ProcessDCAPIResponse(ctx context.Context, s *Session, origin string, data []byte) ([]Presentation, error)
- func (e *Engine) ProcessResponse(ctx context.Context, s *Session, r RawResponse) ([]Presentation, ResponseCode, error)
- func (e *Engine) RedirectURI(s *Session) (string, error)
- func (e *Engine) RequestObjectJWT(ctx context.Context, s *Session) ([]byte, error)
- func (e *Engine) ValidateTransactionDataEcho(s *Session, echoedHashes []string, hashAlgName string) error
- type Flow
- type MemStore
- type Presentation
- type RawResponse
- type RequestSpec
- type ResponseCode
- type ResponseEncryption
- type Session
- type SessionStore
- type VPFormats
- type WalletError
- type WalletInvocation
Constants ¶
const ( // DefaultSessionTTL bounds request_uri fetch + wallet interaction. DefaultSessionTTL = 5 * time.Minute // DefaultMaxResponseBody caps wallet-posted bodies (T-08.6 oversized // body defense). DefaultMaxResponseBody = 1 << 20 )
const ClientIDPrefixX509SANDNS = "x509_san_dns"
ClientIDPrefixX509SANDNS is the only client_id prefix supported in v1 (OID4VP §5 client identifier prefixes; WP-08 decision — verifier_attestation is an extension point, rejected by New until implemented).
Variables ¶
var ( // Session / store lifecycle. ErrSessionInvalid = errors.New("oid4vp: invalid session") // programming error; 500 at boundary ErrSessionNotFound = errors.New("oid4vp: session not found or expired") // err:session:not-found ErrSessionExpired = errors.New("oid4vp: session expired") // err:session:not-found ErrSessionConsumed = errors.New("oid4vp: session already consumed") // err:session:consumed ErrSessionNotConsumed = errors.New("oid4vp: session must be obtained via SessionStore.ConsumeOnce") // Engine configuration / request spec. ErrConfig = errors.New("oid4vp: invalid engine config") ErrSANMismatch = errors.New("oid4vp: client DNS name does not match a SAN dNSName in the WRPAC leaf") // OID4VP §5 x509_san_dns ErrUnsupportedClientIDPrefix = errors.New("oid4vp: unsupported client_id prefix") // WP-08 decision: x509_san_dns only in v1 ErrSpec = errors.New("oid4vp: invalid request spec") ErrNoRegistration = errors.New("oid4vp: registration reference required in every request (ARF RPRC_19a)") ErrFlowMismatch = errors.New("oid4vp: operation not valid for this session flow") // request_uri lifecycle (T-08.4). ErrRequestURIConsumed = errors.New("oid4vp: request object already served (single-use request_uri)") // OID4VP §5 request_uri ErrWalletMetadataInvalid = errors.New("oid4vp: wallet metadata rejected") // Response processing (T-08.5/6/7). All map to // err:presentation:invalid-response unless noted. ErrBodyTooLarge = errors.New("oid4vp: response body exceeds configured cap") ErrMalformedResponse = errors.New("oid4vp: malformed response") ErrDecrypt = errors.New("oid4vp: response decryption failed") // stale/foreign key, bad JWE ErrStateMismatch = errors.New("oid4vp: state does not match session") // err:presentation:nonce-mismatch ErrAPVMismatch = errors.New("oid4vp: JWE apv does not match the session nonce") // err:presentation:nonce-mismatch ErrUnknownCredentialID = errors.New("oid4vp: vp_token key does not identify a credential query") // Same-device return (T-08.8), OID4VP §8.2/§8.3/§12.1. ErrNoResponseCode = errors.New("oid4vp: no response_code minted for this session") ErrResponseCodeMismatch = errors.New("oid4vp: response_code is not bound to this session") // err:presentation:nonce-mismatch ErrResponseCodeConsumed = errors.New("oid4vp: response_code already used") // err:session:consumed // DCAPI (T-08.9), OID4VP Annex A. ErrOriginNotExpected = errors.New("oid4vp: response origin not in expected_origins") // transaction_data (T-08.10, phase-2 flag). ErrTransactionDataDisabled = errors.New("oid4vp: transaction_data requested but the phase-2 flag is off") ErrTransactionDataInvalid = errors.New("oid4vp: invalid transaction_data entry") ErrTransactionDataMissing = errors.New("oid4vp: transaction_data_hashes missing from presentation") ErrTransactionDataUnexpected = errors.New("oid4vp: transaction_data_hashes present but none were requested") ErrTransactionDataMismatch = errors.New("oid4vp: transaction_data_hashes do not match the request") // SessionTranscript delegation (T-08.7). ErrTranscriptParams = errors.New("oid4vp: presentation lacks the parameters for a SessionTranscript") )
Sentinel errors. Libraries return typed errors; services map them to err:domain:reason problem codes (docs/conventions.md). The mapping each sentinel is expected to receive is noted inline. None of these messages ever carries attribute values, request bodies, or token contents (hard rule 3).
Functions ¶
func SessionTranscriptFor ¶
func SessionTranscriptFor(p Presentation) (mdoc.SessionTranscript, error)
SessionTranscriptFor builds the ISO 18013-5 SessionTranscript for one verified-to-be mso_mdoc Presentation by delegating to the go-mdoc constructors (OID4VP Annex B.2; ISO/IEC TS 18013-7 Annex B; Annex A for DCAPI):
- request_uri flows: OID4VPHandover(clientID, nonce, jwkThumbprint, responseURI).
- DCAPI flows (Origin set): OID4VPDCAPIHandover(origin, nonce, jwkThumbprint) — no client_id or response_uri in this variant.
jwkThumbprint (Presentation.JWKThumbprint) is the RFC 7638 thumbprint of the RP's OWN ephemeral response-encryption public key — computed by ProcessResponse from Session.EphemeralKeyPKCS8, never from anything wallet-supplied. The JWE apu value is NOT an input to either constructor here and is not read anywhere in the pipeline (T-08.7 correction 2026-07-06: this task's original brief assumed apu/mdocGeneratedNonce filled this slot; the go-mdoc constructors' actual, EU-reference-verified shape takes jwkThumbprint instead — see WP-08 README Decisions "T-08.7/T-08.9 correction" for the full rationale).
FLAG (carried from go-mdoc's OID4VPHandover/OID4VPDCAPIHandover doc comments and docs/mdoc-eu-gap-report.md): this handover shape is corroborated against a production EU reference verifier, not yet byte-for-byte confirmed against the OpenID4VP 1.0 Annex B.2 primary spec text (not vendored under references/ at the time of writing) — re-verify once that text is available.
verifier-core passes the result to mdoc.Verifier.Verify as VerifyInput.SessionTranscript (WP-03). Fail closed: incomplete parameters are an error, never a zero transcript. Pure delegation — no CBOR/SessionTranscript construction of its own.
func TransactionDataHashes ¶
TransactionDataHashes computes the expected transaction_data_hashes for a set of request entries: base64url( H( base64url(entry) ) ), where H is the digest bound to alg by the ECCG policy (OID4VP §5 transaction_data; HAIP baseline ES256 ⇒ SHA-256). No hash literal here — the algorithm comes from the policy (hard rule 4).
Types ¶
type Config ¶
type Config struct {
Keys crypto.KeyProvider // operator signing key (WRPAC key)
SigningKeyID string
WRPACChain []*x509.Certificate // leaf first; goes into x5c (CIR 2024/2982 Art. 3)
ClientDNSName string // must match a SAN dNSName of the leaf
ClientIDPrefix string // "" = x509_san_dns; anything else is rejected in v1
Policy crypto.Policy // nil = crypto.ECCG()
Clock func() time.Time // nil = time.Now
Rand io.Reader // nil = crypto/rand.Reader
RequestURIBase string // e.g. https://verifier.example.com/request — session id is appended (default request_uri builder; ignored when RequestURIFunc is set)
// RequestURIFunc, when set, fully controls the request_uri embedded in
// the wallet invocation (OID4VP §5: the spec only requires client_id +
// request_uri + request_uri_method by reference, not any particular URL
// shape) — the consumer owns its own routing and builds the exact URL
// its bound route expects. When nil, RequestURIBase+"/"+id is used
// (backward-compatible default). RequestURIBase may be empty when this
// is set.
RequestURIFunc func(sessionID string) string
UniversalLinkBase string // wallet universal-link endpoint, e.g. https://wallet.example.org/authorize
SessionTTL time.Duration // 0 = DefaultSessionTTL
MaxResponseBody int // 0 = DefaultMaxResponseBody
ResponseEncryption ResponseEncryption
VPFormats VPFormats
EnableTransactionData bool // phase-2 flag (T-08.10)
}
Config assembles an Engine. Everything is explicit and validated in New; zero values that have safe defaults are documented per field.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the OpenID4VP verifier protocol engine (WP-08 README). It is stateless between calls: all per-verification state lives in Session.
func New ¶
New validates cfg and derives the client identifier from the WRPAC leaf. Fail closed: any unknown algorithm/curve, prefix, or malformed base URL is a construction error (hard rule 7).
func (*Engine) AbsorbWalletMetadata ¶
AbsorbWalletMetadata is the T-08.4 wallet-metadata absorption hook (OID4VP §5 request_uri_method). v1 pins request_uri_method=get, so no metadata arrives on the request_uri fetch yet; services call this when the future post method (or a DCAPI capability hint) delivers one. The document is validated as a JSON object, size-capped, and stored verbatim on the session for policy use — never parsed further here, never logged (hard rule 3: treat as untrusted).
func (*Engine) ClientID ¶
ClientID returns the full prefixed client identifier, e.g. "x509_san_dns:verifier.example.com" (OID4VP §5).
func (*Engine) ConsumeResponseCode ¶
func (e *Engine) ConsumeResponseCode(s *Session, code ResponseCode) error
ConsumeResponseCode redeems a response_code: constant-time comparison against the code minted FOR THIS SESSION, single-use. This is the §12.1 session-fixation defense — an attacker who fixated their own session id on a victim cannot fetch the victim's result: their code is bound to their session. The caller persists s (SessionStore.Save) so the used-marker sticks.
func (*Engine) DCAPIRequest ¶
DCAPIRequest renders the SIGNED dc_api.jwt request member for navigator.credentials.get (OID4VP Annex A): a JAR-typed JWT with the WRPAC x5c, expected_origins (REQUIRED for signed requests), and no response_uri/state — the browser returns the response and the origin is validated instead.
func (*Engine) DCAPIUnsignedRequest ¶
DCAPIUnsignedRequest renders the UNSIGNED dc_api variant (Annex A): the request parameters as a plain JSON object — no client_id (the calling origin is the identity). WP-08 decision: the unsigned variant still uses response_mode dc_api.jwt because HAIP makes response encryption mandatory.
func (*Engine) ErrorResponse ¶
ErrorResponse serializes an engine error for the WALLET boundary in the OpenID4VP Error Response format ({"error":..,"error_description":..}), NOT problem+json — the protocol wins on the wallet boundary (docs/conventions.md). status is a plain int so this library imports no HTTP package (ADR-0004). Services map the SAME sentinels to err:domain:reason problem codes for their own logging/metrics.
Unknown or internal errors collapse to 500 server_error: fail closed, leak nothing.
func (*Engine) NewSession ¶
func (e *Engine) NewSession(ctx context.Context, spec RequestSpec) (*Session, WalletInvocation, error)
NewSession validates spec, generates the session secrets (id, nonce, state — ≥128-bit from the injected rand; OID4VP §5, §12.1) and the per-session ephemeral response-encryption key (WP-08 decision), and returns the wallet invocation. The caller persists the session (SessionStore.Save).
func (*Engine) ProcessDCAPIResponse ¶
func (e *Engine) ProcessDCAPIResponse(ctx context.Context, s *Session, origin string, data []byte) ([]Presentation, error)
ProcessDCAPIResponse handles the browser-returned dc_api.jwt response (OID4VP Annex A): validate the calling origin against expected_origins, then decrypt and parse like direct_post.jwt — but with no state member (the browser context provides correlation; binding is nonce-based plus origin validation, WP-08 decision) and the DCAPI handover. Fail closed: an origin absent from expected_origins is rejected with ErrOriginNotExpected before any decryption — the DCAPI analog of the §8.2 state binding. Precondition: s obtained via SessionStore.ConsumeOnce.
func (*Engine) ProcessResponse ¶
func (e *Engine) ProcessResponse(ctx context.Context, s *Session, r RawResponse) ([]Presentation, ResponseCode, error)
ProcessResponse handles the §8.2 direct_post.jwt response for request_uri flows: form parse → JWE decrypt with the per-session ephemeral key → apv handling (Annex B.2) → vp_token object parse (§8.1) → state binding → response_code mint (§8.2, same-device).
Precondition: s was obtained from SessionStore.ConsumeOnce (atomic one-time consumption; replays die at the store). The caller persists s afterwards (Save) to store the minted response_code.
func (*Engine) RedirectURI ¶
RedirectURI renders the §8.3 same-device return: the response endpoint answers the wallet's POST with 200 {"redirect_uri": <this value>}, and the wallet navigates the user's browser there. The response_code in the query fences the result fetch to the browser session that actually completed the presentation (OID4VP §8.2, §12.1).
func (*Engine) RequestObjectJWT ¶
RequestObjectJWT builds and signs the Request Object for GET request_uri (RFC 9101 JAR; OID4VP §5; HAIP §5). Single-use: the session is marked served and the caller MUST persist it (SessionStore.Save); a second call fails with ErrRequestURIConsumed (T-08.4).
func (*Engine) ValidateTransactionDataEcho ¶
func (e *Engine) ValidateTransactionDataEcho(s *Session, echoedHashes []string, hashAlgName string) error
ValidateTransactionDataEcho compares the transaction_data_hashes echoed by the wallet (in a KB-JWT for dc+sd-jwt, or the device-signed payload for mso_mdoc — extracted by go-sdjwt/go-mdoc, WP-09) against the hashes of the session's request entries (OID4VP §5). Set semantics: the wallet may reorder. present/absent/mismatch matrix (T-08.10):
- flag off but session carries entries → ErrTransactionDataDisabled
- entries requested, none echoed → ErrTransactionDataMissing
- none requested, some echoed → ErrTransactionDataUnexpected
- counts differ or any hash unmatched → ErrTransactionDataMismatch
type MemStore ¶
type MemStore struct {
// contains filtered or unexported fields
}
MemStore is the in-memory reference SessionStore: single-process, for tests and development. Sessions are stored as JSON snapshots, which (a) gives Load copy semantics identical to a networked store and (b) proves every Session is serializable exactly as the WP-09 Valkey store needs.
func NewMemStore ¶
NewMemStore returns an empty MemStore. clock nil defaults to time.Now.
func (*MemStore) ConsumeOnce ¶
ConsumeOnce atomically hands the session to exactly one caller (OID4VP §8.2 one-time response consumption; §12.1 replay defense). The Consumed marker is sticky: it survives later Save calls, so a replayed wallet POST fails even after the service persisted post-processing state (WP-08 decision, recorded in the README).
type Presentation ¶
type Presentation struct {
QueryCredID string // DCQL credential query id (vp_token key, §8.1)
Format string // dcql.FormatSDJWT | dcql.FormatMdoc (from the session's query)
Payload []byte // dc+sd-jwt: presentation string verbatim; mso_mdoc: base64url-decoded DeviceResponse
Nonce, ClientID, ResponseURI string
Origin string // DCAPI only (Annex A)
// JWKThumbprint is the RFC 7638 thumbprint of the RP's OWN ephemeral
// response-encryption public key (the same key advertised in
// client_metadata, T-08.3) — computed by ProcessResponse from
// Session.EphemeralKeyPKCS8 via crypto.JWKThumbprint, never from
// anything wallet-supplied. The JWE apu header is not read at all (see
// WP-08 README Decisions "T-08.7/T-08.9 correction"): JWKThumbprint is
// the mdoc SessionTranscript handover's sole key-binding input, set
// only for mso_mdoc presentations.
JWKThumbprint string
}
Presentation is one entry of the vp_token object (OID4VP §8.1), paired with the binding parameters downstream verification needs: nonce and client_id for KB-JWT (go-sdjwt), and the OID4VPHandover / OID4VPDCAPIHandover inputs for mdoc (go-mdoc, Annex B.2 / Annex A).
type RawResponse ¶
type RawResponse struct {
Body []byte
}
RawResponse is the wallet's POST body to the response endpoint (OID4VP §8.2 direct_post.jwt: application/x-www-form-urlencoded with response=<JWE>). The service passes it verbatim, size-unchecked — the engine owns the cap.
type RequestSpec ¶
type RequestSpec struct {
Query dcql.Query
Flow Flow // SameDevice | CrossDevice | DCAPI
ResponseURI string
ReturnURI string // same-device only (§8.3)
Registration rpcert.RegistrationRef // always (ARF RPRC_19a)
WRPRC []byte // optional (ADR-0003)
TransactionData [][]byte // phase 2 (T-08.10)
ExpectedOrigins []string // DCAPI signed requests (Annex A)
}
RequestSpec describes one verification request (WP-08 README target interface). ReturnURI is the same-device §8.3 redirect target — a README addition required by T-08.8, flagged in the plan's README corrections.
type ResponseCode ¶
type ResponseCode string
ResponseCode is the single-use §8.2 response_code minted for same-device sessions and redeemed via ConsumeResponseCode (§8.3/§12.1).
type ResponseEncryption ¶
type ResponseEncryption struct {
Curve string // ephemeral key curve, e.g. the HAIP baseline P-256
Alg string // JWE key agreement advertised on the ephemeral JWK
EncValues []string // encrypted_response_enc_values_supported (§8.2)
}
ResponseEncryption configures the per-session ephemeral response encryption advertised in client_metadata (OID4VP §8.2 direct_post.jwt; HAIP §5: encryption mandatory). Values are supplied by the service's configuration and validated against crypto.Policy — this library hardcodes no algorithm strings (hard rule 4).
type Session ¶
type Session struct {
ID string `json:"id"`
Flow Flow `json:"flow"`
ClientID string `json:"client_id"` // full prefixed form, e.g. "x509_san_dns:verifier.example.com"
Nonce string `json:"nonce"` // OID4VP §5: ≥128-bit, crypto/rand, base64url
State string `json:"state"` // response binding (§8.2)
ResponseURI string `json:"response_uri"` // §8.2 direct_post.jwt endpoint
ReturnURI string `json:"return_uri,omitempty"` // same-device §8.3 redirect target (see README corrections)
Query dcql.Query `json:"query"` // OID4VP §6
Registration rpcert.RegistrationRef `json:"registration"` // ARF RPRC_19a — always present
WRPRC []byte `json:"wrprc,omitempty"` // optional registration certificate (ADR-0003)
TransactionData [][]byte `json:"transaction_data,omitempty"` // phase 2 (T-08.10)
ExpectedOrigins []string `json:"expected_origins,omitempty"` // DCAPI (Annex A)
EphemeralKeyPKCS8 []byte `json:"ephemeral_key_pkcs8"` // per-session response-encryption private key, PKCS#8 DER
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
RequestObjectServed bool `json:"request_object_served"` // single-use request_uri (T-08.4)
WalletMetadata []byte `json:"wallet_metadata,omitempty"` // absorbed hook payload (T-08.4)
Consumed bool `json:"consumed"` // sticky one-time marker (ConsumeOnce)
ResponseCode string `json:"response_code,omitempty"` // minted §8.2 (T-08.5), redeemed §8.3 (T-08.8)
ResponseCodeUsed bool `json:"response_code_used,omitempty"`
}
Session is the per-verification protocol state. It is a plain JSON-serializable record so SessionStore implementations (Valkey in WP-09) can persist it verbatim. Fields are exported for storage, not for mutation: services treat a Session as opaque between Engine calls.
The response-encryption key is per-session ephemeral (WP-08 decision; HAIP-aligned): it lives only inside the session record and dies with it.
type SessionStore ¶
type SessionStore interface {
Save(ctx context.Context, s *Session) error
Load(ctx context.Context, id string) (*Session, error)
ConsumeOnce(ctx context.Context, id string) (*Session, error) // atomic: response endpoint
}
SessionStore persists sessions. The Valkey implementation lives in services (WP-09); it MUST pass storetest.Run unchanged.
Contract (verified by storetest.Run):
- Save then Load returns an equal, independent copy (mutating a loaded session does not affect the stored one).
- Load/ConsumeOnce of an unknown OR expired id returns ErrSessionNotFound (expiry may be storage eviction, e.g. Valkey TTL; the Engine additionally enforces ExpiresAt itself, fail closed).
- ConsumeOnce is atomic: for one id, exactly one caller ever receives the session (returned with Consumed=true); every other and every later call gets ErrSessionConsumed — even after subsequent Save calls of the same session (the consumed marker is sticky; WP-08 decision). This is the §12.1 replay defense for the response endpoint (OID4VP §8.2).
type VPFormats ¶
type VPFormats struct {
SDJWTAlgValues []string // dc+sd-jwt sd-jwt_alg_values
KBJWTAlgValues []string // dc+sd-jwt kb-jwt_alg_values
MdocIssuerAuthAlgValues []int64 // mso_mdoc issuerauth_alg_values (COSE labels)
MdocDeviceAuthAlgValues []int64 // mso_mdoc deviceauth_alg_values (COSE labels)
}
VPFormats configures client_metadata vp_formats_supported (OID4VP §5; Annex B.2/B.3). Same rule: values from config, validated by policy.
type WalletError ¶
WalletError is an OID4VP error response sent BY the wallet (e.g. the user declined: error=access_denied). It is not an engine failure; the service records the outcome and acknowledges the wallet. Code and Description are wallet-supplied, length-capped, and must be treated as untrusted text (never logged raw next to attribute data, hard rule 3).
func (*WalletError) Error ¶
func (e *WalletError) Error() string
type WalletInvocation ¶
type WalletInvocation struct {
SchemeURI string // openid4vp://?client_id=...&request_uri=...&request_uri_method=get
UniversalLink string // UniversalLinkBase + same query
QRPayload string // string to encode into the cross-device QR
DCAPI []byte // Annex A request member JSON (populated for Flow == DCAPI, T-08.9)
}
WalletInvocation carries the flow-specific way to put the request in front of a wallet: custom-scheme URI, https universal link and QR payload for request_uri flows, or the DCAPI request member (T-08.9).