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

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. You need this 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 Resolution Architecture

Identifier Types​

The IDK supports multiple identifier methods:

MethodDescriptionExample
DIDDecentralized Identifierdid:web:example.com
JWKJSON Web Key object{ "kty": "EC", "crv": "P-256", ... }
X5CX.509 certificate chainBase64-encoded certificates
KIDKey ID in KMSsigning-key-001
KEY_ALIASKey alias in KMSmy-signing-key
JWKS_URLURL to JWKS endpointhttps://example.com/.well-known/jwks.json
OIDC_DISCOVERYOpenID Connect discoveryhttps://example.com/.well-known/openid-configuration
OID4VCI_ISSUEROID4VCI credential issuerhttps://issuer.example.com/.well-known/openid-credential-issuer
ENTITY_IDOpenID Federation entityhttps://federation.example.com
CNFRFC 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​

val identifierService: IIdentifierService = session.graph.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}")
}
)

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.graph.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:

PriorityDescription
HIGH (100)Preferred resolvers
MEDIUM (500)Default priority
LOW (1000)Fallback resolvers

Custom resolvers can specify their order:

@Inject
@SingleIn(SessionScope::class)
@ContributesIntoSet(SessionScope::class, binding = binding<IExternalIdentifierService>())
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.graph.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:

PropertyDescription
jwkPublic key as JWK (most common)
kidKey identifier - can be a DID or opaque string
jkuJWK Set URL from which to fetch keys

Resolution Rules​

The CNF resolver follows strict priority rules:

CNF Resolution Priority

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:

  1. At least one of kid, jwk, or jku must be present
  2. If only kid is provided (no jwk or jku), it must be a DID
  3. Non-DID kid values can only be used alongside jwk or jku
// 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.graph.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:

SourceDescription
CnfResolutionSource.JWKKey obtained from cnf.jwk directly
CnfResolutionSource.DIDKey obtained by resolving DID from cnf.kid
CnfResolutionSource.JKUKey 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​

ResolverIdentifier TypesDescription
JwkExternalIdentifierResolutionServiceJWKResolves inline JWK objects
JwksUrlExternalIdentifierResolutionServiceJWKS_URLFetches keys from JWKS endpoints
X5cExternalIdentifierResolutionServiceX5CParses X.509 certificate chains
KeyInfoIdentifierResolutionServiceKID, KEY_ALIASResolves from KMS providers
CnfExternalIdentifierResolutionServiceCNFResolves RFC 7800 confirmation claims
ETSITrustListIdentifierResolutionServiceENTITY_IDResolves from ETSI trust lists

Dependencies​

dependencies {
// Core crypto with resolution
implementation("com.sphereon.idk:lib-crypto-core:0.25.0")

// For trust list resolution
implementation("com.sphereon.idk:lib-trust-etsi:0.25.0")
}

Next Steps​