← All articles
SecurityJWTCryptographyJavaScript

Post-quantum JWT signing: ML-DSA is here

What RFC 9964 adds for JWS, when ML-DSA actually matters versus keeping ES256, and why post-quantum JWE encryption is further off than you think.

Axel Isouard

· 9 min read

Most JWT stacks use alg: "ES256" (or RSA) and that remains a reasonable default for ordinary short-lived tokens. ECDSA and RSA are small, fast, and everywhere. They were never designed to survive a quantum computer running Shor’s algorithm, which breaks the discrete logarithm and factoring problems behind them.

That does not mean you should abandon ES256 tomorrow. It means the industry now has a standardized PQ option for the cases where that future matters.

Two deadlines often get mixed up here. CNSA 2.0 is the NSA’s requirement for National Security Systems. It names exact parameter sets and phases in exclusive use between 2030 and 2033 depending on system category. For everyone else in US federal civilian space, NIST IR 8547 is the relevant document: it proposes deprecating quantum-vulnerable public-key algorithms after 2030 and disallowing them after 2035.

If you are not building an NSS, CNSA 2.0 is not your mandate. For most product JWTs, ES256 (or similar) stays appropriate for a long time.

This article walks through signing (JWS): the alg that proves an issuer minted the token. It is not about encrypting JWT payloads. Post-quantum JWE is not just unfinished. As of mid-2026, the JOSE working group’s ML-KEM draft dropped JWE from its scope entirely. More on that below.

A signed JWT is a header, a payload, and a signature. The signature covers the header and payload and gives you integrity and authenticity. Anyone with the token can still read the claims. Under a cryptographically relevant quantum computer, the JWT-specific risk for classical signatures is forgery: break the signing key, mint arbitrary tokens.

When PQ signing is worth knowing about

Keep ES256 (or your current classical alg) unless you have a concrete reason to add ML-DSA. Reach for post-quantum signing when:

  • Tokens must stay trustworthy for months or years (licenses, federation, offline capabilities), not just a short session
  • You are building a National Security System under CNSA 2.0 (ML-DSA-87, no exceptions)
  • Forging one long-lived token would be catastrophic and you need a PQ story
  • You sell identity or API gateway features and customers ask for PQ algs in RFPs

If your requirement is “attackers cannot read the payload”, that is JWE (or TLS / application encryption). ML-DSA does not help with confidentiality.

ML-DSA for JOSE

FIPS 204 defines ML-DSA (Dilithium). RFC 9964 (Proposed Standard, May 2026) registers three JWS alg values:

algNIST categoryNotes
ML-DSA-442Smallest signatures; usual first pick if you adopt ML-DSA
ML-DSA-653 (~192-bit)NIST’s general enterprise recommendation among the three
ML-DSA-875 (~256-bit)Required for CNSA 2.0 / NSS

If you do adopt ML-DSA, ML-DSA-44 is the usual starting point unless policy pushes you higher. CNSA 2.0 approves only ML-DSA-87. ML-DSA-44 and ML-DSA-65 are not compliant for National Security Systems even though both are FIPS-approved. NSA has also clarified that implementations built against pre-standard “CRYSTALS-Dilithium” drafts do not satisfy the requirement; you need FIPS 204 proper.

Keys use a new JWK type: kty: "AKP" with pub / priv. alg is required on the JWK. The private key is the 32-byte seed only (not FIPS 204’s expanded form). HashML-DSA is not registered, and the ctx parameter MUST be the empty string. Thumbprints (RFC 7638) hash alg, kty, and pub. Any JWKS consumer that might verify ML-DSA tokens needs to understand AKP.

SLH-DSA: watch, do not ship

SLH-DSA (SPHINCS+) is NIST-approved and still an IETF draft (draft-ietf-cose-sphincs-plus, revision 10 as of July 2026). It builds on the same AKP key type from RFC 9964. Two details make it a poor fit for JWTs specifically:

  • The draft registers only two parameter sets, both NIST Category 1: SLH-DSA-SHA2-128s and SLH-DSA-SHAKE-128s. Higher categories and the “fast” variants are deliberately left for future documents.
  • Signatures are 7,856 bytes, which the draft notes becomes 10,475 base64url characters in a JWS. That is a ten-kilobyte signature on a token whose claims might be a few hundred bytes.

The flip side is that SLH-DSA public keys are tiny: 32 bytes, versus 1,312 for ML-DSA-44. If you distribute verification keys via JWKS and rarely sign, that trade is interesting. For bearer tokens in an Authorization header it is not. The draft itself names firmware signing in embedded systems as the primary use case.

Also worth knowing: CNSA 2.0 excludes SLH-DSA for general signatures and specifies LMS or XMSS for firmware and software signing instead.

Encryption: further away than it looks

This changed recently and most write-ups have not caught up. draft-ietf-jose-pqc-kem used to be titled “PQ KEMs for JOSE and COSE.” As of revision 06 (July 2026) it was retitled “PQ KEMs for COSE” and its scope narrowed to COSE only. Every algorithm identifier it defines now lands in COSE registries, all still TBD, all marked Recommended: No. There is no longer a working-group draft registering ML-KEM directly as a JWE alg.

The live path to post-quantum JWE is HPKE: draft-ietf-jose-hpke-encrypt brings HPKE to JWE, and draft-ietf-hpke-pq plus draft-reddy-cose-jose-pqc-hybrid-hpke supply the post-quantum and PQ/T hybrid KEMs to plug into it. All drafts. jose can sign ML-DSA today; it has no post-quantum JWE algorithms and will not until this settles.

Need confidential claims now? Use classical JWE, or encrypt outside the JWT.

The size problem

JWTs carry base64url, so every raw byte below costs about a third more inside the actual token. Both numbers matter: the raw size is what your KMS and JWKS deal with, the encoded size is what your gateway sees.

algSignatureIn the tokenPublic key
ES25664 B86 chars64 B
ML-DSA-442,420 B3,227 chars1,312 B
ML-DSA-653,309 B4,412 chars1,952 B
ML-DSA-874,627 B6,170 chars2,592 B
SLH-DSA-*-128s7,856 B10,475 chars32 B

Public keys are raw key material: for ES256 that is the X‖Y coordinate pair, so you will see 65 bytes instead in SEC1 uncompressed form, which prefixes a 0x04 byte.

An ES256 JWT is often around 300 bytes on the wire. The same claims under ML-DSA-44 land around 3.3 KB, because the signature alone occupies 3,227 of those characters once encoded. That cost is the main reason not to treat PQ as a casual swap.

Where size bites is worth getting right, because the usual warning about nginx is misstated. nginx’s large_client_header_buffers defaults to 4 8k: four buffers of 8 KB. The buffers are not additive for a single header line. One Authorization header must fit inside one 8 KB buffer, or you get a 400. So the ceiling for a bearer token on stock nginx is 8 KB, not 32 KB and not 8 KB shared across everything. ML-DSA-44 clears it comfortably; ML-DSA-87 at ~6.5 KB including Bearer and the header/payload is close enough to be uncomfortable.

Another hop often forgotten: client_header_buffer_size defaults to just 1 KB (it falls back to the large buffers, but proxies vary), and a CDN, ALB, or ingress controller in front of nginx may reject the request before nginx ever sees it. If you ever introduce ML-DSA tokens, audit every hop, plus cookies and tracing headers sharing the budget. Prefer thumbprints over embedding public keys inline.

Try it in the browser

The demo below is for curiosity and sizing, not a migration checklist. It signs the same claims with ES256 (Web Crypto) and ML-DSA-44 / ML-DSA-87 via @noble/post-quantum. That library is pure JavaScript; no WASM, and no dependence on browser ML-DSA support.

Two caveats before you lift any of it. noble-post-quantum is self-audited rather than independently audited, and it offers no protection against side-channel attacks. Its authors are explicit that constant-timeness for post-quantum algorithms in JS is still an open research problem. That is fine for a browser toy that measures token sizes. It is not something to drop into a signing service. On Node 24.7+, production code would use jose for the same token shapes.

Same claims, three signatures. ES256 uses Web Crypto; ML-DSA uses @noble/post-quantum  because browsers (and jose in them) do not ship ML-DSA yet.

Signing with jose

panva/jose (v6.x) supports ML-DSA on Node.js ≥ 24.7.0 via Node’s native crypto. No polyfill on Node 20/22. Go and Java JWT libraries are still catching up. If you verify at the edge (Workers, etc.), check that runtime’s Web Crypto matrix before you commit. The ML-DSA support note is on the Node.js matrix specifically.

import * as jose from 'jose'

const { publicKey, privateKey } = await jose.generateKeyPair('ML-DSA-44')

const jwt = await new jose.SignJWT({ sub: 'user-123' })
  .setProtectedHeader({ alg: 'ML-DSA-44' })
  .setIssuedAt()
  .setIssuer('urn:example:issuer')
  .setExpirationTime('2h')
  .sign(privateKey)

const { payload } = await jose.jwtVerify(jwt, publicKey, {
  issuer: 'urn:example:issuer',
})

console.log(payload.sub, 'JWT length:', jwt.length)

Same API shape as ES256. Exported JWKs are kty: "AKP"; pass { extractable: true } if you need to persist keys. If you experiment, print jwt.length and run a sample through your real gateway so you know the size cost before anything else.

Conclusion

ES256 is still fine for typical short-lived JWTs. Do not treat ML-DSA as a drop-in upgrade you owe your users this year.

When you do need post-quantum JWS, ML-DSA-44 is the usual starting point, and ML-DSA-87 is required if and only if CNSA 2.0 applies. ML-DSA-65 is NIST’s general-purpose Category 3 pick among the three, not a CNSA answer for National Security Systems.

What is worth doing earlier than swapping algorithms: keep crypto-agility. Store alg with each key and verify by dispatching on protectedHeader.alg, so a future mixed ES256 / ML-DSA world is boring rather than a rewrite. Signing is standardized now. Encryption is not, and just moved further out.

If you want to poke at ML-DSA anyway, try it on Node 24.7+ behind a feature flag or in a spike, measure header size at every hop, and keep production on classical alg until you have a real reason to change.

Share article