Skip to main content

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

Project description

PQC Side-Channel Scanner

CI PyPI version PyPI downloads License: MIT Python 3.10+

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

FIPS 203 — ML-KEM (all parameter sets), liboqs 0.15.0, 100k traces

Algorithm Operation t-statistic Delta Verdict
ML-KEM-512 Encapsulation -4.9 250ns CRITICAL
ML-KEM-512 Decapsulation 0.5 39ns PASS
ML-KEM-768 Encapsulation 75.5 264ns CRITICAL
ML-KEM-768 Decapsulation -3.2 39ns PASS
ML-KEM-1024 Encapsulation -12.6 1011ns CRITICAL
ML-KEM-1024 Decapsulation -0.4 45ns PASS

Bare metal confirmed (GitHub Actions ubuntu-latest):

Algorithm Operation t-statistic Delta Verdict
ML-KEM-512 Encapsulation -8.37 87ns CRITICAL
ML-KEM-768 Encapsulation -13.68 298ns CRITICAL
ML-KEM-1024 Encapsulation -51.99 517ns CRITICAL

Root cause — encaps_derand (RNG bypassed, same public key):

Build t-statistic Delta Verdict
Portable C 0.66 7ns PASS
AVX2 0.93 15ns PASS

Interpretation: Encapsulation leaks across all three ML-KEM parameter sets on both WSL2 and bare metal. Root cause confirmed as RNG-induced — OQS_randombytes() inside OQS_KEM_encaps() has variable timing. When bypassed via encaps_derand(), both builds pass. The cryptographic core is clean. Decapsulation passes on all parameter sets.


FIPS 204 — ML-DSA (all parameter sets), liboqs 0.15.0, 100k traces

Algorithm Operation t-statistic Verdict
ML-DSA-44 Sign 0.21 PASS
ML-DSA-44 Verify -0.59 PASS
ML-DSA-65 Sign 0.13 PASS
ML-DSA-65 Verify -1.02 PASS
ML-DSA-87 Sign -0.38 PASS
ML-DSA-87 Verify -0.64 PASS

Interpretation: ML-DSA is clean across all three parameter sets and both sign and verify operations.


FIPS 206 — FN-DSA/FALCON (all variants), liboqs 0.15.0, 100k traces

Algorithm Operation t-statistic Delta Verdict
FALCON-512 Sign 1.55 2186ns PASS
FALCON-512 Verify -6.92 / -10.04 ~6100ns CRITICAL
FALCON-1024 Sign 1.03 2940ns PASS
FALCON-1024 Verify -9.81 / -10.58 ~9500ns CRITICAL
FALCON-padded-512 Verify -9.95 7689ns CRITICAL
FALCON-padded-1024 Verify -11.97 9637ns CRITICAL

Root cause investigation:

  • Same message, different signatures → still CRITICAL (t=-53.86)
  • Padded variants (fixed-length signatures) → still CRITICAL
  • Verify has no RNG call — cannot be RNG-induced
  • Conclusion: Signature-content dependent timing in FALCON verify. Algorithmic leak.

Interpretation: FALCON verify leaks across all four variants including padded (fixed-length) variants. The leak persists with the same message and different signatures, confirming it is signature-content dependent, not message or length dependent. Signing passes on all variants. No open issue exists for this finding on the liboqs GitHub.


FIPS 205 — SLH-DSA (all parameter sets), liboqs 0.15.0, 50k traces

SLH-DSA verify shows variable timing across parameter sets (some CRITICAL, some PASS). This is consistent with the inherently variable-time nature of Merkle tree traversal in hash-based signatures and is expected behavior by design, not an implementation vulnerability.


C-level noise floor (AES-128): 11.7ns. ML-KEM encapsulation finding is 22× above noise floor on WSL2, confirmed on bare metal GitHub Actions runners.

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:

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

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 derand --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)

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:

  • --algorithmml-kem, ml-dsa, or both
  • --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.3",
  "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 calling encaps_derand() directly with a fixed 32-byte seed, bypassing OQS_randombytes() entirely, 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_RAW via clock_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 affinitysched_setaffinity pins 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 at t=-3.2 was not stable across runs and cannot be confirmed.


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


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.0.tar.gz (41.2 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.0-py3-none-any.whl (39.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pqc_scanner-1.3.0.tar.gz
  • Upload date:
  • Size: 41.2 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.0.tar.gz
Algorithm Hash digest
SHA256 250c1670a6531a47f7382bb0a831ee74b88ce1caa0ecd68c9a1f27230daa630d
MD5 a8a218d6544638399f2c0b26eebf53d0
BLAKE2b-256 d72ed1a7bd85304f4f3ba85792ff5fc0400929717216904f8f652ea4d6d7fb8c

See more details on using hashes here.

File details

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

File metadata

  • Download URL: pqc_scanner-1.3.0-py3-none-any.whl
  • Upload date:
  • Size: 39.6 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0d92a06cc101d81366b445b4a7c68f765991418cb94f7a4e31f680ae6f979157
MD5 0d2ad3396f9a414febfe6451600f7252
BLAKE2b-256 5e479e741d72989f5771fdb7a0b257a84f27999fb9d89b4fba12af94c06da18d

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