NIST finalized the first post-quantum cryptography standards in August 2024. RSA, ECC, and ECDH are all broken by Shor's algorithm, and adversaries are already capturing encrypted data today to decrypt later once quantum hardware matures. This is called "harvest now, decrypt later" and it's not theoretical, it's happening now.
The migration deadline for US federal agencies is 2030. Most companies have no idea where their vulnerable cryptography lives.
I built pqc-scan to solve that. It's a CLI and GitHub Action that scans codebases for quantum-vulnerable cryptography and surfaces findings as inline PR annotations. Here's what I learned when I ran it on real code.
Why AST-based detection and not regex:
The obvious first approach is string matching. You search for "RSA", flag it and ship it. The problem is that crypto APIs look completely different across libraries and languages. In Python alone, RSA key generation looks like this in the cryptography library:
rsa.generate_private_key(public_exponent=65537, key_size=2048)
But also like this in pycryptodome:
RSA.generate(2048)
And the algorithm might not appear directly in the call at all:
algo = "RS256"
jwt.encode(payload, key, algorithm=algo)
Regex catches maybe 20% of real usages. I used tree-sitter to build a proper AST-based scanner that walks the actual parse tree per language. This lets you detect aliased imports, keyword arguments, and indirect algorithm specification. The tool does a shallow constant-propagation pass to catch variable-passed algorithms like the JWT example above.
What I found on certbot
I ran pqc-scan against certbot, which manages TLS certificates and is a realistic example of a security-critical Python application.
With tests excluded, it found these in production code:
CRITICAL PQC001 · RSA Key Generation
certbot/src/certbot/crypto_util.py:258
rsa.generate_private_key(public_exponent=65537, key_size=bits)
CRITICAL PQC004 · ECDSA Key Generation
certbot/src/certbot/crypto_util.py:268
ec.generate_private_key(curve=curve(), backend=default_backend())
CRITICAL PQC002 · RSA Encryption / Padding
certbot/src/certbot/crypto_util.py:365
PKCS1v15()
HIGH PQC010 · MD5 Usage
certbot/src/certbot/_internal/account.py:70
hashlib.md5()
These are real, actionable findings in production code that a certbot developer would need to act on before 2030.
The false positive I had to fix
The config scanner was flagging this nginx configuration as vulnerable:
SSLProtocol all -SSLv2 -SSLv3
It saw "SSLv2" in the string and fired. But the - prefix means the protocol is being explicitly disabled. This config is actually secure, it's saying "use all protocols except SSLv2 and SSLv3."
A security tool that flags hardened configurations as vulnerable is worse than no tool. It trains developers to ignore warnings. I fixed the scanner to tokenize the directive and skip any protocol token preceded by - or !, and added a regression test asserting zero findings on that exact string.
This is the core challenge of static analysis - being right about what's dangerous, not just what looks dangerous.
*What the tool produces: *
Outputs rich console findings, SARIF 2.1.0 for inline GitHub PR annotations, and CycloneDX 1.6 CBOM for compliance audits.
Try it
pip install pqc-scan
pqc-scan scan .
GitHub Action:
- uses: sachhg/pqc-scan@v0.1.0
Full source: https://github.com/sachhg/pqc-scan
The tool currently supports Python, JavaScript/TypeScript, Java, and Go. 110 tests. Self-scan produces zero findings.
If you run it on your codebase and find patterns it misses, open an issue. The detection rules are the part that most needs real-world feedback.
Top comments (0)