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
- Android/Kotlin
- iOS/Swift
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}")
}
import SphereonSdJwt
let sdJwtService = session.graph.sdJwtService
// Create presentation with all disclosures included
let result = try await sdJwtService.presentSdJwt(
args: PresentSdJwtArgs(
sdJwt: issuedSdJwt,
disclosureSelection: nil // nil = include all disclosures
)
)
if result.isOk {
let presentation = result.value
print("Presentation: \(presentation.presentation)")
print("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:
- Android/Kotlin
- iOS/Swift
// 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}")
}
// Select specific claims to disclose
let disclosureSelection = SdMap(
fields: [
"email": SdField(sd: true), // Disclose email
"given_name": SdField(sd: true) // Disclose given_name
// family_name, age, etc. are NOT disclosed
]
)
let result = try await sdJwtService.presentSdJwt(
args: PresentSdJwtArgs(
sdJwt: issuedSdJwt,
disclosureSelection: disclosureSelection
)
)
if result.isOk {
// Presentation contains only email and given_name disclosures
print("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:
- Android/Kotlin
- iOS/Swift
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
}
let result = try await sdJwtService.presentSdJwt(
args: PresentSdJwtArgs(
sdJwt: issuedSdJwt,
disclosureSelection: SdMap(
fields: [
"given_name": SdField(sd: true),
"family_name": SdField(sd: true)
]
),
// Key binding parameters
audience: "https://verifier.example.com",
nonce: verifierProvidedNonce,
holderKey: holderManagedKeyInfo
)
)
if result.isOk {
// Presentation format: JWT~disclosure1~disclosure2~KeyBindingJWT
let presentation = result.value.presentation
}
The Key Binding JWT contains:
| Claim | Description |
|---|---|
aud | Verifier's identifier (audience) |
nonce | Fresh nonce from verifier |
iat | Issued-at timestamp |
sd_hash | Hash 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
- Android/Kotlin
- iOS/Swift
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}")
}
}
import SphereonSdJwt
let sdJwtService = session.graph.sdJwtService
let result = try await sdJwtService.verifySdJwt(
args: VerifySdJwtArgs(
sdJwt: presentation,
identifier: issuerIdentifier
)
)
if result.isOk {
let verification = result.value
if verification.isValid {
print("Signature valid: \(verification.signatureValid)")
print("Disclosures valid: \(verification.disclosuresValid)")
// Access disclosed claims
let fullPayload = verification.sdJwt.payload.fullPayload
let email = fullPayload["email"]?.description
let givenName = fullPayload["given_name"]?.description
} else {
print("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:
- Android/Kotlin
- iOS/Swift
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}")
}
}
}
let result = try await sdJwtService.verifySdJwt(
args: VerifySdJwtArgs(
sdJwt: presentation,
identifier: issuerIdentifier,
expectedAudience: "https://verifier.example.com",
expectedNonce: providedNonce
)
)
if result.isOk {
let verification = result.value
if verification.isValid {
// All checks passed: signature, disclosures, and key binding
print("Key binding valid: \(verification.keyBindingValid)")
// Access KB-JWT details
if let kbJwt = verification.sdJwt.keyBindingJwt {
print("KB-JWT audience: \(kbJwt.audience ?? "")")
print("KB-JWT nonce: \(kbJwt.nonce ?? "")")
print("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:
- Android/Kotlin
- iOS/Swift
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}")
}
}
if result.isOk && result.value.isValid {
let sdJwt = result.value.sdJwt
// JWT header
let algorithm = sdJwt.algorithm // e.g., "ES256"
let keyId = sdJwt.keyId // "kid" from header
// Payload views
let undisclosed = sdJwt.payload.undisclosedPayload // Contains _sd array
let full = sdJwt.payload.fullPayload // All disclosed claims resolved
// Disclosures
for disclosure in sdJwt.disclosures {
print("\(disclosure.key) = \(disclosure.value)")
}
}
Complete Example
- Android/Kotlin
- iOS/Swift
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
)
import SphereonSdJwt
class PresentationService {
private let sdJwtService: SdJwtService
init(sdJwtService: SdJwtService) {
self.sdJwtService = sdJwtService
}
// Holder: Create presentation for verifier
func createPresentation(
credential: String,
verifierAudience: String,
verifierNonce: String,
claimsToDisclose: Set<String>,
holderKey: ManagedIdentifierOptsOrResult
) async throws -> String {
var fields: [String: SdField] = [:]
for claim in claimsToDisclose {
fields[claim] = SdField(sd: true)
}
let disclosureSelection = SdMap(fields: fields)
let result = try await sdJwtService.presentSdJwt(
args: PresentSdJwtArgs(
sdJwt: credential,
disclosureSelection: disclosureSelection,
audience: verifierAudience,
nonce: verifierNonce,
holderKey: holderKey
)
)
guard result.isOk else {
throw NSError(domain: "Presentation", code: -1)
}
return result.value.presentation
}
// Verifier: Validate presentation
func verifyPresentation(
presentation: String,
issuerIdentifier: IdentifierOptsOrResult,
expectedAudience: String,
expectedNonce: String,
requiredClaims: Set<String>
) async throws -> VerifiedClaims {
let result = try await sdJwtService.verifySdJwt(
args: VerifySdJwtArgs(
sdJwt: presentation,
identifier: issuerIdentifier,
expectedAudience: expectedAudience,
expectedNonce: expectedNonce
)
)
guard result.isOk else {
throw NSError(domain: "Verification", code: -1)
}
let verification = result.value
guard verification.isValid else {
throw NSError(domain: "Verification", code: -2,
userInfo: [NSLocalizedDescriptionKey: verification.errorMessages.joined(separator: ", ")])
}
return VerifiedClaims(
issuer: verification.sdJwt.payload.fullPayload["iss"]?.description ?? "",
subject: verification.sdJwt.payload.fullPayload["sub"]?.description,
claims: verification.sdJwt.payload.fullPayload,
keyBindingVerified: verification.keyBindingValid
)
}
}
struct VerifiedClaims {
let issuer: String
let subject: String?
let claims: [String: Any]
let keyBindingVerified: Bool
}
Verification Result
The SdJwtVerificationResult contains:
| Property | Type | Description |
|---|---|---|
sdJwt | SdJwtCompact | The parsed SD-JWT structure |
signatureValid | Boolean | JWT signature verification result |
disclosuresValid | Boolean | All disclosure digests match |
keyBindingValid | Boolean | KB-JWT validation result (if present) |
errorMessages | List<String> | Validation error details |
isValid | Boolean | Combined validation result |
Presentation Arguments
The PresentSdJwtArgs accepts:
| Parameter | Type | Description |
|---|---|---|
sdJwt | String | Full SD-JWT from issuer |
disclosureSelection | SdMap? | Claims to disclose (null = all) |
audience | String? | Verifier identifier for KB-JWT |
nonce | String? | Verifier nonce for KB-JWT |
holderKey | ManagedIdentifierOptsOrResult? | Key for signing KB-JWT |
kbJwtOpts | CreateJwsOpts | Additional JWS options |