HeroSD-JWT 1.1.7

dotnet add package HeroSD-JWT --version 1.1.7
                    
NuGet\Install-Package HeroSD-JWT -Version 1.1.7
                    
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.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="HeroSD-JWT" Version="1.1.7" />
                    
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.7
                    
#r "nuget: HeroSD-JWT, 1.1.7"
                    
#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.7
                    
#: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.7
                    
Install as a Cake Addin
#tool nuget:?package=HeroSD-JWT&version=1.1.7
                    
Install as a Cake Tool

HeroSD-JWT

NuGet Version NuGet Downloads Build Status License: MIT .NET AOT Compatible

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 = SdJwtIssuerBuilder.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 = SdJwtIssuerBuilder.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 = SdJwtIssuerBuilder.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 = SdJwtIssuerBuilder.Create()
    .WithClaims(claims)
    .MakeSelective("email")
    .SignWithHmac(key)
    .Build();

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

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

// Ed25519 (asymmetric, fast, small keys)
var (ed25519Private, ed25519Public) = keyGen.GenerateEd25519KeyPair();
var sdJwt = SdJwtIssuerBuilder.Create()
    .WithClaims(claims)
    .MakeSelective("email")
    .SignWithEd25519(ed25519Private)
    .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 = SdJwtIssuerBuilder.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 })
};

Development

Building the Project

The project uses the modern .slnx solution format:

# Build the entire solution
dotnet build HeroSD-JWT.slnx

# Build specific configuration
dotnet build HeroSD-JWT.slnx --configuration Release

# Restore dependencies
dotnet restore HeroSD-JWT.slnx

All projects multi-target .NET 8.0, .NET 9.0, and .NET 10.0. The build configuration is centrally managed in Directory.Build.props.

Testing

# Run all tests
dotnet test

# Run with verbose output
dotnet test --verbosity normal

# Run integration tests only
dotnet test --filter "Category=Integration"

# Run unit tests only
dotnet test --filter "Category!=Integration"

Continuous Integration

The project includes comprehensive CI/CD workflows:

  • CI Pipeline (ci.yml) - Runs on every push/PR across all platforms and frameworks
  • Nightly Integration Tests (run-integrations.yml) - Comprehensive test suite running daily at 2 AM UTC
  • Security Scanning (scan-security.yml) - Automated vulnerability and dependency audits
  • Performance Benchmarks (perform-benchmarks.yml) - Continuous performance monitoring

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 (1)

Showing the top 1 NuGet packages that depend on HeroSD-JWT:

Package Downloads
Sorcha.Cryptography

Multi-algorithm cryptography: ED25519, P-256, RSA-4096, BLS threshold signatures, post-quantum (ML-KEM/ML-DSA), zero-knowledge proofs, SD-JWT, HD wallet support (BIP32/39/44), Merkle trees, and encoding utilities

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.7 1,106 11/20/2025
1.1.6 414 11/20/2025
1.1.3 309 11/14/2025
1.1.2 282 11/14/2025
1.1.1 296 11/14/2025
1.1.0 270 11/14/2025
1.0.7 318 11/3/2025
1.0.6 200 11/3/2025
1.0.5 194 10/24/2025