Skip to content

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.

PhaseWhat happensWhere in code
OfferIssuing authority builds a credential offer (SD-JWT VC type) and hands it to the holder out-of-bandsrc/Services/Sorcha.Haip.Service
Pre-authorised tokenHolder wallet exchanges the pre-auth code (plus PIN, where required) for an access tokenHAIP Service token endpoint
Credential endpointHolder wallet POSTs proof-of-possession of its key; HAIP Service mints the SD-JWT VCHAIP Service credential endpoint
Status listIssued credentials reference an IETF Token Status List 2024 (RFC 9972) URL the holder can pollsrc/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, and credential_sets for alternatives). The legacy Presentation Exchange presentation_definition / input_descriptors shape is retired; the shared model lives in src/Common/Sorcha.Verifier.Engine/Dcql. A single request can ask for multiple credentials, and credential_sets expresses "present any one of these" alternatives.
  • Signed request objects + x509_san_dns client_id. The verifier signs its request object (ES256) with an X.509 verifier certificate, embeds the x5c chain, and identifies itself with a prefixed x509_san_dns:{host} client_id whose host equals the certificate's SAN dNSName. Verifier config: Haip:VerifierCertificate (+ optional Haip:VerifierCertificatePassword) and Haip: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 the client_id host, then a chain-walk to a trusted-list anchor. The result is a three-state VerifierAuthStateTrustedListVerified, AuthenticUntrusted, or Unverifiable. A tampered signature or SAN mismatch is a hard refusal; absent trust anchors never block a valid signed request.
PhaseWhat happens
Authorization requestVerifier 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 handoverThe request is encoded as a QR; the holder wallet scans or deep-links from another device and fetches the signed request object
Verifier authenticationThe wallet validates the request-object signature, the SAN-to-client_id binding, and the anchor chain into a VerifierAuthState shown on the consent sheet
PresentationThe 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
VerificationVerifier 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/enrol registers 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 is IIssuerKeyResolver in src/Common/Sorcha.Cryptography.
  • JWK registry (development) — for development and demos, a JwkRegistryIssuerKeyResolver allows 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 the x509-lotl / trustlist trust 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 with CredentialIssuanceConfig.TrustAnchor = x509-lotl so the x5c chains 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 typed CERT_KEY_NOT_ELIGIBLE refusal, not a runtime error.

Where the Implementation Stops

Honest gaps named explicitly:

  • mdoc / ISO 18013-5partial (Feature 135). mso_mdoc issuance and online OpenID4VP verification are implemented (ES256/P-256 only); proximity transport and MAC-based device auth are deferred. STANDARDS.md carries 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 registrydid:sorcha:org: and did:sorcha:holder: are implemented but not registered with the W3C DID method registry. Inter-platform DID resolution requires bilateral agreement today.

Pointers

SourcePurpose
STANDARDS.mdAuthoritative status of every standard cited above
docs/security-model.mdCryptographic posture, including the classical-at-HAIP-boundary discussion
docs/architecture.mdWhere 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.mdImplementation patterns, common gotchas, and selective-disclosure mechanics

Released under the MIT License.