Auth1BlogPost-Quantum Authentication
Cryptography Security · 15 min read

Post-Quantum Authentication:
Why Your JWTs Aren't Quantum-Safe

NIST finalized FIPS 203 (ML-KEM/Kyber) and FIPS 204 (ML-DSA/Dilithium) as the first federal post-quantum cryptography standards. RSA and ECDSA signatures are mathematically vulnerable to Shor's algorithm. Here is how we added PQ hardening to Auth1 without breaking a single existing integration.

What Is Actually at Risk

Not everything breaks equally. Understanding what quantum computers threaten — and what they do not — is essential for making good engineering decisions.

Broken by quantum computers (Shor's algorithm):

Not broken by quantum computers:

Harvest Now, Decrypt Later

Adversaries are already executing "harvest now, decrypt later" attacks: intercepting and storing encrypted traffic today with the expectation of decrypting it once quantum hardware catches up. If your authentication tokens are signed with RSA or ECDSA, they are being collected.


The NIST Post-Quantum Standards

NIST selected two primary algorithms after an eight-year evaluation process:

ML-KEM (FIPS 203, based on Kyber) — for key encapsulation (key exchange). Replaces RSA and ECDH for establishing shared secrets. ML-KEM-768 provides roughly 192-bit classical security.

ML-DSA (FIPS 204, based on Dilithium) — for digital signatures. Replaces RSA and ECDSA for signing data. ML-DSA-65 (Dilithium Level 3) provides roughly 192-bit classical security. Security is based on the hardness of lattice problems.

Both algorithms have significantly larger key and signature sizes. An ML-DSA-65 public key is 1,952 bytes (vs. 32 bytes for Ed25519). A signature is 3,293 bytes (vs. 64 bytes for Ed25519). For server-side authentication, the overhead is negligible.


Our Approach: Post-Quantum JWT Attestation

We did not want to break existing JWT verification. The solution is layered attestation:

TextPQ-attested token format
<standard_jwt>.pq:<base64_dilithium_signature>

The standard JWT is completely intact. After the final dot-separated segment, we append .pq: followed by a Base64-encoded ML-DSA-65 signature over the entire JWT string. Standard JWT verifiers see a token that is slightly longer than expected and ignore trailing data. PQ-aware verifiers split the token at .pq: and verify both signatures.


How It Works: Sign and Verify

Generating a PQ Keypair

JavaScriptpq-setup.js
const { pqGenerateKeypair, pqExportPublicKey } = require('auth-shield');

// Generate once at server startup, store the secret key securely
const keypair = pqGenerateKeypair();
// keypair.publicKey  -- Buffer, 1,952 bytes (ML-DSA-65)
// keypair.secretKey  -- Buffer, 4,032 bytes (ML-DSA-65)
// keypair.algorithm  -- "ML-DSA-65"

const publicKeyB64 = pqExportPublicKey(keypair.publicKey);

Signing a JWT with PQ Attestation

JavaScriptpq-sign.js
const { createTokenPair, pqSignToken } = require('auth-shield');

// 1. Create a standard HS256 JWT pair
const tokens = createTokenPair('user-123', 'user@example.com', 'admin', 'tenant-abc', jwtConfig);

// 2. Add PQ attestation to the access token
const attestedToken = pqSignToken(tokens.accessToken, keypair.secretKey);
// Result: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyLTEyMyJ9.sig.pq:AAEC..."

Verifying a PQ-Attested Token

JavaScriptpq-verify.js
const { pqVerifyToken, verifyAccessToken } = require('auth-shield');

function verifyPqToken(attestedToken, publicKey, jwtConfig) {
  // 1. Verify the Dilithium signature and extract the original JWT
  const pqResult = pqVerifyToken(attestedToken, publicKey);
  if (!pqResult.valid) {
    throw new Error('PQ signature verification failed');
  }

  // 2. Verify the standard HS256 JWT
  const payload = verifyAccessToken(pqResult.token, jwtConfig);

  return { payload, pqAlgorithm: pqResult.algorithm };
}

PQ-Signed Audit Logs

We sign every audit log entry with ML-DSA-65, creating a tamper-evident chain:

JavaScriptaudit-chain.js
const { pqSignChain } = require('auth-shield');

const auditEntries = [
  JSON.stringify({ action: 'login', userId: 'u-1', ip: '203.0.113.1', ts: Date.now() }),
  JSON.stringify({ action: 'role_change', userId: 'u-1', from: 'user', to: 'admin', ts: Date.now() }),
];

const signedChain = pqSignChain(auditEntries, keypair.secretKey);
// Each entry gets: signature, entryHash, previousHash
// Chain integrity: modifying any entry invalidates all subsequent signatures

PQ Webhook Signatures

We provide an additional PQ signature layer for webhooks:

JavaScriptpq-webhooks.js
const { pqSignWebhook, pqVerifyWebhook } = require('auth-shield');

// Server side: sign the webhook payload
const payload = JSON.stringify({ event: 'user.created', userId: 'u-42' });
const pqSignature = pqSignWebhook(payload, keypair.secretKey);

// Client side: verify the webhook
const isValid = pqVerifyWebhook(payload, req.headers['x-pq-signature'], serverPublicKey);

Kyber Key Exchange for Service-to-Service Auth

JavaScriptkyber-exchange.js
const { kemGenerateKeypair, kemEncapsulate, kemDecapsulate } = require('auth-shield');

// --- Service A (initiator) ---
const serviceA = kemGenerateKeypair();

// --- Service B (responder) ---
const { sharedSecret, ciphertext } = kemEncapsulate(serviceA.publicKey);
// sharedSecret: 32-byte Buffer -- use as AES-256 key

// --- Service A (completes exchange) ---
const derivedSecret = kemDecapsulate(ciphertext, serviceA.secretKey);
// derivedSecret === sharedSecret (both are the same 32-byte key)

Performance: Fast Enough for the Hot Path

OperationLatencyNotes
ML-DSA-65 keypair generation~1.2 msOne-time at startup
ML-DSA-65 sign~180 usPer-token or per-entry
ML-DSA-65 verify~111 usPer-verification
ML-DSA-65 sign+verify~291 usFull round trip
ML-KEM-768 keypair~120 usPer-session or cached
ML-KEM-768 encapsulate~160 usPer-key-exchange
ML-KEM-768 decapsulate~140 usPer-key-exchange

For context, a typical database query takes 1-10 milliseconds. The 291 microsecond cost of PQ signing and verification is noise in any real authentication flow.


Migration Path: From Zero to PQ-Hardened

  1. Install auth-shield and generate a PQ keypair at server startup.
  2. Start signing new JWTs with PQ attestation. Existing verifiers ignore the .pq: suffix.
  3. Publish your public key at /.well-known/pq-public-key.
  4. Update verifiers one at a time to check the PQ signature when present.
  5. Add PQ signatures to webhooks via the X-PQ-Signature header.
  6. Sign audit log entries with pqSignChain() for tamper-evident history.
  7. Adopt Kyber key exchange for new service-to-service integrations.
Each Step is Independent

You can stop at any point and still have a working system with strictly more security than you started with. No breaking changes. No protocol modifications.


Why This Matters Now

The cost of adding PQ hardening is measured in microseconds per request and a few kilobytes per token. The cost of not adding it is measured in the retroactive compromise of every authentication event between now and whenever you get around to it.

Auth1 is the first authentication platform with built-in post-quantum hardening. Start with npm install auth-shield. Generate a keypair. Sign your first PQ-attested token. The quantum clock is already ticking.

First Auth Platform with Post-Quantum Security

ML-DSA-65 JWT attestation, PQ-signed audit logs, PQ webhook signatures, and ML-KEM-768 key exchange. Built in, not bolted on.

Start Free → Read the Docs
Free tier · 1,000 verifications/month · No credit card required