Selective Disclosure Mechanics
| Audience | Developers implementing custom SD-JWT issuance or verification, and security engineers auditing cryptographic choices. |
| Purpose | Detail the cryptographic primitives (salts, hashes, digests, decoys) that power selective disclosure so readers can reason about security properties and extend the library. |
| Scope | Salt generation, hash algorithm selection, disclosure encoding, digest computation, nested disclosure, decoy digests, and verification algorithm. Out of scope: high-level lifecycle (see SD-JWT Deep Dive). |
| Success criteria | Reader can trace a disclosure from creation through digest to verification, evaluate salt entropy, and explain why decoy digests prevent information leakage. |
For a conceptual introduction and basic usage, start with the SD-JWT Deep Dive.
Prerequisites
Before reading this document, you should understand:
- Basic SD-JWT concepts from SD-JWT Deep Dive
- Cryptographic hash functions (SHA-256 family)
- Base64url encoding
- JSON serialization rules
Cryptographic Foundations
Why Salts Matter
A disclosure without a salt would be vulnerable to preimage attacks. If an attacker knows the possible values of a claim (e.g., age is between 18-100), they could hash each possibility and compare against the _sd array to discover the hidden value.
Without salt (vulnerable):
Possible ages: 18, 19, 20, ..., 100
Attacker computes: HASH("age", 25), HASH("age", 26), ...
Attacker finds match in _sd array -> discovers age = 25
With salt (secure):
Salt is random 128-bit value: "_26bc4LT-ac6q2KI6cBAceg"
Disclosure: ["_26bc4LT-ac6q2KI6cBAceg", "age", 25]
Attacker cannot guess salt -> cannot precompute hash
Salt Generation Requirements
Per RFC 9901, salts must be:
- Cryptographically random (not predictable)
- At least 128 bits of entropy
- Unique per disclosure (never reused)
// SdJwt.Net salt generation
public static string GenerateSalt(int byteLength = 16) // 128 bits
{
var bytes = new byte[byteLength];
RandomNumberGenerator.Fill(bytes);
return Base64UrlEncoder.Encode(bytes);
}
Hash Algorithm Selection
RFC 9901 requires the hash algorithm to be specified in the _sd_alg claim. This library supports:
| Algorithm | _sd_alg Value |
Security Level | Recommendation |
|---|---|---|---|
| SHA-256 | sha-256 |
Standard | Default, recommended for most use cases |
| SHA-384 | sha-384 |
High | For higher security requirements |
| SHA-512 | sha-512 |
Very High | For maximum security |
| MD5 | N/A | Broken | BLOCKED - cryptographically broken |
| SHA-1 | N/A | Broken | BLOCKED - collision attacks proven |
Disclosure Format Specification
Object Property Disclosure
For object properties, the disclosure is a 3-element JSON array:
[salt, claim_name, claim_value]
Example:
["_26bc4LT-ac6q2KI6cBAceg", "email", "alice@example.com"]
Encoding process:
1. JSON serialize: '["_26bc4LT-ac6q2KI6cBAceg","email","alice@example.com"]'
2. UTF-8 encode to bytes
3. Base64url encode: 'WyJfMjZiYzRMVC1hYzZxMktJNmNCQWNlZyIsImVtYWlsIiwiYWxpY2VAZXhhbXBsZS5jb20iXQ'
Array Element Disclosure
For array elements, the disclosure is a 2-element JSON array (no claim name):
[salt, element_value]
Example (disclosing a nationality from a nationalities array):
["lklxF5jMYlGTPUovMNIvCA", "US"]
Implementation in SdJwt.Net
// From Models/Disclosure.cs
public Disclosure(string salt, string claimName, object claimValue)
{
Salt = salt;
ClaimName = claimName;
ClaimValue = claimValue;
object[] disclosureArray;
if (string.IsNullOrEmpty(ClaimName))
{
// Array element disclosure: [salt, value]
disclosureArray = new object[] { Salt, ClaimValue };
}
else
{
// Object property disclosure: [salt, name, value]
disclosureArray = new object[] { Salt, ClaimName, ClaimValue };
}
var json = JsonSerializer.Serialize(disclosureArray,
SdJwtConstants.DefaultJsonSerializerOptions);
EncodedValue = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(json));
}
Digest Computation
Formula
digest = BASE64URL(HASH(ASCII(base64url_encoded_disclosure)))
Step-by-step example:
1. Disclosure array: ["_26bc4LT-ac6q2KI6cBAceg", "email", "alice@example.com"]
2. JSON serialize (UTF-8):
'["_26bc4LT-ac6q2KI6cBAceg","email","alice@example.com"]'
3. Base64url encode:
'WyJfMjZiYzRMVC1hYzZxMktJNmNCQWNlZyIsImVtYWlsIiwiYWxpY2VAZXhhbXBsZS5jb20iXQ'
4. Convert to ASCII bytes (the encoded string IS ASCII)
5. SHA-256 hash the bytes
6. Base64url encode the hash:
'JnPBS7TpL8ncxL-6mymWKgzZPk4J98xU8C4d1yXt9qE'
Implementation
// Digest computation in SdJwt.Net
public static string ComputeDigest(string encodedDisclosure, string algorithm = "sha-256")
{
using var hashAlgorithm = algorithm switch
{
"sha-256" => SHA256.Create(),
"sha-384" => SHA384.Create(),
"sha-512" => SHA512.Create(),
_ => throw new NotSupportedException($"Algorithm {algorithm} is not supported")
};
var bytes = Encoding.ASCII.GetBytes(encodedDisclosure);
var hash = hashAlgorithm.ComputeHash(bytes);
return Base64UrlEncoder.Encode(hash);
}
Nested Selective Disclosure
SD-JWT supports selective disclosure at any nesting level within JSON objects.
Example: Nested Address
Original claims:
{
"name": "Alice",
"address": {
"street": "123 Main St",
"city": "Springfield",
"country": "US"
}
}
With nested selective disclosure (city and country are disclosable):
{
"name": "Alice",
"address": {
"street": "123 Main St",
"_sd": ["digest_for_city", "digest_for_country"]
}
}
Separate disclosures:
["salt1", "city", "Springfield"]
["salt2", "country", "US"]
Implementation
var options = new SdIssuanceOptions
{
DisclosureStructure = new
{
address = new
{
city = true, // Selectively disclosable
country = true // Selectively disclosable
// street is NOT listed -> always visible
}
}
};
Decoy Digests
Decoy digests prevent information leakage about the number of hidden claims.
The Privacy Problem
Without decoys, a verifier seeing 3 digests in _sd knows there are exactly 3 hidden claims, even if they cannot see the values.
How Decoys Work
Decoy digests are random hashes with no corresponding disclosure. They are cryptographically indistinguishable from real digests.
{
"patient": "John",
"_sd": [
"real_hash_1",
"decoy_hash_a",
"real_hash_2",
"decoy_hash_b",
"real_hash_3"
]
}
Now the verifier cannot determine how many real claims exist.
Decoy Generation
public static string GenerateDecoyDigest()
{
var randomBytes = new byte[32]; // SHA-256 output size
RandomNumberGenerator.Fill(randomBytes);
return Base64UrlEncoder.Encode(randomBytes);
}
Verification Algorithm
When a verifier receives a presentation with disclosures:
1. Parse the SD-JWT and extract the _sd array and _sd_alg
2. For each provided disclosure:
a. Decode the disclosure from Base64url
b. Validate the JSON structure (2 or 3 elements)
c. Compute digest using _sd_alg algorithm
d. Check if computed digest exists in _sd array
e. If not found, REJECT (disclosure was not issued)
3. For each digest in _sd array:
a. Either a matching disclosure was provided (claim revealed)
b. Or no disclosure provided (claim remains hidden)
c. Unmatched digests may be decoys
4. Extract revealed claims into the verified payload
Key Binding JWT (KB-JWT) Hash
The KB-JWT contains an sd_hash claim that binds it to a specific SD-JWT presentation.
SD Hash Computation
sd_hash = BASE64URL(SHA-256(ASCII(sd_jwt_without_kb_jwt)))
This ensures the KB-JWT cannot be reused with a different SD-JWT presentation.
JSON Serialization Rules
Consistent JSON serialization is critical for digest matching.
Rules Enforced by This Library
- No whitespace between elements
- UTF-8 encoding for string values
- Consistent key ordering (as specified in the original claim)
- Standard JSON escaping for special characters
Why This Matters
If the issuer and verifier use different JSON serialization:
Issuer: '["salt","email","alice@example.com"]' -> hash A
Verifier: '["salt", "email", "alice@example.com"]' -> hash B (space added)
hash A != hash B -> Verification fails!
Implementation References
| Component | File | Description |
|---|---|---|
| Disclosure model | Disclosure.cs | Disclosure creation and parsing |
| Hash utilities | SdJwtUtils.cs | Salt generation and digest computation |
| Parser | SdJwtParser.cs | SD-JWT string parsing |
| Constants | SdJwtConstants.cs | Algorithm names and claim constants |
Related Concepts
- SD-JWT Deep Dive - Conceptual introduction and basic usage
- Verifiable Credential Deep Dive - Using SD-JWT for credentials
- HAIP Compliance - Algorithm requirements for high assurance