Skip to main content

Automated timing side-channel scanner for NIST PQC standards ML-KEM and ML-DSA

Project description

PQC Side-Channel Scanner

PyPI version PyPI downloads License: MIT Python 3.10+

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:

  1. Collects timing measurements for a fixed input (same bytes every time)
  2. Collects timing measurements for random inputs (different bytes every time)
  3. Runs Welch's t-test on the two distributions
  4. 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


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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pqc_scanner-1.3.1.tar.gz (39.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pqc_scanner-1.3.1-py3-none-any.whl (38.9 kB view details)

Uploaded Python 3

File details

Details for the file pqc_scanner-1.3.1.tar.gz.

File metadata

  • Download URL: pqc_scanner-1.3.1.tar.gz
  • Upload date:
  • Size: 39.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pqc_scanner-1.3.1.tar.gz
Algorithm Hash digest
SHA256 adc53d5c286b6744e5820217c6210da79161a76ba03e93bf3c5d3c6921251d13
MD5 47baa2d1d972344bac3450545ef2c95a
BLAKE2b-256 b6429ce6b8f0c9cd94947483776ffa246476ac510d1d212bf55f0bb3aad4ced5

See more details on using hashes here.

File details

Details for the file pqc_scanner-1.3.1-py3-none-any.whl.

File metadata

  • Download URL: pqc_scanner-1.3.1-py3-none-any.whl
  • Upload date:
  • Size: 38.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for pqc_scanner-1.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9a041b7e9687d1055df6c2b40889d3efe2301964a3b1ff4a3701d8812c9d04a8
MD5 be997353f08523a1aabc04611725c42f
BLAKE2b-256 2f51e19f31dfe33a14dae34aa47162b4e7accc37d673f95d9ba1d64846b06ffb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page