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