Skip to main content
Node.js securitypost-quantum cryptographycryptographyJWT securityTypeScriptopen sourcestatic analysisbackend security

I Built a Post-Quantum Readiness Scanner for Node.js

Usama Amjid
6 min read
8878 words
I Built a Post-Quantum Readiness Scanner for Node.js

Every RSA-encrypted payload leaving your API can be recorded today and decrypted the day a large enough quantum computer exists. That attack has a name — harvest now, decrypt later — and it's already worth mounting against anything that must stay secret into the 2030s. Signatures run on a different clock: nobody needs to harvest your RS256 JWTs, because once Shor's algorithm can derive your private key from its public half, they can forge fresh ones. I spent the last few days building shorproof, a post-quantum readiness scanner for JavaScript and TypeScript. I wanted something that actually understood how JS code imports and calls crypto, not something that greps for scary strings.

What Shor's algorithm actually breaks

Shor's algorithm is a 1994 quantum algorithm that factors large numbers and computes discrete logarithms in polynomial time. Classical computers can't do either efficiently, and that gap is the entire foundation of RSA, Diffie-Hellman, and elliptic-curve cryptography like ECDSA and ECDH. A large enough quantum computer running Shor's algorithm doesn't weaken those algorithms. It breaks them outright.

Symmetric crypto isn't in the same danger. Grover's algorithm only halves the effective security of AES and SHA, so AES-256 and SHA-256 stay safe at the key sizes we already use. The public-key algorithms are the real emergency: NIST's transition timeline deprecates RSA, ECDSA, and ECDH for new systems by 2030 and disallows them entirely by 2035. Nine years sounds distant until you remember how long a signing key, or a stored ciphertext, actually lives.

The replacements already exist, and they're final standards, not drafts. ML-KEM (FIPS 203) replaces RSA and ECDH for key exchange. ML-DSA (FIPS 204) replaces RSA and ECDSA for signatures, with SLH-DSA (FIPS 205) as the conservative hash-based fallback. In the JS world you can use them today through jose v6 or Node 24.7+. The hard part isn't the migration target — it's finding everything that needs migrating.

Why I built a post-quantum readiness scanner instead of trusting one

A handful of quantum-readiness scanners already exist. Most come from the infrastructure security world — they scan certificates, TLS configs, and dependency manifests across many languages at once. That's a reasonable design if your job is auditing a whole company's estate. It's the wrong design if your job is finding the RS256 call buried in a Node.js auth module, because none of them actually parse JavaScript as a language.

I write backend and fintech code for a living, so JWTs and Node's crypto module are where my real exposure lives, not X.509 certs I don't manage. I've written before about defending Node.js APIs against bots and cost attacks (Defend Your Node.js API) — this is the same instinct, pointed at a slower-moving clock. I wanted a tool that treated JavaScript as a language with imports, aliases, and scopes, not a pile of text to grep. So I built one.

The false positive that forced a rewrite

The first version of shorproof's detection was embarrassingly naive: a regex looking for strings like RS256 and variable names containing rsa or jwt. It ran in about ten minutes, and within the first real test file it flagged something that had nothing to do with cryptography.

typescript
// A naive string/regex scanner flags both of these lines.
// Only the second one is a real quantum-vulnerable crypto call.

const rsaKeyRotationDays = getSecurityConfig('rotation.rsaKeyDays');
logger.info(`Next RS256 rotation window: ${rsaKeyRotationDays} days`);

import { sign as issueToken } from 'jsonwebtoken';

const token = issueToken(claims, privateKey, { algorithm: 'RS256' });

Neither rsaKeyRotationDays nor the log line touches a cryptographic API. They just contain the wrong words, and a regex can't tell a variable name from a function call. So I rewrote the detector on top of @babel/parser and @babel/traverse, resolving every call back to its real binding: the actual import it came from, through any alias, namespace, or re-export.

Once shorproof can see that issueToken really is jsonwebtoken's sign, and that its algorithm option resolves to 'RS256' through constant propagation, the picture changes. The first two lines stop being findings. The real call becomes one, with a file and line number attached.

An honest severity model

Shor-breakable isn't a yes-or-no property once you look closely. A stray RSA-signed session cookie and a ten-year TLS certificate are both technically quantum-vulnerable, but they don't deserve the same reaction. I didn't want shorproof to cry wolf on everything RSA-shaped, and I didn't want it to stay quiet about the things that actually matter. Severity turns on exposure, not on how scary the algorithm's name sounds.

  • critical — Shor-breakable crypto protecting long-lived secrets: stored ciphertext, private key files, certs valid past 2030, JWKS keys.
  • high — typical exposure: JWT signing (RS256, ES256, EdDSA), TLS-adjacent key material.
  • medium — quantum-weakened but survivable (AES-128, fixable by moving to AES-256), or classically broken already (MD5/SHA-1 — a real problem today, just not a quantum one).
  • review — a crypto-capable surface shorproof can't confirm statically.
  • safe — AES-256, SHA-256/SHA-3, bcrypt/argon2, and ML-KEM/ML-DSA/SLH-DSA usage, reported as already post-quantum.

That last category matters more than it sounds. A scanner that only ever reports problems trains people to stop reading it. shorproof reports ML-DSA-65 usage as safe, with a checkmark, the same way it reports a critical RSA key. If you've already migrated a signing path, you get to see that, not just silence.

What it actually scans, and how to run it

Three scanners run in one pass. deps checks package.json against a curated list of crypto packages. ast walks your JS/TS source with the binding-aware detector above, covering Node's crypto module, WebCrypto, and the JWT stack — jsonwebtoken, jose (including the SignJWT builder), and express-jwt. artifacts parses JWKS/JWK files, PEM keys, and X.509 certs with Node's own crypto module, and flags certificates valid past 2030 automatically.

bash
npx shorproof                 # scan the current project
npx shorproof ./api           # scan a specific directory

npx shorproof --json          # stable, documented JSON output
npx shorproof --format sarif  # SARIF for GitHub code scanning
npx shorproof --format cbom   # CycloneDX 1.6 Cryptographic BOM

npx shorproof --fail-on high  # exit 1 on any high+ finding, for CI

Point it at a project with a live RSA key and an RS256-signed JWT, and you get grouped, readable output: a file, a line, an honest one-sentence why, and a migration hint. The --json output carries the same information as a stable schema, meant for scripts and AI coding agents to read, not just humans:

json
{
  "tool": "shorproof",
  "version": "0.1.0",
  "summary": { "critical": 1, "high": 2, "medium": 0, "review": 0, "safe": 1, "info": 0 },
  "findings": [
    {
      "source": "ast",
      "ruleId": "jsonwebtoken/rs256",
      "severity": "high",
      "algorithm": "RS256",
      "why": "RS256 signs with RSA, which Shor's algorithm breaks on a large enough quantum computer.",
      "migration": "Switch to ML-DSA via jose >= v6 or Node 24.7+",
      "confidence": "high",
      "location": { "file": "src/auth/token.ts", "line": 42, "column": 3 }
    }
  ],
  "skipped": []
}

One more honesty detail: if a file fails to parse — a vendored bundle, a duplicate declaration, whatever — shorproof doesn't crash or silently drop it. It lands in the skipped array with the reason, and the scan keeps going. One bad file in vendor/ shouldn't hide the real findings in the other nine hundred.

Wiring it into CI

SARIF output means findings show up directly in GitHub's Security → Code scanning tab and on the PR diff — the same place CodeQL results land. Severity maps to SARIF's level and GitHub's security-severity, and anything marked safe is reported as informational, not an alert, so a completed PQC migration doesn't light your security tab up red.

yaml
name: shorproof
on: [push, pull_request]
permissions:
  security-events: write
  contents: read
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npx shorproof@0.1 . --format sarif > shorproof.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: shorproof.sarif

Fast enough to run on every PR

A scanner you won't run on every pull request is a scanner you'll run twice a year. So I benchmarked the published package the way CI actually hits it: a fresh process per run, no warm caches, median of five runs. The targets are generated TypeScript services — about forty-five lines per file, with a vulnerable JWT module seeded into every fiftieth file.

bash
   100 files    0.6 s      7 findings
   500 files    1.6 s     31 findings
 1,000 files    2.4 s     61 findings
 5,000 files    8.1 s    301 findings
10,000 files   18.6 s    601 findings

Scan time grows linearly with file count, and a 1,000-file service finishes inside the time your npm ci step already takes. The number I actually cared about is in the findings column. Every count decomposes exactly: three findings per seeded module — the RS256 sign, the RS256 verify, the RSA key generation — plus one dependency finding for jsonwebtoken. Nothing else. All 332 seeded modules found, zero false positives across 16,268 clean files. The binding-aware rewrite paid for itself.

These trees are synthetic, so they're a floor, not a proof — real codebases have weirder code than my generator writes. That's what the false-positive torture fixtures in the repo are for: aliased re-exports, shadowed crypto variables, RS256 appearing as an innocent string.

What I don't know yet

I've run shorproof against real production code at work, not just the test fixtures, and it behaves the way the fixtures promise it will. I won't share specifics — it's fintech code, and none of it is mine to publish. What I can say is that binding-aware detection earns its complexity the moment a codebase has more than one way to import jsonwebtoken.

shorproof is 0.1.0. It covers JavaScript and TypeScript only — no Python, no Go, no Java, and I'm not promising I'll add them. Two runtime dependencies, both from Babel, both doing exactly one job: parse the AST, walk the bindings. If you maintain a Node or TypeScript codebase and don't know what's actually signing your tokens, that's the gap this fills.

The source lives at github.com/shorproof/shorproof — MIT licensed, issues and PRs welcome — and the package is on npmjs.com/package/shorproof. npx shorproof costs nothing and installs nothing. Run it once and see what's actually in your dependency tree.