The repo can now be found here.
- 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
Checkout the documentation regarding SD-JWTs, to find out more.
This libary implements the Selective Disclosure JWT (SD-JWT) specification: draft-ietf-oauth-selective-disclosure-jwt-04.
- 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
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")
}
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
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"
]
}
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~
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"}
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.
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)
- Connect and get the latest updates: Discord | Newsletter | YouTube | Twitter
- Get help, request features and report bugs: GitHub Discussions
Licensed under the Apache License, Version 2.0