Automated timing side-channel scanner for NIST PQC standards ML-KEM and ML-DSA
Project description
PQC Side-Channel Scanner
An open-source automated timing side-channel scanner for NIST post-quantum cryptography standards. Detects constant-time violations in ML-KEM (FIPS 203) and ML-DSA (FIPS 204) implementations using TVLA statistical methodology.
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 and ML-DSA right now to protect against future quantum attacks. But timing side-channel vulnerabilities in cryptographic implementations can leak secret key material to an attacker — 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 by years. No open-source automated scanner existed for FIPS 203 and FIPS 204 specifically.
This tool fills that gap.
What It Found
Python layer — ML-KEM-768 (kyber-py)
| Operation | t-statistic | Verdict |
|---|---|---|
| Decapsulation | -3868 | CRITICAL LEAK |
| Encapsulation | -3377 | CRITICAL LEAK |
| KeyGen | 3.1 | PASS |
Note: kyber-py is a documented non-constant-time reference implementation. These findings are expected and not novel.
Python layer — ML-DSA-65 (dilithium-py)
| Operation | t-statistic | Verdict |
|---|---|---|
| Sign | 0.4 | PASS |
| Verify | 15.4 | LEAK |
| KeyGen | 7.1 | LEAK |
C layer — liboqs 0.15.0 ML-KEM-768, Portable C build (CLOCK_MONOTONIC_RAW, 100k traces)
| Operation | t-statistic | Delta | Verdict |
|---|---|---|---|
| Encapsulation | 75.5 | 264ns | CRITICAL |
| Decapsulation | -12.4 | 37ns | LEAK |
C layer — liboqs 0.15.0 ML-KEM-768, AVX2 build (100k traces)
| Operation | t-statistic | Delta | Verdict |
|---|---|---|---|
| Encapsulation | -7.5 | 600ns | CRITICAL |
| Decapsulation | -0.4 | 27ns | PASS |
Root cause analysis — encaps_derand (RNG bypassed, 100k traces)
| Build | t-statistic | Delta | Verdict |
|---|---|---|---|
| Portable C | -4.3 | -1112ns | PASS |
| AVX2 | 3.7 | 262ns | PASS |
C layer — liboqs 0.15.0 ML-DSA-65 (100k traces)
| Operation | Build | Verdict |
|---|---|---|
| Sign | Portable C | PASS |
| Verify | Portable C | PASS |
| Sign | AVX2 | PASS |
| Verify | AVX2 | PASS |
C-level noise floor (AES-128): 11.7ns. ML-KEM encapsulation finding is 22× above noise floor.
Interpretation: The ML-KEM encapsulation signal in both portable C and AVX2 builds is RNG-induced — the OQS_randombytes() call inside OQS_KEM_encaps() has variable timing that produces a statistically significant TVLA signal. When the derand harness is used (bypassing the OS RNG with a fixed-seed DRBG), both builds drop to PASS. The cryptographic core is clean. The default public API leaks. The decapsulation finding at t=-12.4 persists on bare metal Linux and is under active investigation with the OQS project. ML-DSA-65 is clean at the C layer across all operations and both builds.
How It Works
The scanner implements Test Vector Leakage Assessment (TVLA) — the same methodology used by Riscure and documented in ISO/IEC 17825. For each cryptographic operation it:
- Collects timing measurements for a fixed input (same value every time)
- Collects timing measurements for random inputs (different every time)
- Runs Welch's t-test on the two distributions
- Flags as leaking if |t| > 4.5 — the ISO 17825 threshold
The scanner operates at two measurement layers:
- Python layer — targets kyber-py and dilithium-py via
time.perf_counter_ns(), detects millisecond-level leaks - C layer — targets liboqs directly with
CLOCK_MONOTONIC_RAW, detects nanosecond-level leaks
Installation
Quickstart
pip install pqc-scanner
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)
1. Clone the repository
git clone https://github.com/Disha23112004/pqc-scanner
cd pqc-scanner
2. Set up Python environment
python3 -m venv venv
source venv/bin/activate
pip install kyber-py dilithium-py scipy numpy click rich
3. 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
4. Build the C harnesses
cd harness
# Production scanner (liboqs ML-KEM-768)
gcc -O2 -o ml_kem_harness ml_kem_harness.c -loqs -lssl -lcrypto
# Noise floor baseline (AES-128)
gcc -O2 -o baseline_harness baseline_harness.c -lssl -lcrypto
cd ..
Usage
Always activate the virtual environment first:
source venv/bin/activate
Production scan (liboqs C library) — primary scan
python cli.py liboqs --traces 100000
Uses CLOCK_MONOTONIC_RAW via C harness for nanosecond precision. 100,000 traces recommended for statistical confidence.
Scan ML-KEM and ML-DSA (Python targets)
python cli.py scan --algorithm both --traces 10000 --pin-cpu
Options:
--algorithm—ml-kem,ml-dsa, orboth--traces— number of measurements per operation (default 10000)--pin-cpu— pin process to CPU core 0 for lower noise--no-exit— do not exit with code 1 on findings (for scripting)--open-html— open HTML report in browser automatically
Algorithm comparison
python cli.py compare --traces 10000 --pin-cpu
Scans ML-KEM-768 and ML-DSA-65 side by side. Produces a comparison HTML report with bar chart.
Python noise floor characterization
python cli.py baseline --traces 10000 --pin-cpu
Runs SHA-256 as a control experiment. SHA-256 is provably constant-time so any signal here is measurement noise.
C-level noise floor characterization
python cli.py c-baseline --traces 100000
Runs AES-128 (AES-NI hardware instruction) through the same C harness. Reports the noise floor delta so you can assess signal-to-noise ratio of liboqs findings.
RNG root cause isolation (derand)
python cli.py derand --traces 100000 Bypasses the OS RNG using encaps_derand() with a fixed seed. PASS = leak is RNG-induced. LEAK = algorithmic. Run after liboqs scan to confirm root cause.
Compiler flag sweep
python cli.py compiler-sweep --traces 50000 --runs 3
Builds the C harness with six compiler flag combinations and scans each. Searches the same optimization space where KyberSlash was found.
Output
Every scan produces:
*.json— structured findings for CI/CD integration*.html— interactive report with timing distribution histograms
JSON format
{
"scanner": "pqc-scanner v1.1.2",
"target": "liboqs ML-KEM-768",
"summary": {
"total": 1,
"critical": 1,
"high": 0,
"medium": 0
},
"findings": [
{
"operation": "ML-KEM Encapsulation",
"t_statistic": 75.5185,
"p_value": 0.0,
"delta_ns": 264.0,
"severity": "CRITICAL",
"verdict": "LEAK DETECTED"
}
]
}
CI/CD Integration
The scanner exits with code 1 when findings are detected:
# GitHub Actions example
- name: PQC Timing Scan
run: |
source venv/bin/activate
python cli.py liboqs --traces 50000
# Pipeline fails automatically if leaks are found
Use --no-exit to suppress the exit code in reporting-only mode.
SARIF output (GitHub Advanced Security)
python cli.py liboqs --traces 100000 --format sarif --output results
- name: PQC Timing Scan
run: python cli.py liboqs --traces 50000 --no-exit --format sarif --output pqc
- name: Upload to GitHub Security
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: pqc.sarif
Findings appear under Security → Code scanning in your repository.
Methodology
TVLA (Test Vector Leakage Assessment)
Developed by Cryptography Research Inc. and documented in ISO/IEC 17825. The core principle: if an implementation is constant-time, its execution time should be statistically independent of the input.
Welch's t-test
Used instead of Student's t-test because it does not assume equal variance between distributions. Threshold: |t| > 4.5 indicates statistically significant timing leakage.
Derandomization (derand) technique
A novel root-cause isolation method introduced in this tool. By substituting the system RNG with a fixed-seed DRBG via OQS_randombytes_custom_algorithm() before measurement, RNG-induced timing variation is eliminated from the measurement window. Comparing standard TVLA results against derand results distinguishes algorithmic leakage from RNG-induced leakage — a distinction critical for correct remediation. This technique is not present in prior published tooling.
Measurement precision
- Python layer —
time.perf_counter_ns(), noise floor ~100–130ns on WSL2 - C layer —
CLOCK_MONOTONIC_RAWviaclock_gettime(), noise floor ~10–12ns on WSL2
Noise reduction
- 500-trace warmup — discards initial measurements to stabilize CPU cache and branch predictor
- 5% outlier trimming — removes top 5% of measurements to eliminate OS interrupt spikes
- CPU affinity —
sched_setaffinitypins process to core 0 to reduce scheduler noise - Pre-generated test vectors — random inputs generated before the timed window so
os.urandom()does not contaminate measurements
WSL2 limitations
WSL2 adds ~10–130ns of hypervisor noise depending on system load. Sub-50ns signals require bare metal Linux for confirmation. The decapsulation finding (37ns delta) has been confirmed on bare metal Linux.
Project Structure
pqc-scanner/
├── cli.py ← all commands
├── requirements.txt
├── src/pqc_scanner/
│ ├── scanner/
│ │ ├── timing.py ← TVLA engine, warmup, outlier trim, CPU affinity
│ │ ├── static.py ← static analysis for crypto context
│ │ └── report.py ← JSON and HTML report generation
│ └── targets/
│ ├── ml_kem_target.py ← ML-KEM-768 Python harness (kyber-py)
│ ├── ml_dsa_target.py ← ML-DSA-65 Python harness (dilithium-py)
│ ├── baseline_target.py ← SHA-256 control experiment
│ └── liboqs_target.py ← liboqs subprocess interface
└── harness/
├── ml_kem_harness.c ← C timing harness, CLOCK_MONOTONIC_RAW
├── ml_kem_harness_derand.c ← derand harness for RNG root cause isolation
├── ml_dsa_harness.c ← ML-DSA-65 sign/verify harness
└── baseline_harness.c ← AES-128 noise floor harness
Comparison With Existing Tools
| Capability | dudect | Riscure | This tool |
|---|---|---|---|
| ML-KEM / ML-DSA targets | ✗ | ✗ | ✓ |
| liboqs integration | ✗ | ✗ | ✓ |
| Python + C dual layer | ✗ | ✗ | ✓ |
| Derand root-cause isolation | ✗ | ✗ | ✓ |
| Automated HTML reporting | ✗ | ✓ (hardware) | ✓ |
| CI/CD exit codes | ✗ | ✗ | ✓ |
| Noise floor validation | ✗ | ✓ (hardware) | ✓ |
| No hardware required | ✓ | ✗ | ✓ |
| Single command scan | ✗ | ✗ | ✓ |
| Open source | ✓ | ✗ | ✓ |
Related Work
- dudect — Reparaz et al. (2016). General-purpose constant-time testing library in C. Requires source-level integration. No PQC targets.
- KyberSlash — Bernstein et al. (2024). Manual timing analysis finding secret-dependent division leak in Kyber implementations.
- TVLA — Cryptography Research Inc. (2011). Original leakage assessment methodology, standardized in ISO/IEC 17825.
- ISO/IEC 17825:2024 — Updated standard for testing non-invasive attack mitigations in cryptographic modules.
- liboqs — Open Quantum Safe project. C library for post-quantum cryptographic algorithms.
Limitations
- WSL2 hypervisor adds noise. Sub-50ns findings require bare metal Linux for confirmation.
- kyber-py and dilithium-py are documented as non-constant-time reference implementations. Python layer findings for these targets are expected and not novel.
- liboqs findings are statistically confirmed. Exploitability in a real network attack scenario requires further research beyond timing measurement.
- Compiler sweep results below 50ns delta on WSL2 are marked inconclusive.
- TVLA detects first-order leakage. Higher-order leakage in masked implementations may require higher-order TVLA extensions not yet implemented.
- A PASS result does not prove constant-time behavior — it means no significant signal was detected at the given trace count and threshold.
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.1.3.tar.gz.
File metadata
- Download URL: pqc_scanner-1.1.3.tar.gz
- Upload date:
- Size: 36.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c1a23212e084ad9a3e022bb5b93e7b57a6366798f4f9983e98218f7b4426f610
|
|
| MD5 |
47189452fe31662af3f0b32714634e94
|
|
| BLAKE2b-256 |
89e725ab5830bcaeb53b9bee856e59ccf1f91ca79ca80fc78aa27ef11d6d08a9
|
File details
Details for the file pqc_scanner-1.1.3-py3-none-any.whl.
File metadata
- Download URL: pqc_scanner-1.1.3-py3-none-any.whl
- Upload date:
- Size: 35.0 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 |
eaecc69466b16dc263b13f33128087f084c56741ba4961356da3f4e654fe8bcc
|
|
| MD5 |
7b863d1abbbcbcc1cc50410499040596
|
|
| BLAKE2b-256 |
d9fd63b3ad8d16380a0a3500a2c2970128f23e9b8f35ad223fbde54e46d5718f
|