HeroSD-JWT 1.1.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package HeroSD-JWT --version 1.1.1
                    
NuGet\Install-Package HeroSD-JWT -Version 1.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="HeroSD-JWT" Version="1.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="HeroSD-JWT" Version="1.1.1" />
                    
Directory.Packages.props
<PackageReference Include="HeroSD-JWT" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add HeroSD-JWT --version 1.1.1
                    
#r "nuget: HeroSD-JWT, 1.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package HeroSD-JWT@1.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=HeroSD-JWT&version=1.1.1
                    
Install as a Cake Addin
#tool nuget:?package=HeroSD-JWT&version=1.1.1
                    
Install as a Cake Tool

HeroSD-JWT

NuGet Version NuGet Downloads Build Status License: MIT .NET C# AOT Compatible Minimal Dependencies

A .NET library implementing SD-JWT (Selective Disclosure for JSON Web Tokens) according to the IETF draft-ietf-oauth-selective-disclosure-jwt specification.

Overview

SD-JWT enables privacy-preserving credential sharing by allowing holders to selectively disclose only necessary claims to verifiers, while cryptographically proving the disclosed claims are authentic and unmodified.

Table of Contents

Key Features

  • Create SD-JWTs with selectively disclosable claims
  • Nested object selective disclosure - Full support for nested properties like address.street, address.geo.lat (multi-level nesting)
  • Array element selective disclosure - Syntax like degrees[1] for individual array elements
  • Array & Object Reconstruction API - Automatically reconstruct hierarchical structures from disclosed claims
  • JWT Key Rotation Support - RFC 7515 compliant kid parameter with key resolver pattern for secure key management
  • Key binding (proof of possession) - RFC 7800 compliant with temporal validation
  • Decoy digests - Privacy protection against claim enumeration
  • Holder-controlled claim disclosure
  • Cryptographic verification of signatures and claim integrity
  • Zero third-party dependencies (uses only .NET BCL including System.Security.Cryptography, System.Text.Json, System.Buffers.Text)
  • Constant-time comparison for security-critical operations
  • Algorithm confusion prevention (rejects "none" algorithm)
  • Multi-targeting .NET 8.0, .NET 9.0, and .NET 10.0

Installation

dotnet add package HeroSD-JWT

Or via NuGet Package Manager:

Install-Package HeroSD-JWT

Quick Start

The fluent builder provides an easy, discoverable API:

using HeroSdJwt.Issuance;
using HeroSdJwt.Common;
using HeroSdJwt.Core;

// Generate a signing key
var keyGen = KeyGenerator.Instance;
var key = keyGen.GenerateHmacKey();

// Create SD-JWT with fluent builder
var sdJwt = SdJwtBuilder.Create()
    .WithClaim("sub", "user-123")
    .WithClaim("name", "Alice")
    .WithClaim("email", "alice@example.com")
    .WithClaim("age", 30)
    .MakeSelective("email", "age")  // Selectively disclosable
    .SignWithHmac(key)
    .Build();

// Create presentation revealing only email
var presentation = sdJwt.ToPresentation("email");

🏒 Nested Object Selective Disclosure

Selectively disclose nested properties with full JSONPath-style syntax:

using HeroSdJwt.Cryptography;
using HeroSdJwt.KeyBinding;
using HeroSdJwt.Verification;

// Create SD-JWT with nested object claims
var sdJwt = SdJwtBuilder.Create()
    .WithClaim("sub", "user-456")
    .WithClaim("address", new
    {
        street = "123 Main Street",
        city = "Boston",
        state = "MA",
        zip = "02101",
        geo = new { lat = 42.3601, lon = -71.0589 }
    })
    .MakeSelective("address.street", "address.city", "address.geo.lat", "address.geo.lon")
    .SignWithHmac(key)
    .Build();

// Holder creates presentation with only specific nested claims
var presentation = sdJwt.ToPresentation("address.street", "address.geo.lat");

// Verifier receives and verifies
var verifier = new SdJwtVerifier(
    new SdJwtVerificationOptions(),
    new EcPublicKeyConverter(),
    new SignatureValidator(),
    new DigestValidator(),
    new KeyBindingValidator(),
    new ClaimValidator());
var result = verifier.VerifyPresentation(presentation, key);

// Automatically reconstruct the nested object structure
var address = result.GetDisclosedObject("address");
// Returns: { "street": "123 Main Street", "geo": { "lat": 42.3601 } }

πŸ“Š Array Element Selective Disclosure

var sdJwt = SdJwtBuilder.Create()
    .WithClaim("degrees", new[] { "BS", "MS", "PhD" })
    .MakeSelective("degrees[1]", "degrees[2]") // Only MS and PhD are selective
    .SignWithHmac(key)
    .Build();

// Create presentation
var presentation = sdJwt.ToPresentation("degrees[2]"); // Only reveal PhD

// Reconstruct array from disclosed elements
var result = verifier.VerifyPresentation(presentation, key);
var degrees = result.GetDisclosedArray("degrees");
// Returns: [null, null, "PhD"] - sparse array with only disclosed element

πŸ”‘ Different Signature Algorithms

var keyGen = KeyGenerator.Instance;

// HMAC (simple, symmetric)
var key = keyGen.GenerateHmacKey();
var sdJwt = SdJwtBuilder.Create()
    .WithClaims(claims)
    .MakeSelective("email")
    .SignWithHmac(key)
    .Build();

// RSA (asymmetric, widely supported)
var (rsaPrivate, rsaPublic) = keyGen.GenerateRsaKeyPair();
var sdJwt = SdJwtBuilder.Create()
    .WithClaims(claims)
    .MakeSelective("email")
    .SignWithRsa(rsaPrivate)
    .Build();

// ECDSA (asymmetric, compact)
var (ecPrivate, ecPublic) = keyGen.GenerateEcdsaKeyPair();
var sdJwt = SdJwtBuilder.Create()
    .WithClaims(claims)
    .MakeSelective("email")
    .SignWithEcdsa(ecPrivate)
    .Build();

πŸ”„ JWT Key Rotation Support

HeroSD-JWT supports JWT key rotation using the kid (key ID) parameter per RFC 7515 Section 4.1.4. This enables secure key management practices including regular key rotation, emergency revocation, and multi-key deployments.

Issuing SD-JWTs with Key IDs

Add a key identifier when creating SD-JWTs:

var keyGen = KeyGenerator.Instance;
var key = keyGen.GenerateHmacKey();

// Issue SD-JWT with key ID
var sdJwt = SdJwtBuilder.Create()
    .WithClaim("sub", "user-123")
    .WithClaim("email", "alice@example.com")
    .MakeSelective("email")
    .WithKeyId("key-2024-10")  // Add key identifier
    .SignWithHmac(key)
    .Build();
Verifying SD-JWTs with Key Resolver

Implement a key resolver to dynamically select verification keys based on the kid parameter:

using HeroSdJwt.Cryptography;
using HeroSdJwt.KeyBinding;
using HeroSdJwt.Primitives;
using HeroSdJwt.Verification;

// Set up key resolver with multiple keys
var keys = new Dictionary<string, byte[]>
{
    ["key-2024-09"] = oldKey,
    ["key-2024-10"] = currentKey,
    ["key-2024-11"] = newKey
};

// Create resolver delegate
KeyResolver resolver = keyId => keys.GetValueOrDefault(keyId);

// Verify presentation using key resolver
var verifier = new SdJwtVerifier(
    new SdJwtVerificationOptions(),
    new EcPublicKeyConverter(),
    new SignatureValidator(),
    new DigestValidator(),
    new KeyBindingValidator(),
    new ClaimValidator());
var result = verifier.TryVerifyPresentation(presentation, resolver);

if (result.IsValid)
{
    // Access disclosed claims
    var email = result.DisclosedClaims["email"].GetString();
}
Key Rotation Workflow

Typical key rotation lifecycle (30-day overlap period):

// Day 1-15: Only key-v1 active
var keysPhase1 = new Dictionary<string, byte[]>
{
    ["key-v1"] = keyV1
};

// Day 15-30: Both keys active (overlap period)
var keysPhase2 = new Dictionary<string, byte[]>
{
    ["key-v1"] = keyV1,  // Old key still valid
    ["key-v2"] = keyV2   // New key added
};
// Start issuing new tokens with key-v2, but both still verify

// Day 30+: Only key-v2 active (old key removed)
var keysPhase3 = new Dictionary<string, byte[]>
{
    ["key-v2"] = keyV2   // Only new key remains
};
// Old tokens with key-v1 now fail verification
Emergency Key Revocation

Immediately revoke a compromised key:

// Before: Both keys active
var keys = new Dictionary<string, byte[]>
{
    ["compromised-key"] = compromisedKey,
    ["emergency-key"] = emergencyKey
};

// After: Immediately remove compromised key
keys.Remove("compromised-key");

// All tokens issued with compromised-key now fail verification immediately
KeyResolver resolver = keyId => keys.GetValueOrDefault(keyId);
Backward Compatibility

Tokens without kid parameter work seamlessly with a fallback key:

// Verify tokens with or without kid
var result = verifier.TryVerifyPresentation(
    presentation,
    keyResolver: resolver,
    fallbackKey: legacyKey  // Used when JWT has no 'kid'
);

πŸ”§ Advanced API (Full Control)

For advanced scenarios, use the low-level API:

using HeroSdJwt.Cryptography;
using HeroSdJwt.Issuance;

var issuer = new SdJwtIssuer(
    new DisclosureGenerator(),
    new DigestCalculator(),
    new EcPublicKeyConverter(),
    new JwtSigner());
var claims = new Dictionary<string, object>
{
    ["sub"] = "user-123",
    ["email"] = "alice@example.com"
};

var signingKey = new byte[32];
var sdJwt = issuer.CreateSdJwt(
    claims,
    selectivelyDisclosableClaims: new[] { "email" },
    signingKey,
    HashAlgorithm.Sha256,
    SignatureAlgorithm.HS256);

Array Element Example:

var claims = new Dictionary<string, object>
{
    ["sub"] = "user-456",
    ["degrees"] = new[] { "BS", "MS", "PhD" }
};

// Make only MS and PhD selectively disclosable, keep BS always visible
var sdJwt = issuer.CreateSdJwt(
    claims,
    selectivelyDisclosableClaims: new[] { "degrees[1]", "degrees[2]" },
    signingKey,
    HashAlgorithm.Sha256
);

// JWT payload will contain:
// "degrees": ["BS", {"...": "digest_for_MS"}, {"...": "digest_for_PhD"}]

RS256/ES256 Example:

using System.Security.Cryptography;

// For RSA (RS256)
using var rsa = RSA.Create(2048);
var privateKey = rsa.ExportPkcs8PrivateKey();
var publicKey = rsa.ExportSubjectPublicKeyInfo();

var sdJwt = issuer.CreateSdJwt(
    claims,
    new[] { "email" },
    privateKey,
    HashAlgorithm.Sha256,
    SignatureAlgorithm.RS256);  // Specify algorithm

// For ECDSA (ES256)
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var privateKey = ecdsa.ExportPkcs8PrivateKey();
var publicKey = ecdsa.ExportSubjectPublicKeyInfo();

var sdJwt = issuer.CreateSdJwt(
    claims,
    new[] { "email" },
    privateKey,
    HashAlgorithm.Sha256,
    SignatureAlgorithm.ES256);  // Specify algorithm

2. Holder: Create Presentation

using HeroSdJwt.Presentation;

// Holder receives sdJwt from issuer and creates a presentation
var presenter = new SdJwtPresenter();

var presentation = presenter.CreatePresentation(
    sdJwt,
    claimsToDisclose: new[] { "birthdate" } // Only disclose birthdate
);

// Format for transmission
string presentationString = presentation.ToCombinedFormat();
// Format: "eyJhbGc...jwt...~WyI2cU1R...disclosure..."

3. Verifier: Verify Presentation

using HeroSdJwt.Cryptography;
using HeroSdJwt.KeyBinding;
using HeroSdJwt.Verification;

// Parse presentation string
var parts = presentationString.Split('~');
var jwt = parts[0];
var disclosures = parts[1..^1]; // All parts between JWT and key binding

// Verify presentation
var verifier = new SdJwtVerifier(
    new SdJwtVerificationOptions(),
    new EcPublicKeyConverter(),
    new SignatureValidator(),
    new DigestValidator(),
    new KeyBindingValidator(),
    new ClaimValidator());
var verificationKey = new byte[32]; // Same key used for signing (HS256)

// Option 1: Throws exception on failure (recommended for most cases)
var result = verifier.VerifyPresentation(presentationString, verificationKey);
Console.WriteLine($"Birthdate: {result.DisclosedClaims["birthdate"]}");

// Option 2: Returns result without throwing (Try* pattern)
var result = verifier.TryVerifyPresentation(presentationString, verificationKey);
if (result.IsValid)
{
    Console.WriteLine("βœ… Verification succeeded!");
    var birthdate = result.DisclosedClaims["birthdate"];
    Console.WriteLine($"Birthdate: {birthdate}");
}
else
{
    Console.WriteLine("❌ Verification failed!");
    foreach (var error in result.Errors)
    {
        Console.WriteLine($"Error: {error}");
    }
}

Architecture

The library follows the three-party SD-JWT model:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Issuer  β”‚                  β”‚ Holder β”‚                  β”‚ Verifier β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜                  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”˜                  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
     β”‚                            β”‚                           β”‚
     β”‚  1. Create SD-JWT          β”‚                           β”‚
     β”‚  with selective disclosuresβ”‚                           β”‚
     │───────────────────────────>β”‚                           β”‚
     β”‚                            β”‚                           β”‚
     β”‚                            β”‚  2. Select claims         β”‚
     β”‚                            β”‚  to disclose              β”‚
     β”‚                            β”‚                           β”‚
     β”‚                            β”‚  3. Create presentation   β”‚
     β”‚                            │──────────────────────────>β”‚
     β”‚                            β”‚                           β”‚
     β”‚                            β”‚                           β”‚  4. Verify
     β”‚                            β”‚                           β”‚  signature
     β”‚                            β”‚                           β”‚  & digests
     β”‚                            β”‚                           β”‚

Security

This library implements security best practices:

  • Constant-time comparison: Uses CryptographicOperations.FixedTimeEquals for digest validation to prevent timing attacks
  • Algorithm confusion prevention: Rejects "none" algorithm (both lowercase and uppercase)
  • Cryptographically secure salts: Uses RandomNumberGenerator for 128-bit salts
  • No third-party dependencies: Zero supply chain risk from third-party packages (uses only .NET BCL)
  • Strict validation: Treats warnings as errors, validates all inputs

Supported Algorithms

  • Hash algorithms: SHA-256 (default), SHA-384, SHA-512
  • Signature algorithms:
    • HS256 (HMAC-SHA256) - Symmetric signing with HMAC
    • RS256 (RSA-SHA256) - Asymmetric signing with RSA (minimum 2048 bits)
    • ES256 (ECDSA-P256-SHA256) - Asymmetric signing with ECDSA (P-256 curve)

Security Policy

For vulnerability reporting and security best practices, see:

  • SECURITY.md - Vulnerability disclosure policy and security contact
  • Security Guide - Detailed security best practices and guidelines

Requirements

  • .NET 8.0 (LTS), .NET 9.0, or .NET 10.0
  • No third-party dependencies (uses only .NET BCL)
    • Note: .NET 8.0 includes a polyfill dependency (Microsoft.Bcl.Memory) to backport .NET 9.0+'s native Base64Url APIs

Native AOT and Trimming Compatibility

AOT-Compatible with Standard JSON Types: This library works with .NET Native AOT compilation when used with standard JSON-serializable types.

Implementation approach:

  • Uses Utf8JsonWriter for all internal JSON serialization (disclosures, JWTs, key binding)
  • Direct dictionary parsing for JWK handling (no serialize-then-deserialize round-trips)
  • All cryptographic operations use standard BCL APIs
  • Minimal reflection usage - only at API boundary for user-provided claim values

API Boundary Consideration: The public API accepts Dictionary<string, object> for claim values to support any JSON-serializable type. This means:

  • Primitive types work in AOT: string, int, long, double, bool, arrays, dictionaries
  • JsonElement works perfectly in AOT: Pre-parsed JSON values
  • Custom classes may require trimming annotations: If you pass custom POCOs, ensure they're preserved

Key technical details:

  • SD-JWT disclosure arrays use Utf8JsonWriter: [salt, claim_name, claim_value]
  • Internal processing is fully AOT-compatible (no reflection beyond JSON serialization)
  • JWT headers and payloads serialized with explicit type handling

Recommendation for AOT applications:

// Instead of custom classes:
var claims = new Dictionary<string, object>
{
    ["sub"] = "user-123",
    ["email"] = "alice@example.com",
    ["age"] = 30
};

// Or use JsonElement for pre-parsed JSON:
var claims = new Dictionary<string, object>
{
    ["sub"] = "user-123",
    ["profile"] = JsonSerializer.SerializeToElement(new { name = "Alice", age = 30 })
};

Testing

# Run all tests
dotnet test

# Run with verbose output
dotnet test --verbosity normal

Performance

  • Verification: < 100ms for 50-claim SD-JWTs
  • Processing: < 500ms for 100-claim SD-JWTs
  • Thread-agnostic design: Reuse SdJwtIssuer, SdJwtPresenter, SdJwtVerifier instances (you handle synchronization)

Documentation

Comprehensive documentation is available in the docs/ directory:

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for detailed guidelines.

Quick overview:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Write tests first (TDD)
  4. Ensure all tests pass (dotnet test)
  5. Follow .NET naming conventions
  6. Add XML documentation for public APIs
  7. Commit your changes (git commit -m 'feat: add amazing feature')
  8. Push to the branch (git push origin feature/amazing-feature)
  9. Open a Pull Request

See CONTRIBUTING.md for more details on code style, testing, and the review process.

License

MIT License - see LICENSE file for details

References

Support

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.7 387 11/20/2025
1.1.6 383 11/20/2025
1.1.3 283 11/14/2025
1.1.2 243 11/14/2025
1.1.1 250 11/14/2025
1.1.0 238 11/14/2025
1.0.7 293 11/3/2025
1.0.6 178 11/3/2025
1.0.5 165 10/24/2025