DIDs (did:jwk, did:key, did:web)¶
Helix can accept SD-JWT VC issuers identified by decentralised identifiers (DIDs), publish a realm's own did:web document as a credential issuer, and accept DID-named holder keys in issuance proofs. (Preview, not yet tested with a real national wallet.)
Status: Preview — not yet tested with a real national wallet
New in 1.1.0-rc.1. Tested with the project's own test wallet and Helix's own issuer and verifier only.
Outside the EUDI framework only
The EU Architecture and Reference Framework and HAIP trust PID and (Q)EAA providers through X.509 chains and trusted lists. That remains the default and the only thing an EU wallet ecosystem needs; EU wallets and EUDI verifiers do not look up DIDs. DID support is for ecosystems outside that framework that identify issuers by DIDs. It is opt-in, and a DID is never trusted because it resolves: only DIDs an administrator lists are accepted.
Supported methods¶
| Method | Resolution | Used for |
|---|---|---|
did:jwk |
offline (the key is in the DID) | accepted issuers; holder keys in issuance proofs |
did:key (P-256) |
offline | accepted issuers; holder keys in issuance proofs |
did:web |
over https, see below | accepted issuers; the realm's own issuer DID |
Only ES256 (P-256) keys: an Ed25519 / EdDSA key, P-384 or RSA is refused with a message that says so.
Accepting DID issuers (verifier)¶
A wallet connection lists the DIDs it accepts:
didIssuers: the PID's issuers (Terraformdid_issuersonhelix_wallet_identity_provider);credentials[].didIssuers: each other credential's issuers (Terraform:didIssuersinsidecredentials).
Each is a JSON array of at most 20 entries. An entry is either:
- a full DID:
did:web:issuer.example[:path],did:jwk:…,did:key:z…; - or a bare lower-case domain (
issuer.example), which accepts onlydid:web:issuer.exampleitself: no port, no path, no wildcards.
"config": {
"credentialFormats": "[\"dc+sd-jwt\"]",
"didIssuers": "[\"issuer.example\", \"did:web:id.example:realms:acme\"]"
}
SD-JWT VC only: an mdoc credential cannot list DIDs, and a PID that lists DIDs needs dc+sd-jwt in credentialFormats. With DIDs, the PID or credential may have no X.509 anchors at all.
List path DIDs exactly
One host can publish many did:web DIDs — every Helix realm is did:web:<host>:realms:<realm>. A domain entry therefore never covers a DID with a path. A domain entry naming this server's own host (the host of IDP_BASE_URL or one in HELIX_ALLOWED_HOSTS) is refused with 400: it would let the administrator of any other realm on this server publish a did:web whose credentials you accept, even as a PID. List the exact realm DID instead — and only for a credential besides the PID: a realm DID of this deployment (or a did:jwk / did:key of one of its issuer keys) is refused for the PID. See This deployment's own issuer.
Editing a list is an identity-provider save: it goes through the same permission checks as trust anchors and trusted lists.
Resolve and show keys¶
The console's DID issuers field (under the trust anchors, and per credential) has Resolve and show keys: it resolves a listed DID through the server's resolver and shows each verification method, whether it can verify credentials, and why not. Nothing is stored.
curl -s -b cookies.txt -c cookies.txt "$HELIX_URL/admin/realms/$REALM/identity-providers" >/dev/null
CSRF=$(awk '($6=="__Host-XSRF-TOKEN"||$6=="XSRF-TOKEN")&&$7~/^r1\./{t=$7} END{print t}' cookies.txt)
curl -s -b cookies.txt -H "Content-Type: application/json" -H "X-XSRF-TOKEN: $CSRF" \
-d '{"did":"did:web:issuer.example"}' \
"$HELIX_URL/admin/realms/$REALM/wallet/did/resolve"
Send one DID; a bare domain entry names no single DID and is refused with a 400 on did. It needs manage-identity-providers and is limited to 20 checks a minute per administrator. Every network failure — a private or blocked host, an unknown host, TLS, HTTP errors, timeouts — gives the same "could not be resolved" answer (the reason is in the server log), so the check cannot be used to map internal DNS.
How a DID-issued credential is verified¶
A credential with x5c takes the X.509 path exactly as before. Only a credential without x5c whose header kid is a DID URL takes the DID path:
- the DID must be on this credential type's allow-list — checked before anything is resolved, so an unlisted
did:webcauses no network request (ISSUER_UNTRUSTED); - the DID is resolved, and
kidmust name a verification method inassertionMethod(ISSUER_UNTRUSTEDotherwise) with an EC P-256 key (ALGORITHM_NOT_ALLOWEDotherwise); algmust be ES256 and the signature must verify (ISSUER_SIGNATURE_INVALID);issmust be present and equal the DID;- type, validity, disclosures, key binding and status as for every SD-JWT VC. The status list of a DID-issued credential must be signed by a key of the same, still listed DID; an X.509-issued credential's list must chain to the anchors as before, never by DID.
The audit records the DID as the issuer. With DIDs configured, the DCQL query of that PID or credential carries no trusted_authorities, because the aki hint names X.509 anchors and would make a wallet withhold a DID-issued credential.
did:web resolution¶
did:web:host[%3Aport][:path…] resolves to https://host[:port]/path…/did.json (no path: /.well-known/did.json).
- Hosts must be DNS names (IP literals are refused).
- The fetch goes through the outbound guard: private, loopback, link-local and metadata addresses are refused unless
helix.egress.allowed-private-hostslists the host; https only; no redirect is followed; connect 3 s, 5 s in total including DNS, at most 64 KiB. - The document is parsed strictly and its
idmust be the DID. - Successful resolutions are cached for the smaller of the response's
Cache-Controlmax-age andhelix.did.web.cache-max-ttl(default 300 s, at most 1 h); failures are not cached.
Settings: helix.did.web.connect-timeout, helix.did.web.total-timeout, helix.did.web.max-bytes, helix.did.web.cache-max-ttl, helix.did.web.cache-max-entries.
The issuer's did.json¶
A realm that issues credentials can publish a did:web document: issuer settings → did:web → Publish did:web (publishDidWeb: true on PUT /admin/realms/{realm}/credential-issuance/settings, realm-admin; Terraform helix_credential_issuer.publish_did_web). The DID is derived from the credential issuer identifier — configuration, never the request's Host:
| Issuer identifier | DID | Document |
|---|---|---|
https://id.example/realms/acme |
did:web:id.example:realms:acme |
https://id.example/realms/acme/did.json |
https://id.example:8443/realms/acme |
did:web:id.example%3A8443:realms:acme |
https://id.example:8443/realms/acme/did.json |
Helix serves GET /realms/{realm}/did.json (application/did+json, public, Cache-Control: max-age=300, also while the realm is disabled) only while the realm issues and publishes; otherwise 404. It lists one JsonWebKey2020 verification method per issuer key that is PENDING, ACTIVE or ROTATED, with id <did>#<RFC 7638 thumbprint>, all in assertionMethod. A RETIRED key is withdrawn, so credentials it signed stop verifying with DID-resolving verifiers.
The console shows the DID, the URL and a live preview of the document (GET /admin/realms/{realm}/credential-issuance/did-document), also before you publish.
- An issuer identifier whose host is an IP address has no did:web.
- The host-wide
/.well-known/did.json(a DID without a path) is not served: one host serves many realms. Map it in your reverse proxy if you need it. - A template signs with the did:web key when its signing method is did:web key id (
signingMethod: "did_web"); see Credential issuance.
DID holder keys (issuance)¶
When a wallet collects a credential, it may name its proof key with kid = a did:jwk or did:key DID URL instead of a jwk header. It is resolved offline, must be EC P-256, and the credential's cnf.jwk is that key. did:web holder keys are refused: the issuer would fetch a URL the wallet chose, and a holder key that can change under the credential defeats holder binding. The issuer metadata advertises cryptographic_binding_methods_supported: ["jwk", "did:jwk", "did:key"] for SD-JWT VC configurations.
Not in this release¶
did:ebsi, did:peer, did:ion and universal resolvers; EdDSA keys; DID-based mdoc issuers; DID document proofs or Data Integrity signatures; DID-based wallet or client identification; did:web holder keys; the host-wide /.well-known/did.json. A did:web is only as strong as control of its domain and TLS certificate: for qualified or high-assurance use, stay with X.509 and trusted lists.