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 ([OID4VP §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 (the err:domain:reason 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 (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 — 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") // 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. 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. 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, [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, OID4VP Annex A. ErrOriginNotExpected = errors.New("oid4vp: response origin not in expected_origins") // transaction_data (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. 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. The mapping each sentinel is expected to receive is noted inline. None of these messages ever carries attribute values, request bodies, or token contents — never leaked to the wallet.
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 (a correction of an 2026-07-06: an earlier draft assumed apu/mdocGeneratedNonce filled this slot; the go-mdoc constructors' actual, EU-reference-verified shape takes jwkThumbprint instead).
FLAG (carried from go-mdoc's OID4VPHandover/OID4VPDCAPIHandover doc comments): 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.
eudi-verifier-core passes the result to mdoc.Verifier.Verify as VerifyInput.SessionTranscript. 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 (no hardcoded algorithm literals).
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
}
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. 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 (fail closed).
func (*Engine) AbsorbWalletMetadata ¶
AbsorbWalletMetadata is the 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 (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 [OID4VP §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). 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. status is a plain int so this library imports no HTTP package. 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, 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) and the DCAPI handover. Fail closed: an origin absent from expected_origins is rejected with ErrOriginNotExpected before any decryption — the DCAPI analog of the [OID4VP §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 [OID4VP §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 ([OID4VP §8.1]) → state binding → response_code mint ([OID4VP §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 [OID4VP §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.
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) against the hashes of the session's request entries ([OID4VP §5]). Set semantics: the wallet may reorder. present/absent/mismatch matrix:
- 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 a persistent (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; [OID4VP §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.
type Presentation ¶
type Presentation struct {
QueryCredID string // DCQL credential query id (vp_token key, [OID4VP §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) — computed by ProcessResponse from
// Session.EphemeralKeyPKCS8 via crypto.JWKThumbprint, never from
// anything wallet-supplied. The JWE apu header is not read at all:
// 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 ([OID4VP §8.3])
Registration rpcert.RegistrationRef // always (ARF RPRC_19a)
WRPRC []byte // optional
TransactionData [][]byte // phase 2
ExpectedOrigins []string // DCAPI signed requests (Annex A)
}
RequestSpec describes one verification request (the target interface). ReturnURI is the same-device [OID4VP §8.3] redirect target for the same-device flow.
type ResponseCode ¶
type ResponseCode string
ResponseCode is the single-use [OID4VP §8.2] response_code minted for same-device sessions and redeemed via ConsumeResponseCode ([OID4VP §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 ([OID4VP §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.
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 ([OID4VP §8.2])
ResponseURI string `json:"response_uri"` // [OID4VP §8.2] direct_post.jwt endpoint
ReturnURI string `json:"return_uri,omitempty"` // same-device [OID4VP §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
TransactionData [][]byte `json:"transaction_data,omitempty"` // phase 2
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
WalletMetadata []byte `json:"wallet_metadata,omitempty"` // absorbed hook payload
Consumed bool `json:"consumed"` // sticky one-time marker (ConsumeOnce)
ResponseCode string `json:"response_code,omitempty"` // minted [OID4VP §8.2], redeemed [OID4VP §8.3]
ResponseCodeUsed bool `json:"response_code_used,omitempty"`
}
Session is the per-verification protocol state. It is a plain JSON-serializable record so SessionStore implementations (a Valkey-backed store) 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 (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; 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). This is the [OID4VP §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).
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)
}
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.