Selective Disclosure for JWTs (SD-JWT)
On this page
Signed JSON Web Tokens (JWTs) are essentially signed JSON documents. Whenever you share a signed JWT, you share all the data ("claims") it contains. This is not necessarily a problem, if the JWT is intended for a single audience but with the rise of verifiable credentials JWTs become multipurpose documents that a wallet may share with many different parties. Without mitigation, this new use-case for JWTs eventually leads to oversharing. This is where SD-JWT comes in. SD-JWT is a format that allows a sender to selectively disclose claims of a signed JWT instead of sharing all data with every recipient.
SD-JWT Format
In its simple form, an SD-JWT is a composite structure with the following elements:
- A signed JWT
- Zero or more Disclosures
In comparison to conventional JWTs, the payload of the signed JWT contains hidden claims. Hidden claims are basically hashed claims (to be precise, hashed Disclosures) that are opaque to the receiver of an SD-JWT without the corresponding Disclosures. That is to say, Disclosures contain the cleartext for hidden claims.
The sender of an SD-JWT can include the Disclosures of the hidden claims it wants to reveal. All the hidden claims whose Disclosures are not part of the SD-JWT remain hidden, though. Some claims may have multiple values, i.e. they are arrays. SD-JWT not only supports hiding claims but also single elements in an array of a claim value.
The following is an example of an SD-JWT with hidden claims and Disclosures.
eyJ0eXAiOiJleGFtcGxlK3NkLWp3dCIsImFsZyI6IkVTMjU2In0.eyJjb21wYW55IjoiQ3VyaXR5IiwiZW1haWwiOiJpbmZvQGN1cml0eS5pbyIsImxlYXJuaW5nX3Jlc291cmNlcyI6WyJodHRwczovL2N1cml0eS5pby9yZXNvdXJjZXMiLHsiLi4uIjoiVk94cXh5SWdDWXNpM18xNmF0Q1VodUdvY2NYTmR0QlBFeU1rWURaakRHZyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidjhNZVVNYWd2SUpQNmlDOEU4S1lNdXpKWWVZeENIYTFqRWZwakcxQUJPdyJdfQ.vYFbrLc3pQj1sAYGXHukiY5ae3t-NlqDWOqCK5fmWx1uLkWCT8lDa-E3YWheaOt_wiR5yeyV4s_d0w_KpdamRw~WyJadUlnWkRtd3JINl9WelNkNUpvOUlRIiwgIkNsb3VkIE5hdGl2ZSBEYXRhIFNlY3VyaXR5IFdpdGggT0F1dGggLSBBIFNjYWxhYmxlIFplcm8gVHJ1c3QgQXJjaGl0ZWN0dXJlIChPJ1JlaWxseSkiXQ~WyJ0OVBZR1pwTXFaU0oxbG5GVGNhNmZBIiwgInBob25lX251bWJlciIsIis0Njg0MTA3Mzc3MCJd~
The SD-JWT has the form of <signed-JWT>~[Disclosure~]*
, that is, a tilde symbol (~
) separates the elements. An SD-JWT always contains at least one ~
symbol. In this way you can distinguish it from a JWT. In addition, the signed JWT in the SD-JWT should contain the typ
header field with an application-specific type identifier to avoid confusion attacks.
The sender of an SD-JWT may add different Disclosures for a signed JWT depending on the recipient. For example, in the case of verifiable credentials, a Wallet may reveal different claims of a JWT-based credential for different Verifiers. It can do so by adding only the relevant Disclosures in an SD-JWT when presenting the credential.
Decoding SD-JWTs
Any JWT library should be able to parse the signed JWT of an SD-JWT. OAuth Tools supports SD-JWTs and also parses Disclosures. This means that you can use OAuth Tools to decode and study SD-JWTs.
When verifying an SD-JWT, you need to verify the signature of the signed JWT in the SD-JWT, calculate the hashes over all the Disclosures and check whether the signed JWT contains those hashes. SD-JWT introduces special claim names for that purpose.
Special Claim Names
To be able to calculate hashes over Disclosures, you need to know which hash algorithm to apply. For this, the payload of the SD-JWT may contain an _sd_alg
claim with the name of the hash algorithm. If the claim is absent, use SHA-256.
SD-JWT collects all hidden claims in an array in the special claim called _sd
. This claim may appear several times within a payload, because it appears on the same level in the JSON object hierarchy of the JWT payload as the claim it obfuscates. Consequently, you can hide top-level claims and any child claims with SD-JWT. The following is an example of an JWT payload according to the SD-JWT format that hides one top-level claim (indicated via the element in the top-level _sd
claim):
{"company": "Curity","_sd_alg": "sha-256","_sd": ["v8MeUMagvIJP6iC8E8KYMuzJYeYxCHa1jEfpjG1ABOw"]}
As mentioned before, Disclosures may also refer to elements in an array. SD-JWT introduces the special claim ...
(three dots) to indicate that an element in an array was omitted due to obfuscation. In the following example, the JWT payload hides an element in the learning_resources
claim:
{"company": "Curity","email": "info@curity.io","learning_resources": ["https://curity.io/resources",{ "...": "VOxqxyIgCYsi3_16atCUhuGoccXNdtBPEyMkYDZjDGg" }],"_sd_alg": "sha-256","_sd": ["v8MeUMagvIJP6iC8E8KYMuzJYeYxCHa1jEfpjG1ABOw"]}
Note that the payload includes hidden and cleartext claims. While cleartext claim values are readable, it is not possible to guess the hidden claims in the _sd
claim or hidden array element in the learning_resources
array without the Disclosures.
Disclosures
The Disclosures contain the values for claims or array elements. A Disclosure is a string that is a base64url-encoded JSON array containing
- a salt (to randomize the output of the hash function),
- the name of the claim (if applicable) and
- the value to hide.
The value can be any valid JSON value. If the value to hide is an element of an array, then the claim name is not part of the Disclosure. Consequently, there are two formats of Disclosures:
- one for object properties (claims) in form of
[<salt-string>,<claim-name>,<claim-value>]
and - one for array elements
[<salt-string>,<claim-value>]
.
Take the following Disclosure as an example
WyJ0OVBZR1pwTXFaU0oxbG5GVGNhNmZBIiwgInBob25lX251bWJlciIsIis0Njg0MTA3Mzc3MCJd
If you decode the string using base64url decoding, you can read the JSON array ["t9PYGZpMqZSJ1lnFTca6fA", "phone_number","+46841073770"]
which contains
- a salt (
t9PYGZpMqZSJ1lnFTca6fA
), - a claim name (
phone_number
) and - a claim value (
+46841073770
).
This means the Disclosure refers to the claim "phone_number":"+46841073770"
. The question is, if you had to verify an SD-JWT and resolve any hidden claims with the help of Disclosures, where in the claims set would you place a disclosed claim? This is where the hash function, the _sd
and ...
claims become relevant.
Verifying Disclosures
To verify a Disclosure, you need to first calculate its hash value using the hash algorithm in the _sd_alg
claim. The input to the hash function is the ASCII bytes of the Disclosure (the base64url-encoded JSON array including the salt, value and optionally a claim name). The output is a byte array that you need to base64url encode. You can then compare the encoded string of the hash with the strings in all the _sd
and ...
claims in an SD-JWT. If you find a match, you know
- that the Disclosure is related to the signed JWT and
- which claim or array value in the JSON object the Disclosure represents.
Study the example SD-JWT from above. This SD-JWT includes hidden claims (recognizable via the _sd
and ...
claims) as well as Disclosures.
eyJ0eXAiOiJleGFtcGxlK3NkLWp3dCIsImFsZyI6IkVTMjU2In0.eyJjb21wYW55IjoiQ3VyaXR5IiwiZW1haWwiOiJpbmZvQGN1cml0eS5pbyIsImxlYXJuaW5nX3Jlc291cmNlcyI6WyJodHRwczovL2N1cml0eS5pby9yZXNvdXJjZXMiLHsiLi4uIjoiVk94cXh5SWdDWXNpM18xNmF0Q1VodUdvY2NYTmR0QlBFeU1rWURaakRHZyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidjhNZVVNYWd2SUpQNmlDOEU4S1lNdXpKWWVZeENIYTFqRWZwakcxQUJPdyJdfQ.vYFbrLc3pQj1sAYGXHukiY5ae3t-NlqDWOqCK5fmWx1uLkWCT8lDa-E3YWheaOt_wiR5yeyV4s_d0w_KpdamRw~WyJadUlnWkRtd3JINl9WelNkNUpvOUlRIiwgIkNsb3VkIE5hdGl2ZSBEYXRhIFNlY3VyaXR5IFdpdGggT0F1dGggLSBBIFNjYWxhYmxlIFplcm8gVHJ1c3QgQXJjaGl0ZWN0dXJlIChPJ1JlaWxseSkiXQ~WyJ0OVBZR1pwTXFaU0oxbG5GVGNhNmZBIiwgInBob25lX251bWJlciIsIis0Njg0MTA3Mzc3MCJd~
If you decode the signed JWT of the SD-JWT, you get the following payload:
{"company": "Curity","email": "info@curity.io","learning_resources": ["https://curity.io/resources",{ "...": "VOxqxyIgCYsi3_16atCUhuGoccXNdtBPEyMkYDZjDGg" }],"_sd_alg": "sha-256","_sd": ["v8MeUMagvIJP6iC8E8KYMuzJYeYxCHa1jEfpjG1ABOw"]}
This payload includes two hidden claims - one at the top level (indicated via the _sd
claim at the top level) and an element of the learning_resources
array (indicated via the ...
claim). For demonstration purpose, the SD-JWT also contains two Disclosures, one for each hidden claim:
WyJadUlnWkRtd3JINl9WelNkNUpvOUlRIiwgIkNsb3VkIE5hdGl2ZSBEYXRhIFNlY3VyaXR5IFdpdGggT0F1dGggLSBBIFNjYWxhYmxlIFplcm8gVHJ1c3QgQXJjaGl0ZWN0dXJlIChPJ1JlaWxseSkiXQ
WyJ0OVBZR1pwTXFaU0oxbG5GVGNhNmZBIiwgInBob25lX251bWJlciIsIis0Njg0MTA3Mzc3MCJd
Selective Disclosure
Despite the example showing Disclosures for all hidden claims, there is no requirement to do so. Most commonly an SD-JWT will contain only Disclosures for zero or some hidden claims.
Take the Disclosure from before.
WyJ0OVBZR1pwTXFaU0oxbG5GVGNhNmZBIiwgInBob25lX251bWJlciIsIis0Njg0MTA3Mzc3MCJd
To verify this Disclosure, first hash the string using SHA-256 and then base64url encode the hash value. The result is v8MeUMagvIJP6iC8E8KYMuzJYeYxCHa1jEfpjG1ABOw
.
Now, search for that string in the SD-JWT payload. You can find that string in the top-level _sd
claim of the example SD-JWT. You may ignore any remaining hidden claims. This means, you can resolve the JSON payload to an object similar to the following ("???"
serves as a placeholder in learning_resources
array to mark the non-disclosed value in it).
{"company": "Curity","email": "info@curity.io","learning_resources": ["https://curity.io/resources"],"???""phone_number": "+46841073770"}
Obviously, this matching technique only works if Disclosures and their hash values are unique within a given SD-JWT. Therefore, each Disclosure encodes a unique salt value.
Feel free to repeat the exercise with the other Disclosure of the example SD-JWT. Can you find out the other learning resource?
Summary
SD-JWT is a format that enables the selective disclosure of hidden claims in a signed JWT. It defines how to hide and reveal claims with the help of hash algorithms. SD-JWT is an important part of issuing verifiable credentials because it adds critical security properties to JWT-based credentials that at the end enable users to avoid oversharing by allowing them to select what data to share with whom.
Judith Kahrer
Product Marketing Engineer at Curity
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial