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

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:

  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

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.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_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 (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


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.1.3.tar.gz (36.0 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.1.3-py3-none-any.whl (35.0 kB view details)

Uploaded Python 3

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

Hashes for pqc_scanner-1.1.3.tar.gz
Algorithm Hash digest
SHA256 c1a23212e084ad9a3e022bb5b93e7b57a6366798f4f9983e98218f7b4426f610
MD5 47189452fe31662af3f0b32714634e94
BLAKE2b-256 89e725ab5830bcaeb53b9bee856e59ccf1f91ca79ca80fc78aa27ef11d6d08a9

See more details on using hashes here.

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

Hashes for pqc_scanner-1.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 eaecc69466b16dc263b13f33128087f084c56741ba4961356da3f4e654fe8bcc
MD5 7b863d1abbbcbcc1cc50410499040596
BLAKE2b-256 d9fd63b3ad8d16380a0a3500a2c2970128f23e9b8f35ad223fbde54e46d5718f

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