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.
- Android/Kotlin
- iOS/Swift
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")
}
import SphereonCrypto
import SphereonSdJwt
// Build payload with selective disclosure
let payload = JwsPayloadBuilder()
.iss("https://issuer.example.com")
.sub("user-123")
.iat(Int64(Date().timeIntervalSince1970))
.exp(Int64(Date().timeIntervalSince1970) + 86400)
// Selectively disclosable claims
.claimSd(name: "email", value: "user@example.com")
.claimSd(name: "given_name", value: "John")
.claimSd(name: "family_name", value: "Doe")
.claimSd(name: "age", value: 25)
// Regular claims (always visible)
.custom("credential_type", "IdentityCredential")
.build()
Selective Disclosure Extensions
The IDK provides several extension functions for marking claims as selectively disclosable:
| Function | Description |
|---|---|
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:
- Android/Kotlin
- iOS/Swift
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}")
}
}
import SphereonSdJwt
// Get the SD-JWT service from the session
let sdJwtService = session.component.sdJwtService
// Create issuer identifier from managed key
let issuer = ManagedOptsKeyInfo(
identifier: issuerKeyInfo,
context: IdentifierContext(
clientId: "issuer-001",
issuer: "https://issuer.example.com"
)
)
// Issue the SD-JWT
let result = try await sdJwtService.issueSdJwt(
args: IssueSdJwtArgs(
payload: payload,
issuer: issuer,
spec: SdJwtSpec.default
)
)
if result.isOk {
let sdJwtResult = result.value
// Complete SD-JWT string: JWT~disclosure1~disclosure2~...
print("SD-JWT: \(sdJwtResult.sdJwt)")
// Individual components
print("JWT: \(sdJwtResult.jwt)")
print("Disclosures: \(sdJwtResult.disclosures.count)")
for disclosure in sdJwtResult.disclosures {
print(" \(disclosure.key): \(disclosure.value)")
}
}
Nested Object Disclosure
Use objClaimSd() to make entire nested objects selectively disclosable:
- Android/Kotlin
- iOS/Swift
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
}
}
let payload = JwsPayloadBuilder()
.iss("https://issuer.example.com")
.sub("user-123")
// Simple selectively disclosable claims
.claimSd(name: "given_name", value: "John")
.claimSd(name: "family_name", value: "Doe")
// Nested object as selectively disclosable
.objClaimSd(name: "address") { builder in
builder.custom("street", "123 Main St")
builder.custom("city", "Springfield")
builder.custom("postal_code", "12345")
builder.custom("country", "US")
}
.build()
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.
- Android/Kotlin
- iOS/Swift
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
)
)
)
)
// Configure decoys via SdJwtSpec
let result = try await sdJwtService.issueSdJwt(
args: IssueSdJwtArgs(
payload: payload,
issuer: issuer,
spec: SdJwtSpec(
decoyConfig: DecoyConfig(
mode: .minimum,
count: 5
)
)
)
)
Available decoy modes:
| Mode | Description |
|---|---|
NONE | No decoy digests (default) |
FIXED | Add exactly count decoy digests |
MINIMUM | Ensure at least count total digests |
RANDOM | Add random number of decoys up to count |
Hash Algorithm Configuration
Configure the hash algorithm used for disclosure digests:
- Android/Kotlin
- iOS/Swift
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
)
)
)
let result = try await sdJwtService.issueSdJwt(
args: IssueSdJwtArgs(
payload: payload,
issuer: issuer,
spec: SdJwtSpec(
digestAlg: .sha256, // Default per RFC 9901
includeAlgClaim: true
)
)
)
Complete Issuance Example
- Android/Kotlin
- iOS/Swift
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}")
}
}
}
import SphereonCrypto
import SphereonSdJwt
class CredentialIssuer {
private let sdJwtService: SdJwtService
private let keyManager: KeyManagerService
init(sdJwtService: SdJwtService, keyManager: KeyManagerService) {
self.sdJwtService = sdJwtService
self.keyManager = keyManager
}
func issueIdentityCredential(
subjectId: String,
givenName: String,
familyName: String,
email: String,
birthDate: String
) async throws -> String {
// Generate or retrieve issuer key
let keyPair = try await keyManager.generateKeyAsync(
alg: .ecdsaSha256
)
let keyInfo = keyPair.joseToManagedKeyInfo()
let issuer = ManagedOptsKeyInfo(
identifier: keyInfo,
context: IdentifierContext(
clientId: "identity-issuer",
issuer: "https://issuer.example.com"
)
)
let now = Int64(Date().timeIntervalSince1970)
// Build payload with selective disclosure
let payload = JwsPayloadBuilder()
.iss("https://issuer.example.com")
.sub(subjectId)
.iat(now)
.exp(now + 31536000) // 1 year
.claimSd(name: "given_name", value: givenName)
.claimSd(name: "family_name", value: familyName)
.claimSd(name: "email", value: email)
.claimSd(name: "birth_date", value: birthDate)
.custom("vct", "IdentityCredential")
.custom("credential_schema", "https://schemas.example.com/identity/v1")
.build()
// Issue with decoy protection
let result = try await sdJwtService.issueSdJwt(
args: IssueSdJwtArgs(
payload: payload,
issuer: issuer,
spec: SdJwtSpec(
decoyConfig: DecoyConfig(
mode: .minimum,
count: 8
)
)
)
)
guard result.isOk else {
throw NSError(domain: "Issuance", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Issuance failed"])
}
return result.value.sdJwt
}
}
Result Types
The issuance result contains all components of the SD-JWT:
| Property | Type | Description |
|---|---|---|
sdJwt | String | Complete SD-JWT string (JWT~disclosure1~disclosure2~...) |
jwt | String | The signed JWT component |
disclosures | List<Disclosure> | List of disclosure objects |
Each Disclosure contains:
| Property | Type | Description |
|---|---|---|
salt | String | Random salt value |
key | String | Claim name |
value | JsonElement | Claim value |
encoded | String | Base64url-encoded disclosure |
digest | String? | Hash of the encoded disclosure |