Automated timing side-channel scanner for NIST PQC standards ML-KEM and ML-DSA
Project description
PQC Side-Channel Scanner
The first open-source, automated, pip-installable timing side-channel scanner for all four NIST post-quantum cryptography standards — FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), FIPS 205 (SLH-DSA), and FIPS 206 (FN-DSA/FALCON). Scans both liboqs 0.15.0 and OpenSSL+oqs-provider using TVLA statistical methodology at nanosecond precision.
Built by Disha P, Technical Director at Collective Qubits.
Why This Exists
NIST finalized the post-quantum cryptography standards in August 2024. Banks, cloud providers, and government agencies are actively deploying ML-KEM, ML-DSA, FALCON, and SLH-DSA to protect against future quantum attacks. But timing side-channel vulnerabilities in cryptographic implementations can leak secret key material even when the underlying algorithm is mathematically secure.
Existing tools like dudect require manual C-level integration, have no PQC-specific targets, and predate the NIST standards. No automated scanner existed for the full NIST PQC suite.
This tool fills that gap.
What It Found
Finding 1 — ML-KEM Encapsulation (RNG-induced), liboqs 0.15.0
| Algorithm | Operation | t-statistic (WSL2) | t-statistic (bare metal) | Verdict |
|---|---|---|---|---|
| ML-KEM-512 | Encapsulation | -4.9 | -8.37 | CRITICAL |
| ML-KEM-512 | Decapsulation | 0.5 | 1.15 | PASS |
| ML-KEM-768 | Encapsulation | 75.5 | -13.68 | CRITICAL |
| ML-KEM-768 | Decapsulation | -3.2 | -0.09 | PASS |
| ML-KEM-1024 | Encapsulation | -12.6 | -51.99 | CRITICAL |
| ML-KEM-1024 | Decapsulation | -0.4 | 3.88 | PASS |
Root cause confirmed: OQS_randombytes() inside OQS_KEM_encaps() has variable timing. Derand confirmation — bypassing RNG with encaps_derand() drops both portable C (t=0.66) and AVX2 (t=0.93) to PASS. The cryptographic core is clean. Fix: use OQS_KEM_encaps_derand() with a pre-seeded DRBG.
Note: This finding disappears through OpenSSL+oqs-provider, which uses OpenSSL's RAND_bytes instead of OQS_randombytes. Enterprises using oqs-provider are not affected by this specific issue.
Finding 2 — FALCON Verify (Algorithmic), liboqs 0.15.0 + OpenSSL+oqs-provider
| Algorithm | Operation | t (liboqs bare metal) | t (OpenSSL bare metal) | Verdict |
|---|---|---|---|---|
| FALCON-512 | Sign | 6.38 | — | PASS |
| FALCON-512 | Verify | -217.58 | -192.33 | CRITICAL |
| FALCON-1024 | Sign | 0.77 | — | PASS |
| FALCON-1024 | Verify | -303.05 | -343.24 | CRITICAL |
| FALCON-padded-512 | Verify | -168.63 | -255.66 | CRITICAL |
| FALCON-padded-1024 | Verify | -187.19 | -292.10 | CRITICAL |
Root cause: Golomb-Rice signature decompression in comp_decode() (codec.c). The inner unary decoding loop iterates a variable number of times proportional to coefficient magnitude — producing signature-content dependent timing. Confirmed across both library targets. No existing fix — a constant-time comp_decode() is required.
Root cause investigation:
- Same message, different signatures → still CRITICAL (t=-53.86) — not message-dependent
- Padded variants with fixed-length signatures → still CRITICAL — not length-dependent
- Verify has no RNG call — cannot be RNG-induced
- No open issue on liboqs GitHub for this finding (as of April 2026)
Finding 3 — ML-DSA Verify (Weak signal), bare metal only
| Algorithm | Operation | t (liboqs bare metal) | t (OpenSSL bare metal) | Verdict |
|---|---|---|---|---|
| ML-DSA-44 | Verify | -5.41 | -9.28 | CRITICAL |
| ML-DSA-65 | Verify | -5.66 | -9.04 | CRITICAL |
| ML-DSA-87 | Verify | -4.51 | -10.38 | CRITICAL |
| ML-DSA-44/65/87 | Sign | all PASS | all PASS | PASS |
Note: This signal is weak (t ≈ 5-10) and only detectable on bare metal at 100k traces. Not detected on WSL2. Root cause under investigation.
FIPS 205 — SLH-DSA (all parameter sets)
SLH-DSA verify shows variable timing consistent with the inherently variable-time Merkle tree traversal in hash-based signatures. Expected behavior by design — not an implementation vulnerability.
OpenSSL+oqs-provider Summary (bare metal, 100k traces)
| Algorithm | Operation | t-statistic | Verdict |
|---|---|---|---|
| ML-KEM-512/768/1024 | Encapsulation | ≈ 2-5 | PASS |
| FALCON-512 | Verify | -192.33 | CRITICAL |
| FALCON-1024 | Verify | -343.24 | CRITICAL |
| FALCON-padded-512 | Verify | -255.66 | CRITICAL |
| FALCON-padded-1024 | Verify | -292.10 | CRITICAL |
| ML-DSA-44/65/87 | Verify | -9 to -10 | CRITICAL |
C-level noise floor (AES-128): 11.7ns. FALCON verify finding is 500× above noise floor.
How It Works
The scanner implements Test Vector Leakage Assessment (TVLA) as defined in ISO/IEC 17825. For each cryptographic operation:
- Collects timing measurements for a fixed input (same bytes every time)
- Collects timing measurements for random inputs (different bytes every time)
- Runs Welch's t-test on the two distributions
- Flags as leaking if |t| > 4.5 — the ISO 17825 threshold
Two measurement layers:
- Python layer — targets kyber-py and dilithium-py via
time.perf_counter_ns(), detects millisecond-level leaks - C layer — targets liboqs and OpenSSL directly with
CLOCK_MONOTONIC_RAW, detects nanosecond-level leaks
Installation
Quickstart
pip install pqc-scanner
Docker (no setup required)
docker build -t pqc-scanner .
docker run pqc-scanner python3 cli.py liboqs --traces 50000
docker run pqc-scanner python3 cli.py openssl --algorithm falcon --traces 100000
No liboqs installation, no gcc, no Python setup required. Everything is bundled.
From source (recommended for full C harness support)
Requirements
- Ubuntu 20.04+ or WSL2 (Ubuntu 24.04 tested)
- Python 3.10+
- gcc and clang
- liboqs (instructions below)
- oqs-provider (for OpenSSL target)
1. Clone and set up
git clone https://github.com/Disha23112004/pqc-scanner
cd pqc-scanner
python3 -m venv venv
source venv/bin/activate
pip install -e .
pip install kyber-py dilithium-py scipy numpy click rich reportlab
2. Install liboqs
sudo apt install -y cmake ninja-build libssl-dev clang
git clone --depth=1 https://github.com/open-quantum-safe/liboqs ~/liboqs
cmake -S ~/liboqs -B ~/liboqs/build -DBUILD_SHARED_LIBS=ON -GNinja
cmake --build ~/liboqs/build --parallel 4
sudo cmake --build ~/liboqs/build --target install
sudo ldconfig
3. Install oqs-provider (for OpenSSL target)
git clone --depth=1 https://github.com/open-quantum-safe/oqs-provider ~/oqs-provider
cmake -S ~/oqs-provider -B ~/oqs-provider/build \
-DOPENSSL_ROOT_DIR=/usr \
-Dliboqs_DIR=/usr/local/lib/cmake/liboqs -GNinja
cmake --build ~/oqs-provider/build --parallel 4
sudo cmake --build ~/oqs-provider/build --target install
4. Build C harnesses
cd harness
gcc -O2 -o ml_kem_harness ml_kem_harness.c -loqs -lm
gcc -O2 -o ml_dsa_harness ml_dsa_harness.c -loqs -lm
gcc -O2 -o baseline_harness baseline_harness.c -lssl -lcrypto
gcc -O2 -o openssl_kem_harness openssl_kem_harness.c -lssl -lcrypto -lm
gcc -O2 -o openssl_sig_harness openssl_sig_harness.c -lssl -lcrypto -lm
cd ..
Usage
source venv/bin/activate
liboqs scan (primary target)
# ML-KEM-768 production scan
pqc-scanner liboqs --traces 100000
# RNG root cause isolation
pqc-scanner derand --traces 100000
OpenSSL+oqs-provider scan (second target)
# Scan all algorithms
pqc-scanner openssl --traces 100000
# Scan specific algorithm family
pqc-scanner openssl --algorithm falcon --traces 100000
pqc-scanner openssl --algorithm ml-kem --traces 100000
pqc-scanner openssl --algorithm ml-dsa --traces 100000
Python layer scans
# Scan ML-KEM and ML-DSA Python implementations
pqc-scanner scan --algorithm both --traces 10000
# Side by side comparison
pqc-scanner compare --traces 10000
Noise floor characterization
pqc-scanner baseline # Python layer (SHA-256)
pqc-scanner c-baseline # C layer (AES-128)
Compiler sweep
pqc-scanner compiler-sweep --traces 50000 --runs 3
Output
Every scan automatically generates three output files:
- JSON — structured findings for CI/CD integration (exit code 1 on findings)
- HTML — interactive report with timing distribution histograms
- PDF — professional compliance report for CISO and auditor review
pqc-scanner liboqs --traces 100000
# Generates: liboqs.json, liboqs.html, liboqs.pdf
pqc-scanner openssl --algorithm falcon --traces 100000
# Generates: openssl_scan.json, openssl_scan.pdf
SARIF output (GitHub Advanced Security)
pqc-scanner liboqs --traces 100000 --format sarif --output results
CI/CD Integration
- name: PQC Timing Scan
run: pqc-scanner liboqs --traces 50000
# Pipeline fails automatically if leaks are found
Use --no-exit to suppress exit code in reporting-only mode.
Methodology
TVLA (Test Vector Leakage Assessment)
Documented in ISO/IEC 17825. Core principle: a constant-time implementation's execution time must be statistically independent of its input.
Welch's t-test
Used instead of Student's t-test because it does not assume equal variance. Threshold: |t| > 4.5 indicates statistically significant timing leakage at p < 0.001.
Derandomization (derand) technique
A novel root-cause isolation method introduced in this tool. By calling encaps_derand() directly with a fixed seed, bypassing OQS_randombytes() entirely, RNG-induced timing variation is eliminated. Comparing standard TVLA against derand results distinguishes algorithmic from RNG-induced leakage — not present in any prior published tooling.
Measurement precision
- Python layer —
time.perf_counter_ns(), noise floor ~100–130ns on WSL2 - C layer —
CLOCK_MONOTONIC_RAW, noise floor ~10–12ns on WSL2, lower on bare metal
Noise reduction
- 500-trace warmup — stabilizes CPU cache, branch predictor, and TLB state
- 5% outlier trimming — removes OS interrupt spikes and context switches
- CPU affinity — pins process to core 0 via
sched_setaffinity - Pre-generated test vectors — all random inputs generated before the timed window
Project Structure
pqc-scanner/
├── cli.py
├── src/pqc_scanner/
│ ├── scanner/
│ │ ├── timing.py ← TVLA engine, Welch's t-test
│ │ ├── report.py ← JSON and HTML generation
│ │ ├── pdf_report.py ← PDF compliance report generation
│ │ └── sarif.py ← SARIF 2.1.0 output
│ └── targets/
│ ├── ml_kem_target.py ← ML-KEM Python harness
│ ├── ml_dsa_target.py ← ML-DSA Python harness
│ ├── baseline_target.py ← SHA-256 control
│ └── liboqs_target.py ← liboqs subprocess interface
└── harness/
├── ml_kem_harness.c ← ML-KEM 512/768/1024
├── ml_dsa_harness.c ← ML-DSA + FALCON + SLH-DSA
├── ml_kem_harness_derand.c ← RNG root cause isolation
├── baseline_harness.c ← AES-128 noise floor
├── openssl_kem_harness.c ← OpenSSL EVP KEM harness
└── openssl_sig_harness.c ← OpenSSL EVP signature harness
Comparison With Existing Tools
| Capability | dudect | Riscure | pqc-scanner |
|---|---|---|---|
| Full NIST PQC suite (FIPS 203-206) | ✗ | ✗ | ✓ |
| liboqs integration | ✗ | ✗ | ✓ |
| OpenSSL+oqs-provider target | ✗ | ✗ | ✓ |
| Derand root-cause isolation | ✗ | ✗ | ✓ |
| PDF compliance reports | ✗ | ✓ (hardware) | ✓ |
| CI/CD exit codes + SARIF | ✗ | ✗ | ✓ |
| Docker support | ✗ | ✗ | ✓ |
| No hardware required | ✓ | ✗ | ✓ |
| pip installable | ✗ | ✗ | ✓ |
| Open source | ✓ | ✗ | ✓ |
Related Work
- dudect — Reparaz et al. (2016). General-purpose constant-time testing library. No PQC targets.
- KyberSlash — Bernstein et al. (2024). Manual timing analysis of Kyber implementations.
- TVLA — Cryptography Research Inc. (2011). Leakage assessment methodology, standardized in ISO/IEC 17825.
- liboqs — Open Quantum Safe project. C library for post-quantum cryptographic algorithms.
Responsible Disclosure
- ML-KEM finding — Disclosed to Open Quantum Safe project (security@openquantumsafe.org), April 2026
- FALCON verify finding — Disclosed to PQShield (security@pqshield.com), April 2026
Limitations
- WSL2 hypervisor adds noise. Sub-50ns findings require bare metal Linux for confirmation.
- kyber-py and dilithium-py are non-constant-time reference implementations. Python layer findings are expected and not novel.
- A PASS result does not prove constant-time behavior — it means no significant signal was detected at the given trace count and threshold.
- TVLA detects first-order leakage. Higher-order leakage in masked implementations requires higher-order TVLA extensions not yet implemented.
License
MIT License. See LICENSE for details.
Citation
If you use this tool in research, please cite:
Disha P, "PQC Side-Channel Scanner: Automated TVLA-based timing analysis for
NIST post-quantum cryptography standards," 2026.
https://github.com/Disha23112004/pqc-scanner
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pqc_scanner-1.3.2.tar.gz.
File metadata
- Download URL: pqc_scanner-1.3.2.tar.gz
- Upload date:
- Size: 41.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4b8593209c1e2d93e6a199498ab18cdabc858bebd034fb35873c61b2f2dbc13c
|
|
| MD5 |
a23cddb11833c1b61ad8e3db55aabc16
|
|
| BLAKE2b-256 |
d3585f5bc38f2e34f370cd31639e0ffe9a5a5a0e77d37bb36b52295ac66c4574
|
File details
Details for the file pqc_scanner-1.3.2-py3-none-any.whl.
File metadata
- Download URL: pqc_scanner-1.3.2-py3-none-any.whl
- Upload date:
- Size: 40.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
881320c4da09c7eabb761698cbb9b30ba16df58d587055aae9029a60e9d823e2
|
|
| MD5 |
2b83d8753024ccef7fedc429a3b7f552
|
|
| BLAKE2b-256 |
176431cad78faddc6d2d5e29e6a66466f401a1f24dcdcb0c46166e0aee1921f7
|