Skip to main content
Version: v0.13

SD-JWT Issuance

The IDK provides tools for creating SD-JWT credentials with selective disclosure. This guide covers credential creation using the payload builder extensions and the SD-JWT service.

Building the Payload

SD-JWT issuance starts with building a JWT payload using JwsPayloadBuilder combined with selective disclosure extensions. Claims marked with claimSd() become selectively disclosable in the issued credential.

import com.sphereon.crypto.jose.jws.jwsPayload
import com.sphereon.sdjwt.claimSd
import com.sphereon.sdjwt.subSd
import com.sphereon.sdjwt.objClaimSd

// Build payload with selective disclosure
val payload = jwsPayload {
iss("https://issuer.example.com")
sub("user-123")
iat(System.currentTimeMillis() / 1000)
exp(System.currentTimeMillis() / 1000 + 86400)

// Selectively disclosable claims
claimSd("email", "user@example.com")
claimSd("given_name", "John")
claimSd("family_name", "Doe")
claimSd("age", 25)

// Regular claims (always visible)
custom("credential_type", "IdentityCredential")
}

Selective Disclosure Extensions

The IDK provides several extension functions for marking claims as selectively disclosable:

FunctionDescription
claimSd(name, value)Mark a custom claim as selectively disclosable
subSd(value)Mark the subject claim as selectively disclosable
iatSd(value)Mark the issued-at claim as selectively disclosable
jtiSd(value)Mark the JWT ID as selectively disclosable
objClaimSd(name) { }Mark an entire nested object as selectively disclosable

Security-sensitive claims like iss, aud, exp, nbf, and cnf should not be made selectively disclosable per RFC 9901 recommendations.

Issuing the SD-JWT

Once the payload is built, use SdJwtService to issue the credential:

import com.sphereon.sdjwt.IssueSdJwtArgs
import com.sphereon.sdjwt.SdJwtSpec

// Get the SD-JWT service from the session
val sdJwtService = session.component.sdJwtService

// Create issuer identifier from managed key
val issuer = ManagedOptsKeyInfo(
identifier = issuerKeyInfo,
context = IdentifierContext(
clientId = "issuer-001",
issuer = "https://issuer.example.com"
)
)

// Issue the SD-JWT
val result = sdJwtService.issueSdJwt(
IssueSdJwtArgs(
payload = payload,
issuer = issuer,
spec = SdJwtSpec.Default
)
)

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

// Complete SD-JWT string: JWT~disclosure1~disclosure2~...
println("SD-JWT: ${sdJwtResult.sdJwt}")

// Individual components
println("JWT: ${sdJwtResult.jwt}")
println("Disclosures: ${sdJwtResult.disclosures.size}")

sdJwtResult.disclosures.forEach { disclosure ->
println(" ${disclosure.key}: ${disclosure.value}")
}
}

Nested Object Disclosure

Use objClaimSd() to make entire nested objects selectively disclosable:

val payload = jwsPayload {
iss("https://issuer.example.com")
sub("user-123")

// Simple selectively disclosable claims
claimSd("given_name", "John")
claimSd("family_name", "Doe")

// Nested object as selectively disclosable
objClaimSd("address") {
custom("street", "123 Main St")
custom("city", "Springfield")
custom("postal_code", "12345")
custom("country", "US")
}

// Nested object with inner selective disclosure
objClaimSd("company") {
custom("name", "Acme Corp")
custom("department", "Engineering")
claimSd("employee_id", "EMP-12345") // Nested SD claim
}
}

Decoy Digests

Add decoy digests to the _sd array to prevent correlation attacks. Decoys make it harder to determine how many claims are actually selectively disclosable.

import com.sphereon.sdjwt.minimumDigests
import com.sphereon.sdjwt.DecoyConfig
import com.sphereon.sdjwt.DecoyMode

// Using minimumDigests in the payload builder
val payload = jwsPayload {
iss("https://issuer.example.com")
claimSd("email", "user@example.com")
claimSd("phone", "+1234567890")

// Ensure at least 5 digests in _sd array (adds decoys if needed)
minimumDigests(5)
}

// Or configure via SdJwtSpec
val result = sdJwtService.issueSdJwt(
IssueSdJwtArgs(
payload = payload,
issuer = issuer,
spec = SdJwtSpec(
decoyConfig = DecoyConfig(
mode = DecoyMode.MINIMUM,
count = 5
)
)
)
)

Available decoy modes:

ModeDescription
NONENo decoy digests (default)
FIXEDAdd exactly count decoy digests
MINIMUMEnsure at least count total digests
RANDOMAdd random number of decoys up to count

Hash Algorithm Configuration

Configure the hash algorithm used for disclosure digests:

import com.sphereon.crypto.core.generic.DigestAlg

val result = sdJwtService.issueSdJwt(
IssueSdJwtArgs(
payload = payload,
issuer = issuer,
spec = SdJwtSpec(
digestAlg = DigestAlg.SHA256, // Default per RFC 9901
includeAlgClaim = true // Include _sd_alg in payload
)
)
)

Complete Issuance Example

import com.sphereon.crypto.jose.jws.jwsPayload
import com.sphereon.crypto.core.generic.SignatureAlgorithm
import com.sphereon.sdjwt.*

class CredentialIssuer(
private val sdJwtService: SdJwtService,
private val keyManager: KeyManagerService
) {
suspend fun issueIdentityCredential(
subjectId: String,
givenName: String,
familyName: String,
email: String,
birthDate: String
): String {
// Generate or retrieve issuer key
val keyPair = keyManager.generateKeyAsync(
alg = SignatureAlgorithm.ECDSA_SHA256
)
val keyInfo = keyPair.joseToManagedKeyInfo()

val issuer = ManagedOptsKeyInfo(
identifier = keyInfo,
context = IdentifierContext(
clientId = "identity-issuer",
issuer = "https://issuer.example.com"
)
)

// Build payload with selective disclosure
val payload = jwsPayload {
iss("https://issuer.example.com")
sub(subjectId)
iat(System.currentTimeMillis() / 1000)
exp(System.currentTimeMillis() / 1000 + 31536000) // 1 year

// All personal data is selectively disclosable
claimSd("given_name", givenName)
claimSd("family_name", familyName)
claimSd("email", email)
claimSd("birth_date", birthDate)

// Credential metadata is always visible
custom("vct", "IdentityCredential")
custom("credential_schema", "https://schemas.example.com/identity/v1")
}

// Issue with decoy protection
val result = sdJwtService.issueSdJwt(
IssueSdJwtArgs(
payload = payload,
issuer = issuer,
spec = SdJwtSpec(
decoyConfig = DecoyConfig(
mode = DecoyMode.MINIMUM,
count = 8
)
)
)
)

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

Result Types

The issuance result contains all components of the SD-JWT:

PropertyTypeDescription
sdJwtStringComplete SD-JWT string (JWT~disclosure1~disclosure2~...)
jwtStringThe signed JWT component
disclosuresList<Disclosure>List of disclosure objects

Each Disclosure contains:

PropertyTypeDescription
saltStringRandom salt value
keyStringClaim name
valueJsonElementClaim value
encodedStringBase64url-encoded disclosure
digestString?Hash of the encoded disclosure