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
Metadata
Release files for pqc-scanner 1.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pqc_scanner-1.3.2.tar.gz | 41.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pqc_scanner-1.3.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 82.1 kB
Release files / pqc_scanner-1.3.2.tar.gz
| Download URL | pqc_scanner-1.3.2.tar.gz |
|---|---|
| Size | 41.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4b8593209c1e2d93e6a199498ab18cdabc858bebd034fb35873c61b2f2dbc13c
|
|
BLAKE2b-256 checksum How to use checksums |
d3585f5bc38f2e34f370cd31639e0ffe9a5a5a0e77d37bb36b52295ac66c4574
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / pqc_scanner-1.3.2-py3-none-any.whl
| Download URL | pqc_scanner-1.3.2-py3-none-any.whl |
|---|---|
| Size | 40.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
881320c4da09c7eabb761698cbb9b30ba16df58d587055aae9029a60e9d823e2
|
|
BLAKE2b-256 checksum How to use checksums |
176431cad78faddc6d2d5e29e6a66466f401a1f24dcdcb0c46166e0aee1921f7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|