Skip to main content
Version: v0.25.0 (Latest)

DID Container

sphereon/enterprise-did is the only EDK container with a significant public surface that is not protocol-shaped. Where the issuer, verifier, and AS expose protocol endpoints (OID4VCI, OID4VP, OAuth/OIDC) on the public ingress, the DID container exposes a Universal Resolver path and, when did:web is enabled for a tenant, the served DID document URLs themselves. Anything else lives on the internal admin surface.

That asymmetry is intentional. DID resolution is a public good in the verifiable-credentials ecosystem: wallets, issuers, verifiers, and arbitrary third parties need to be able to resolve DIDs that the deployment hosts. Splitting the resolver into its own container keeps that public surface isolated from the issuer's and verifier's protocol surfaces, lets resolution scale horizontally independent of credential issuance volume, and keeps the admin functions (publishing, log management, method allowlists) safely internal.

DID container public and internal surface

Public Surface: The Universal Resolver

The public side serves the Universal Resolver path: GET /1.0/identifiers/{did} returns the resolved DID document with the standard W3C resolver result shape. This is what external clients hit.

When did:web is enabled for a tenant, the tenant's did:web document URLs are also public: a tenant at acme.did.example.com can publish documents at did:web:acme.did.example.com:doc:id-1, and the document is served at the corresponding https://acme.did.example.com/.well-known/did.json or https://acme.did.example.com/doc/id-1/did.json path. The DID container is the HTTP server for those documents.

Per-tenant multi-tenancy on the public surface works through subdomain resolution and verified custom domains, the same resolver stack the other EDK containers use. acme.did.example.com resolves to the acme tenant and serves only that tenant's documents and method registrations.

The public surface is rate-limited and cached. The DidResolutionCache sits in front of the four method libraries (did:web, did:webvh, did:jwk, did:key) with a configurable TTL. Cache invalidation flows through the same event subsystem that handles tenant routing and config updates.

Internal Surface: Manager, Registrar, Webvh

Three families of admin endpoints sit behind the internal ingress.

The manager admin is the larger set: more than ten HTTP adapters that handle did: document lifecycle (DidManager, DidLifecycle, DidService, Controller, VerificationMethod, VerificationRelationship, KeyMapping, AlsoKnownAs, Capability, EquivalentId, DocumentCache). Tenant administrators use this surface to manage the documents they publish and the cache behaviour for resolution.

The Universal Registrar (POST /1.0/create, POST /1.0/update, POST /1.0/deactivate) is a Sphereon-implemented Universal Registrar that supports creating, updating, and deactivating DIDs through the standard Universal Registrar shape. It is internal because creating DIDs is privileged: the registrar generates key material on the KMS, writes the DID document, and (for did:webvh) writes the first log entry.

The did:webvh log server serves the verifiable history log for did:webvh DIDs. The log itself is public (any verifier needs to walk the log to validate did:webvh resolution), but the writing of new log entries, the witness-proof generation, and the log replay are internal admin operations.

The Four Methods

The container ships all four EDK-supported DID methods:

  • did:key: fully self-contained, no infrastructure dependency. Resolution is a pure function of the DID string.
  • did:jwk: similar to did:key but with the public key embedded as a JWK. Pure resolution.
  • did:web: the DID document is served at a well-known URL on the tenant's host. Suitable when the tenant controls a stable domain and wants identifiers that anyone can resolve over plain HTTPS.
  • did:webvh: a verifiable history extension to did:web with a tamper-evident log, witness proofs (multiple independent parties co-sign), and the SCID (Self-Certifying Identifier) bound at creation. Suitable when third parties need to verify that a DID document has not been silently rewritten by its controller.

The four method libraries (resolver, provider, plus webvh's log writer, log replayer, witness-proof tooling, and SCID hasher) are all on the classpath. Tenants opt each method in or out individually through the per-tenant method allowlist.

Per-Tenant Method Allowlist

By default, nothing is enabled for a new tenant: the deployment never blanket-trusts a method on a tenant's behalf. The tenant administrator opts each method in through the admin REST (POST /api/v1/did/methods), setting the enabled flag and any method-specific configuration (the did:web document base path, the did:webvh witness policy, the trust policy for resolution).

Disabling a method blocks both resolution (the public resolver path returns a 404 for that method on that tenant) and registration (the registrar refuses to create new identifiers using that method on that tenant). Existing identifiers remain in storage but become inert until the method is re-enabled.

Persistence

The DID container's Postgres schema covers:

  • did_resolution_cache(tenant_id, did, document_jsonb, resolved_at, ttl_seconds): backing for DidResolutionCacheImpl. Replaces the in-memory cache when running in production.
  • did_web_document(tenant_id, did_url, document_jsonb, path, …): per-tenant published did:web documents.
  • webvh_log_entry(tenant_id, did, version_id, entry_jsonb, signed_at, …): did:webvh log storage. The log server reads from this table.
  • tenant_did_method(tenant_id, method, enabled, config_jsonb): the per-tenant method allowlist plus method-specific configuration.

Postgres impls live in lib-did-persistence-postgresql. The MySQL counterpart (lib-did-persistence-mysql) ships for MySQL-standardised deployments.

Building and Running

The Dockerfile is the standard EDK fat-JAR pattern: pre-built JAR copied in, eclipse-temurin:21-jre base, non-root appuser, port 8080.

The published Helm chart wires two ingresses for this container:

  • Public ingress. Routes only the resolver path (/1.0/identifiers/...) and, when did:web is in use, the document paths. Bound to the public LB, TLS-terminated, no auth.
  • Internal ingress. Carries /api/v1/did/... (manager admin), /1.0/create, /1.0/update, /1.0/deactivate (Universal Registrar), and the webvh log-management admin. Bearer-JWT required, scoped to tenant-admin and platform-admin principals.

The container reaches the KMS over the internal network for any key operation (registrar creating a new identifier, did:webvh log entry signing).

Trust Policy for Resolution

did:web and did:webvh are both fetched from external hosts during resolution, so the container needs an explicit trust policy for what it will resolve from where. Two questions matter:

  • What hosts are we willing to resolve from? A per-tenant allowlist or denylist, configurable through the manager admin. The default is no restriction.
  • For did:webvh, what witness signatures count as sufficient? The witness policy declares the required number of witness signatures, the witness identities, and the validity window. The policy is per-tenant and configurable.

For did:web documents the container itself publishes, the trust policy is moot, the document is read from local Postgres rather than fetched.

Operational Notes

  • Resolution traffic dwarfs registration traffic. Plan for the public resolver to handle most of the load, with the manager admin sitting comparatively idle most of the time.
  • Cache hit rate is the main lever. The did_resolution_cache TTL is configurable per method and per tenant. For did:key and did:jwk, the cache adds little because resolution is already pure; for did:web and did:webvh it matters a great deal.
  • did:webvh log latency. Resolving a did:webvh involves walking the log. Long logs incur per-resolution cost. The Postgres webvh_log_entry table is indexed on (tenant_id, did, version_id); an in-memory log cache sits in front of it.
  • Public route hygiene. Because the resolver path is public and unauthenticated, the container deliberately exposes no admin information through it. Resolving an unknown DID returns a standard W3C resolver result with the appropriate error code; it does not reveal whether the DID is in storage or what tenants exist.