Quantum Outpost
post quantum crypto intermediate · 23 min read ·

Auditing a Codebase for Y2Q Readiness

A hands-on tutorial that walks through building a crypto-agility scanner for any codebase — Python, JavaScript, Go, Rust, Java, C/C++. Identifies every place RSA, ECDSA, ECDH, and DH are used, produces a prioritized migration report, and is the exact deliverable that PQC consulting engagements sell.

Prerequisites: Tutorial 22: ML-KEM and ML-DSA in Practice

This tutorial turns everything from the PQC track into a concrete product: a scanner that audits any codebase for cryptographic primitives at risk from Shor’s algorithm, produces a prioritized migration report, and is the exact sort of deliverable that forms the opening sprint of a PQC consulting engagement. We build it in Python using pattern matching and lightweight AST parsing, cover the detection patterns for the six biggest languages, and end with a template report you can actually send to a client.

This is the one tutorial in the series that maps most directly to indie revenue. If you’re reading this planning to build a PQC consulting or tooling product, start here.

What we’re looking for

From Tutorial 21: every public-key primitive that Shor breaks. Categorized:

CategoryExamples to detect
RSARSA, rsa_encrypt, EVP_PKEY_RSA, rsaEncryption, PKCS1v15, OAEP
ECDSA / ECDH / EdDSAECDSA, ECDH, Ed25519, Ed448, secp256r1, prime256v1, secp384r1, secp521r1
DH / DSADiffie-Hellman, DH_generate_key, DSA, EVP_PKEY_DH
Weak symmetric (Grover-halved)AES-128, AES_128, 128-bit keys
Weak hashesMD5, SHA-1, SHA1, RIPEMD160

We won’t detect AES-256 / SHA-256 — those are fine. We won’t detect ML-KEM / ML-DSA — those are the target state.

Language-specific patterns

Python

# Vulnerable patterns to match
from cryptography.hazmat.primitives.asymmetric import rsa, ec, dsa, dh, ed25519
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)    # RSA
key = ec.generate_private_key(ec.SECP256R1())                           # ECDSA / ECDH
key = dsa.generate_private_key(key_size=2048)                            # DSA
key = ed25519.Ed25519PrivateKey.generate()                               # Ed25519 (vulnerable)

# Lower-level OpenSSL bindings
from Crypto.PublicKey import RSA, DSA, ECC
import rsa as rsapy  # the 'rsa' PyPI package

Pattern set:

  • from cryptography.hazmat.primitives.asymmetric import (rsa|ec|dsa|dh|ed25519|ed448)
  • from Crypto.PublicKey import (RSA|DSA|ECC|ElGamal)
  • rsa\.generate_private_key, ec\.SECP\d+R1, dsa\.generate_private_key
  • ec\.(SECP256R1|SECP384R1|SECP521R1)

JavaScript / Node

const crypto = require('crypto');
const key = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });      // RSA
const key = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });       // ECDSA
const key = crypto.generateKeyPairSync('ed25519');                           // Ed25519
const shared = crypto.diffieHellman(privateKey, publicKey);                  // ECDH / DH

// WebCrypto
await crypto.subtle.generateKey({ name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, ... });
await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' });

// OpenSSL string literals in native addons
"RSA-PSS", "ecdsa-with-SHA256", "dsaWithSHA256"

Pattern set:

  • (generateKeyPairSync|generateKeyPair)\s*\(\s*['"](?:rsa|ec|ed25519|dsa|dh)
  • namedCurve\s*:\s*['"](?:P-256|P-384|P-521|secp256r1)
  • name\s*:\s*['"](?:ECDSA|RSASSA|RSA-PSS|RSA-OAEP|ECDH)

Go

import (
    "crypto/rsa"
    "crypto/ecdsa"
    "crypto/ed25519"
    "crypto/elliptic"
    "crypto/dsa"
)

priv, _ := rsa.GenerateKey(rand.Reader, 2048)                    // RSA
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)       // ECDSA
pub, priv, _ := ed25519.GenerateKey(rand.Reader)                  // Ed25519

Pattern set:

  • import\s+"crypto/(rsa|ecdsa|ed25519|ed448|dsa|dh)"
  • (elliptic\.P256|elliptic\.P384|elliptic\.P521|elliptic\.P224)
  • \w+\.GenerateKey\s*\( where \w+ is one of the above packages

Rust

use rsa::{RsaPrivateKey, RsaPublicKey};
use p256::ecdsa::SigningKey;
use ed25519_dalek::SigningKey as Ed25519SigningKey;
use x25519_dalek::EphemeralSecret;

Pattern set:

  • use\s+(rsa|ring|p256|p384|p521|ed25519_dalek|x25519_dalek|secp256k1)::
  • RsaPrivateKey|RsaPublicKey|SigningKey
  • Cargo.toml: rsa\s*=, p256\s*=, ed25519-dalek\s*=, x25519-dalek\s*=

Java

import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.ECGenParameterSpec;

KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");                      // RSA
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");                       // ECDSA
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EdDSA");                    // EdDSA

// BouncyCastle
import org.bouncycastle.crypto.generators.RSAKeyPairGenerator;

Pattern set:

  • KeyPairGenerator\.getInstance\s*\(\s*"(?:RSA|EC|DSA|DH|EdDSA|Ed25519|Ed448)"
  • import\s+java\.security\.interfaces\.(RSAPublicKey|RSAPrivateKey|ECPublicKey)
  • import\s+org\.bouncycastle\.crypto.*\.(RSA|EC|Ed25519|Ed448|DSA|DH)

C / C++

#include <openssl/rsa.h>
#include <openssl/ec.h>
#include <openssl/dh.h>
#include <openssl/dsa.h>

RSA *key = RSA_new();
RSA_generate_key_ex(key, 2048, bn, NULL);
EC_KEY *ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);

Pattern set:

  • #include\s+<openssl/(rsa|ec|dsa|dh)\.h>
  • RSA_(new|generate_key|verify|sign), EC_KEY_new_by_curve_name, EVP_PKEY_(RSA|EC|DSA|DH)
  • NID_(X9_62_prime256v1|secp384r1|secp521r1|X25519|X448|Ed25519|Ed448)

Config files and certificates

Also check:

  • *.pem, *.crt, *.key — look for -----BEGIN RSA PRIVATE KEY-----, -----BEGIN EC PRIVATE KEY-----
  • TLS config files (nginx.conf, apache2.conf, haproxy.cfg) — ssl_ecdh_curve, ssl_ciphers specifying RSA or ECDSA
  • Package manifests (package.json, Cargo.toml, go.mod, requirements.txt) — dependency on rsa, ed25519-dalek, secp256k1, etc.

Building the scanner

A working single-file Python scanner. Save as pqc_audit.py:

#!/usr/bin/env python3
"""Scan a codebase for cryptographic primitives vulnerable to quantum attacks."""

from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
import argparse, json, re, sys
from typing import Iterable

# --- Risk categories ---
RISK_SHOR = "shor-vulnerable"   # Broken by quantum (RSA, ECDSA, ECDH, DH, DSA, Ed*)
RISK_GROVER = "grover-weakened" # Key size needs doubling (AES-128, SHA-1)
RISK_INFO = "info"              # Already safe (AES-256, SHA-256) — noted, not flagged

@dataclass
class Pattern:
    name: str
    regex: str
    risk: str
    category: str                # RSA, ECC, DH, DSA, AES-128, MD5, ...

@dataclass
class Finding:
    path: str
    line: int
    pattern: str
    risk: str
    category: str
    excerpt: str

PATTERNS: list[Pattern] = [
    # --- Python ---
    Pattern("py-cryptography-rsa",
            r"from cryptography\.hazmat\.primitives\.asymmetric import [^\n]*\brsa\b",
            RISK_SHOR, "RSA"),
    Pattern("py-cryptography-ec",
            r"from cryptography\.hazmat\.primitives\.asymmetric import [^\n]*\bec\b",
            RISK_SHOR, "ECDSA/ECDH"),
    Pattern("py-cryptography-ed25519",
            r"from cryptography\.hazmat\.primitives\.asymmetric import [^\n]*\bed25519\b",
            RISK_SHOR, "Ed25519"),
    Pattern("py-cryptography-dsa",
            r"from cryptography\.hazmat\.primitives\.asymmetric import [^\n]*\bdsa\b",
            RISK_SHOR, "DSA"),
    Pattern("py-cryptography-dh",
            r"from cryptography\.hazmat\.primitives\.asymmetric import [^\n]*\bdh\b",
            RISK_SHOR, "DH"),
    Pattern("py-pycryptodome-rsa",
            r"from Crypto\.PublicKey import [^\n]*\bRSA\b",
            RISK_SHOR, "RSA"),
    Pattern("py-pycryptodome-ecc",
            r"from Crypto\.PublicKey import [^\n]*\b(ECC|DSA|ElGamal)\b",
            RISK_SHOR, "ECC/DSA"),

    # --- JavaScript / Node ---
    Pattern("node-rsa-keypair",
            r"generateKeyPair(Sync)?\s*\(\s*['\"]rsa['\"]",
            RISK_SHOR, "RSA"),
    Pattern("node-ec-keypair",
            r"generateKeyPair(Sync)?\s*\(\s*['\"]ec['\"]",
            RISK_SHOR, "ECDSA/ECDH"),
    Pattern("node-ed25519-keypair",
            r"generateKeyPair(Sync)?\s*\(\s*['\"](ed25519|ed448)['\"]",
            RISK_SHOR, "EdDSA"),
    Pattern("web-crypto-rsa",
            r"name\s*:\s*['\"]RSA(-PSS|-OAEP|SSA[^'\"]*)['\"]",
            RISK_SHOR, "RSA"),
    Pattern("web-crypto-ecdsa",
            r"name\s*:\s*['\"](ECDSA|ECDH)['\"]",
            RISK_SHOR, "ECDSA/ECDH"),
    Pattern("node-namedcurve-p256",
            r"namedCurve\s*:\s*['\"](P-256|prime256v1|secp256r1|P-384|secp384r1|P-521|secp521r1)['\"]",
            RISK_SHOR, "ECC"),

    # --- Go ---
    Pattern("go-import-rsa",
            r'import\s+(?:\(\s*)?"crypto/rsa"',
            RISK_SHOR, "RSA"),
    Pattern("go-import-ecdsa",
            r'import\s+(?:\(\s*)?"crypto/ecdsa"',
            RISK_SHOR, "ECDSA"),
    Pattern("go-import-ed25519",
            r'import\s+(?:\(\s*)?"crypto/(ed25519|ed448)"',
            RISK_SHOR, "EdDSA"),
    Pattern("go-import-dsa",
            r'import\s+(?:\(\s*)?"crypto/(dsa|dh)"',
            RISK_SHOR, "DSA/DH"),
    Pattern("go-elliptic-curve",
            r"elliptic\.(P224|P256|P384|P521)\s*\(\s*\)",
            RISK_SHOR, "ECC"),

    # --- Rust ---
    Pattern("rust-rsa-use",
            r"use\s+rsa::",
            RISK_SHOR, "RSA"),
    Pattern("rust-p256-use",
            r"use\s+(p256|p384|p521|secp256k1)::",
            RISK_SHOR, "ECDSA"),
    Pattern("rust-ed25519-use",
            r"use\s+ed25519[_-]dalek::",
            RISK_SHOR, "EdDSA"),
    Pattern("rust-x25519-use",
            r"use\s+x25519[_-]dalek::",
            RISK_SHOR, "X25519"),
    Pattern("rust-cargo-deps",
            r'^\s*(rsa|p256|p384|p521|ed25519-dalek|x25519-dalek|secp256k1)\s*=',
            RISK_SHOR, "ECC/RSA"),

    # --- Java ---
    Pattern("java-keypairgen",
            r'KeyPairGenerator\.getInstance\s*\(\s*"(RSA|EC|DSA|DH|EdDSA|Ed25519|Ed448)"',
            RISK_SHOR, "Asymmetric"),
    Pattern("java-rsa-interface",
            r"java\.security\.interfaces\.(RSAPublicKey|RSAPrivateKey|ECPublicKey|ECPrivateKey)",
            RISK_SHOR, "RSA/ECC"),

    # --- C / C++ / OpenSSL ---
    Pattern("c-openssl-rsa",
            r"#include\s+<openssl/(rsa|dsa|dh)\.h>",
            RISK_SHOR, "RSA/DSA/DH"),
    Pattern("c-openssl-ec",
            r"#include\s+<openssl/ec\.h>",
            RISK_SHOR, "ECC"),
    Pattern("c-openssl-evp-pkey",
            r"EVP_PKEY_(RSA|EC|DSA|DH|ED25519|ED448|X25519|X448)",
            RISK_SHOR, "Asymmetric"),
    Pattern("c-openssl-nid",
            r"NID_(X9_62_prime256v1|secp384r1|secp521r1|secp256k1|X25519|X448|Ed25519|Ed448)",
            RISK_SHOR, "ECC"),
    Pattern("c-rsa-generate",
            r"RSA_generate_key(_ex)?",
            RISK_SHOR, "RSA"),

    # --- PEM keys in config / cert files ---
    Pattern("pem-rsa-key",
            r"-----BEGIN RSA (PRIVATE|PUBLIC) KEY-----",
            RISK_SHOR, "RSA certificate"),
    Pattern("pem-ec-key",
            r"-----BEGIN EC (PRIVATE|PUBLIC) KEY-----",
            RISK_SHOR, "ECC certificate"),

    # --- Grover-weakened (flag for key-size upgrade, not replacement) ---
    Pattern("aes-128-flag",
            r"\bAES[_-]?128\b|\baes[_-]?128[_-](cbc|ctr|gcm|ccm)\b",
            RISK_GROVER, "AES-128 (double to AES-256)"),
    Pattern("md5",
            r"\bMD5\b|\bmd5\b",
            RISK_GROVER, "MD5 (broken, replace with SHA-256)"),
    Pattern("sha1",
            r"\bSHA[_-]?1\b|\bsha1\b",
            RISK_GROVER, "SHA-1 (broken, replace with SHA-256)"),
]

COMPILED = [(p, re.compile(p.regex, re.MULTILINE)) for p in PATTERNS]

IGNORE_DIRS = {".git", "node_modules", "target", "dist", "build", ".venv", "venv", "__pycache__"}
ALLOWED_EXTS = {
    ".py", ".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs",
    ".go", ".rs", ".java", ".kt", ".scala",
    ".c", ".cc", ".cpp", ".cxx", ".h", ".hpp",
    ".rb", ".php", ".swift",
    ".toml", ".yaml", ".yml", ".json",
    ".conf", ".cfg", ".ini",
    ".pem", ".crt", ".key", ".cer",
}


def scan_file(path: Path) -> list[Finding]:
    findings: list[Finding] = []
    try:
        text = path.read_text(errors="replace")
    except (OSError, UnicodeDecodeError):
        return findings
    for pattern, rx in COMPILED:
        for match in rx.finditer(text):
            # Find the line number
            line = text.count("\n", 0, match.start()) + 1
            start = text.rfind("\n", 0, match.start()) + 1
            end = text.find("\n", match.end())
            excerpt = text[start:end if end != -1 else None].strip()[:200]
            findings.append(Finding(
                path=str(path),
                line=line,
                pattern=pattern.name,
                risk=pattern.risk,
                category=pattern.category,
                excerpt=excerpt,
            ))
    return findings


def walk_repo(root: Path) -> Iterable[Path]:
    for path in root.rglob("*"):
        if not path.is_file():
            continue
        if any(part in IGNORE_DIRS for part in path.parts):
            continue
        if path.suffix.lower() in ALLOWED_EXTS or path.name in {"Cargo.toml", "go.mod", "package.json"}:
            yield path


def audit(root: Path) -> list[Finding]:
    findings: list[Finding] = []
    for path in walk_repo(root):
        findings.extend(scan_file(path))
    return findings


def report_text(findings: list[Finding]) -> str:
    if not findings:
        return "No quantum-vulnerable cryptographic primitives detected.\n"

    by_risk = {RISK_SHOR: [], RISK_GROVER: []}
    for f in findings:
        by_risk.setdefault(f.risk, []).append(f)

    lines = []
    lines.append("=" * 70)
    lines.append("POST-QUANTUM CRYPTOGRAPHIC AUDIT")
    lines.append("=" * 70)
    lines.append(f"Total findings: {len(findings)}")
    lines.append(f"  Shor-vulnerable: {len(by_risk[RISK_SHOR])}")
    lines.append(f"  Grover-weakened: {len(by_risk[RISK_GROVER])}")
    lines.append("")

    for risk, group in by_risk.items():
        if not group: continue
        lines.append("-" * 70)
        lines.append(f"[{risk.upper()}]  {len(group)} finding(s)")
        lines.append("-" * 70)
        by_cat = {}
        for f in group:
            by_cat.setdefault(f.category, []).append(f)
        for cat, items in sorted(by_cat.items()):
            lines.append(f"\n  {cat}: {len(items)} occurrences")
            for item in items[:5]:                   # truncate per-category for readability
                lines.append(f"    {item.path}:{item.line}{item.excerpt}")
            if len(items) > 5:
                lines.append(f"    ... and {len(items) - 5} more")
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("path", nargs="?", default=".", help="Directory to scan")
    parser.add_argument("--json", action="store_true", help="Output JSON instead of text")
    args = parser.parse_args()

    findings = audit(Path(args.path))
    if args.json:
        print(json.dumps([f.__dict__ for f in findings], indent=2))
    else:
        print(report_text(findings))
    return 1 if any(f.risk == RISK_SHOR for f in findings) else 0


if __name__ == "__main__":
    sys.exit(main())

Run against any repo:

$ python3 pqc_audit.py /path/to/your/codebase
======================================================================
POST-QUANTUM CRYPTOGRAPHIC AUDIT
======================================================================
Total findings: 14
  Shor-vulnerable: 11
  Grover-weakened: 3

----------------------------------------------------------------------
[SHOR-VULNERABLE]  11 finding(s)
----------------------------------------------------------------------

  RSA: 4 occurrences
    src/auth/keys.py:12  from cryptography.hazmat.primitives.asymmetric import rsa
    src/auth/certs.py:8  RSA_generate_key_ex(key, 2048, bn, NULL);
    config/server.crt:1  -----BEGIN RSA PRIVATE KEY-----
    ...

Exit code 1 if any Shor-vulnerable finding exists → integrates cleanly into CI.

Augmenting with context

The scanner above is deliberately simple — pattern matching. A real consulting deliverable adds:

  1. Data longevity annotations. Which lines hold keys protecting data valued for >10 years? Scanner can’t know this; ask the client to annotate.
  2. Rotation cost. How hard is it to replace this key? Flag hardware-embedded keys, long-chain certificates, root CAs as “rotation cost: high.”
  3. Dependency graph analysis. A function that generates a key has downstream consumers (APIs, storage). Rotate one key in isolation and you break 50 downstream services. Map these.
  4. Timeline per finding. Combine data longevity + rotation cost + risk → migration priority + suggested deadline.

For a real engagement, the Finding record grows to include data_longevity_years, rotation_cost, downstream_deps, and the final report sorts by a risk score rather than just enumerating findings.

Template migration report

A real deliverable looks like this. Write it up for the client after running the scanner:

# Post-Quantum Cryptographic Migration Audit
**Client:** Acme Corp.  **Date:** 2026-05-01  **Auditor:** [You]

## Executive Summary

Acme Corp's codebase uses RSA-2048 and ECDSA in 11 places across 4 services.
All of these use primitives that will be broken by a sufficiently large
quantum computer (timeline: mid-2030s per NIST guidance). "Harvest now,
decrypt later" attacks mean encrypted traffic already at risk if data
retention exceeds ~10 years.

**Recommended migration window: 18-24 months.** Early targets (6 months):
root signing keys, long-lived certificates. Later targets (18 months):
session TLS, ephemeral key exchange.

## Findings Summary

| Category | Count | Priority | Migration target |
|---|---|---|---|
| RSA-2048 for code signing | 2 | CRITICAL | ML-DSA-65 by Q3 2026 |
| ECDSA certs (TLS) | 5 | HIGH | Hybrid X25519+ML-KEM-768 by Q1 2027 |
| ECDH ephemeral | 3 | MEDIUM | Hybrid mode by Q2 2027 |
| Ed25519 auth | 1 | MEDIUM | ML-DSA-44 by Q2 2027 |
| AES-128 in DB | 3 | HIGH (cheap) | AES-256 by Q3 2026 |

## Prioritized Remediation Plan

### Phase 1 (months 1-6): Critical primitives
- Replace RSA-2048 code signing with ML-DSA-65
  - src/build/sign.py:42 — CI signer
  - apple-app-store/provisioning.plist — binary sign
  ...

### Phase 2 (months 6-12): Hybrid TLS rollout
- Enable X25519MLKEM768 on all public endpoints
- Update client SDKs to negotiate hybrid groups
...

### Phase 3 (months 12-24): Pure-PQC where regulation mandates
...

## Performance and Cost Impact

Measured on representative load: hybrid TLS handshake adds 0.3ms CPU,
1.8KB bandwidth. For current 5M RPS peak traffic: approx. 1 additional
CPU core, 9 GB/s added bandwidth. No material impact on user-perceived
latency.

## Appendix: Full Findings
... (JSON export attached as pqc-audit-2026-05-01.json)

This is the deliverable. Different clients want different emphasis, but the structure — executive summary, prioritized findings, remediation plan with dates, performance numbers, full raw data — stays the same.

Turn the scanner into a product

Three concrete productizations:

  1. GitHub App. Run the scanner on every PR; comment findings; gate merge on no critical findings. Charge per seat ($10-50/user/month) or per repo.
  2. SaaS dashboard. Connect to GitHub/GitLab; run scanner nightly on all repos; visualize PQC readiness over time. Recurring revenue.
  3. On-prem CLI with commercial license. Enterprise security teams prefer tools that don’t send source code over the internet. License per engineer or per LOC scanned.

All three are indie-shippable with 4-8 weeks of polish on top of the scanner above. Pricing: 110/LOCscanned(onetimeormonthly)getsyou1-10/LOC scanned (one-time or monthly) gets you 10k-100k/yr on a single mid-sized client.

The consulting engagement pitch

When you pitch a PQC audit to a client, this is the shape:

Scope: 4-week engagement. I run an automated scan across your entire codebase and infrastructure config, manually review findings for context, and produce a prioritized 18-month migration plan.

Deliverables: (1) Full JSON audit of all cryptographic primitives flagged by risk; (2) Executive summary deck for your CISO; (3) Detailed remediation runbook per finding, sorted by priority; (4) Performance impact estimate for hybrid TLS rollout; (5) Draft compliance-language update for your SOC2/ISO27001/FedRAMP documentation if relevant.

Fee: 4080kflat,dependingoncodebasesize.Payable5040-80k flat, depending on codebase size. Payable 50% on kickoff, 50% on delivery. Post-engagement support available at 300/hr or retainer.

That’s a realistic pitch for a mid-sized engagement. Above you’re selling hands-on support; below you’re selling a one-off SaaS report.

Exercises

1. Run the scanner on three repos

Pick three open-source projects and run the scanner. Report counts by category. Any surprises?

Expected
  • A web framework (Django, Rails): RSA+EC heavy (TLS machinery), AES-128 showing up in old test fixtures.
  • A cryptocurrency wallet: massive ECDSA/ECDH count, secp256k1 everywhere. Hard to migrate because blockchain protocol is baked in.
  • A web frontend (React app): mostly clean; some fetch APIs reference TLS details.

2. Add a language

Pick a language the scanner doesn’t cover well (Swift, Kotlin, Elixir, Ruby, PHP). Add detection patterns. Test on an example file.

Show Ruby patterns

Ruby has OpenSSL::PKey::RSA, OpenSSL::PKey::EC, OpenSSL::PKey::DSA, OpenSSL::PKey::DH. Also ed25519 gem and rbnacl. Patterns:

OpenSSL::PKey::(RSA|EC|DSA|DH)
require\s+['"](ed25519|rbnacl|openssl)['"]

3. Write a mitigation suggestion generator

Extend the Finding class with a suggested_mitigation field. Based on the pattern category, propose a replacement. E.g., RSA → “Replace with ML-DSA-65 (signatures) or ML-KEM-768 (encryption).” ECDSA → “Replace with ML-DSA-65.”

Show snippet
MITIGATION_MAP = {
    "RSA":          "Replace with ML-DSA-65 (signatures) or ML-KEM-768 (encryption).",
    "ECDSA/ECDH":   "Replace with ML-DSA-65 + ML-KEM-768 (hybrid with X25519 during migration).",
    "Ed25519":      "Replace with ML-DSA-44 (matching security category).",
    "DSA":          "Replace with ML-DSA-65.",
    "DH":           "Replace with ML-KEM-768.",
    "AES-128":      "Upgrade to AES-256 (same API, different key length).",
    "MD5":          "Replace with SHA-256 (already broken even classically).",
    "SHA-1":        "Replace with SHA-256.",
}

Attach this to each Finding based on category.

4. CI integration

Add a GitHub Action to a sample repo that runs the scanner on every PR and fails the PR if new Shor-vulnerable primitives were introduced (diff-based, not full-repo).

Show .github/workflows/pqc-audit.yml
name: PQC Audit
on: [pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run scanner on changed files
        run: |
          git diff --name-only origin/${{ github.base_ref }} HEAD \
            | xargs -r python3 pqc_audit.py --json > pqc.json
          if jq '.[] | select(.risk == "shor-vulnerable")' pqc.json | grep -q .; then
            echo "::error::New quantum-vulnerable crypto introduced."
            cat pqc.json | jq .
            exit 1
          fi

What you should take away

  • A 300-line scanner covers 90% of PQC audit needs for the six biggest languages.
  • The deliverable has structure: executive summary → prioritized findings → remediation runbook → performance impact → full raw data.
  • This is productizable as a GitHub App, SaaS dashboard, or licensed CLI. Each maps to a valid indie-revenue path.
  • Consulting engagements pay $40-80k for 4-week engagements producing the above report. Real work, real market.
  • This is where quantum-education knowledge converts to revenue without waiting for fault-tolerant hardware. Start offering PQC audits tomorrow.

This closes iteration 5 and the Error Correction + Hardware + Post-Quantum Cryptography arc. You now have 23 tutorials spanning foundations to production-ready PQC tooling. The site is a genuine 0-to-hero curriculum — and the last two tutorials are an actual business plan as much as a technical reference.


Weekly dispatch

Quantum, for people who already code.

One serious tutorial per week, plus the industry moves that actually matter. No hype, no hand-waving.

Free. Unsubscribe anytime. We will never sell your email.