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

DID Management

The IDK provides a DID management system for creating, updating, and deactivating DIDs with a fluent Kotlin DSL.

A DID goes through a lifecycle: it is created with one or more verification methods (keys), optionally updated to add or remove keys and services, and eventually deactivated when it should no longer be trusted. The IDK manages this lifecycle and tracks the mapping between verification methods in the DID document and the actual keys stored in your KMS.

DID Manager

The DidManager interface provides lifecycle operations for DIDs:

OperationDescription
createCreate a new DID with generated or existing keys
updateModify a DID document (add/remove keys, services)
addVerificationMethodAdd a verification method to an existing DID
deactivateDeactivate a DID
listQuery stored DIDs with filters
getFind a DID by its identifier
getByAliasFind a DID by alias

Creation DSL

The IDK provides a Kotlin DSL for creating DIDs with a fluent, type-safe syntax. The DSL's method() call selects which DID method to use, while the key configuration block (autoGenerateKey or useExistingKey) controls which cryptographic key backs the DID. Each key can be assigned one or more verification purposes like authentication or assertion.

Simple did:key

Use did:key when you need a self-contained DID that encodes the public key directly in the identifier. There is no external resolution needed, making it a good fit for ephemeral identities, testing, or peer-to-peer protocols.

import com.sphereon.did.manager.dsl.didCreateOptions
import com.sphereon.crypto.core.generic.KeyTypeMapping
import com.sphereon.crypto.core.generic.Curve
import com.sphereon.did.models.VerificationPurpose

val options = didCreateOptions {
method("key")
alias("my-signing-did")

autoGenerateKey {
keyType(KeyTypeMapping.OKP)
curve(Curve.Ed25519)
purposes(
VerificationPurpose.AUTHENTICATION,
VerificationPurpose.ASSERTION_METHOD
)
}
}

val result = didManager.create(options.options)
println("Created: ${result.did}")
// Output: Created: did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK

did:jwk

did:jwk is similar to did:key but encodes the public key as a JWK in the identifier. This is useful when your ecosystem is JWK-centric and you want the DID to directly carry JWK metadata like alg and use.

val options = didCreateOptions {
method("jwk")
alias("my-jwk-did")

autoGenerateKey {
keyType(KeyTypeMapping.EC)
curve(Curve.P_256)
purposes(VerificationPurpose.AUTHENTICATION)
}
}

val result = didManager.create(options.options)
// Output: did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Ii...

did:web with Domain and Path

did:web anchors a DID to a DNS domain. The DID document is hosted at a well-known HTTPS URL, so resolvers fetch it over the web. This makes did:web a good choice for organizations that want their DID tied to a domain they control. The path() segments let you scope multiple DIDs under a single domain.

val options = didCreateOptions {
method("web")
domain("example.com")
path("users", "alice") // Results in did:web:example.com:users:alice
alias("alice-did")

autoGenerateKey {
keyType(KeyTypeMapping.EC)
curve(Curve.P_256)
purposes(
VerificationPurpose.AUTHENTICATION,
VerificationPurpose.ASSERTION_METHOD
)
}

// Add service endpoints
service("website") {
type("LinkedDomains")
endpoint("https://alice.example.com")
}
}

val result = didManager.create(options.options)

// After creation, publish the DID document:
// https://example.com/users/alice/did.json

Multiple Verification Methods

For DIDs that need multiple keys (common with did:web), you define separate verificationMethod blocks, each with its own key type, curve, and purpose. A typical setup uses one key for authentication, another for signing credentials (assertion), and optionally a third for encrypted communication (key agreement).

val options = didCreateOptions {
method("web")
domain("example.com")

// Authentication key (Ed25519 for signatures)
verificationMethod("key-1") {
autoGenerateKey {
keyType(KeyTypeMapping.OKP)
curve(Curve.Ed25519)
kmsProvider("software") // Optional: specify KMS provider
}
purposes(VerificationPurpose.AUTHENTICATION)
}

// Assertion key (P-256 for credentials)
verificationMethod("key-2") {
autoGenerateKey {
keyType(KeyTypeMapping.EC)
curve(Curve.P_256)
}
purposes(VerificationPurpose.ASSERTION_METHOD)
}

// Key agreement (X25519 for encryption)
verificationMethod("key-3") {
autoGenerateKey {
keyType(KeyTypeMapping.OKP)
curve(Curve.X25519)
}
purposes(VerificationPurpose.KEY_AGREEMENT)
}

// Multiple services
service("hub") {
type("LinkedDomains")
endpoint("https://hub.example.com")
}

service("messaging") {
type("DIDCommMessaging")
endpoint("https://messaging.example.com")
}
}

Using Existing Keys

If you have already generated keys through the KeyManagerService, you can reference them by alias instead of generating new ones. This is common when you manage keys separately from DID creation, for example when keys are provisioned in a cloud KMS.

Reference keys already stored in KMS:

val options = didCreateOptions {
method("web")
domain("example.com")

// Use existing key by alias
verificationMethod("key-1") {
useExistingKey {
alias("my-existing-signing-key")
providerId("aws-kms") // Optional: specify provider
}
purposes(VerificationPurpose.AUTHENTICATION)
}
}

Using an Existing JWK

Create a DID from an existing public key:

val existingJwk = Jwk(
kty = JwaKeyType.EC,
crv = "P-256",
x = "WKn-ZIGevcwGFOMJ0GeEei2HpXXU6H0z...",
y = "y77t-RvAHRKTsSGd5KhPq8dJ7VhxqPxM..."
)

val options = didCreateOptions {
method("jwk")
useExistingKey(existingJwk)
alias("imported-key")
}

val result = didManager.create(options.options)

DSL Reference

DidCreateBuilder

MethodDescription
method(name)DID method: "key", "jwk", "web"
alias(name)Human-readable alias
domain(domain)Domain for did:web
path(segments...)Path segments for did:web
controller(did)Controller DID (defaults to self)
autoGenerateKey { }Generate a new key via KMS
useExistingKey { }Use existing key by alias
useExistingKey(jwk)Use existing JWK directly
verificationMethod(id) { }Add named verification method
service(id) { }Add service endpoint

AutoGenerateKeyBuilder

MethodDescription
keyType(type)KeyTypeMapping.OKP, EC, or RSA
curve(curve)Curve.Ed25519, P_256, P_384, etc.
algorithm(alg)Optional signature algorithm
kmsProvider(id)KMS provider ID
purposes(...)Verification purposes
verificationMethodId(id)Custom key ID

ExistingKeyByAliasBuilder

MethodDescription
alias(name)Key alias in KMS
providerId(id)KMS provider ID
purposes(...)Verification purposes

ServiceBuilder

MethodDescription
type(type)Service type (e.g., "LinkedDomains")
endpoint(url)Service endpoint URL
endpoint(map)Complex endpoint as map

Update DID

Updating a DID lets you add or remove verification methods and service endpoints without creating a new identifier. Note that did:key and did:jwk are immutable by design, so updates only apply to methods like did:web where the document can be republished.

Modify an existing DID document:

val updateResult = didManager.update(
did = "did:web:example.com",
options = DidUpdateOptions(
// Add a new service
addServices = listOf(
DidService(
id = "#messaging",
type = "DIDCommMessaging",
serviceEndpoint = "https://messaging.example.com"
)
),

// Remove services
removeServiceIds = listOf("#old-service"),

// Add verification methods
addVerificationMethods = listOf(
VerificationMethod(
id = "did:web:example.com#key-4",
type = "JsonWebKey2020",
controller = "did:web:example.com",
publicKeyJwk = newPublicKey
)
),

// Remove verification methods
removeVerificationMethodIds = listOf("#deprecated-key")
)
)

updateResult.fold(
success = { result ->
println("Updated: ${result.did}")
// For did:web, publish the updated document
},
failure = { error ->
println("Update failed: ${error.message}")
}
)

Add Verification Method

This is a convenience method for the common case of adding a single new key to a DID document without touching anything else. It handles both the document update and the KMS key mapping in one call.

Add a verification method to an existing DID:

val addKeyResult = didManager.addVerificationMethod(
did = "did:web:example.com",
options = AddKeyOptions(
publicKeyJwk = newPublicKey,
verificationMethodId = "key-5",
verificationMethodType = VerificationMethodType.JSON_WEB_KEY_2020,
purposes = listOf(VerificationPurpose.ASSERTION_METHOD)
)
)

Deactivate DID

Deactivation signals to resolvers that a DID should no longer be considered valid. For did:web, you still need to publish the deactivated document so that resolvers can discover the deactivation. This is typically done in response to key compromise or when an identity is retired.

Mark a DID as deactivated:

val deactivateResult = didManager.deactivate(
did = "did:web:example.com",
options = DidDeactivateOptions(
reason = "Key compromise"
)
)

deactivateResult.fold(
success = { result ->
println("Deactivated: ${result.did}")

// For did:web, publish the deactivated document
val deactivatedDoc = result.deactivatedDocument
// Publish to https://example.com/.well-known/did.json
},
failure = { error ->
println("Deactivation failed: ${error.message}")
}
)

Persistence

Storage Backends

BackendUse CaseModule
MemoryTesting, ephemerallib-did-persistence-memory
SQLiteMobile, embeddedlib-did-persistence-sqlite
PostgreSQLEnterprise (EDK)EDK module

DidRepository API

Direct access to DID storage:

val didRepository: DidRepository = session.graph.didRepository

// Save a DID record
didRepository.save(didRecord).getOrThrow()

// Find by DID
val record = didRepository.findByDid("did:web:example.com").getOrNull()

// Find by alias
val record = didRepository.findByAlias("my-did").getOrNull()

// Query with filter
val records = didRepository.findAll(
DidRecordFilter(
method = "web",
role = DidRole.MANAGED
)
).getOrThrow()

// Update
didRepository.update(updatedRecord).getOrThrow()

// Delete
didRepository.delete("did:web:example.com").getOrThrow()

Key Mappings

The IDK tracks which KMS key backs each verification method in a DID document. These mappings let the system automatically find the right signing key when you present a DID for credential issuance or authentication, without you having to manually wire up key aliases to verification method IDs.

Track the relationship between verification methods and KMS keys:

// Get key mappings for a DID
val mappings = didRepository.getKeyMappings(didRecordId).getOrThrow()

mappings.forEach { mapping ->
println("Verification method: ${mapping.verificationMethodId}")
println("KMS key alias: ${mapping.kmsKeyAlias}")
println("KMS provider: ${mapping.kmsProviderId}")
println("Purposes: ${mapping.purposesJson}")
}

// Save a key mapping
didRepository.saveKeyMapping(
didRecordId = recordId,
mapping = DidKeyMappingRecord(
id = uuid(),
verificationMethodId = "#key-1",
kmsKeyAlias = "my-signing-key",
kmsProviderId = "software",
purposesJson = """["authentication", "assertionMethod"]"""
)
).getOrThrow()

// Delete key mappings
didRepository.deleteKeyMapping(mappingId)
didRepository.deleteKeyMappingsForDid(didRecordId)

Publishing did:web

For did:web, you must publish the DID document to your web server:

val result = didManager.create(options.options)
val didDocument = result.didDocument

// Serialize to JSON
val json = Json.encodeToString(didDocument)

// Publish to the correct URL based on the DID:
// did:web:example.com → https://example.com/.well-known/did.json
// did:web:example.com:users:alice → https://example.com/users/alice/did.json

// Example with Ktor client
httpClient.put("https://example.com/.well-known/did.json") {
contentType(ContentType.Application.Json)
setBody(json)
}

did:web URL Mapping

DIDPublished URL
did:web:example.comhttps://example.com/.well-known/did.json
did:web:example.com:pathhttps://example.com/path/did.json
did:web:example.com:users:alicehttps://example.com/users/alice/did.json
did:web:example.com%3A8080https://example.com:8080/.well-known/did.json

REST API (Universal Registrar)

The EDK provides REST APIs for DID management. See EDK Decentralized Identifiers.

EndpointDescription
POST /1.0/createCreate a new DID
POST /1.0/updateUpdate an existing DID
POST /1.0/deactivateDeactivate a DID
GET /1.0/methodsList supported methods
GET /1.0/propertiesGet capabilities

Platform Examples

// Complete example: Create and use a DID
val didManager: DidManager = session.graph.didManager

// 1. Create DID
val createOptions = didCreateOptions {
method("key")
alias("signing-key")
autoGenerateKey {
keyType(KeyTypeMapping.OKP)
curve(Curve.Ed25519)
purposes(VerificationPurpose.ASSERTION_METHOD)
}
}

val createResult = didManager.create(
createOptions.options
)
val did = createResult.did

// 2. Use DID for signing
val signCommand: SignJwsCommand = session.graph.signJwsCommand
val signResult = signCommand.execute(
SignJwsArgs(
payload = "Hello, World!".encodeToByteArray(),
identifierOpts = ManagedIdentifierOpts(
method = IdentifierMethodDefaults.DID,
identifier = did
)
),
sessionContext
)

println("Signed JWS: ${signResult.getOrThrow().jws}")

Next Steps