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:
| Operation | Description |
|---|---|
create | Create a new DID with generated or existing keys |
update | Modify a DID document (add/remove keys, services) |
addVerificationMethod | Add a verification method to an existing DID |
deactivate | Deactivate a DID |
list | Query stored DIDs with filters |
get | Find a DID by its identifier |
getByAlias | Find 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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
alias(name) | Key alias in KMS |
providerId(id) | KMS provider ID |
purposes(...) | Verification purposes |
ServiceBuilder
| Method | Description |
|---|---|
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
| Backend | Use Case | Module |
|---|---|---|
| Memory | Testing, ephemeral | lib-did-persistence-memory |
| SQLite | Mobile, embedded | lib-did-persistence-sqlite |
| PostgreSQL | Enterprise (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
| DID | Published URL |
|---|---|
did:web:example.com | https://example.com/.well-known/did.json |
did:web:example.com:path | https://example.com/path/did.json |
did:web:example.com:users:alice | https://example.com/users/alice/did.json |
did:web:example.com%3A8080 | https://example.com:8080/.well-known/did.json |
REST API (Universal Registrar)
The EDK provides REST APIs for DID management. See EDK Decentralized Identifiers.
| Endpoint | Description |
|---|---|
POST /1.0/create | Create a new DID |
POST /1.0/update | Update an existing DID |
POST /1.0/deactivate | Deactivate a DID |
GET /1.0/methods | List supported methods |
GET /1.0/properties | Get capabilities |
Platform Examples
- Kotlin/JVM
- Swift/iOS
// 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}")
// Create DID on iOS
let didManager = session.graph.didManager
let options = DidCreateOptions(
method: "key",
alias: "signing-key"
)
let result = try await didManager.create(options: options)
print("Created DID: \(result.did)")
// Use for signing
let signCommand = session.graph.signJwsCommand
let signResult = try await signCommand.execute(
args: SignJwsArgs(
payload: "Hello, World!".data(using: .utf8)!,
identifierOpts: ManagedIdentifierOpts(
method: .DID,
identifier: result.did
)
),
context: sessionContext
)
Next Steps
- DID Resolution - Resolve and query DIDs
- Key Management - Manage cryptographic keys
- Identifier Resolution - Unified identifier resolution