Skip to main content
Version: v0.13

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 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.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}")
}
)

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:

PriorityDescription
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:

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

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.13.0")

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

Next Steps