Wallet
Wallet (protocolsoup-wallet)
Section titled “Wallet (protocolsoup-wallet)”Image: ghcr.io/parlesec/protocolsoup-wallet
When To Use
Section titled “When To Use”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.
Runtime Contract
Section titled “Runtime Contract”| 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 |
Configuration
Section titled “Configuration”| 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.
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:latestOID4VCI Client
Section titled “OID4VCI Client”The wallet’s metadata-driven OID4VCI client supports:
- Pre-authorized and
authorization_codegrants (PAR + PKCE S256 when the AS requires/advertises PAR; client attestation + DPoP when HAIP attestation material is configured). After PAR, authorize redirects use onlyclient_id+request_uri; token uses the same client attestation as PAR. When AS metadata includeschallenge_endpoint, the wallet fetchesattestation_challengeand puts it in the Client Attestation PoP JWTchallengeclaim. The wallet UI opens AS authorization in a popup so the offer stays in the current tab (same-tab +sessionStoragerestore if popups are blocked). DPoPuse_dpop_nonceretries cover AS HTTP 400 and RS HTTP 401 challenges and mint a fresh client attestation PoPjtiwhen attestation headers are present. The same client can authorize for a protected resource viaPOST /api/import(discovery_url,resource_endpoint,scope) and GET that resource with DPoP after token exchange. Discovery documents are rejected whenissuerdoes 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 usesopenid-configuration;{issuer}/.well-known/oauth-authorization-serveris not fetched. - When issuer metadata advertises
batch_credential_issuance, the wallet requests multiple pairwise-distinct JWT proofs (capped bybatch_size) and matches returned credentials by holder-key binding rather than array order. HAIP batch proofs share one Key Attestation JWT whoseattested_keyslists every proof key (HAIP 1.0 Section 4.5.1). Formso_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 setscredential_request_encryption.encryption_requiredor the wallet includescredential_response_encryption(OID4VCI 1.0 §8.2 / §9). Advertising request-encryption JWKs withencryption_requiredfalse 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_acceptedwith DPoP +x-fapi-interaction-id, HTTP 204), and token-responsecredential_identifiers - Fail-closed issuer metadata trust (
credential_issuermust match the offer) - mdoc IACA verification via
WALLET_MDOC_IACA_ROOT_PEMbefore storage
Operational Notes
Section titled “Operational Notes”- 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 atWALLET_DEVICE_KEY_PATHpersists across restarts. WALLET_TARGET_BASE_URLmust 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 publiciaca_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 storingmso_mdoccredentials. - Hosted ProtocolSoup CI copies the showcase verifier request-signer CA into
WALLET_VERIFIER_X509_TRUST_ANCHOR_PEMso Looking Glassx509_hash/x509_san_dnspresentations can validate the livex5cchain. - Keep strict session isolation enabled in shared environments.
- Use narrow CORS origin allow-lists outside local development.
- External
request_uriand issuer fetches resolve DNS and refuse non-public destinations, including redirect hops. Failed fetches do not reflect upstream response bodies. Pre-patch wallet tagsv2.0.0–v4.1.0are 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 onprotocolsoup.com(see frontendWALLET_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 rejectverifier_attestationrequest objects even when the JWT signature is valid. - Set
WALLET_DEVICE_KEY_PATHto a durable file formso_mdoc(ISO/IEC 18013-5) credentials: the device key bound at issuance must be reused to sign theDeviceResponse, so regenerating it silently breaks every stored mdoc credential’s device binding. - Set
WALLET_MDOC_IACA_ROOT_PEMto 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 asPOST /wallet-harness/api/import). The wallet returns_protocol_exchanges— the real wallet→issuer hops Looking Glass renders as glass-box steps — plus_looking_glass_eventsfor proof JWT construction. Issuer-initiated Looking Glass creates/delivers an offer and observesstatus_urimilestones; 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-issuermetadata URL). The wallet fetches metadata, listscredential_configurations_supported, and startsauthorization_codeonly after the holder selects a configuration. Do not paste Authorization Serveropenid-configuration/oauth-authorization-serverURLs 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_uriis 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_urifrom Credential Issuer Metadata and RFC 8414 Authorization Server metadata. ProtocolSoup OID4VCI advertisesjwks_urias{issuer}/.well-known/jwks.json. It does not GET/.well-known/jwt-vc-issuer(SD-JWT VC Issuer Metadata is not a JWK Set). HAIPdc+sd-jwtholder binding is RFC 7800cnf(typicallycnf.jwk); a missing JWTsubis not a bind failure. - The harness presents to
response_mode=direct_post.jwtrequests by reading the verifier’s ephemeral ECDH-ES response-encryption key fromclient_metadata.jwks, binding its RFC 7638 thumbprint into theOpenID4VPHandoverfor mdoc, and returning an ECDH-ES JWE (A256GCM when the verifier advertises it, else A128GCM). Forx509_hash, the request object’sx5cchain must validate to system roots or a root configured inWALLET_VERIFIER_X509_TRUST_ANCHOR_PEM; the chain cannot establish its own trust. Looking Glass OID4VP/submitissues the selected HAIP key-attested configuration, then presents. Missing attestation orkey_storagethat does not meetkey_attestations_requiredis the issuer’s HTTP 400. - After a successful HAIP presentation, the wallet follows a verifier-provided HTTPS
redirect_uriwhen present. The SPA refuses navigation unless the URL ishttpswith no userinfo and no loopback, private, or internal host.
Agent discovery
Section titled “Agent discovery”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.