PostQuantum.Jwt
0.3.0-preview.1
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
<PackageReference Include="PostQuantum.Jwt" Version="0.3.0-preview.1" />
<PackageVersion Include="PostQuantum.Jwt" Version="0.3.0-preview.1" />
<PackageReference Include="PostQuantum.Jwt" />
paket add PostQuantum.Jwt --version 0.3.0-preview.1
#r "nuget: PostQuantum.Jwt, 0.3.0-preview.1"
#:package PostQuantum.Jwt@0.3.0-preview.1
#addin nuget:?package=PostQuantum.Jwt&version=0.3.0-preview.1&prerelease
#tool nuget:?package=PostQuantum.Jwt&version=0.3.0-preview.1&prerelease
PostQuantum.Jwt
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. ReadKNOWN-GAPS.mdbefore depending on this for anything that matters.
Table of contents
- Why
- What's new in 0.3.0-preview.1
- Install
- 60-second tour
- Usage
- Token format
- Public API at a glance
- Compared to System.IdentityModel.Tokens.Jwt
- Operational tradeoffs
- Security posture
- Compatibility
- Building from source
- Contributing
- License
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 ofAddJwtBearerfromMicrosoft.AspNetCore.Authentication.JwtBearer, so post-quantum tokens slot into the standard auth pipeline.PqJwtBearerHandler— fail-closedAuthenticationHandlerthat delegates toPqJwtValidator. BypassesMicrosoft.IdentityModel, which doesn't knowML-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-basedWithClaim(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 declareIsAotCompatible=true. - CycloneDX SBOM packed inside the
.nupkg.bom.jsonlives 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-requiredjob installs OpenSSL 3.5+ viaconda-forgeand 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_PASSWORDsecrets on thenuget-publishGitHub Environment triggerdotnet nuget signwith 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.3is wired in conditionally — pass-p:EnableBaselineValidation=trueonce 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 throwsArgumentExceptionif neitherSignatureVerificationKeynorSignatureKeyResolveris 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.Importnow parses the embedded ML-KEM-768 encapsulation key at ingestion. A length-correct but structurally invalid key fails withPqJwtExceptionon import rather than later insideXWing.Encapsulate. Consumers handling untrusted key input see a single exception boundary. - SBOM (CycloneDX). Every release now emits a
bom.jsoncovering the project's dependency graph, includes it inSHA256SUMS.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
nbfin the future, clock-skew tolerance bounds, multi-audience tokens,algconfusion ("none"substitution), missingalg, malformed JSON header, array-shaped payload, wrong content-encryption (A128GCMinstead ofA256GCM), missing/wrongctyon encrypted tokens, tampered ciphertext, decryption with the wrong private key, replay protection across encrypted tokens, custom-claim round-trips, claim removal viaWithClaim(name, null),XWingPrivateKeydispose semantics, length-correct-but-malformed X-Wing public keys, negativeClockSkewconfiguration, validator-without-key configuration, and concurrent registration inInMemoryReplayCache.Validator hardening. Encrypted tokens now require
cty: JWTon the outer header. The validator constructor refuses negativeClockSkewvalues and validators with no verification key. The decrypted plaintext buffer is zeroed alongside the shared secret. Malformed X-Wing public keys surface asPqJwtExceptionrather than leakingCryptographicExceptionfrom the BCL.Release transparency.
scripts/check-version-sync.shasserts the version is identical across.csproj, README, and CHANGELOG, and runs in CI on every push. The release workflow writes aSHA256SUMS.txtcovering the.nupkg,.snupkg, andbom.json, and emits GitHub build-provenance attestations for both the.nupkgand the SBOM — any consumer can rungh attestation verify <file> --repo systemslibrarian/postquantum-jwtto confirm an artifact came from this repo's release workflow. Release steps and trust signals are documented indocs/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.mdandKNOWN-GAPS.mdrefreshed to match the current state.Build hygiene. Build is zero warnings (was one CA1859 hint in 0.1).
EnablePackageValidationis on.LICENSEandCHANGELOG.mdare 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+publishwith 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 anddraft-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, missingalg, or analgwe don't expect — all throw. There is noalg: 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/encvalues 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:
- Run
dotnet buildanddotnet test— both must be green, with zero warnings (the build treats compiler warnings as errors). - Keep the discipline in
CLAUDE.md: honesty over polish, fail-closed always, no rolled-your-own crypto, native BCL first. - 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 | Versions 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. |
-
net10.0
- BouncyCastle.Cryptography (>= 2.6.2)
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.