Skip to content

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 (Terraform did_issuers on helix_wallet_identity_provider);
  • credentials[].didIssuers: each other credential's issuers (Terraform: didIssuers inside credentials).

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 only did:web:issuer.example itself: 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:

  1. the DID must be on this credential type's allow-list — checked before anything is resolved, so an unlisted did:web causes no network request (ISSUER_UNTRUSTED);
  2. the DID is resolved, and kid must name a verification method in assertionMethod (ISSUER_UNTRUSTED otherwise) with an EC P-256 key (ALGORITHM_NOT_ALLOWED otherwise);
  3. alg must be ES256 and the signature must verify (ISSUER_SIGNATURE_INVALID);
  4. iss must be present and equal the DID;
  5. 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-hosts lists 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 id must be the DID.
  • Successful resolutions are cached for the smaller of the response's Cache-Control max-age and helix.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
curl -s "$HELIX_URL/realms/$REALM/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.

See also