HeroSD-JWT 1.0.5

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

HeroSD-JWT

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.

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
  • 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 and .NET 9.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:

// 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();
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();

🔧 Advanced API (Full Control)

For advanced scenarios, use the low-level API:

using HeroSdJwt.Issuance;
using HeroSdJwt.Common;

var issuer = new SdJwtIssuer();
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.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();
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)

Requirements

  • .NET 8.0 (LTS) or .NET 9.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

Contributing

Contributions are welcome! Please follow these guidelines:

  1. Write tests first (TDD)
  2. Ensure all tests pass
  3. Follow .NET naming conventions
  4. Add XML documentation for public APIs
  5. No third-party dependencies (BCL only)

License

MIT License - see LICENSE file for details

References

Support


Status: ✅ Production Ready - All features complete

Version: 1.0.0 (stable release)

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 was computed.  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.0.5 72 10/24/2025

Initial v1.0.0 release with full SD-JWT support including array elements, nested claims, key binding, and RS256/ES256 algorithms.