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

SD-JWT Presentation

The IDK provides tools for holders to create presentations with selective disclosure and for verifiers to validate those presentations.

Presentation is where selective disclosure happens. The holder starts with the full SD-JWT received from the issuer (JWT plus all disclosures) and produces a new compact string that includes only the disclosures the verifier needs to see. Disclosures that are omitted are cryptographically hidden because the verifier only sees their hashed digests in the JWT, with no way to reverse them.

Creating Presentations

Holders use SdJwtService.presentSdJwt() to create presentations from issued credentials, selecting which claims to disclose.

Basic Presentation

import com.sphereon.sdjwt.PresentSdJwtArgs
import com.sphereon.sdjwt.SdMap
import com.sphereon.sdjwt.SdField

val sdJwtService = session.graph.sdJwtService

// Create presentation with all disclosures included
val result = sdJwtService.presentSdJwt(
PresentSdJwtArgs(
sdJwt = issuedSdJwt,
disclosureSelection = null // null = include all disclosures
)
)

if (result.isOk) {
val presentation = result.value
println("Presentation: ${presentation.presentation}")
println("Disclosed claims: ${presentation.disclosedClaims}")
}

Selective Disclosure

The SdMap controls which claims to include in the presentation. You list the claim names you want to reveal and set sd = true for each. Any claim not listed in the map is excluded from the presentation, meaning its disclosure string is stripped out and the verifier only sees an opaque digest.

Choose which claims to reveal by providing an SdMap:

// Select specific claims to disclose
val disclosureSelection = SdMap(
fields = mapOf(
"email" to SdField(sd = true), // Disclose email
"given_name" to SdField(sd = true) // Disclose given_name
// family_name, age, etc. are NOT disclosed
)
)

val result = sdJwtService.presentSdJwt(
PresentSdJwtArgs(
sdJwt = issuedSdJwt,
disclosureSelection = disclosureSelection
)
)

if (result.isOk) {
// Presentation contains only email and given_name disclosures
println("Disclosed: ${result.value.disclosedClaims}")
}

Key Binding JWT

A Key Binding JWT (KB-JWT) proves that the entity presenting the SD-JWT holds the private key referenced in the credential's cnf claim. It is required when the issuer has bound the credential to a specific holder key. The KB-JWT includes the verifier's audience and a fresh nonce, which prevents replay of the presentation to a different verifier or at a different time.

For credentials with holder key binding, include a KB-JWT to prove possession of the holder key:

val result = sdJwtService.presentSdJwt(
PresentSdJwtArgs(
sdJwt = issuedSdJwt,
disclosureSelection = SdMap(
fields = mapOf(
"given_name" to SdField(sd = true),
"family_name" to SdField(sd = true)
)
),
// Key binding parameters
audience = "https://verifier.example.com",
nonce = verifierProvidedNonce,
holderKey = holderManagedKeyInfo
)
)

if (result.isOk) {
// Presentation format: JWT~disclosure1~disclosure2~KeyBindingJWT
val presentation = result.value.presentation
}

The Key Binding JWT contains:

ClaimDescription
audVerifier's identifier (audience)
nonceFresh nonce from verifier
iatIssued-at timestamp
sd_hashHash of the presentation (binds KB-JWT to specific disclosures)

Verifying Presentations

Verification reconstructs the full payload by re-hashing each included disclosure and matching it against the _sd digests in the JWT. It then checks the issuer's signature over the JWT, confirms that every disclosure digest is present, and (if applicable) validates the KB-JWT signature and claims. A failure at any step causes isValid to return false.

Verifiers use SdJwtService.verifySdJwt() to validate presentations.

Basic Verification

import com.sphereon.sdjwt.VerifySdJwtArgs

val sdJwtService = session.graph.sdJwtService

val result = sdJwtService.verifySdJwt(
VerifySdJwtArgs(
sdJwt = presentation,
identifier = issuerIdentifier
)
)

if (result.isOk) {
val verification = result.value

if (verification.isValid) {
println("Signature valid: ${verification.signatureValid}")
println("Disclosures valid: ${verification.disclosuresValid}")

// Access disclosed claims
val fullPayload = verification.sdJwt.payload.fullPayload
val email = fullPayload["email"]?.toString()?.trim('"')
val givenName = fullPayload["given_name"]?.toString()?.trim('"')
} else {
println("Verification failed: ${verification.errorMessages}")
}
}

Verifying Key Binding

When the SD-JWT includes a KB-JWT, verification additionally checks that the KB-JWT's aud and nonce match the values the verifier expects, and that the sd_hash in the KB-JWT matches a hash of the actual presentation string. This ties the key binding proof to this specific set of disclosed claims.

When validating presentations with holder binding:

val result = sdJwtService.verifySdJwt(
VerifySdJwtArgs(
sdJwt = presentation,
identifier = issuerIdentifier,
expectedAudience = "https://verifier.example.com",
expectedNonce = providedNonce
)
)

if (result.isOk) {
val verification = result.value

if (verification.isValid) {
// All checks passed: signature, disclosures, and key binding
println("Key binding valid: ${verification.keyBindingValid}")

// Access KB-JWT details
verification.sdJwt.keyBindingJwt?.let { kbJwt ->
println("KB-JWT audience: ${kbJwt.audience}")
println("KB-JWT nonce: ${kbJwt.nonce}")
println("KB-JWT sd_hash: ${kbJwt.sdHash}")
}
}
}

Accessing Verified Data

After successful verification, the result gives you two views of the payload. The undisclosedPayload is the raw JWT payload with _sd digest arrays intact. The fullPayload resolves all included disclosures back into their original claim positions, giving you a flat map of the claims the holder chose to reveal.

The verification result provides access to both the undisclosed and full payloads:

if (result.isOk && result.value.isValid) {
val sdJwt = result.value.sdJwt

// JWT header
val algorithm = sdJwt.algorithm // e.g., "ES256"
val keyId = sdJwt.keyId // "kid" from header

// Payload views
val undisclosed = sdJwt.payload.undisclosedPayload // Contains _sd array
val full = sdJwt.payload.fullPayload // All disclosed claims resolved

// Disclosures
sdJwt.disclosures.forEach { disclosure ->
println("${disclosure.key} = ${disclosure.value}")
}

// Digest to disclosure mapping
sdJwt.payload.digestedDisclosures.forEach { (digest, disclosure) ->
println("$digest -> ${disclosure.key}")
}
}

Complete Example

import com.sphereon.sdjwt.*

class PresentationService(
private val sdJwtService: SdJwtService
) {
// Holder: Create presentation for verifier
suspend fun createPresentation(
credential: String,
verifierAudience: String,
verifierNonce: String,
claimsToDisclose: Set<String>,
holderKey: ManagedIdentifierOptsOrResult
): String {
val disclosureSelection = SdMap(
fields = claimsToDisclose.associateWith { SdField(sd = true) }
)

val result = sdJwtService.presentSdJwt(
PresentSdJwtArgs(
sdJwt = credential,
disclosureSelection = disclosureSelection,
audience = verifierAudience,
nonce = verifierNonce,
holderKey = holderKey
)
)

return if (result.isOk) {
result.value.presentation
} else {
throw IllegalStateException("Presentation failed: ${result.error}")
}
}

// Verifier: Validate presentation
suspend fun verifyPresentation(
presentation: String,
issuerIdentifier: IdentifierOptsOrResult,
expectedAudience: String,
expectedNonce: String,
requiredClaims: Set<String>
): VerifiedClaims {
val result = sdJwtService.verifySdJwt(
VerifySdJwtArgs(
sdJwt = presentation,
identifier = issuerIdentifier,
expectedAudience = expectedAudience,
expectedNonce = expectedNonce
)
)

if (!result.isOk) {
throw IllegalStateException("Verification error: ${result.error}")
}

val verification = result.value

if (!verification.isValid) {
throw SecurityException(
"Verification failed: ${verification.errorMessages.joinToString()}"
)
}

// Check required claims are present
val disclosedClaims = verification.sdJwt.payload.fullPayload
val missingClaims = requiredClaims.filter { !disclosedClaims.containsKey(it) }

if (missingClaims.isNotEmpty()) {
throw IllegalArgumentException("Missing required claims: $missingClaims")
}

return VerifiedClaims(
issuer = disclosedClaims["iss"]?.toString()?.trim('"') ?: "",
subject = disclosedClaims["sub"]?.toString()?.trim('"'),
claims = disclosedClaims,
keyBindingVerified = verification.keyBindingValid
)
}
}

data class VerifiedClaims(
val issuer: String,
val subject: String?,
val claims: Map<String, Any?>,
val keyBindingVerified: Boolean
)

Verification Result

The SdJwtVerificationResult contains:

PropertyTypeDescription
sdJwtSdJwtCompactThe parsed SD-JWT structure
signatureValidBooleanJWT signature verification result
disclosuresValidBooleanAll disclosure digests match
keyBindingValidBooleanKB-JWT validation result (if present)
errorMessagesList<String>Validation error details
isValidBooleanCombined validation result

Presentation Arguments

The PresentSdJwtArgs accepts:

ParameterTypeDescription
sdJwtStringFull SD-JWT from issuer
disclosureSelectionSdMap?Claims to disclose (null = all)
audienceString?Verifier identifier for KB-JWT
nonceString?Verifier nonce for KB-JWT
holderKeyManagedIdentifierOptsOrResult?Key for signing KB-JWT
kbJwtOptsCreateJwsOptsAdditional JWS options