DEV Community

sachin ruhil
sachin ruhil

Posted on

JWT Is Not Quantum-Safe — So I Built a Library That Is

I built @pq-jwt/core — a post-quantum JWT library using NIST FIPS 204 (ML-DSA) and FIPS 205 (SLH-DSA). Drop-in successor to RS256/ES256. Here's why it matters and how to use it.

Every Node.js app using jsonwebtoken with RS256 or ES256 has the same problem.
A sufficiently powerful quantum computer running Shor's algorithm can derive your private key from your public key — in polynomial time. That means every token your server has ever signed becomes forgeable. Not "hard to forge." Forgeable.
I spent the last few weeks building a fix: @pq-jwt/core — a post-quantum JWT library built on NIST-standardized algorithms.
bashnpm install @pq-jwt/core

The Problem With RS256 and ES256
Standard JWTs use one of these signing algorithms:

RS256 — RSA + SHA-256. Security basis: integer factorization.
ES256 — ECDSA + SHA-256. Security basis: elliptic curve discrete logarithm.

Both of these problems are efficiently solvable by a quantum computer running Shor's algorithm (1994). The math has been known for 30 years. The hardware is catching up.
In August 2024, NIST published three post-quantum cryptography standards:

FIPS 203 — ML-KEM (key encapsulation)
FIPS 204 — ML-DSA (digital signatures) ← what we need for JWTs
FIPS 205 — SLH-DSA (hash-based signatures)

These algorithms are designed to be secure against both classical and quantum computers. They are now official US federal standards. The NSA's CNSA 2.0 suite mandates them for all national security systems by 2030.
Nobody had built a developer-friendly JWT library around them. So I did.

What @pq-jwt/core Does
It replaces the signing algorithm in your JWT workflow. Everything else stays the same — same three-part token structure (header.payload.signature), same claims (iss, sub, aud, exp), same API shape.
import { generateKeyPair, sign, verify } from '@pq-jwt/core';

// Generate once, store securely
const { publicKey, secretKey } = generateKeyPair('ML-DSA-65');

// Sign (at login)
const token = sign(
{ sub: 'user_42', role: 'admin' },
secretKey,
{
algorithm: 'ML-DSA-65',
expiresIn: '1h',
issuer: 'auth.myapp.com',
audience: 'api.myapp.com',
}
);

// Verify (in middleware)
const { header, payload } = verify(token, publicKey, {
issuer: 'auth.myapp.com',
audience: 'api.myapp.com',
});

console.log(header.alg); // 'ML-DSA-65'
console.log(payload.sub); // 'user_42'
That's it. If you've used jsonwebtoken before, you already know how to use this.

Supported Algorithms
All four are NIST-standardized:
AlgorithmStandardQuantum SecurityBest For
ML-DSA-44 FIPS 20464-bit QIoT / constrained
ML-DSA-65 FIPS 20496-bit QGeneral use (recommended)
ML-DSA-87 FIPS 204128-bit QHigh security / government
SLH-DSA-SHA2-128s FIPS 20564-bit QConservative / hash-based
ML-DSA is CRYSTALS-Dilithium under its standardized name. SLH-DSA is SPHINCS+.

TypeScript Support
Full TypeScript types are included — no @types/ package needed:
typescript
import {
generateKeyPair,
sign,
verify,
type Algorithm,
type KeyPair,
type SignOptions,
TokenExpiredError,
SignatureError,
} from '@pq-jwt/core';

const alg: Algorithm = 'ML-DSA-65';
const kp: KeyPair = generateKeyPair(alg);

const opts: SignOptions = {
algorithm: 'ML-DSA-65',
expiresIn: '8h',
issuer: 'auth.myapp.com',
};

const token: string = sign({ userId: 'u1' }, kp.secretKey, opts);

try {
const { payload } = verify(token, kp.publicKey, { issuer: 'auth.myapp.com' });
console.log(payload.userId); // 'u1'
} catch (e) {
if (e instanceof TokenExpiredError) { /* 401 / }
if (e instanceof SignatureError) { /
403 */ }
}

Express.js Middleware Example
javascriptimport { verify, importKey, TokenExpiredError, SignatureError } from '@pq-jwt/core';

const PUBLIC_KEY = importKey(process.env.PQ_PUBLIC_KEY);

function pqAuth(req, res, next) {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer '))
return res.status(401).json({ error: 'Missing token' });

try {
const { payload } = verify(auth.slice(7), PUBLIC_KEY, {
issuer: 'auth.myapp.com',
audience: 'api.myapp.com',
});
req.user = payload;
next();
} catch (e) {
if (e instanceof TokenExpiredError) return res.status(401).json({ error: 'Token expired' });
if (e instanceof SignatureError) return res.status(403).json({ error: 'Invalid signature' });
res.status(400).json({ error: 'Bad token' });
}
}

app.get('/profile', pqAuth, (req, res) => res.json(req.user));

Why Shor's Algorithm Doesn't Break This
This is the key technical point.
Shor's algorithm exploits the periodic structure of cyclic group problems — integer rings (RSA) and elliptic curve groups (ECDSA). It finds the period of a function defined over those groups, which lets it factor numbers or solve discrete logs efficiently.
ML-DSA is based on Module-LWE (Learning With Errors) — a lattice problem. Lattice problems have no group-periodic structure. Shor's algorithm provides zero speedup against them. This was acknowledged in Shor's original 1994 paper.
Grover's algorithm does apply — it gives a square-root speedup against signature verification. That's why ML-DSA-65 is parameterized for 192-bit classical security: under Grover it reduces to 96-bit quantum security, which is still computationally infeasible.

Performance Numbers
Benchmarked on Node.js v22, single thread:

The trade-off is real. Tokens are larger (~4.5 KB vs ~0.5 KB), and signing is slower (~11 ms vs < 1 ms). For login flows and session tokens, this is imperceptible to users. For high-frequency per-request signing, you'd want to benchmark your specific workload.
SLH-DSA signing at 5+ seconds is only suitable for low-frequency operations like initial authentication — not per-request refresh.

What It's Built On
The cryptographic implementation is @noble/post-quantum by Paul Miller — zero native dependencies, independently audited, TypeScript-native. I built the JWT layer on top of it.
The cryptographic primitives themselves are not mine to invent. My job was to make them as easy to use as jsonwebtoken. That's the whole value of the library.

Security Properties (Tested)
From the test suite:

✓ 50/50 single-bit signature flips detected
✓ 20/20 payload substitution attempts caught
✓ Algorithm confusion (alg: none) rejected
✓ Cross-key attacks rejected
✓ 10,000 random forgery attempts — 0 succeeded
✓ 61/61 tests passing (TypeScript strict + runtime)

The Honest Limitations
I want to be direct about what this is and isn't.
What it is: A production-usable library for post-quantum JWT signing, built on NIST-standardized algorithms, with full TypeScript support, typed errors, and a clean **API.
**What it isn't
: A mathematically proven unbreakable system. No such thing exists in practical cryptography. The security of ML-DSA reduces to the hardness of Module-LWE, which has survived 15+ years of public cryptanalysis. The same category of hardness assumption underpins every cryptographic system deployed in production today.
What to never put in the payload: Passwords, private keys, sensitive data. The JWT payload is base64url-encoded, not encrypted. Anyone can decode it. Put user IDs, roles, and session metadata — not secrets. This is true of all JWTs, not just this library.

Getting Started
bash npm install @pq-jwt/core
Full working demo (Express + MongoDB + TypeScript):
https://github.com/pq-jwt/PQ-JWT-Demo
Source code:
https://github.com/pq-jwt/PQ-JWT
-- https://github.com/pq-jwt/PQ-JWT/blob/main/README.md
npm:
https://www.npmjs.com/package/@pq-jwt/core

If you're working on authentication infrastructure and want to contribute, issues and PRs are open at github.com/pq-jwt/PQ-JWT

Built by Sachin Ruhil. Published May 2026.

Top comments (0)