Skip to content

Security: systemslibrarian/postquantum-jwt

Security

SECURITY.md

Security Policy

PostQuantum.Jwt is a production-oriented preview (1.0.0-preview.N) for controlled issuer/verifier systems — environments where the same team owns both token issuing and token validation. "Production-oriented" describes the hardened defaults (strict validation, fail-closed behavior, replay and key-rotation support), not an audit sign-off: the construction has not been independently audited, and this is not a drop-in replacement for OAuth/OIDC/JWT middleware. The leading 1.0 denotes the maturity of the design and a stable public API/wire format across the preview.* series; the preview.N suffix marks the pending independent audit, not expected API churn (a security review could still force a change before the final 1.0.0). This document states the security model honestly so you can make an informed decision before relying on it.

Supported versions

Version Supported
1.0.0-preview.8 ✅ (latest preview)
1.0.0-preview.5preview.7 ❌ (superseded; security fixes ship in the latest preview only)
1.0.0-preview.4 ❌ (tagged, not published)
1.0.0-preview.3 ❌ (superseded)
0.3.0-preview.* ❌ (superseded)
0.2.0-preview.* ❌ (superseded)
0.1.0-preview.* ❌ (superseded)
anything older

During the 1.0.0-preview.* series only the most recent preview receives fixes.

Reporting a vulnerability

Please report security issues privately — do not open a public issue for an exploitable flaw.

  • Preferred: GitHub's "Report a vulnerability" (Security → Advisories) on the repository. This routes through GitHub's coordinated-disclosure flow, keeps the report private, and lets us request a CVE if one is warranted.
  • Alternative: email the maintainer listed on the GitHub profile.

Please include a description, affected version, and a reproduction if possible.

What we'll do

Step Target time Notes
Acknowledge receipt within 5 business days A human will reply, not a bot.
Initial triage (confirm / not-a-bug / out-of-scope) within 14 days We'll tell you which.
Fix in a private branch + draft advisory as fast as we can You're CC'd on the advisory.
Coordinated release (patched preview + advisory + CVE if applicable) typically within 60 days of triage Pre-disclosure embargo honoured; reporters are credited unless they decline.

As an unfunded preview project these are best-effort targets, not SLAs. We'll keep you informed if anything slips.

What counts as a security issue

✅ In scope ❌ Out of scope
Signature-bypass, signature-forgery, alg-confusion, or any way to make Validate return success on a token the legitimate signer did not produce Issues that the library already documents as caller-controlled (key storage, distributed replay cache, TLS, application authorization)
Decryption of an encrypted token without the recipient private key, or any AEAD bypass on the encrypted profile The lack of an independent audit (transparently documented; this is the gating concern, not a vulnerability per se)
A token that escapes Validate with an exception type other than PqJwtException / PqJwtValidationException (a fail-closed totality violation) The lack of formal constant-time guarantees beyond what the BCL + BouncyCastle provide (transparently documented in KNOWN-GAPS.md)
Information leak of the token, claim values, key material, or replay-cache contents via the optional metrics surface The single-process InMemoryReplayCache being inappropriate for multi-node deployments (documented; IPqJwtReplayCache is a contract for the caller)
Memory-zeroization gaps for key material Generic JOSE-library bugs in Microsoft.IdentityModel.Tokens or BouncyCastle.Cryptography (please report upstream)
Supply-chain issues affecting the PostQuantum.Jwt* packages Reports against unsupported versions (see "Supported versions" above)

If you're unsure whether something is in scope, report it anyway — we'd rather triage a non-issue than miss a real one.

Acknowledgements

PostQuantum.Jwt is an unfunded preview project with no bug bounty. Reporters who follow coordinated disclosure are credited by name (or pseudonym, your choice) in the published advisory and in CHANGELOG.md for the release that ships the fix. We're grateful for every report.

Threat model

Goals

  • Token integrity & authenticity via ML-DSA-65 signatures (FIPS 204).
  • Confidentiality (optional) via X-Wing key agreement + AES-256-GCM, where the AES key is the X-Wing shared secret.
  • Hybrid resilience. Confidentiality holds unless both X25519 and ML-KEM-768 are broken. This protects against both a future quantum adversary and an undiscovered weakness in the (newer) post-quantum primitive.
  • Fail-closed behavior. Every validation failure raises an exception. There is no unsigned path, no alg: none, and no algorithm downgrade.

Non-goals / out of scope

  • Key management & storage. Generating, protecting, rotating, and distributing keys is the caller's responsibility. The library supports kid-based key selection for rotation (SignatureKeyResolver) but does not store keys or fetch them remotely.
  • Replay protection enforcement. The library supports replay defense via IPqJwtReplayCache (with a bundled single-process InMemoryReplayCache) and will fail closed at validator construction when RequireReplayProtection = true. But with no cache configured, jti is carried and not enforced — providing and operating a suitable (distributed) cache is the application's job.
  • Side-channel resistance beyond the underlying primitives. We rely on the constant-time properties of the .NET BCL and BouncyCastle; we add no guarantees of our own.
  • Standards interoperability. ML-DSA-65 (RFC 9964) and A256GCM (RFC 7518) are registered JOSE identifiers, but the X-Wing key-management profile that combines them here is not a standardized JOSE/JWE profile. Tokens are not meant to validate or decrypt in generic JWT/JWE libraries.
  • OAuth / OIDC. This is not a replacement for OAuth/OpenID Connect or for ASP.NET Core's JWT bearer middleware.
  • Application authorization. Authenticating a token is not authorizing a request; enforcing scopes/roles/policies is the application's job.

Threats considered (the validator is built to reject these — see the fail-closed test suite below)

  • Token tampering (header, payload, or signature bytes modified).
  • Signature forgery without the signing private key.
  • Algorithm confusion, alg: none, missing alg, and unknown/unexpected alg.
  • Expired tokens, not-yet-valid (nbf) tokens beyond the allowed skew, and missing exp.
  • Wrong issuer / wrong audience.
  • Replay of a previously seen jti (when a replay cache is configured).
  • Modified ciphertext, authentication tag, AAD, or protected header on encrypted tokens; decryption with the wrong recipient key.
  • Malformed, truncated, or wrong-segment-count token input.

Threats not solved by the library alone (your deployment must address these)

  • Compromise of the signing private key, or of the issuing/validating server.
  • Weak application-level authorization logic.
  • A missing or misconfigured replay cache (e.g. a single-process cache used across multiple nodes).
  • Absence of TLS, poor secret storage, insider threats, or supply-chain compromise.
  • Client-side risks (XSS/CSRF, insecure token storage in the browser).

Required production controls (for a controlled issuer/verifier deployment)

  • TLS everywhere; never transmit tokens over plaintext channels.
  • Strong key storage (OS key store, cloud KMS, or vault), private keys encrypted at rest and never committed to source control.
  • Scheduled key rotation, with old verification keys retained only for the longest accepted token lifetime; rotate immediately on suspected compromise.
  • Issuer and audience validation enabled; short token lifetimes.
  • A distributed IPqJwtReplayCache whenever more than one node validates tokens, with RequireReplayProtection = true so a missing cache fails at startup.
  • Logs that never contain raw tokens, private keys, shared secrets, or decrypted sensitive claims.
  • Dependency scanning and a vulnerability-disclosure process (below).

See samples/HARDENING-CHECKLIST.md for a copy-pasteable production-readiness checklist and samples/SECURE-USAGE.md for the architecture around the token.

Cryptographic construction

Role Algorithm Source
Signature ML-DSA-65 .NET BCL (MLDsa)
KEM (PQ half) ML-KEM-768 .NET BCL (MLKem)
KEM (classical half) X25519 BouncyCastle
KEM combiner SHA3-256 BouncyCastle
Content encryption AES-256-GCM .NET BCL (AesGcm)

X-Wing combiner. The 32-byte shared secret is

SHA3-256( ss_ML-KEM || ss_X25519 || ct_X25519 || pk_X25519 || label )

where label is the six bytes 0x5C 0x2E 0x2F 0x2F 0x5E 0x5C (\.//^\) concatenated last, per draft-connolly-cfrg-xwing-kem. This shared secret is used directly as the AES-256-GCM key. The JWE protected header is bound as AES-GCM additional authenticated data (AAD).

Parser & protocol robustness

Most JWT vulnerabilities are protocol-orchestration and parser flaws, not breaks of the underlying mathematics. Two structural properties are what make this library's parsing watertight, and both are exercised by the fuzz and security-invariant test suites rather than merely asserted:

  • The signature covers the exact transmitted bytes. The ML-DSA-65 signing input is the ASCII string base64url(header) "." base64url(payload) — the literal encoded segments as they appear on the wire, not a re-serialization of the decoded objects. Any byte an attacker changes to exploit a header/payload parser differential (whitespace, key ordering, duplicate members, encoding tricks) changes the signing input and so fails verification. Combined with the ordering guarantee that the signature is verified before any claim is trusted (docs/SPEC.md steps 6→7, locked by SecurityInvariantsTests), a "firewall sees one token, app sees another" differential cannot forge an accepted token.

  • Canonical base64url — token strings are non-malleable. Decoding is strict (RFC 7515 §2): exactly one base64url string maps to a given byte sequence. Embedded whitespace and non-zero "slack" bits in a segment's final character (which a lenient decoder would silently accept, and which would let a different string decode to identical bytes and still verify/decrypt) are rejected with a fail-closed error. This anti-malleability property was added after PqJwtFuzzTests surfaced the lenient-decode behaviour.

  • AES-GCM parameters are pinned to the profile, not read from the token. The nonce must be exactly 12 bytes and the authentication tag exactly 16 bytes (128-bit); any other length is rejected before decryption. AesGcm itself accepts any 12–16 byte tag, so deriving the tag length from the token would let an attacker truncate it to 120/112/… bits — downgrading authentication strength and making the token malleable (a truncated tag still authenticates against its prefix). This was also surfaced by PqJwtFuzzTests.

The validator is also a total function over its input: every malformed, truncated, oversized, or adversarial token is funnelled into the documented fail-closed exception types (PqJwtValidationException / PqJwtException); no other exception escapes. PqJwtFuzzTests checks this over random strings, structurally token-shaped garbage, and structure-aware mutations of valid tokens.

Dependency rationale

The only third-party dependency is BouncyCastle.Cryptography, used exclusively for:

  1. X25519 — the classical half of X-Wing, which the .NET BCL does not ship.
  2. SHA3-256 — the X-Wing combiner hash, used via BouncyCastle for cross-platform consistency.

We deliberately did not hand-roll X25519. Rolling your own elliptic-curve arithmetic is exactly the kind of risk this project exists to avoid. ML-KEM-768 and ML-DSA-65 use the native, FIPS-validated BCL implementations.

Telemetry and data handling

The library performs no logging and no network I/O of its own, and collects no telemetry. The only signal it emits is an opt-in metric (the pqjwt.validations counter on the System.Diagnostics.Metrics meter PostQuantum.Jwt); nothing is recorded unless you attach a meter listener.

That metric is designed to be safe to export: its only tags are outcome (success/failure) and a coarse, closed-vocabulary reason derived from the typed PqJwtFailureReason. It never includes the token, claim values, jti, issuer/audience values, or any key material. Validation failures surfaced through the optional ASP.NET Core handler log the exception (its message and reason), never the token or key bytes — keep Authorization/Cookie headers out of your own request logs (see samples/SECURE-USAGE.md §8).

Honesty statement

This is preview cryptographic software written in the open. It has not been audited. The X-Wing key-generation and decapsulation/combiner paths are validated against the official known-answer vectors; the encapsulation path is not (the native ML-KEM API is randomized — see KNOWN-GAPS.md). Known limitations are tracked transparently there. Until a stable 1.0.0 and an external review, treat the lack of an independent audit as the gating concern: this library is appropriate for controlled issuer/verifier systems whose owners accept that risk with eyes open — not for high-risk deployments, public-facing auth, or anywhere generic JWT/JWE interoperability is required.

The fail-closed contract is locked in by the library test suite (176 tests in the default dotnet test run, plus an opt-in constant-time timing-distribution probe via --filter Category=Timing), including explicit checks for alg: none substitution, missing alg, header JSON corruption, payload that is not a JSON object, wrong content-encryption (A128GCM instead of A256GCM), tampered ciphertext, decryption with a different recipient key, replay across encrypted tokens, and nbf/exp skew boundaries. If a future change weakens any of these, the suite goes red. See docs/TESTING.md for the per-layer test pyramid and docs/SUPPLY-CHAIN.md for how to verify what you installed.


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

There aren't any published security advisories