Skip to main content

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

Project description

PyPI CI

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 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 algorithm itself is mathematically secure.

Existing tools like dudect require manual C 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.

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 22x above noise floor.

Interpretation: The ML-KEM encapsulation leak in both portable C and AVX2 builds is RNG-induced — the OS randombytes() call inside OQS_KEM_encaps() has variable timing that produces a statistically significant signal. When encaps_derand is used (bypassing the RNG), both builds pass. The cryptographic core is clean. The default public API leaks. 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 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. If |t| > 4.5 the implementation is leaking — its timing depends on the input, which means an attacker can learn information about secret keys

The scanner has two measurement layers:

  • Python layer — targets kyber-py and dilithium-py, detects millisecond-level leaks
  • C layer — targets liboqs directly with CLOCK_MONOTONIC_RAW, detects nanosecond-level leaks

Installation

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/Disha231102004/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

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

Production scan (liboqs C library)

python cli.py liboqs --traces 100000

This is the primary scan. Uses CLOCK_MONOTONIC_RAW via C harness for nanosecond precision. 100,000 traces recommended for statistical confidence.

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. Run this before reporting findings.

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 the signal-to-noise ratio of liboqs findings.

Compiler flag sweep

python cli.py compiler-sweep --traces 50000 --runs 3

Builds the C harness with six compiler flag combinations and scans each. Looking for optimization-induced timing leaks — the same search space where KyberSlash was found. Each config is run --runs times and the median t-statistic reported for stability.


Output

Every scan produces two files:

  • *.json — structured findings for CI/CD integration
  • *.html — interactive report with timing distribution charts

JSON format

{
  "scanner": "pqc-scanner v0.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, making it compatible with any CI pipeline:

# 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 when running in reporting-only mode.

SARIF output (GitHub Advanced Security)

Generate SARIF output for direct integration with GitHub Advanced Security, Microsoft Defender, and enterprise security tools:

python cli.py liboqs --traces 100000 --format sarif --output results

Upload to GitHub Security tab in Actions:

- 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@v3
  with:
    sarif_file: pqc.sarif

Findings will appear under the Security → Code scanning tab of your repository.


Methodology

TVLA (Test Vector Leakage Assessment)

Developed by Cryptography Research Inc. and documented in ISO 17825. The core principle: if an implementation is constant-time, its execution time should be statistically independent of the input. We test this by comparing timing distributions for fixed vs random inputs.

Welch's t-test

Used instead of Student's t-test because it does not assume equal variance between the two distributions. Threshold: |t| > 4.5 indicates a statistically significant timing difference, regardless of noise floor.

Measurement precision

  • Python layertime.perf_counter_ns(), noise floor ~100-130ns in WSL
  • C layerCLOCK_MONOTONIC_RAW via clock_gettime(), noise floor ~10-12ns in WSL

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 the 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

WSL limitations

WSL2 adds ~10-130ns of hypervisor noise depending on system load. This affects:

  • Decapsulation finding — 37ns delta is borderline, confirmed in 3 of 4 runs
  • Compiler sweep — results below 50ns delta are marked inconclusive

Production assessment of sub-50ns signals requires bare metal Linux.


Project Structure

pqc-scanner/
├── cli.py                      ← all commands
├── requirements.txt
├── 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
    └── 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
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. No PQC targets.
  • KyberSlash — Cryspen et al. (2023). Manual timing analysis finding division-based leak in Kyber reference implementation.
  • TVLA — Cryptography Research Inc. (2011). Original leakage assessment methodology.
  • ISO 17825 — Testing methods for the mitigation of non-invasive attack classes against cryptographic modules.

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 are expected and not novel.
  • liboqs findings are statistically confirmed. Exploitability in a real network attack requires further research.
  • Compiler sweep is inconclusive below 50ns on WSL. clang -O3 finding above 50ns is reproducible.

License

MIT License. See LICENSE for details.


Collective Qubits

This project was built by Disha P (@Disha23112004), 3rd year student at VIT, and Technical Director at Collective Qubits. Research conducted using TVLA methodology on liboqs 0.15.0.

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

Uploaded Python 3

File details

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

File metadata

  • Download URL: pqc_scanner-1.1.0.tar.gz
  • Upload date:
  • Size: 34.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.1.0.tar.gz
Algorithm Hash digest
SHA256 b0a9490764403a7cfd2159fbf80d2e41436d71fa16a1e0160fdd0f91e6ab2035
MD5 6fc4cf5f1e1bc3d4235aa6a39e7f5a75
BLAKE2b-256 80113703c40d5672cc09d417881c26c2bdd838bd4f4adf542a95b6bb59015e8d

See more details on using hashes here.

File details

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

File metadata

  • Download URL: pqc_scanner-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 34.1 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3cb523cebd21dcd45ce4e0ed514fe041133042bed51557807ac62b1a894a9743
MD5 7438894498cce5822179179c571d9276
BLAKE2b-256 a0309a96a00a96d71047a255abf4ece1241270fa1f36d7fb2ae17ea325192134

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