Skip to content

Wallet

Image: ghcr.io/parlesec/protocolsoup-wallet

Use the wallet service to run wallet-side OID4VCI issuance (bootstrap or external offer import) and OID4VP presentation submission against a VC issuer/verifier target.

Property Value
Internal port 8080 (commonly mapped to host 8081)
Health GET /health
Authorize GET /authorize (OID4VP wallet authorization endpoint → SPA consent; auto-issues the request-inferred credential when the store has no DCQL match)
Submit POST /submit
Issue / import POST /api/issue, POST /api/import
OID4VCI callback GET /api/oid4vci/callback
Variable Required Default Description
WALLET_TARGET_BASE_URL No https://protocolsoup.com VC verifier/issuer service URL for OID4VP submissions and default OID4VCI target
WALLET_ALLOWED_CORS_ORIGINS No https://protocolsoup.com,https://www.protocolsoup.com,https://protocolsoup.fly.dev Comma-separated allowed browser origins
WALLET_DEFAULT_SUBJECT No did:example:wallet:alice Default holder DID seed for demo sessions. This identifies wallet key material and is not an issuer identity-record ID
WALLET_DEFAULT_CREDENTIAL_FORMAT No mso_mdoc Default stored and presented credential format when a request omits a format/configuration. mso_mdoc (mDL) is the default; dc+sd-jwt and the W3C formats remain selectable
WALLET_DID_METHOD No key DID method for the wallet holder identifier (e.g. key, web)
WALLET_DEFAULT_SIGNING_ALG No ES256 Default holder-binding signing algorithm; ES256 (EC P-256) matches the mso_mdoc default
WALLET_SESSION_TTL No 20m Session lifetime for in-memory wallet material
WALLET_STRICT_SESSION_ISOLATION No true Enforce strict session boundaries between demo runs
WALLET_ALLOW_EXTERNAL_VERIFIERS No true Allow fetches of OID4VP request_uri and OID4VCI endpoints outside the configured target/issuer origin. External URLs must be HTTPS; DNS-resolved and redirect destinations that are private, loopback, link-local, multicast, or shared-address are rejected.
WALLET_TRUSTED_VERIFIER_ATTESTATION_ISSUERS No (empty) Comma-separated issuer URLs the wallet trusts for verifier_attestation request validation
WALLET_DEVICE_KEY_PATH No (empty) File path for the persistent mso_mdoc holder device key (EC P-256). Empty uses an ephemeral key; set a durable path so the device binding of stored mdoc credentials survives restarts
WALLET_VERIFIER_X509_TRUST_ANCHOR_PEM No System roots only Additional PEM CA roots trusted for x509_san_dns / x509_hash request objects; roots carried in x5c are never self-trusted
WALLET_MDOC_IACA_ROOT_PEM Required for mdoc storage (empty) PEM IACA root(s) used to verify IssuerAuth before storing an issued mso_mdoc. Multiple PEMs are additive.
WALLET_CLIENT_ATTESTATION_ATTESTER_JWK_JSON No (empty) Attester private JWK (+ x5c) that mints OAuth-Client-Attestation JWTs. Required with the key-attestation attester JWK to enable HAIP issuance
WALLET_CLIENT_ATTESTATION_KEY_ATTESTATION_JWK_JSON No (empty) Attester private JWK (+ x5c) that mints Appendix D key_attestation JWTs for HAIP proofs
WALLET_OID4VCI_ATTESTED_CLIENT_ID No protocolsoup-wallet Attested client_id / client attestation JWT sub / client attestation PoP JWT iss when HAIP attestation material is configured
WALLET_CLIENT_ATTESTATION_ISSUER No https://wallet.protocolsoup.com/attester iss on client attestation JWTs
WALLET_KEY_ATTESTATION_KEY_STORAGE No (empty) Comma-separated key_storage levels asserted in key attestation (e.g. iso_18045_moderate). Leave unset unless the claim is intentional for this deployment; the hosted Looking Glass wallet sets iso_18045_moderate so HAIP mDL configs can be demonstrated
WALLET_KEY_ATTESTATION_USER_AUTHENTICATION No (empty) Comma-separated user_authentication levels asserted in key attestation. Leave unset unless intentional; hosted Looking Glass wallet pairs this with iso_18045_moderate

fly.wallet.toml sets WALLET_DEVICE_KEY_PATH=/data/device-key.pem on the protocolsoup_wallet_data volume. WALLET_MDOC_IACA_ROOT_PEM is staged as a Fly secret from the live issuer IACA, not baked into the toml.

Terminal window
docker run -p 8081:8080 \
-e WALLET_TARGET_BASE_URL=http://host.docker.internal:8080 \
-e WALLET_ALLOWED_CORS_ORIGINS=http://localhost:3000 \
ghcr.io/parlesec/protocolsoup-wallet:latest

The wallet’s metadata-driven OID4VCI client supports:

  • Pre-authorized and authorization_code grants (PAR + PKCE S256 when the AS requires/advertises PAR; client attestation + DPoP when HAIP attestation material is configured). After PAR, authorize redirects use only client_id + request_uri; token uses the same client attestation as PAR. When AS metadata includes challenge_endpoint, the wallet fetches attestation_challenge and puts it in the Client Attestation PoP JWT challenge claim. The wallet UI opens AS authorization in a popup so the offer stays in the current tab (same-tab + sessionStorage restore if popups are blocked). DPoP use_dpop_nonce retries cover AS HTTP 400 and RS HTTP 401 challenges and mint a fresh client attestation PoP jti when attestation headers are present. The same client can authorize for a protected resource via POST /api/import (discovery_url, resource_endpoint, scope) and GET that resource with DPoP after token exchange. Discovery documents are rejected when issuer does not match the Issuer URL implied by the discovery URL (OIDC Discovery §4.3 / RFC 8414 §3.3); the wallet stops and does not call PAR or another well-known path. FAPI2 OIDC discovery uses openid-configuration; {issuer}/.well-known/oauth-authorization-server is not fetched.
  • When issuer metadata advertises batch_credential_issuance, the wallet requests multiple pairwise-distinct JWT proofs (capped by batch_size) and matches returned credentials by holder-key binding rather than array order. HAIP batch proofs share one Key Attestation JWT whose attested_keys lists every proof key (HAIP 1.0 Section 4.5.1). For mso_mdoc, batch secondaries bound to ephemeral proof keys are stored after IACA checks but are not activated; the wallet device-key-bound credential remains presentable.
  • Credential and deferred requests are sent as compact JWE (Content-Type: application/jwt, ECDH-ES) when metadata sets credential_request_encryption.encryption_required or the wallet includes credential_response_encryption (OID4VCI 1.0 §8.2 / §9). Advertising request-encryption JWKs with encryption_required false does not by itself force encryption.
  • Credential response encryption (ECDH-ES / A128GCM|A256GCM) when required by issuer metadata or requested on the HAIP path
  • Deferred credential polling (HTTP 202 + transaction_id / interval, with DPoP nonce retry on deferred, credential, and notification endpoints), notifications (credential_accepted with DPoP + x-fapi-interaction-id, HTTP 204), and token-response credential_identifiers
  • Fail-closed issuer metadata trust (credential_issuer must match the offer)
  • mdoc IACA verification via WALLET_MDOC_IACA_ROOT_PEM before storage
  • Credentials and session key material are in-memory (WALLET_SESSION_TTL). Durable encrypted at-rest storage (OID4VCI 1.0 §15.3 SHOULD) is deferred until the platform vault initiative; only the mdoc device key at WALLET_DEVICE_KEY_PATH persists across restarts.
  • WALLET_TARGET_BASE_URL must point to the VC service or federation service running OID4VP (and typically OID4VCI).
  • During automatic OID4VCI bootstrap, the wallet leaves identity selection to the issuer’s designated default record. The issuer-authorized subject returned in the credential offer remains separate from the wallet’s holder DID.
  • The wallet must independently trust the issuer’s mdoc IACA root through WALLET_MDOC_IACA_ROOT_PEM. Copy the issuer’s public iaca_root.pem (for example from the issuer data volume at /data/mdoc/iaca_root.pem) into that wallet env var or secret before issuing or storing mso_mdoc credentials.
  • Hosted ProtocolSoup CI copies the showcase verifier request-signer CA into WALLET_VERIFIER_X509_TRUST_ANCHOR_PEM so Looking Glass x509_hash / x509_san_dns presentations can validate the live x5c chain.
  • Keep strict session isolation enabled in shared environments.
  • Use narrow CORS origin allow-lists outside local development.
  • External request_uri and issuer fetches resolve DNS and refuse non-public destinations, including redirect hops. Failed fetches do not reflect upstream response bodies. Pre-patch wallet tags v2.0.0v4.1.0 are GHSA-q7jr-2q6f-gw44.
  • When the browser-based Looking Glass uses a separately hosted wallet, keep at least one wallet instance warm. Looking Glass calls the wallet through the same-origin /wallet-harness/* Next rewrite on protocolsoup.com (see frontend WALLET_BACKEND_ORIGIN), so a cold-start failure surfaces as a normal HTTP error instead of a CORS-stripped edge 502.
  • Add each trusted verifier attestation issuer URL to WALLET_TRUSTED_VERIFIER_ATTESTATION_ISSUERS; otherwise the wallet will reject verifier_attestation request objects even when the JWT signature is valid.
  • Set WALLET_DEVICE_KEY_PATH to a durable file for mso_mdoc (ISO/IEC 18013-5) credentials: the device key bound at issuance must be reused to sign the DeviceResponse, so regenerating it silently breaks every stored mdoc credential’s device binding.
  • Set WALLET_MDOC_IACA_ROOT_PEM to an independently obtained IACA root. The wallet rejects mdoc issuance when no root is configured or when the credential’s document-signer chain, signature, digests, or validity fail verification.
  • Looking Glass pre-authorized OID4VCI executors create the issuer offer (preferring by-value), then redeem it through this wallet via POST /api/import (proxied as POST /wallet-harness/api/import). The wallet returns _protocol_exchanges — the real wallet→issuer hops Looking Glass renders as glass-box steps — plus _looking_glass_events for proof JWT construction. Issuer-initiated Looking Glass creates/delivers an offer and observes status_uri milestones; the wallet (when used as the offer endpoint / import client) drives PAR, token, and credential exchange — including HAIP attestation+DPoP+encryption when the attestation env vars are configured.
  • Wallet-initiated issuance: paste a Credential Issuer identifier (or /.well-known/openid-credential-issuer metadata URL). The wallet fetches metadata, lists credential_configurations_supported, and starts authorization_code only after the holder selects a configuration. Do not paste Authorization Server openid-configuration / oauth-authorization-server URLs as a credential issuer. Before PAR, the UI shows issuer identifier, format/vct, and requirements advertised in metadata (PAR, DPoP, client attestation, encryption, holder binding). After Discover, the configuration picker stays available so Request again can start a new PAR without rediscovering (request_uri is single-use). Used Issue steps collapse; Request again and Continue to Issuer stay on the collapsed headers.
  • After an issued JWT or SD-JWT credential is received, the wallet verifies the issuer signature against jwks_uri from Credential Issuer Metadata and RFC 8414 Authorization Server metadata. ProtocolSoup OID4VCI advertises jwks_uri as {issuer}/.well-known/jwks.json. It does not GET /.well-known/jwt-vc-issuer (SD-JWT VC Issuer Metadata is not a JWK Set). HAIP dc+sd-jwt holder binding is RFC 7800 cnf (typically cnf.jwk); a missing JWT sub is not a bind failure.
  • The harness presents to response_mode=direct_post.jwt requests by reading the verifier’s ephemeral ECDH-ES response-encryption key from client_metadata.jwks, binding its RFC 7638 thumbprint into the OpenID4VPHandover for mdoc, and returning an ECDH-ES JWE (A256GCM when the verifier advertises it, else A128GCM). For x509_hash, the request object’s x5c chain must validate to system roots or a root configured in WALLET_VERIFIER_X509_TRUST_ANCHOR_PEM; the chain cannot establish its own trust. Looking Glass OID4VP /submit issues the selected HAIP key-attested configuration, then presents. Missing attestation or key_storage that does not meet key_attestations_required is the issuer’s HTTP 400.
  • After a successful HAIP presentation, the wallet follows a verifier-provided HTTPS redirect_uri when present. The SPA refuses navigation unless the URL is https with no userinfo and no loopback, private, or internal host.

An agent that hits this origin should start at GET /llms.txt (or Accept: text/markdown on /). The process also serves /.well-known/api-catalog, /.well-known/agent-skills/use-wallet-harness/SKILL.md, and RFC 8288 Link headers on the HTML UI. Indexed copies of those URLs live on docs.protocolsoup.com/llms.txt, the GitHub README, and agent discovery.

The HTTP JSON API (POST /api/issue, POST /api/import, POST /api/resolve, POST /api/present, GET /api/session, POST /submit) is the headless interface. QR and deeplinks are human handoffs. This image is not an MCP server; apex /mcp is read-only catalog/decode and does not proxy the wallet. API errors use a stable error token plus error_description, not RFC 9457.