Identifier Resolution
The IDK's identifier resolution system provides a unified way to resolve various cryptographic identifiers (DIDs, JWKs, X.509 certificates, key aliases) to their corresponding key material. This is essential for signature verification, encryption, and trust validation.
Overview
When verifying signatures or establishing trust, you often receive identifiers rather than raw keys:
- A JWT with a
kid(key ID) header - A DID (Decentralized Identifier) like
did:web:example.com - An X.509 certificate chain
- A JWKS URL pointing to a key set
The identifier resolution system resolves these to usable key objects.
Architecture
Identifier Types
The IDK supports multiple identifier methods:
| Method | Description | Example |
|---|---|---|
DID | Decentralized Identifier | did:web:example.com |
JWK | JSON Web Key object | { "kty": "EC", "crv": "P-256", ... } |
X5C | X.509 certificate chain | Base64-encoded certificates |
KID | Key ID in KMS | signing-key-001 |
KEY_ALIAS | Key alias in KMS | my-signing-key |
JWKS_URL | URL to JWKS endpoint | https://example.com/.well-known/jwks.json |
OIDC_DISCOVERY | OpenID Connect discovery | https://example.com/.well-known/openid-configuration |
OID4VCI_ISSUER | OID4VCI credential issuer | https://issuer.example.com/.well-known/openid-credential-issuer |
ENTITY_ID | OpenID Federation entity | https://federation.example.com |
CNF | RFC 7800 Confirmation claim | { "kid": "did:key:...", "jwk": {...} } |
Managed vs External Identifiers
The system distinguishes between two categories:
Managed Identifiers
Keys managed by your KMS (Key Management System):
// Resolve by key ID
val opts = ManagedIdentifierOpts(
method = IdentifierMethodDefaults.KID,
identifier = "signing-key-001",
providerId = "azure-keyvault"
)
val result = identifierService.resolve(opts)
val keyInfo = result.getOrThrow().keyInfo
External Identifiers
Keys from external sources (URLs, certificates, DIDs):
// Resolve from JWKS URL
val opts = ExternalIdentifierOpts(
method = IdentifierMethodDefaults.JWKS_URL,
identifier = "https://example.com/.well-known/jwks.json",
kid = "key-1" // Select specific key from the set
)
val result = identifierService.resolve(opts)
val jwk = result.getOrThrow().jwk
Usage Examples
Resolve DID to Key
- Kotlin
- Swift
val identifierService: IIdentifierService = session.component.identifierService
// Resolve a did:web identifier
val result = identifierService.resolve(
ExternalIdentifierOpts(
method = IdentifierMethodDefaults.DID,
identifier = "did:web:example.com",
kid = "key-1" // Optional: select specific key from DID document
)
)
result.fold(
success = { resolved ->
val key = resolved.jwk
println("Resolved key type: ${key?.kty}")
println("Key ID: ${key?.kid}")
},
failure = { error ->
println("Resolution failed: ${error.message}")
}
)
let identifierService = session.component.identifierService
let result = try await identifierService.resolve(
opts: ExternalIdentifierOpts(
method: .DID,
identifier: "did:web:example.com",
kid: "key-1"
)
)
if let resolved = result.value {
print("Resolved key type: \(resolved.jwk?.kty ?? "unknown")")
}
Resolve X.509 Certificate Chain
// X.509 certificates as base64-encoded strings
val x5c = listOf(
"MIIBkTCB+wIJAJ...", // End-entity certificate
"MIIBkTCB+wIJAK..." // CA certificate
)
val result = identifierService.resolve(
ExternalIdentifierOpts(
method = IdentifierMethodDefaults.X5C,
identifier = x5c
)
)
val resolved = result.getOrThrow()
println("Subject: ${resolved.certificateInfo?.subjectDN}")
println("Issuer: ${resolved.certificateInfo?.issuerDN}")
Resolve from OpenID Connect Discovery
// Resolve keys from OIDC provider
val result = identifierService.resolve(
ExternalIdentifierOpts(
method = IdentifierMethodDefaults.OIDC_DISCOVERY,
identifier = "https://accounts.google.com/.well-known/openid-configuration",
kid = "abc123" // Select key by ID
)
)
Resolve from KMS
// Resolve a managed key by alias
val result = identifierService.resolve(
ManagedIdentifierOpts(
method = IdentifierMethodDefaults.KEY_ALIAS,
identifier = "production-signing-key",
providerId = "aws-kms"
)
)
val keyInfo = result.getOrThrow().keyInfo
println("Provider: ${keyInfo?.providerId}")
println("Algorithm: ${keyInfo?.algorithm}")
Context Information
Provide context to help resolution:
val context = IdentifierContext(
issuer = "https://issuer.example.com",
clientId = "my-client-id",
clientIdScheme = "redirect_uri"
)
val opts = ExternalIdentifierOpts(
method = IdentifierMethodDefaults.JWKS_URL,
identifier = "https://example.com/jwks.json",
context = context
)
Checking Supported Methods
Query what identifier types a service supports:
val identifierService: IIdentifierService = session.component.identifierService
// List all supported methods
val methods = identifierService.supportedIdentifierMethods
println("Supported: ${methods.map { it.methodName }}")
// Check if specific identifier is supported
val isSupported = identifierService.isSupportedIdentifier("did:web:example.com")
// Check if method is supported
val methodSupported = identifierService.isSupportedIdentifierMethod(
IdentifierMethodDefaults.DID
)
Resolution Order
When multiple resolvers can handle an identifier, they are tried in priority order:
| Priority | Description |
|---|---|
HIGH (100) | Preferred resolvers |
MEDIUM (500) | Default priority |
LOW (1000) | Fallback resolvers |
Custom resolvers can specify their order:
@Inject
@SingleIn(SessionScope::class)
@ContributesBinding(SessionScope::class, IExternalIdentifierService::class, multibinding = true)
class MyCustomResolver : ExternalIdentifierServiceAdapter() {
override val order: Int = Order.HIGH.orderValue // Run before default resolvers
override val supportedIdentifierMethods = listOf(
IdentifierMethodDefaults.DID
)
override suspend fun doResolve(
opts: ExternalIdentifierOptsOrResult
): IdkResult<ExternalIdentifierOptsOrResult, IdkErrorType> {
// Custom resolution logic
}
}
Caching
Resolution results can be cached to avoid repeated network requests:
val opts = ExternalIdentifierOpts(
method = IdentifierMethodDefaults.JWKS_URL,
identifier = "https://example.com/jwks.json",
noCache = false // Use cached result if available (default)
)
// Force fresh resolution
val freshOpts = opts.copy(noCache = true)
Integration with JWS Verification
The identifier resolution system integrates with JWS verification:
val verifyJwsCommand: VerifyJwsCommand = session.component.verifyJwsCommand
// The command automatically resolves the key from the JWT header
val result = verifyJwsCommand.execute(
VerifyJwsArgs(
jws = signedJwt,
// Resolution happens automatically based on 'kid', 'jwk', 'x5c' headers
identifierOpts = ExternalIdentifierOpts(
method = IdentifierMethodDefaults.JWKS_URL,
identifier = "https://issuer.example.com/.well-known/jwks.json"
)
),
sessionContext
)
CNF (Confirmation) Claims
The IDK supports RFC 7800 Confirmation claims for holder binding in SD-JWT and other proof-of-possession scenarios.
CNF Structure
A CNF claim can contain any combination of:
| Property | Description |
|---|---|
jwk | Public key as JWK (most common) |
kid | Key identifier - can be a DID or opaque string |
jku | JWK Set URL from which to fetch keys |
Resolution Rules
The CNF resolver follows strict priority rules:
Key rule: A kid that is not a DID cannot be resolved on its own. It must be accompanied by a jwk or jku that provides the actual key material.
Creating CNF Claims
Use the CnfBuilder DSL to create CNF claims:
import com.sphereon.sdjwt.cnf
import com.sphereon.sdjwt.cnfFromDid
import com.sphereon.sdjwt.cnfFromJwk
// Simple CNF with just a JWK
val simpleCnf = cnf {
jwk(holderPublicKey)
}
// DID-based CNF (recommended for interoperability)
val didCnf = cnf {
kid("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
jwk(holderPublicKey) // Include for backwards compatibility
}
// Convenience function for DID-based CNF
val didCnf2 = cnfFromDid(
verificationMethodId = "did:key:z6Mk...#z6Mk...",
publicKey = holderPublicKey
)
// JWK Set URL reference
val jkuCnf = cnf {
jku("https://holder.example.com/.well-known/jwks.json")
kid("key-1") // Select specific key from the set
}
CNF Validation Rules
When building a CNF claim, the following rules are enforced:
- At least one of
kid,jwk, orjkumust be present - If only
kidis provided (nojwkorjku), it must be a DID - Non-DID
kidvalues can only be used alongsidejwkorjku
// Valid: DID-only kid (can be resolved)
val valid1 = cnf { kid("did:key:z6Mk...") }
// Valid: non-DID kid with jwk
val valid2 = cnf {
kid("key-1")
jwk(publicKey)
}
// INVALID: non-DID kid alone (throws IllegalArgumentException)
val invalid = cnf { kid("key-1") } // Error!
Resolving CNF Claims
val identifierService: IIdentifierService = session.component.identifierService
// Resolve a CNF claim
val result = identifierService.resolve(
ExternalIdentifierCnfOpts(
identifier = mapOf(
"kid" to "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"jwk" to jwkMap
),
kid = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
jwk = holderJwk
)
)
result.fold(
success = { cnfResult: ExternalIdentifierResult.Cnf ->
val holderKey = cnfResult.keyInfo.key
val resolvedFrom = cnfResult.resolvedFrom // DID, JWK, or JKU
println("Key resolved from: $resolvedFrom")
},
failure = { error ->
println("CNF resolution failed: ${error.message}")
}
)
CNF Resolution Sources
The result indicates how the key was resolved:
| Source | Description |
|---|---|
CnfResolutionSource.JWK | Key obtained from cnf.jwk directly |
CnfResolutionSource.DID | Key obtained by resolving DID from cnf.kid |
CnfResolutionSource.JKU | Key obtained by fetching JWK Set from cnf.jku |
SD-JWT Holder Binding
CNF claims are primarily used for holder binding in SD-JWT:
// When issuing an SD-JWT with holder binding
val sdJwtPayload = sdJwtPayload {
iss("https://issuer.example.com")
sub("user123")
cnf {
kid("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#key-1")
jwk(holderPublicKey)
}
// ... other claims
}
// When verifying, the CNF is automatically resolved
val verifyResult = verifySdJwtCommand.execute(
VerifySdJwtArgs(
sdJwt = sdJwtPresentation,
issuerIdentifier = ExternalIdentifierOpts(
method = IdentifierMethodDefaults.DID,
identifier = issuerDid
)
// Holder binding verification uses the CNF claim automatically
),
sessionContext
)
Available Resolvers
| Resolver | Identifier Types | Description |
|---|---|---|
JwkExternalIdentifierResolutionService | JWK | Resolves inline JWK objects |
JwksUrlExternalIdentifierResolutionService | JWKS_URL | Fetches keys from JWKS endpoints |
X5cExternalIdentifierResolutionService | X5C | Parses X.509 certificate chains |
KeyInfoIdentifierResolutionService | KID, KEY_ALIAS | Resolves from KMS providers |
CnfExternalIdentifierResolutionService | CNF | Resolves RFC 7800 confirmation claims |
ETSITrustListIdentifierResolutionService | ENTITY_ID | Resolves from ETSI trust lists |
Dependencies
dependencies {
// Core crypto with resolution
implementation("com.sphereon.idk:lib-crypto-core:0.13.0")
// For trust list resolution
implementation("com.sphereon.idk:lib-trust-etsi:0.13.0")
}
Next Steps
- Key Management - Managing keys in KMS providers
- Signing & Verification - Using resolved keys
- Trust Validation - Validating certificate chains