HeroSD-JWT
1.0.5
dotnet add package HeroSD-JWT --version 1.0.5
NuGet\Install-Package HeroSD-JWT -Version 1.0.5
<PackageReference Include="HeroSD-JWT" Version="1.0.5" />
<PackageVersion Include="HeroSD-JWT" Version="1.0.5" />
<PackageReference Include="HeroSD-JWT" />
paket add HeroSD-JWT --version 1.0.5
#r "nuget: HeroSD-JWT, 1.0.5"
#:package HeroSD-JWT@1.0.5
#addin nuget:?package=HeroSD-JWT&version=1.0.5
#tool nuget:?package=HeroSD-JWT&version=1.0.5
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
✨ Simple API (Recommended)
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.FixedTimeEqualsfor digest validation to prevent timing attacks - Algorithm confusion prevention: Rejects "none" algorithm (both lowercase and uppercase)
- Cryptographically secure salts: Uses
RandomNumberGeneratorfor 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 nativeBase64UrlAPIs
- Note: .NET 8.0 includes a polyfill dependency (
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
Utf8JsonWriterfor 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,SdJwtVerifierinstances (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
_sdarrays - 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:
- Write tests first (TDD)
- Ensure all tests pass
- Follow .NET naming conventions
- Add XML documentation for public APIs
- No third-party dependencies (BCL only)
License
MIT License - see LICENSE file for details
References
- Specification: IETF draft-ietf-oauth-selective-disclosure-jwt
- Repository: https://github.com/KoalaFacts/HeroSD-JWT
- Quick Start: See specs/001-sd-jwt-library/quickstart.md
- Implementation Plan: See specs/001-sd-jwt-library/plan.md
Support
- Issues: Report bugs at https://github.com/KoalaFacts/HeroSD-JWT/issues
- Discussions: Community support via GitHub Discussions
Status: ✅ Production Ready - All features complete
Version: 1.0.0 (stable release)
| Product | Versions 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. |
-
net8.0
- Microsoft.Bcl.Memory (>= 9.0.10)
-
net9.0
- No dependencies.
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.