PostQuantum.Jwt 0.3.0-preview.1

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

PostQuantum.Jwt

NuGet Downloads CI .NET License

Post-quantum hybrid JWTs for .NET 10. Signs with ML-DSA-65 (FIPS 204). Optionally encrypts with X-Wing (X25519 + ML-KEM-768) and AES-256-GCM. Built on the native .NET BCL post-quantum primitives. Fail-closed by design, small-surface, and honest about what it is.

Status — 0.3.0-preview.1. Preview software. Not for production use. The API may change before 1.0. The cryptographic construction has not been independently audited. Read KNOWN-GAPS.md before depending on this for anything that matters.


Table of contents


Why

A cryptographically relevant quantum computer would break the elliptic-curve math behind today's JWT signatures (EdDSA, ECDSA, RSA). Pure post-quantum schemes are new and comparatively under-attacked. Hybrid hedges both at once:

  • Signatures — ML-DSA-65. NIST-standardized lattice signature, FIPS 204, security category 3.
  • Key agreement — X-Wing. The IETF hybrid KEM combining the battle-tested X25519 with ML-KEM-768 (FIPS 203), bound together by a SHA3-256 combiner. An attacker must break both to recover the key.

If either half stands, your token stands. That is the whole point.


What's new in 0.3.0-preview.1

A real-world adoption release. v0.2 made the existing surface trustworthy; v0.3 makes it pleasant to wire into a real ASP.NET Core 10 app, makes it AOT-friendly, and adds the supply-chain signals a production-grade crypto package needs. Changes are stacked newest-first.

New in v0.3.0-preview.1

  • New companion package PostQuantum.Jwt.AspNetCore.
    • services.AddAuthentication().AddPqJwtBearer(...) — mirrors the shape of AddJwtBearer from Microsoft.AspNetCore.Authentication.JwtBearer, so post-quantum tokens slot into the standard auth pipeline.
    • PqJwtBearerHandler — fail-closed AuthenticationHandler that delegates to PqJwtValidator. Bypasses Microsoft.IdentityModel, which doesn't know ML-DSA-65.
    • IPqJwtKeyRing + HttpPqJwtKeyRing — JWKS-equivalent: fetch a key directory from a trusted HTTPS endpoint with configurable refresh, in-memory cache, AOT-safe (source-gen JSON), single-suite enforcement.
  • AOT/trim-safe API path. New WithClaim<T>(name, value, JsonTypeInfo<T>) overload alongside the existing reflection-based WithClaim(name, object?). The reflection overload carries [RequiresUnreferencedCode] and [RequiresDynamicCode] so AOT publishers see one targeted warning; primitive setters (WithIssuer, WithSubject, etc.) bypass reflection internally and stay trim-safe. Both packages declare IsAotCompatible=true.
  • CycloneDX SBOM packed inside the .nupkg. bom.json lives at the root of the package so consumers can inspect the dependency graph directly from nuget.org.
  • Property-based tests via FsCheck.Xunit (Base64Url involutive round-trip, signature-tamper invariance, etc.). Total: 68 tests, zero skips on PQ-capable hosts.
  • Linux PQ-required CI lane. New linux-pq-required job installs OpenSSL 3.5+ via conda-forge and fails the run on any skipped test — joining the Windows lane in proving the ML-KEM / ML-DSA / X-Wing paths actually executed on every push, on both platforms.
  • Release workflow author-signing hook. Optional NUGET_SIGNING_CERT + NUGET_SIGNING_CERT_PASSWORD secrets on the nuget-publish GitHub Environment trigger dotnet nuget sign with a DigiCert timestamp before push. Absent secrets log a notice and skip signing — the package still ships under nuget.org's repository signature.
  • API baseline infrastructure. PackageValidationBaselineVersion=0.2.0-preview.3 is wired in conditionally — pass -p:EnableBaselineValidation=true once the baseline is published to nuget.org and future versions are checked for accidental API breaks against it.

New in v0.2.0-preview.3 (the previous release line, kept for reference)

  • Fail-fast misconfiguration. PqJwtValidator's constructor now throws ArgumentException if neither SignatureVerificationKey nor SignatureKeyResolver is configured — a security validator without a way to obtain a verification key is misconfigured by definition, and that should surface before the first token arrives, not after.
  • Eager X-Wing public-key validation. XWingPublicKey.Import now parses the embedded ML-KEM-768 encapsulation key at ingestion. A length-correct but structurally invalid key fails with PqJwtException on import rather than later inside XWing.Encapsulate. Consumers handling untrusted key input see a single exception boundary.
  • SBOM (CycloneDX). Every release now emits a bom.json covering the project's dependency graph, includes it in SHA256SUMS.txt, and issues a separate GitHub build-provenance attestation for it. The SBOM travels with the GitHub release artifacts rather than packed inside the .nupkg.

The 0.1 → 0.2 delta, cumulative through preview.2

  • Test coverage more than doubled (27 → 57 tests, zero skips on PQ-capable hosts). New fail-closed locks for nbf in the future, clock-skew tolerance bounds, multi-audience tokens, alg confusion ("none" substitution), missing alg, malformed JSON header, array-shaped payload, wrong content-encryption (A128GCM instead of A256GCM), missing/wrong cty on encrypted tokens, tampered ciphertext, decryption with the wrong private key, replay protection across encrypted tokens, custom-claim round-trips, claim removal via WithClaim(name, null), XWingPrivateKey dispose semantics, length-correct-but-malformed X-Wing public keys, negative ClockSkew configuration, validator-without-key configuration, and concurrent registration in InMemoryReplayCache.

  • Validator hardening. Encrypted tokens now require cty: JWT on the outer header. The validator constructor refuses negative ClockSkew values and validators with no verification key. The decrypted plaintext buffer is zeroed alongside the shared secret. Malformed X-Wing public keys surface as PqJwtException rather than leaking CryptographicException from the BCL.

  • Release transparency. scripts/check-version-sync.sh asserts the version is identical across .csproj, README, and CHANGELOG, and runs in CI on every push. The release workflow writes a SHA256SUMS.txt covering the .nupkg, .snupkg, and bom.json, and emits GitHub build-provenance attestations for both the .nupkg and the SBOM — any consumer can run gh attestation verify <file> --repo systemslibrarian/postquantum-jwt to confirm an artifact came from this repo's release workflow. Release steps and trust signals are documented in docs/RELEASE.md.

  • Windows CI is now the PQ-required lane. It fails the run if any test reports skipped, so the ML-KEM / ML-DSA / X-Wing paths are proven to run in CI on every push, rather than relying on local verification alone. Linux remains the portability lane.

  • Documentation overhaul. Rewritten README with a 60-second tour, a direct comparison vs. System.IdentityModel.Tokens.Jwt, and a clearer security posture. SECURITY.md and KNOWN-GAPS.md refreshed to match the current state.

  • Build hygiene. Build is zero warnings (was one CA1859 hint in 0.1). EnablePackageValidation is on. LICENSE and CHANGELOG.md are packed alongside the README so consumers see them in the package details on nuget.org.

  • CI hardening. Workflows now run on actions versions that support Node.js 24, and the release pipeline is split into pack + publish with a GitHub Environment gate (nuget-publish) so publishing requires explicit manual approval.

  • Docs fix. Corrected the X-Wing combiner formula in SECURITY.md — the label is concatenated last, matching the code and draft-connolly-cfrg-xwing-kem.

Full notes in CHANGELOG.md.


Install

dotnet add package PostQuantum.Jwt --version 0.3.0-preview.1

Or in a .csproj:

<PackageReference Include="PostQuantum.Jwt" Version="0.3.0-preview.1" />

Runtime requirement: the native ML-KEM / ML-DSA primitives need an OpenSSL build that exposes them — OpenSSL 3.5 or later on Linux, or a recent Windows. PostQuantum.Jwt fails closed with a clear error where they are unavailable rather than silently falling back to weaker crypto.


60-second tour

using System.Security.Cryptography;
using PostQuantum.Jwt;

using var signingKey      = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);
using var verificationKey = MLDsa.ImportMLDsaPublicKey(
    MLDsaAlgorithm.MLDsa65, signingKey.ExportMLDsaPublicKey());

string token = new PqJwtBuilder()
    .WithSubject("user-123")
    .WithLifetime(TimeSpan.FromMinutes(30))
    .SignWith(signingKey)
    .Build();

var result = new PqJwtValidator(new PqJwtValidationParameters
{
    SignatureVerificationKey = verificationKey,
}).Validate(token);

Console.WriteLine(result.Subject); // user-123

That's it: sign, validate. Anything wrong with the token — bad signature, tampering, expiry, claim mismatch — throws PqJwtValidationException. There is no "best-effort" result.


Usage

Sign and validate

using System.Security.Cryptography;
using PostQuantum.Jwt;

using var signingKey = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);

string token = new PqJwtBuilder()
    .WithIssuer("https://issuer.example")
    .WithSubject("user-123")
    .WithAudience("https://api.example")
    .WithLifetime(TimeSpan.FromMinutes(30))
    .WithClaim("role", "admin")
    .SignWith(signingKey)
    .Build();

using var verificationKey = MLDsa.ImportMLDsaPublicKey(
    MLDsaAlgorithm.MLDsa65, signingKey.ExportMLDsaPublicKey());

var validator = new PqJwtValidator(new PqJwtValidationParameters
{
    SignatureVerificationKey = verificationKey,
    ValidIssuer   = "https://issuer.example",
    ValidAudience = "https://api.example",
});

PqJwtValidationResult result = validator.Validate(token);
Console.WriteLine(result.Subject);            // user-123
Console.WriteLine(result.GetString("role"));  // admin

Sign and encrypt

When the payload is confidential, hand the builder a recipient's X-Wing public key. The token is signed first, then encrypted ("sign-then-encrypt").

using PostQuantum.Jwt.Cryptography;

// Recipient generates a key pair and publishes the public half.
using var recipient = XWingPrivateKey.Generate();
byte[] recipientPublic = recipient.PublicKey.Export();   // share this

string token = new PqJwtBuilder()
    .WithSubject("confidential-subject")
    .WithLifetime(TimeSpan.FromMinutes(5))
    .SignWith(signingKey)
    .EncryptFor(XWingPublicKey.Import(recipientPublic))
    .Build();

var validator = new PqJwtValidator(new PqJwtValidationParameters
{
    SignatureVerificationKey = verificationKey,
    DecryptionKey            = recipient,   // required for encrypted tokens
});

PqJwtValidationResult result = validator.Validate(token);
Console.WriteLine(result.WasEncrypted);  // True

Key rotation and replay protection

Tag a signature with a kid and resolve it at validation time, and reject replayed tokens with a jti cache:

string token = new PqJwtBuilder()
    .WithKeyId("signing-key-2026")
    .WithJwtId(Guid.NewGuid().ToString("N"))
    .WithLifetime(TimeSpan.FromMinutes(5))
    .SignWith(signingKey)
    .Build();

var validator = new PqJwtValidator(new PqJwtValidationParameters
{
    // Pick a verification key from the token's kid (key rotation).
    SignatureKeyResolver = kid => keyRing.TryGetValue(kid, out var k) ? k : null,
    // Reject any jti seen before. InMemoryReplayCache is single-process;
    // implement IPqJwtReplayCache over a shared store for multi-node setups.
    ReplayCache = new InMemoryReplayCache(),
});

An unknown kid, a missing jti, or a replayed jti all fail closed.

ASP.NET Core integration

Install the companion package and call AddPqJwtBearer(...) on the standard AuthenticationBuilder — the same shape as AddJwtBearer from Microsoft.AspNetCore.Authentication.JwtBearer, but routing through PqJwtValidator instead of the IdentityModel handler that can't speak ML-DSA-65.

dotnet add package PostQuantum.Jwt.AspNetCore --version 0.3.0-preview.1
using System.Security.Cryptography;
using PostQuantum.Jwt;
using PostQuantum.Jwt.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuthentication(PqJwtBearerDefaults.AuthenticationScheme)
    .AddPqJwtBearer(options =>
    {
        var keyBytes = Convert.FromBase64String(
            builder.Configuration["Auth:VerificationKey"]
                ?? throw new InvalidOperationException("Missing Auth:VerificationKey"));
        options.ValidationParameters = new PqJwtValidationParameters
        {
            SignatureVerificationKey = MLDsa.ImportMLDsaPublicKey(
                MLDsaAlgorithm.MLDsa65, keyBytes),
            ValidIssuer   = builder.Configuration["Auth:Issuer"],
            ValidAudience = builder.Configuration["Auth:Audience"],
            // Single-process replay defense. Swap to a Redis-backed
            // IPqJwtReplayCache for a horizontally scaled deployment.
            ReplayCache   = new InMemoryReplayCache(),
        };
    });
builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/me", (HttpContext ctx) => new
{
    sub  = ctx.User.FindFirst("sub")?.Value,
    role = ctx.User.FindFirst("role")?.Value,
}).RequireAuthorization();

app.Run();

That's the whole integration. The handler is fail-closed by construction (tampered / expired / wrong-issuer tokens produce AuthenticateResult.Fail), RequireAuthorization() returns 401 to unauthenticated callers, and standard [Authorize(Roles = "...")] attributes work against the "role" claim by default.

Key rotation across services. Use HttpPqJwtKeyRing to fetch verification keys from a trusted HTTPS endpoint (the post-quantum analogue of JWKS):

builder.Services.AddHttpClient<HttpPqJwtKeyRing>();
builder.Services.AddSingleton(sp =>
{
    var http = sp.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(HttpPqJwtKeyRing));
    return new HttpPqJwtKeyRing(http, new Uri(builder.Configuration["Auth:KeysEndpoint"]!));
});

builder.Services
    .AddAuthentication(PqJwtBearerDefaults.AuthenticationScheme)
    .AddPqJwtBearer(options =>
    {
        options.ValidationParameters = new PqJwtValidationParameters
        {
            // Resolved per token from the token's `kid` header.
            SignatureKeyResolver = kid =>
                builder.Services.BuildServiceProvider()
                    .GetRequiredService<HttpPqJwtKeyRing>()
                    .Resolve(kid),
            ValidIssuer   = builder.Configuration["Auth:Issuer"],
            ValidAudience = builder.Configuration["Auth:Audience"],
        };
    });

The expected key-directory document is JSON: { "keys": [ { "kid": "...", "alg": "ML-DSA-65", "key": "<base64>" }, ... ] }. Entries with any other alg are ignored — the single-suite policy holds across services.

Don't AddJwtBearer alongside this. The standard handler will try to parse the token's alg and fail. Either use AddPqJwtBearer as your only bearer auth, or restrict each scheme to specific routes with [Authorize(AuthenticationSchemes = ...)].


Token format

PostQuantum.Jwt uses JOSE-style compact serialization:

Form Segments Header alg / enc
Signed 3 ML-DSA-65
Encrypted 5 X-Wing / A256GCM (nested JWT)

These algorithm identifiers are not registered with IANA — see Security posture.

Full wire-format and combiner details are in docs/design.md.


Public API at a glance

Type Purpose
PqJwtBuilder Fluent builder for signed (3-part) or signed-then-encrypted (5-part) tokens.
PqJwtValidator Fail-closed validator. Thread-safe and reusable.
PqJwtValidationParameters Validation configuration: keys, issuer/audience, lifetime, replay cache.
PqJwtValidationResult The validated claims; only returned when every check passed.
PqJwtAlgorithms Canonical alg/enc identifiers (e.g. ML-DSA-65, X-Wing, A256GCM).
PqJwtException Misconfiguration / usage error.
PqJwtValidationException Token failed validation (subclass of PqJwtException).
IPqJwtReplayCache Optional jti replay-detection hook.
InMemoryReplayCache Default single-process replay cache (use a distributed store in clusters).
XWingPrivateKey / …PublicKey X-Wing hybrid KEM keys; Generate(), Import(), Export().

Compared to System.IdentityModel.Tokens.Jwt

System.IdentityModel.Tokens.Jwt (and the wider Microsoft.IdentityModel.* family) is the right choice for the vast majority of JWT work today: it speaks the IANA JOSE algorithms, interops with the entire OAuth / OpenID Connect ecosystem, and has been hardened over a decade of production use. Use it unless you have a specific reason not to.

PostQuantum.Jwt is a focused, deliberately non-interoperable tool for one problem: hybrid post-quantum JWTs. The trade-offs:

Concern System.IdentityModel.Tokens.Jwt PostQuantum.Jwt
Algorithms RS256/384/512, PS256/384/512, ES256/384/512, EdDSA, HS256/384/512, etc. One suite only: ML-DSA-65 for signatures, X-Wing + AES-256-GCM for encryption.
Quantum resistance None of the standard algorithms are quantum-resistant. Hybrid: classical and post-quantum, both must fall.
Algorithm agility Yes (and historically the source of alg: none, RS/HS confusion, and downgrade attacks). No, by design. The validator does not trust the token's alg to choose a path; it accepts exactly one. See docs/adr/0001-algorithm-agility.md.
Standards interop Fully IANA-registered identifiers; tokens validate in every JWT library. Identifiers (ML-DSA-65, X-Wing) are not IANA-registered. Tokens will not validate in generic JWT tooling.
alg: none Historically supported (and disastrous); now disabled by default. Impossible. No unsigned path exists in the code.
Default exp enforcement Configurable; default depends on the consumer (TokenValidationParameters). Required by default. A token without an exp claim is rejected.
Encryption JWE with many supported alg/enc combos. Sign-then-encrypt only; X-Wing (X25519 + ML-KEM-768) → AES-256-GCM. One recipient per token.
Replay defense Not built-in. Built-in IPqJwtReplayCache + InMemoryReplayCache, opt-in via configuration.
OAuth / OIDC integration First-class (Microsoft.AspNetCore.Authentication.JwtBearer, JWKS, etc.). None. You wire the validator into your pipeline yourself.
External audit Yes — widely deployed and reviewed. No. Preview, not audited.
Dependencies A family of Microsoft.IdentityModel.* packages. Native .NET BCL + one package (BouncyCastle.Cryptography) for X25519 + SHA3-256.
Target framework Multi-target (netstandard2.0 through net10). net10.0 only.

Use System.IdentityModel.Tokens.Jwt if you need OAuth/OIDC interop, JWKS, multi-algorithm agility, or any standards-conformant JWT.

Use PostQuantum.Jwt if you specifically want hybrid post-quantum tokens now, you control both the issuer and the verifier, and you accept that your tokens won't validate in any other ecosystem until IANA registers these identifiers and standard libraries catch up.


Operational tradeoffs

Honest, decision-useful notes for the moment you're deciding whether to wire this in.

Token size. A plain HS256 JWT is ~200 bytes. A signed PostQuantum.Jwt token is ~4.5 KB: ML-DSA-65 signatures are 3,309 bytes (vs. 32–64 for HMAC/EdDSA), and that's after base64url encoding. A sign-then-encrypt token adds another ~1.5 KB (1,120-byte X-Wing ciphertext + 12-byte nonce + 16-byte AES-GCM tag, base64url-encoded). Plan for ~5 KB signed, ~6.5 KB encrypted. This matters if you put tokens in cookies, query strings, or constrained headers — for most Authorization: Bearer flows it's fine, for cookies it likely is not.

When to reach for encryption. The sign-then-encrypt form is the right choice only when the claims themselves are confidential (PII, account IDs you don't want a leaked log holding). For the more common case — opaque session references, role/scope strings — a signed-only token is correct: the signature already prevents forgery, encryption just trades cost for secrecy you may not need.

Replay protection in a cluster. InMemoryReplayCache works for a single process and is fine for a development server or a single-instance worker. The moment you scale horizontally, jti-based replay defense requires a shared store — implement IPqJwtReplayCache over Redis, a database table, or whichever cache the rest of your stack already uses. Until then a token "replayed" on a different node is not detected.

Key rotation. SignatureKeyResolver selects a verification key from the token's kid header. It does not fetch keys — there is no JWKS endpoint or remote-discovery story. Your application is responsible for the key ring; this library is just disciplined about asking for the right key when validating.

"Preview, not for production" — what that means operationally. The wire format and public API are not stable yet. If you ship 0.3.0-preview.1 in a service and a future release bumps to 0.3.0 with a wire-format change, you will be re-signing every active token (and possibly running a flag-day migration). For an internal service you control end-to-end this is manageable. For a public API where third parties hold issued tokens, treat this as a blocker until 1.0.


Security posture

We aim to be honest about exactly what this library does and does not give you.

What you get

  • Hybrid by construction. Encryption stays secure unless both X25519 and ML-KEM-768 fall; signatures rest on ML-DSA-65.
  • Native post-quantum primitives. ML-KEM-768 and ML-DSA-65 are the .NET BCL implementations, not a re-implementation.
  • Fail-closed validation. Bad signature, tampered ciphertext, expired or not-yet-valid token, wrong issuer/audience, missing exp, missing alg, or an alg we don't expect — all throw. There is no alg: none, no unsigned path, and no silent downgrade.
  • Strict, small-surface defaults. Expiration is required, clock skew is a modest 60 seconds, and only the exact post-quantum algorithms are accepted.

What you must know

  • One dependency — BouncyCastle — and why. The .NET BCL does not ship X25519, the classical half of X-Wing. Rather than hand-roll elliptic-curve code, we use BouncyCastle's vetted X25519 (and its SHA3-256 for the X-Wing combiner). ML-KEM-768 and ML-DSA-65 remain on the native BCL. This trade-off is deliberate: we will not roll our own curve arithmetic.
  • Not audited. No third party has reviewed this construction. X-Wing key generation and the decapsulation/combiner path are validated against the official IETF known-answer vectors; the encapsulation path is not (the native ML-KEM API is randomized). See KNOWN-GAPS.md.
  • Non-standard identifiers. The alg/enc values describe a scheme the IANA JOSE registry does not cover, so these tokens are intentionally not interoperable with generic JWT tooling.
  • Preview. Treat the API and wire format as unstable until 1.0.

Full detail lives in SECURITY.md and KNOWN-GAPS.md. To report a vulnerability, see SECURITY.md.


Compatibility

Surface Supported
Target framework net10.0
Languages C# 13 (any CLS-consuming language; the assembly is [CLSCompliant(false)] because the public surface exposes raw byte[] key material).
Operating system Windows, Linux, macOS — anywhere .NET 10 + an OpenSSL build that exposes ML-KEM / ML-DSA runs. On Linux that's OpenSSL 3.5 or later.
AOT / trimming Not yet validated. The library uses System.Text.Json reflection paths; expect to need source-generated contexts before publishing AOT.

Building from source

dotnet build
dotnet test

Tests that exercise the native post-quantum primitives skip themselves (with a clear reason) on hosts that lack ML-KEM / ML-DSA support, and run fully where OpenSSL 3.5+ is present.

If you're on a Linux box whose system OpenSSL predates 3.5, point the runtime at a newer one:

LD_LIBRARY_PATH=/path/to/openssl-3.5/lib dotnet test

The full suite is 68 tests, zero skips on a Windows 11 / .NET 10 host with native ML-KEM and ML-DSA support. Both the Windows and Linux CI lanes fail the run if any test skips.


Contributing

Issues and pull requests are welcome. Before opening a PR:

  1. Run dotnet build and dotnet test — both must be green, with zero warnings (the build treats compiler warnings as errors).
  2. Keep the discipline in CLAUDE.md: honesty over polish, fail-closed always, no rolled-your-own crypto, native BCL first.
  3. Security-sensitive changes should land alongside a test that locks in the fail-closed behavior.

Cutting a release is documented in docs/RELEASE.md. It enumerates exactly what CI enforces, what humans review, and what provenance signals each release carries — and is honest about what is still missing (author code signing, SBOM).

Reporting a vulnerability: please do not open a public issue. Use GitHub's Report a vulnerability button on the repository, or follow the process in SECURITY.md.


License

MIT.


To God be the glory — 1 Corinthians 10:31.

Product Compatible and additional computed target framework versions.
.NET 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 (3)

Showing the top 3 NuGet packages that depend on PostQuantum.Jwt:

Package Downloads
PostQuantum.Jwt.AspNetCore

[SUPERSEDED — use PostQuantum.AspNetCore instead.] ASP.NET Core authentication handler for PostQuantum.Jwt — adds AddPqJwtBearer() so post-quantum (ML-DSA-65) tokens slot into the standard authentication pipeline. Includes an HTTP-fetched key ring (a JWKS-equivalent) for kid-based rotation across services. Fail-closed by construction. This package has been replaced by PostQuantum.AspNetCore (cleaner naming, dedicated release cadence, event-hook surface, hosted-service warmup, SignalR support). The new package is wire-compatible — tokens minted under either validate in the other. Migration guide: https://github.com/systemslibrarian/postquantum-aspnetcore/blob/main/docs/MIGRATION.md. Production-oriented preview companion package for controlled issuer/verifier systems; not independently audited and not a drop-in OAuth/OIDC replacement.

PostQuantum.AspNetCore

The high-level ASP.NET Core integration for post-quantum JWT authentication. Add one line — AddPostQuantumJwtBearer() — and hybrid ML-DSA-65 + X-Wing tokens authenticate through the standard ASP.NET Core pipeline exactly the way AddJwtBearer always has: same AuthenticationBuilder, same [Authorize], same ClaimsPrincipal. Fail-closed by construction, AOT-compatible, observable via System.Diagnostics (Metrics + ActivitySource). This is the application-layer wiring (handler, options, four event hooks, JWKS-equivalent key ring, hosted-service warmup) — NOT a cryptography library. The actual ML-DSA / X-Wing / AES-GCM work happens in PostQuantum.Jwt and the .NET 10 BCL primitives; we sit on top of them. Distributed single-use-jti enforcement available via the companion PostQuantum.AspNetCore.RedisReplayCache package. Production-quality integration for controlled issuer/verifier systems — environments where the same team owns both token issuing and validation. NOT independently audited (a permanent, documented limitation of the underlying construction — see KNOWN-GAPS.md) and not a drop-in OAuth/OIDC replacement; tokens use non-IANA algorithm identifiers and do not validate in generic JWT tooling.

PostQuantum.Identity

Post-quantum integration for ASP.NET Core Identity, with two surfaces at deliberately different scopes. (1) Argon2id password hashing for every deployment — PHC-formatted, secure-by-default work factors, transparent rehash-on-login, one-line PBKDF2→Argon2id migration via AddArgon2idPasswordHasherWithMigration, fail-closed verification with bounded parse cost, RFC 9106 §5.3 Known Answer Tests + reference-CLI PHC interop + emitter wire-format pin + deterministic generative fuzz corpus. Runs on net8.0 / net9.0 / net10.0. (2) Hybrid post-quantum JWT issuance for owned/trusted ecosystems (you control both the issuer and every verifier) — ML-DSA-65 signatures (FIPS 204) with optional X-Wing (X25519 + ML-KEM-768) encryption, kid-based key rotation, JSON-source-gen claim path, built on PostQuantum.Jwt 1.0.0 (stable). Uses non-IANA algorithm identifiers; intentionally not interoperable with generic JWT tooling until the IETF JOSE PQC drafts land. Requires .NET 10 (and OpenSSL 3.5+, 3.x series, on Linux). Supply chain: embedded CycloneDX SBOM, GitHub Sigstore-signed build-provenance attestation (verifiable with gh attestation verify), NuGet Trusted Publishing, deterministic SourceLink builds, .snupkg symbols, CodeQL, Dependabot. 1.0 is a semver API-stability commitment — NOT an audit claim; the library is not independently audited, stated plainly in SECURITY.md. See https://github.com/systemslibrarian/postquantum-identity for the full stance, migration guide, quantum-readiness playbook, and runnable issuer/verifier/key-tool samples.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 356 6/30/2026
1.0.0-preview.10 94 6/6/2026
1.0.0-preview.9 79 6/6/2026
1.0.0-preview.8 73 6/5/2026
1.0.0-preview.7 70 6/4/2026
1.0.0-preview.6 75 6/4/2026
1.0.0-preview.5 74 6/3/2026
1.0.0-preview.3 78 6/2/2026
1.0.0-preview.2 83 6/2/2026
1.0.0-preview.1 430 6/2/2026
0.3.0-preview.1 301 5/30/2026

0.3.0-preview.1: Real-world adoption release. New companion package PostQuantum.Jwt.AspNetCore brings AddPqJwtBearer(...) and an HTTP-fetched key ring so post-quantum JWT validation slots into the standard ASP.NET Core auth pipeline. AOT/trim-safe annotations on the reflection-using WithClaim(object?) overload plus an AOT-safe WithClaim<T>(JsonTypeInfo<T>) overload. CycloneDX SBOM now packed inside the .nupkg. Property-based tests on top of the deterministic suite (68 tests total). Containerised Linux PQ-required CI lane (OpenSSL 3.5+ via conda-forge) joins the Windows lane in proving the cryptographic paths run in CI. Release workflow has an optional author-signing hook (NUGET_SIGNING_CERT). Full notes: https://github.com/systemslibrarian/postquantum-jwt/blob/main/CHANGELOG.md — known limitations: https://github.com/systemslibrarian/postquantum-jwt/blob/main/KNOWN-GAPS.md.