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

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.

SD-JWT issuance produces a compact string in the format JWT~disclosure1~disclosure2~.... The JWT portion is a standard signed JWT whose payload contains hashed references (_sd digests) instead of the actual claim values. Each disclosure is a separate base64url-encoded tuple of [salt, claim_name, claim_value] that, when combined with the JWT, reveals the original claim. The holder receives the full set of disclosures at issuance time and later chooses which ones to present.

Building the Payload

SD-JWT issuance starts with building a JWT payload using SdJwtPayloadBuilder combined with selective disclosure extensions. Claims marked with claimSd() become selectively disclosable in the issued credential. Claims added with claim() or the standard JWT methods (iss, sub, exp) remain always visible in the signed JWT and cannot be hidden during presentation.

import com.sphereon.sdjwt.dsl.sdJwtPayload
import com.sphereon.sdjwt.claimSd
import com.sphereon.sdjwt.subSd
import com.sphereon.sdjwt.dsl.objSd

// Build payload with selective disclosure
val payload = sdJwtPayload {
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)
claim("credential_type", "IdentityCredential")
}

Selective Disclosure Extensions

You control which claims are disclosable by choosing the right builder function. Any claim marked with an Sd variant gets its own disclosure and a corresponding digest in the JWT's _sd array. Everything else stays in the clear.

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
objSd(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, the SdJwtService signs the JWT, generates random salts for each disclosable claim, computes the disclosure digests, and assembles the final JWT~disclosure~... string. The issuer identity (key + context) determines which key signs the JWT.

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

When a claim value is a JSON object, you can make the entire object disclosable as a unit with objSd(). The holder either reveals the whole object or nothing. You can also nest claimSd() calls inside the object to make individual fields within it independently disclosable.

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

val payload = sdJwtPayload {
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
objSd("address") {
claim("street", "123 Main St")
claim("city", "Springfield")
claim("postal_code", "12345")
claim("country", "US")
}

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

Decoy Digests

Decoy digests are fake entries added to the _sd array that do not correspond to any real disclosure. Without decoys, an observer who sees the JWT can count the digests and infer how many disclosable claims exist, even without knowing their values. Adding decoys pads the array to obscure the true count.

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

// Using minimumDigests in the payload builder
val payload = sdJwtPayload {
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

The hash algorithm controls how disclosure digests in the _sd array are computed. SHA-256 is the default and the only algorithm required by RFC 9901. Setting includeAlgClaim = true adds the _sd_alg claim to the JWT payload, which tells verifiers which algorithm to use when re-hashing disclosures during verification.

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.sdjwt.dsl.sdJwtPayload
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 = sdJwtPayload {
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
claim("vct", "IdentityCredential")
claim("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