HeroSD-JWT 1.1.0

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

HeroSD-JWT

NuGet Version NuGet Downloads Build Status License: MIT .NET C# AOT Compatible Zero Dependencies GitHub Stars GitHub Issues PRs Welcome

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
     │                            │                           │

Project Structure

src/
├── Core/               # Domain models (SdJwt, Disclosure, Digest, VerificationResult)
├── Common/             # Shared utilities (HashAlgorithm, ErrorCodes, Base64UrlEncoder)
├── Issuance/           # SD-JWT creation (SdJwtIssuer, DisclosureGenerator, DigestCalculator)
├── Presentation/       # Claim selection & formatting (SdJwtPresenter, SdJwtPresentation)
└── Verification/       # Signature & digest validation (SdJwtVerifier, SignatureValidator, DigestValidator)

tests/
├── Contract/           # Public API contract tests
├── Unit/               # Unit tests for individual components
└── Security/           # Security-specific tests (timing attacks, algorithm confusion, salt entropy)

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

API Stability & Versioning

HeroSD-JWT follows strict Semantic Versioning 2.0.0 with strong API stability guarantees:

Version Policy

MAJOR.MINOR.PATCH
  │     │      └─── Bug fixes (backward-compatible)
  │     └────────── New features (backward-compatible)
  └──────────────── Breaking changes (requires migration)

API Stability Guarantees

Stable Public API (guaranteed until next MAJOR version):

  • All public types, methods, and properties
  • Extension methods (ToPresentation(), ToPresentationWithAllClaims())
  • Configuration types (SdJwtVerificationOptions, SdJwtAuthenticationOptions)
  • Error codes and exception types

Binary Compatibility:

  • MINOR and PATCH releases maintain binary compatibility
  • No recompilation required for updates within same MAJOR version

Long-Term Support (LTS):

  • LTS versions supported for 12 months with security fixes
  • v1.x targets .NET 8.0 (LTS) - supported until November 2026

Breaking Change Policy

Deprecation Timeline:

  1. API marked [Obsolete] for minimum 6 months
  2. Migration guide provided before removal
  3. Breaking changes bundled into MAJOR releases only

For detailed versioning policy, see:

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

Current test coverage: 277 passing tests across:

  • Contract tests (API behavior)
  • Unit tests (component logic, array elements, claim paths, disclosures, signature algorithms, Base64Url encoding, decoy digests, JWK handling, builder API, crypto helpers)
  • Integration tests (end-to-end flows with arrays and nested claims)
  • Security tests (timing attacks, algorithm confusion, salt entropy, key binding)

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)

Roadmap

✅ Completed

  • Key binding (proof of possession) - RFC 7800 compliant with temporal validation
  • Array element selective disclosure - Full support with syntax like degrees[1]
  • Decoy digests for privacy enhancement - Cryptographically secure decoy generation
  • Security hardening - Critical claim protection, _sd_alg placement validation, KB-JWT replay prevention
  • Integration tests for end-to-end flows - 7 comprehensive tests
  • Nested property path parsing - Foundation with dot notation support (address.street)
  • Nested claims selective disclosure - Full support for nested properties with _sd arrays
  • RS256/ES256 signature algorithm support - All three algorithms (HS256, RS256, ES256) fully implemented

🚧 In Progress / Planned

  • Performance benchmarks - Systematic benchmarking suite
  • NuGet package publishing - Production-ready release

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


Status: ✅ Production Ready - All features complete

Version: 1.0.7

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 382 11/20/2025
1.1.6 380 11/20/2025
1.1.3 282 11/14/2025
1.1.2 243 11/14/2025
1.1.1 249 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 164 10/24/2025