Sorcha and the OpenID4VC / HAIP Wallet Ecosystem
Sorcha is the workflow infrastructure above HAIP-conformant holder wallets. It does not replace GOV.UK Wallet, it does not replace the EU Digital Identity Wallet, and it does not control the citizen experience. What it does is issue verifiable credentials to those wallets, verify presentations from those wallets, and operate the multi-party workflows that issue and verify in the first place.
This document is how an integrator working on the wallet ecosystem reads what Sorcha actually implements, where the implementation lives, and where it stops.
What HAIP Is and Why Sorcha Implements It
The High Assurance Interoperability Profile (HAIP 1.0, OpenID Foundation, finalised December 2025) is the cross-jurisdiction profile that the EU Digital Identity Wallet, the GOV.UK Wallet, and an expanding set of national wallets are converging on. It pins down the credential format (SD-JWT VC), the issuance flow (OpenID4VCI pre-authorised flow with PKCE), the presentation flow (OpenID4VP cross-device QR), and the cryptographic suites at the wallet wire boundary (classical: ES256, EdDSA).
Sorcha implements HAIP 1.0 as the boundary protocol because that is the standard the holder ecosystem speaks. Inside the platform, Sorcha uses post-quantum cryptography (ML-DSA, FIPS 204) for its own signing operations — but at the HAIP boundary every signature is classical, because that is what the wallets accept.
What Sorcha Does Not Do
- Sorcha is not a wallet. The citizen wallet PWA shipped in Feature 114 is a reference holder for development and demos. Production deployments expect citizens to use GOV.UK Wallet or EUDIW.
- Sorcha does not control the citizen UX. The wallet decides how a credential is rendered, how consent is gathered, and how disclosures are surfaced. Sorcha provides the data contract.
- Sorcha is not a credential schema authority. Schemas are referenced by URI; the issuing authority is the source of truth for what each field means.
- Sorcha does not arbitrate trust. Trust anchors come from the surrounding ecosystem — government registers, the EU trust list, or a Sorcha system register seeded from a published genesis ceremony (spec 099).
The Issuer Path — OpenID4VCI
The HAIP Service exposes the OpenID4VCI issuer endpoints under the gateway. The flow Sorcha supports is the pre-authorised code flow with PKCE, which is the HAIP-mandated profile for high-assurance issuance.
| Phase | What happens | Where in code |
|---|---|---|
| Offer | Issuing authority builds a credential offer (SD-JWT VC type) and hands it to the holder out-of-band | src/Services/Sorcha.Haip.Service |
| Pre-authorised token | Holder wallet exchanges the pre-auth code (plus PIN, where required) for an access token | HAIP Service token endpoint |
| Credential endpoint | Holder wallet POSTs proof-of-possession of its key; HAIP Service mints the SD-JWT VC | HAIP Service credential endpoint |
| Status list | Issued credentials reference an IETF Token Status List 2024 (RFC 9972) URL the holder can poll | src/Services/Sorcha.Wallet.Service/Services/Implementation/CitizenStatusListPublisher.cs |
The credential format is SD-JWT VC with selective disclosure encoded as JSON Pointer paths plus per-disclosure salts. Inside the SD-JWT VC the disclosures are bound to the holder's key — only the holder can present them. The W3C Verifiable Credentials Data Model 2.0 envelope wraps the SD-JWT VC.
The Verifier Path — OpenID4VP
The HAIP Service also runs the OpenID4VP verifier endpoint. The supported flow is the cross-device same-origin profile that HAIP mandates: a verifier presents a QR encoding an openid4vp:// URL; the holder wallet on a separate device authenticates the verifier, gathers consent, and POSTs a signed presentation back to the verifier.
Sorcha tracks the OpenID4VP 1.0 final wire shape (Feature 181):
- DCQL query dialect. The authorization request carries a
dcql_query(query id,format,claims,claim_sets, andcredential_setsfor alternatives). The legacy Presentation Exchangepresentation_definition/input_descriptorsshape is retired; the shared model lives insrc/Common/Sorcha.Verifier.Engine/Dcql. A single request can ask for multiple credentials, andcredential_setsexpresses "present any one of these" alternatives. - Signed request objects +
x509_san_dnsclient_id. The verifier signs its request object (ES256) with an X.509 verifier certificate, embeds thex5cchain, and identifies itself with a prefixedx509_san_dns:{host}client_idwhose host equals the certificate's SAN dNSName. Verifier config:Haip:VerifierCertificate(+ optionalHaip:VerifierCertificatePassword) andHaip:PublicHost; dev falls back to a self-signed certificate, prod/staging fail fast when unconfigured. - Holder-side verifier authentication. Before showing consent the wallet runs
RequestObjectValidator(src/Common/Sorcha.Verifier.Engine/RequestObjectValidator.cs, BouncyCastle, WASM-safe): ES256 JWS verify over the x5c leaf, leaf SAN equals theclient_idhost, then a chain-walk to a trusted-list anchor. The result is a three-stateVerifierAuthState—TrustedListVerified,AuthenticUntrusted, orUnverifiable. A tampered signature or SAN mismatch is a hard refusal; absent trust anchors never block a valid signed request.
| Phase | What happens |
|---|---|
| Authorization request | Verifier builds the OpenID4VP request with a dcql_query (which credentials and disclosures it needs) and a nonce, and signs the request object with its X.509 verifier certificate |
| QR handover | The request is encoded as a QR; the holder wallet scans or deep-links from another device and fetches the signed request object |
| Verifier authentication | The wallet validates the request-object signature, the SAN-to-client_id binding, and the anchor chain into a VerifierAuthState shown on the consent sheet |
| Presentation | The holder wallet returns a Verifiable Presentation containing the SD-JWT VC(s) and a Key-Bound JWT signing the nonce + the full prefixed client_id audience |
| Verification | Verifier checks the issuer signature, the holder key-binding, the nonce match, and the IETF status list — every check is independent |
The verifier reference UI lives in src/Apps/Sorcha.Verifier.Web (Blazor Server). It is an integration-test surface and a demo — production verifiers in customer deployments are expected to embed the verification logic into their own application.
The Wallet Backend — Citizen Wallet PWA (Feature 114)
The citizen wallet PWA is a Blazor WASM holder wallet that Sorcha ships for development, demos, and the cases where a customer wants a Sorcha-branded wallet rather than relying on a government-issued one. It is server-anchored: holder keys are derived from BIP44 slot 108 (sorcha:citizen-holder) on the server, then a delegation to a per-device WebCrypto P-256 key is signed and shipped to the device. This is aligned with the EUDIW WSCA (Wallet Secure Cryptographic Application) model.
The citizen wallet ships with:
- Device enrolment — POST
/devices/enrolregisters a per-device key and receives the holder→device delegation - Credential sync — incremental sync via signed cursor JWTs (30-day TTL)
- Consent UX — deep-link paste of OpenID4VP requests, KB-JWT verification before display
- Self-renewal of delegation — 30 days before expiry the wallet rotates its device key
- Activity log — server-forwarded log of presentations made by the wallet
See .claude/skills/sorcha-architecture/SKILL.md § "Citizen Wallet PWA (Feature 114)" for the server-side surface in detail.
Status Lists — IETF Token Status List 2024 (RFC 9972)
Every issued credential carries an iss claim and a status_list claim pointing at an RFC 9972 status list URL. The list itself is a compact bitstring; each issued credential corresponds to one bit. Revocation is a single-bit flip plus a re-signed list.
Sorcha implements two status list backends:
- Per-org status list — one status list per issuing org, signed by the org's issuer key. Citizen wallet credentials go here.
- Per-blueprint status list — internal-path status list signed by the platform, used by Sorcha's own internal credential issuance. Lives in
src/Services/Sorcha.Blueprint.Service/Services/StatusListManager.cs.
The W3C Bitstring Status List envelope is supported as an alternative surface for the same underlying bitstring — same data, different envelope, both indexed by the same status list index.
Trust Anchors and DID Resolution
Issuer keys must be resolvable by every verifier. Sorcha uses two key resolution paths:
- Tenant register — production verifiers resolve
did:sorcha:org:issuer DIDs to the issuer's signing key by reading verification methods from the Tenant Service register. The seam isIIssuerKeyResolverinsrc/Common/Sorcha.Cryptography. - JWK registry (development) — for development and demos, a
JwkRegistryIssuerKeyResolverallows issuers to register their key directly. The citizen wallet's verifier-side demo flow uses this.
The system register genesis (spec 099) defines how the very first set of trust anchors is seeded — the public ceremony, the validator key import, the publication of the genesis docket. A production deployment cannot bypass this; trust must be anchored in something a third party can audit.
For interop with parties that only speak PKI, Sorcha runs a second, X.509 trust rail (Feature 181):
- Inbound (US3). An operator imports a signed ETSI TS 119 612 trusted-list snapshot through the Tenant Service (
POST /api/v1/trust/trustlists/import); verifying services resolve CA anchors from it for thex509-lotl/trustlisttrust source. Live List-of-Trusted-Lists refresh is deferred — the snapshot is imported, not polled. - Outbound (US4/US5). An org generates a CSR bound to its P-256 issuing key, imports an externally-issued certificate + chain (
/api/v1/trust/tenants/{tenantId}/orgs/{orgWalletAddress}/{csr,certificates/import}), and issues credentials withCredentialIssuanceConfig.TrustAnchor = x509-lotlso thex5cchains to the external root. Issuance fails closed (CERT_EXTERNAL_ANCHOR_UNAVAILABLE) when the external chain is unavailable. A non-P-256 org key is a typedCERT_KEY_NOT_ELIGIBLErefusal, not a runtime error.
Where the Implementation Stops
Honest gaps named explicitly:
- mdoc / ISO 18013-5 —
partial(Feature 135).mso_mdocissuance and online OpenID4VP verification are implemented (ES256/P-256 only); proximity transport and MAC-based device auth are deferred.STANDARDS.mdcarries the authoritative status. - Live trusted-list refresh — the ETSI TS 119 612 rail imports a signed snapshot but does not yet poll a live List of Trusted Lists; XAdES and LOTL pivot-chain validation are deferred.
- mTLS at internal hops — not yet enforced. The constitutional principle is in place; the wire enforcement is on the roadmap.
- DID method registry —
did:sorcha:org:anddid:sorcha:holder:are implemented but not registered with the W3C DID method registry. Inter-platform DID resolution requires bilateral agreement today.
Pointers
| Source | Purpose |
|---|---|
STANDARDS.md | Authoritative status of every standard cited above |
docs/security-model.md | Cryptographic posture, including the classical-at-HAIP-boundary discussion |
docs/architecture.md | Where the HAIP Service sits in the overall service topology |
specs/094-haip-issuer/, specs/097-haip-credential-issuance/, specs/098-haip-credential-presentation/ | Feature specs behind the implementation |
.claude/skills/verifiable-credentials/SKILL.md | Implementation patterns, common gotchas, and selective-disclosure mechanics |