Skip to content

eu-digital-identity-wallet/eudi-lib-jvm-sdjwt-kt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

EUDI SD-JWT

âť— Important! Before you proceed, please read the EUDI Wallet Reference Implementation project description

License

Table of contents

Overview

This is a library offering a DSL (domain-specific language) for defining how a set of claims should be made selectively disclosable.

Library implements SD-JWT draft 12 is implemented in Kotlin, targeting JVM.

Library's SD-JWT DSL leverages the DSL provided by KotlinX Serialization library for defining JSON elements

Installation

// Include library in dependencies in build.gradle.kts
dependencies {
    implementation("eu.europa.ec.euidw:eudi-lib-jvm-sdjwt-kt:$version")
}

Use cases supported

Issuance

To issue a SD-JWT, an Issuer should have:

  • Decided on how the issued claims will be selectively disclosed (check DSL examples)
  • Whether to use decoy digests or not
  • An appropriate signing key pair
  • optionally, decided if and how to include the holder's public key in the SD-JWT

In the example below, the Issuer decides to issue an SD-JWT as follows:

  • Includes in plain standard JWT claims (sub,iss, iat, exp)
  • Makes selectively disclosable a claim named address using structured disclosure. This allows individually disclosing every subclaim of address
  • Uses his RSA key pair to sign the SD-JWT
val issuedSdJwt: String = runBlocking {
    val sdJwtSpec = sdJwt {
        claim("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c")
        claim("iss", "https://example.com/issuer")
        claim("iat", 1516239022)
        claim("exp", 1735689661)
        objClaim("address") {
            sdClaim("street_address", "Schulstr. 12")
            sdClaim("locality", "Schulpforta")
            sdClaim("region", "Sachsen-Anhalt")
            sdClaim("country", "DE")
        }
    }
    with(NimbusSdJwtOps) {
        val issuer = issuer(signer = ECDSASigner(issuerEcKeyPair), signAlgorithm = JWSAlgorithm.ES256)
        issuer.issue(sdJwtSpec).getOrThrow().serialize()
    }
}

You can get the full code here.

Tip

Please check KeyBindingTest for a more advanced issuance scenario, including adding to the SD-JWT, holder public key, to leverage key binding.

Holder Verification

Holder must know:

  • the public key of the Issuer and the algorithm used by the Issuer to sign the SD-JWT
val verifiedIssuanceSdJwt: SdJwt<SignedJWT> = runBlocking {
    with(NimbusSdJwtOps) {
        val jwtSignatureVerifier = RSASSAVerifier(issuerRsaKeyPair).asJwtVerifier()
        val unverifiedIssuanceSdJwt = loadSdJwt("/exampleIssuanceSdJwt.txt")
        verify(jwtSignatureVerifier, unverifiedIssuanceSdJwt).getOrThrow()
    }
}

You can get the full code here.

Holder Presentation

In this case, a Holder of an SD-JWT issued by an Issuer, wants to create a presentation for a Verifier. The Holder should know which of the selectively disclosed claims to include in the presentation. The selectively disclosed claims to include in the presentation are expressed using Claim Paths as per SD-JWT VC draft 6.

val presentationSdJwt: SdJwt<SignedJWT> = runBlocking {
    with(NimbusSdJwtOps) {
        val issuedSdJwt = run {
            val sdJwtSpec = sdJwt {
                claim("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c")
                claim("iss", "https://example.com/issuer")
                claim("iat", 1516239022)
                claim("exp", 1735689661)
                sdObjClaim("address") {
                    sdClaim("street_address", "Schulstr. 12")
                    sdClaim("locality", "Schulpforta")
                    sdClaim("region", "Sachsen-Anhalt")
                    sdClaim("country", "DE")
                }
            }
            val issuer = issuer(signer = RSASSASigner(issuerRsaKeyPair), signAlgorithm = JWSAlgorithm.RS256)
            issuer.issue(sdJwtSpec).getOrThrow()
        }

        val addressPath = ClaimPath.claim("address")
        val claimsToInclude = setOf(addressPath.claim("region"), addressPath.claim("country"))
        issuedSdJwt.present(claimsToInclude)!!
    }
}

You can get the full code here.

In the above example, the Holder has decided to disclose the claims region and country of the selectively disclosed claim address.

The resulting presentation will contain 3 disclosures:

  • 1 disclosure for the selectively disclosed claim address
  • 1 disclosure for the selectively disclosed claim region
  • 1 disclosure for the selectively disclosed claim country

This is because to disclose either the claim region or the claim country, the claim address must be disclosed as well.

Presentation Verification

Using compact serialization

Verifier should know the public key of the Issuer and the algorithm used by the Issuer to sign the SD-JWT. Also, if verification includes Key Binding, the Verifier must also know how the public key of the Holder was included in the SD-JWT and which algorithm the Holder used to sign the Key Binding JWT

val verifiedPresentationSdJwt: SdJwt<SignedJWT> = runBlocking {
    with(NimbusSdJwtOps) {
        val jwtSignatureVerifier = RSASSAVerifier(issuerRsaKeyPair).asJwtVerifier()
        val unverifiedPresentationSdJwt = loadSdJwt("/examplePresentationSdJwt.txt")
        verify(
            jwtSignatureVerifier,
            unverifiedPresentationSdJwt,
        ).getOrThrow()
    }
}

You can get the full code here.

Library provides various variants of the above method that:

  • Preserve the KB-JWT, if present, to the successful outcome of a verification
  • Accept the unverified SD-JWT serialized in JWS JSON

Please check KeyBindingTest for a more advanced presentation scenario which includes key binding

Recreate original claims

Given an SdJwt, either issuance or presentation, the original claims used to produce the SD-JWT can be recreated. This includes the claims that are always disclosed (included in the JWT part of the SD-JWT) having the digests replaced by selectively disclosable claims found in disclosures.

val claims: JsonObject = runBlocking {
    val sdJwt: SdJwt<SignedJWT> = run {
        val spec = sdJwt {
            claim("sub", "6c5c0a49-b589-431d-bae7-219122a9ec2c")
            claim("iss", "https://example.com/issuer")
            claim("iat", 1516239022)
            claim("exp", 1735689661)
            objClaim("address") {
                sdClaim("street_address", "Schulstr. 12")
                sdClaim("locality", "Schulpforta")
                sdClaim("region", "Sachsen-Anhalt")
                sdClaim("country", "DE")
            }
        }
        val issuer = NimbusSdJwtOps.issuer(signer = RSASSASigner(issuerRsaKeyPair), signAlgorithm = JWSAlgorithm.RS256)
        issuer.issue(spec).getOrThrow()
    }

    with(NimbusSdJwtOps) {
        sdJwt.recreateClaimsAndDisclosuresPerClaim().first
    }
}

You can get the full code here.

The claims contents would be

{
  "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
  "address": {
    "street_address": "Schulstr. 12",
    "locality": "Schulpforta",
    "region": "Sachsen-Anhalt",
    "country": "DE"
  },
  "iss": "https://example.com/issuer",
  "exp": 1735689661,
  "iat": 1516239022
}

Decoy digests

By default, the library doesn't add decoy digests to the issued SD-JWT. If an issuer wants to use digests, it can do so using the DSL.

DSL functions that mark a container composed of potentially selectively disclosable
elements, such as sdJwt{}, plain{} e.t,c, accept an optional parameter named minimumDigests: Int? = null.

The issuer can use this parameter to set the minimum number of digests for the immediate level of this container. Library will make sure that the underlying digests array will have at minimum a length equal to digestNumberHint.

Initially, during issuance, the digests array will contain disclosure digests and if needed, additional decoy digests to reach the hint provided. If the array contains more disclosure digests than the hint, no decoys will be added.

val sdJwtWithMinimumDigests = sdJwt(minimumDigests = 5) {
    // This 5 guarantees that at least 5 digests will be found
    // to the digest array, regardless of the content of the SD-JWT
    objClaim("address", minimumDigests = 10) {
        // This affects the nested array of the digests that will
        // have at list 10 digests.
    }

    sdObjClaim("address1", minimumDigests = 8) {
        // This will affect the digests array that will be found
        // in the disclosure of this recursively disclosable item
        // the whole object will be embedded in its parent
        // as a single digest
    }

    arrClaim("evidence", minimumDigests = 2) {
        // Array will have at least 2 digests
        // regardless of its elements
    }

    sdArrClaim("evidence1", minimumDigests = 2) {
        // Array will have at least 2 digests
        // regardless of its elements
        // the whole array will be embedded in its parent
        // as a single digest
    }
}

You can get the full code here.

Tip

In addition to the DSL defined hints, the issuer may set a global hint to the SdJwtFactory. This will be used as a fallback limit for every container of selectively disclosable elements that don't explicitly provide a limit.

DSL Examples

For a comprehensive guide to the SD-JWT DSL, including core concepts, basic usage, advanced features, and working with metadata, see the DSL Documentation.

All examples assume that we have the following claim set

{
  "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
  "address": {
    "street_address": "Schulstr. 12",
    "locality": "Schulpforta",
    "region": "Sachsen-Anhalt",
    "country": "DE"
  }
}

SD-JWT VC support

The library provides comprehensive support for SD-JWT-based Verifiable Credentials, including advanced features for type metadata, validation, and credential building.

SD-JWT VC Verification

Issuer-signed JWT Verification Key Validation is provided by SdJwtVcVerifier.
Please check KeyBindingTest for code examples of verifying an SD-JWT VC and an SD-JWT+KB VC (including verification of the Key Binding JWT).

Example:

val sdJwtVcVerification = runBlocking {
    val issuer = Url("https://issuer.example.com")

    with(NimbusSdJwtOps) {
        val sdJwt = run {
            val spec = sdJwt {
                claim(RFC7519.ISSUER, issuer.toString())
                claim(SdJwtVcSpec.VCT, "urn:credential:sample")
            }

            val signer = issuer(signer = ECDSASigner(issuerEcKeyPairWithCertificate), signAlgorithm = JWSAlgorithm.ES512) {
                type(JOSEObjectType("vc+sd-jwt"))
                x509CertChain(issuerEcKeyPairWithCertificate.x509CertChain)
            }
            signer.issue(spec).getOrThrow().serialize()
        }

        val verifier = SdJwtVcVerifier(
            issuerVerificationMethod = IssuerVerificationMethod.usingX5c { chain, _ ->
                chain.first().base64 == issuerEcKeyPairWithCertificate.x509CertChain.first()
            },
            typeMetadataPolicy = TypeMetadataPolicy.NotUsed,
        )
        verifier.verify(sdJwt)
    }
}

You can get the full code here.

Note

Support for OctetKeyPair required the optional dependency com.google.crypto.tink:tink.

SD-JWT VC Type Metadata

The library provides robust support for SD-JWT-VC type metadata through the SdJwtDefinition class. This hierarchical representation accurately models the disclosure and display properties of SD-JWT-VC credentials.

Key features include:

  • Rich metadata representation with VctMetadata including name, description, display information, and schemas
  • Automatic handling of claims that should never be selectively disclosable according to the SD-JWT-VC specification
  • Hierarchical structure that accurately represents the disclosure properties of nested objects and arrays

Type Metadata Resolution

The ResolveTypeMetadata interface provides powerful capabilities for resolving and merging SD-JWT-VC type metadata:

  • Support for inheritance through the "extends" property
  • Resolution of external references through URI lookups
  • Merging of type metadata from different sources
  • Handling of display information in multiple languages

Example usage:

val resolver = ResolveTypeMetadata(
    lookupTypeMetadata = LookupTypeMetadataUsingKtor(),
    lookupJsonSchema = LookupJsonSchemaUsingKtor()
)
val typeMetadata = resolver(Vct("https://example.com/credentials/sample")).getOrThrow()

When constructing an SdJwtVcVerifier, a Verifier can provide a TypeMetadataPolicy that describes his policy concerning Type Metadata. Currently, the library provides the following policies:

  • TypeMetadataPolicy.NotUsed: Type Metadata are not used.
  • TypeMetadataPolicy.Optional: Type Metadata are optional. If resolution succeeds, Type Metadata are used for extra validation checks of the SD-JWT VC. If resolution fails, no further checks are performed.
  • TypeMetadataPolicy.AlwaysRequired: Type Metadata are always required. If resolution succeeds, Type Metadata are used for extra validation checks of the SD-JWT VC. If resolution fails, the SD-JWT VC is rejected.
  • TypeMetadataPolicy.RequiredFor: Applies TypeMetadataPolicy.AlwaysRequired for a set of specified Vcts, and TypeMetadataPolicy.Optional for everything else.

Definition-Based SD-JWT Object Building

The DefinitionBasedSdJwtObjectBuilder provides a powerful way to build SD-JWT objects based on a predefined template:

  • Uses an SdJwtDefinition as a template for creating SD-JWT objects
  • Automatically handles selective disclosure based on the definition
  • Transforms raw JSON data into structured SD-JWT objects
  • Provides validation and warnings if the data doesn't match the definition

Example usage:

val sdJwtObject = sdJwtVc(sdJwtDefinition) {
    put("given_name", "John")
    put("family_name", "Doe")
    // Additional claims...
}.getOrThrow()

Definition-Based Validation

The DefinitionBasedSdJwtVcValidator provides validation of SD-JWT-VC credentials against a definition:

  • Validates that the credential conforms to the expected structure
  • Ensures claims are properly disclosed according to the definition
  • Provides detailed validation results with specific violation information
  • Supports validation of both issuance and presentation SD-JWT-VCs

The validation result (DefinitionBasedValidationResult) can be either:

  • Valid: Contains the recreated credential and disclosures per claim path
  • Invalid: Contains a list of specific violations (missing claims, wrong types, etc.)

How to contribute

We welcome contributions to this project. To ensure that the process is smooth for everyone involved, follow the guidelines found in CONTRIBUTING.md.

License

License details

Copyright (c) 2023 European Commission

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

About

A library for issuing and verifying SD-JWT

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors 13

Languages