Kotlin Multiplatform SD-JWT libraryby walt.id
Create JSON Web Tokens (JWTs) that support Selective Disclosure
Getting Started
- Usage with Maven or Gradle (JVM)
- Usage with NPM/NodeJs (JavaScript)
- Sign SD-JWT tokens
- Present SD-JWT tokens with selection of disclosed and undisclosed payload fields
- Parse and verify SD-JWT tokens, resolving original payload with disclosed fields
- Integrate with your choice of framework or library, for cryptography and key management, on your platform
Further information
Checkout the documentation regarding SD-JWTs, to find out more.
What is the SD-JWT library?
This libary implements the Selective Disclosure JWT (SD-JWT) specification: draft-ietf-oauth-selective-disclosure-jwt-04.
Features
- Create and sign SD-JWT tokens
- Choose selectively disclosable payload fields (SD fields)
- Create digests for SD fields and insert into JWT body payload
- Create and append encoded disclosure strings for SD fields to JWT token
- Add random or fixed number of decoy digests on each nested object property
- Present SD-JWT tokens
- Selection of fields to be disclosed
- Support for appending optional holder binding
- Full support for nested SD fields and recursive disclosures
- Parse SD-JWT tokens and restore original payload with disclosed fields
- Verify SD-JWT token
- Signature verification
- Hash comparison and tamper check of the appended disclosures
- Support for integration with various crypto libraries and frameworks, to perform the cryptographic operations and key management
- Multiplatform support:
- Java/JVM
- JavaScript
- Native
Usage with Maven or Gradle (JVM)
Maven / Gradle repository:
https://maven.walt.id/repository/waltid-ssi-kit/
Maven
[...]
<repositories>
<repository>
<id>waltid-ssikit</id>
<name>waltid-ssikit</name>
<url>https://maven.walt.id/repository/waltid-ssi-kit/</url>
</repository>
</repositories>
[...]
<dependency>
<groupId>id.walt</groupId>
<artifactId>waltid-sd-jwt-jvm</artifactId>
<version>[ version ]</version>
</dependency>
Gradle
Kotlin DSL
[...]
repositories {
maven("https://maven.walt.id/repository/waltid-ssi-kit/")
}
[...]
val sdJwtVersion = "1.2306071235.0"
[...]
dependencies {
implementation("id.walt:waltid-sd-jwt-jvm:$sdJwtVersion")
}
Usage with NPM/NodeJs (JavaScript)
Install NPM package:
npm install waltid-sd-jwt
Manual build from source:
./gradlew jsNodeProductionLibraryPrepare jsNodeProductionLibraryDistribution
Then include in your NodeJS project like this:
npm install /path/to/waltid-sd-jwt/build/productionLibrary
NodeJS example
Example script in:
examples/js
Execute like:
npm install
node index.js
Examples
Kotlin / JVM
Create and sign an SD-JWT using the NimbusDS-based JWT crypto provider
This example creates and signs an SD-JWT, using the SimpleJWTCryptoProvider implementation, that's shipped with the waltid-sd-jwt library, which uses the nimbus-jose-jwt
library for cryptographic operations.
In this example we sign the JWT with the HS256 algorithm, and a UUID as a shared secret.
Here we generate the SD payload, by comparing the full payload and the undisclosed payload (with selective fields removed).
Alternatively, we can create the SD payload by specifying the SDMap, which indicates the selective disclosure for each field. This approach also allows more fine-grained control, particularly in regard to recursive disclosures and nested payload fields.
// Shared secret for HMAC crypto algorithm
val sharedSecret = "ef23f749-7238-481a-815c-f0c2157dfa8e"
fun main() {
// Create SimpleJWTCryptoProvider with MACSigner and MACVerifier
val cryptoProvider = SimpleJWTCryptoProvider(JWSAlgorithm.HS256, MACSigner(sharedSecret), MACVerifier(sharedSecret))
// Create original JWT claims set, using nimbusds claims set builder
val originalClaimsSet = JWTClaimsSet.Builder()
.subject("123")
.audience("456")
.build()
// Create undisclosed claims set, by removing e.g. subject property from original claims set
val undisclosedClaimsSet = JWTClaimsSet.Builder(originalClaimsSet)
.subject(null)
.build()
// Create SD payload by comparing original claims set with undisclosed claims set
val sdPayload = SDPayload.createSDPayload(originalClaimsSet, undisclosedClaimsSet)
// Create and sign SD-JWT using the generated SD payload and the previously configured crypto provider
val sdJwt = SDJwt.sign(sdPayload, cryptoProvider)
// Print SD-JWT
println(sdJwt)
}
Example output
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~WyJ4RFk5VjBtOG43am82ZURIUGtNZ1J3Iiwic3ViIiwiMTIzIl0
Parsed JWT body
{
"aud": "456",
"_sd": [
"hlzfjf04o5ZsLR25ha4c-Y-9HW2DUlxcgiMYd324NgY"
]
}
Present an SD-JWT
In this example we parse the SD-JWT generated in the previous example, and present it by disclosing all, none or selective fields.
In the next example we will show how to parse and verify the presented SD-JWTs.
fun presentSDJwt() {
// parse previously created SD-JWT
val sdJwt = SDJwt.parse("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~WyJ4RFk5VjBtOG43am82ZURIUGtNZ1J3Iiwic3ViIiwiMTIzIl0")
// present without disclosing SD fields
val presentedUndisclosedJwt = sdJwt.present(discloseAll = false)
println(presentedUndisclosedJwt)
// present disclosing all SD fields
val presentedDisclosedJwt = sdJwt.present(discloseAll = true)
println(presentedDisclosedJwt)
// present disclosing selective fields, using SDMap
val presentedSelectiveJwt = sdJwt.present(mapOf(
"sub" to SDField(true)
).toSDMap())
println(presentedSelectiveJwt)
// present disclosing fields, using JSON paths
val presentedSelectiveJwt2 = sdJwt.present(
SDMap.generateSDMap(listOf("sub"))
)
println(presentedSelectiveJwt2)
}
Example output
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~WyJ4RFk5VjBtOG43am82ZURIUGtNZ1J3Iiwic3ViIiwiMTIzIl0~
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~WyJ4RFk5VjBtOG43am82ZURIUGtNZ1J3Iiwic3ViIiwiMTIzIl0~
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~WyJ4RFk5VjBtOG43am82ZURIUGtNZ1J3Iiwic3ViIiwiMTIzIl0~
Parse and verify an SD-JWT using the NimbusDS-based JWT crypto provider
This example shows how to parse and verify the SD-JWT, created and presented in the previous examples, and how to restore its original payload, with the disclosed payload fields only.
For verification, we use the same shared secret as before and a MACVerifier
with the SimpleJWTCryptoProvider
.
The parsing and verification can be done in one step using the SDJwt.verifyAndParse()
method, throwing an exception if verification fails,
or in two steps using the SDJwt.parse()
method followed by the member method SDJwt.verify()
, which returns true or false.
The output below shows the restored JWT body payloads, with the selectively disclosable field sub
disclosed or undisclosed.
// Shared secret for HMAC crypto algorithm
private val sharedSecret = "ef23f749-7238-481a-815c-f0c2157dfa8e"
fun parseAndVerify() {
// Create SimpleJWTCryptoProvider with MACSigner and MACVerifier
val cryptoProvider = SimpleJWTCryptoProvider(JWSAlgorithm.HS256, jwsSigner = null, jwsVerifier = MACVerifier(sharedSecret))
val undisclosedJwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~"
// verify and parse presented SD-JWT with all fields undisclosed, throws Exception if verification fails!
val parsedVerifiedUndisclosedJwt = SDJwt.verifyAndParse(undisclosedJwt, cryptoProvider)
// print full payload with disclosed fields only
println("Undisclosed JWT payload:")
println(parsedVerifiedUndisclosedJwt.sdPayload.fullPayload.toString())
// alternatively parse and verify in 2 steps:
val parsedUndisclosedJwt = SDJwt.parse(undisclosedJwt)
val isValid = parsedUndisclosedJwt.verify(cryptoProvider)
println("Undisclosed SD-JWT verified: $isValid")
val parsedVerifiedDisclosedJwt = SDJwt.verifyAndParse(
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~WyJ4RFk5VjBtOG43am82ZURIUGtNZ1J3Iiwic3ViIiwiMTIzIl0~",
cryptoProvider
)
// print full payload with disclosed fields
println("Disclosed JWT payload:")
println(parsedVerifiedDisclosedJwt.sdPayload.fullPayload.toString())
}
Example output
Undisclosed JWT payload:
{"aud":"456"}
Undisclosed SD-JWT verified: true
Disclosed JWT payload:
{"aud":"456","sub":"123"}
Integrate with custom JWT crypto provider
To integrate with your custom JWT crypto provider, on your platform, you need to override and implement the JWTCryptoProvider
interface, which has two interface methods to sign and verify standard JWT tokens.
In this example, you see how I made use of this interface to implement the JWT crypto provider based on the NimbusDS Jose/JWT library for JVM:
import com.nimbusds.jose.*
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT
import kotlinx.serialization.json.JsonObject
class SimpleJWTCryptoProvider(
val jwsAlgorithm: JWSAlgorithm,
private val jwsSigner: JWSSigner?,
private val jwsVerifier: JWSVerifier?
) : JWTCryptoProvider {
/**
* Interface method to create a signed JWT for the given JSON payload object, with an optional keyID.
* @param payload The JSON payload of the JWT to be signed
* @param keyID Optional keyID of the signing key to be used, if required by crypto provider
*/
override fun sign(payload: JsonObject, keyID: String?): String {
if(jwsSigner == null) {
throw Exception("No signer available")
}
return SignedJWT(
JWSHeader.Builder(jwsAlgorithm).type(JOSEObjectType.JWT).keyID(keyID).build(),
JWTClaimsSet.parse(payload.toString())
).also {
it.sign(jwsSigner)
}.serialize()
}
/**
* Interface method for verifying a JWT signature
* @param jwt A signed JWT token to be verified
*/
override fun verify(jwt: String): Boolean {
if(jwsVerifier == null) {
throw Exception("No verifier available")
}
return SignedJWT.parse(jwt).verify(jwsVerifier)
}
}
The custom JWT crypto provider can now be used like shown in the examples above, for signing and verifying SD-JWTs.
JavaScript / NodeJS
See also example project in examples/js
Build payload, sign and present examples
import sdlib from "waltid-sd-jwt"
const sharedSecret = "ef23f749-7238-481a-815c-f0c2157dfa8e"
const cryptoProvider = new sdlib.id.walt.sdjwt.SimpleAsyncJWTCryptoProvider("HS256", new TextEncoder().encode(sharedSecret))
const sdMap = new sdlib.id.walt.sdjwt.SDMapBuilder(sdlib.id.walt.sdjwt.DecoyMode.FIXED.name, 2).
addField("sub", true,
new sdlib.id.walt.sdjwt.SDMapBuilder().addField("child", true).build()
).build()
console.log(sdMap, JSON.stringify(sdMap))
const sdPayload = new sdlib.id.walt.sdjwt.SDPayloadBuilder({ "sub": "123", "aud": "345" }).buildForUndisclosedPayload({"aud": "345"})
const sdPayload2 = new sdlib.id.walt.sdjwt.SDPayloadBuilder({ "sub": "123", "aud": "345" }).buildForSDMap(sdMap)
const jwt = await sdlib.id.walt.sdjwt.SDJwtJS.Companion.signAsync(
sdPayload, cryptoProvider)
console.log(jwt.toString())
const jwt2 = await sdlib.id.walt.sdjwt.SDJwtJS.Companion.signAsync(
sdPayload2, cryptoProvider)
console.log(jwt2.toString())
console.log("Verified:", (await jwt.verifyAsync(cryptoProvider)).verified)
console.log("Verified:", (await jwt2.verifyAsync(cryptoProvider)).verified)
const presentedJwt = await jwt.presentAllAsync(false)
console.log("Presented undisclosed SD-JWT:", presentedJwt.toString())
console.log("Verified: ", (await presentedJwt.verifyAsync(cryptoProvider)).verified)
const sdMap2 = new sdlib.id.walt.sdjwt.SDMapBuilder().buildFromJsonPaths(["sub"])
console.log("SDMap2:", sdMap2)
const presentedJwt2 = await jwt.presentAsync(sdMap2)
console.log("Presented disclosed SD-JWT:", presentedJwt2.toString())
const verificationResultPresentedJwt2 = await presentedJwt2.verifyAsync(cryptoProvider)
console.log("Presented payload", verificationResultPresentedJwt2.sdJwt.fullPayload)
console.log("Presented disclosures", verificationResultPresentedJwt2.sdJwt.disclosureObjects)
console.log("Presented disclosure strings", verificationResultPresentedJwt2.sdJwt.disclosures)
console.log("Verified: ", verificationResultPresentedJwt2.verified)
console.log("SDMap reconstructed", presentedJwt2.sdMap)
Join the community
- Connect and get the latest updates: Discord | Newsletter | YouTube | Twitter
- Get help, request features and report bugs: GitHub Discussions
License
Licensed under the Apache License, Version 2.0