Skip to main content

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

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.2.tar.gz (41.4 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.2-py3-none-any.whl (40.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pqc_scanner-1.3.2.tar.gz
  • Upload date:
  • Size: 41.4 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.2.tar.gz
Algorithm Hash digest
SHA256 4b8593209c1e2d93e6a199498ab18cdabc858bebd034fb35873c61b2f2dbc13c
MD5 a23cddb11833c1b61ad8e3db55aabc16
BLAKE2b-256 d3585f5bc38f2e34f370cd31639e0ffe9a5a5a0e77d37bb36b52295ac66c4574

See more details on using hashes here.

File details

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

File metadata

  • Download URL: pqc_scanner-1.3.2-py3-none-any.whl
  • Upload date:
  • Size: 40.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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 881320c4da09c7eabb761698cbb9b30ba16df58d587055aae9029a60e9d823e2
MD5 2b83d8753024ccef7fedc429a3b7f552
BLAKE2b-256 176431cad78faddc6d2d5e29e6a66466f401a1f24dcdcb0c46166e0aee1921f7

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.3.2 This release

2 files

1.3.1

2 files

1.3.0

2 files

1.2.0

2 files

1.1.3

2 files

1.1.2

2 files

1.1.1

2 files

1.1.0

2 files

1.0.1

2 files

1.0.0

2 files

Supported by

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