European Digital Identity Wallet · Federation
Every CodeB Sovereign Communications tenant runs its own issuer, its own verifier, and its own trust anchors. When two tenants need to accept each other's Verifiable Credentials — a hotel accepting guest ID cards issued by a resort chain's HR wallet, a municipality accepting mDLs issued by a neighbouring city, an enterprise consortium sharing a single European Digital Identity Wallet fabric — the admin at each side enrolls the other from an admin page. Trust is directional, admin-controlled, and hot-path zero-network.
Admin at tenant B navigates to /trust-tenants-admin.html, authenticated via the tenant's OpenID Connect Provider.
Admin types the foreign tenant's fully qualified domain (for example phone.aloaha.com) and clicks Add trust.
Backend fetches https://<host>/.well-known/did.json with an SSRF-hardened HTTP client, validates the certificate against the original hostname, and extracts the issuer's EC P-256 public key. Same-machine shortcut: if the target tenant is co-hosted under this server's App_Data/, the issuer key is loaded directly from local disk — no network hop, no SSRF concern.
The publicKeyJwk is written to App_Data/<tenantB>/trust/trusted-tenants.json via an atomic write with automatic backup. RFC 7517 hints (use=sig, alg=ES256, key_ops=[verify]) are supplied when the remote DID Document omits them.
Some time later, a wallet presents a Verifiable Credential to tenant B whose payload has iss = did:web:phone.aloaha.com. B's verifier calls TenantTrust.ResolveJwk, gets the cached key, and runs ES256 verification — pure disk read, no network hop.
Cache freshness is reported as a pill (fresh ≤ 7 days). Verification never expires on the cached key; a Refresh button re-fetches the DID Document to pick up issuer-key rotation.
Adding a foreign tenant means fetching an arbitrary URL from inside the tenant server — that surface is worth taking seriously. The fetch layer:
Host header and TLS SNI to the original hostname and installs a per-request certificate-validation callback that accepts the resulting name-mismatch only when the certificate's Common Name or Subject Alternative Name covers the original hostname (with conservative single-label wildcard matching).ServicePointManager certificate callback, so other outbound TLS calls are unaffected.Not every key in a remote DID Document is legitimate for signing credentials. We only accept a publicKeyJwk when:
id matches did:web:<requested-host>. A document served from one host that claims to be another is rejected outright (W3C DID Core §7.1).id appears in the DID Document's assertionMethod or authentication list.controller, if present, matches the DID subject.assertionMethod/authentication keys at all, we reject the whole document (fail-closed).| Layer | Standard | How we match |
|---|---|---|
| Identifier | W3C DID Core 1.0 · did:web | Foreign tenants identified as did:web:<host>; keys retrieved from their /.well-known/did.json. |
| Public key format | RFC 7517 JWK | Cached with kty, crv, x, y, kid, use=sig, alg=ES256, key_ops=["verify"]. |
| Signature algorithm | RFC 7515 · ES256 | P-256 with SHA-256; IEEE P1363 (64-byte) wire format enforced with explicit length check. |
| Credential envelope | W3C VCDM 2.0 · JOSE | Foreign vc+jwt credentials verify through the same code path as our own issuer; the trust check is what makes federation work. |
| Publication analogue | ETSI TS 119 612 Trusted List | Our per-tenant JSON list is functionally equivalent to a Trusted List entry — roadmap: also expose it as a mini-LOTL for off-the-shelf DSS consumers. |
| Federation analogue | OpenID Federation 1.0 | Roadmap: sign each cross-tenant entry as an OIDF Subordinate Statement to prove "B declared A trusted at time T". |
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/trust-tenants-admin.html | GET | OIDC (admin/superuser) | Admin UI: list, add, refresh, remove, live-test. |
/trust-tenants.ashx?action=list | GET | Bearer (admin/superuser) | Enumerate configured trust entries. |
/trust-tenants.ashx?action=add | POST | Bearer (admin/superuser) | Enroll a new tenant. Body: {"host":"..."}. |
/trust-tenants.ashx?action=refresh | POST | Bearer (admin/superuser) | Re-fetch the cached DID Document. |
/trust-tenants.ashx?action=remove | POST | Bearer (admin/superuser) | Stop trusting a tenant. |
/trust-tenants.ashx?action=resolve&iss=did:web:X | GET | Public | Verifier hook — returns the cached JWKs for X if trusted, or trusted:false. Only exposes public keys the admin already approved. |
/vcdm.ashx?action=verify | POST | Public | W3C VCDM 2.0 verifier. Foreign issuers are automatically routed through the trust list. |
Every tenant has its own trust file. Cross-tenant isolation is enforced by the multi-tenant-by-domain rule (each tenant is a separate App_Data/<tenant>/ subtree).
App_Data/
phone.example.com/
trust/
trusted-tenants.json <- live file
trusted-tenants.json.backup <- pre-write copy for atomic-replace
Every tenant publishes a signed statement at /.well-known/trust-federation.jws — a compact JWS with typ = trust-federation+jwt, signed by the tenant's own issuer key (ES256), listing every peer this tenant currently trusts along with each peer's cached public keys. Third parties (auditors, other verifiers, regulators) can pull this endpoint, resolve did:web:<host> to get the signing key, and independently verify who this tenant trusts. No central anchor, no smart contract, no LOTL required.
curl https://phone.example.com/.well-known/trust-federation.jws
# returns: base64url(header).base64url(payload).base64url(signature)
# header: {"typ":"trust-federation+jwt","alg":"ES256","kid":"did:web:phone.example.com#issuer"}
# payload: {"iss":"did:web:phone.example.com","sub":"did:web:phone.example.com",
# "iat":..., "exp":..., "trust_model":"codeb-directional-peer-trust/1",
# "trusted_peers_count":N, "trusted_peers":[ {host,did,added_at,jwks[]} ]}
/trusted-issuers/{did} for EBSI-native verifiers.