Files
kz-tob 9e3fd2f9e8 constant-time-analysis: pair ilspycmd's TFM with a matching runtime (#283)
* constant-time-analysis: pair ilspycmd's TFM with a matching runtime

The tool-store fallback in _get_il_output globbed for
`*/ilspycmd/*/tools/net8.0/any/ilspycmd.dll`. ilspycmd 9.x installs
under `tools/net9.0/`, so on any newer install the glob matched
nothing, the loop body never ran, and C# IL analysis fell through to
monodis and then to "IL disassembly tools not available" — while
`dotnet tool install -g ilspycmd`, the command that error recommends,
produces exactly the layout the glob could not see.

Widening the glob to net* alone would have been wrong. The next lines
run the assembly under a Homebrew dotnet@8 runtime, and that pairing is
deliberate: ilspycmd installs framework-dependent and .NET does not
roll forward across a major version by default, so a net9.0 assembly
will not start on an 8.0 runtime. A wide glob with a pinned runtime
turns a silent no-match into a silent failed exec.

So the TFM found on disk now selects the runtime. Candidates are tried
newest-first, sorting the parsed (major, minor) tuple rather than the
moniker string, since lexically "net10.0" sorts below "net8.0"; a TFM
whose runtime is absent falls back to an older one that has one. Store
entries that do not name a .NET runtime major (netstandard2.0, net48)
are skipped rather than producing a dotnet@netstandard2 path. The
arbitrary `break` after the first matching dll is gone.

Extracted to _il_via_versioned_runtime so _get_il_output stays flat.

TestCSharpILRuntimePairing covers it with a fake store and fake runtime
paths, needing no dotnet install. The suite was mutation-tested rather
than trusted green: against the old pinned glob two tests fail, and
against the naive widening four fail, including the one asserting a
net9.0 assembly is never handed to dotnet@8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* constant-time-analysis: don't depend on one Homebrew keg existing

Review P2 on #283: pairing the store TFM with /opt/homebrew/opt/dotnet@N
assumes a keg for that major exists, and if it does not the motivating
failure just moves from the glob to the exists() check.

`brew info dotnet@9` says the keg does exist — stable 9.0.120 — so the
ilspycmd 9.x case the PR was written for does work as shipped. The
general concern holds anyway: homebrew-core carries dotnet@6, dotnet@8
and dotnet@9 but dropped dotnet@7 at end of life, and no Linux
distribution ships a versioned keg at all.

So the exact-major keg is now preferred rather than required. After it
come the unversioned Homebrew prefixes, the official installer paths for
macOS and Linux, and the PATH dotnet, each tried with
DOTNET_ROLL_FORWARD=Major, which is what permits a net9.0 assembly to
start on a 10.x runtime. Roll-forward is set only on those candidates:
setting it on the exact-major keg would mask a genuinely mismatched pair
instead of letting it fail, which is the property the existing test
pins. The PATH entry resolves through shutil.which, since dotnet_path
defaults to the bare name "dotnet" and exists() on a bare name tests the
cwd.

The except clause widens from FileNotFoundError to OSError. This is P3,
included because the change makes it reachable: adding the Intel prefixes
means an Intel keg on an Apple Silicon box without Rosetta is now a live
candidate, and it raises OSError("Bad CPU type in executable") rather
than FileNotFoundError, which escaped the helper and aborted the whole
analysis.

Review P2 on references/vm-compiled.md: the only user-facing remedy still
said `brew install dotnet@8` and claimed the analyzer detects dotnet@8.
It now tells the reader to read the TFM out of the tool store, names the
keg gap, and explains that the roll-forward path means no install is
strictly required. The store lookup uses find rather than ls on a glob,
for the reason #282 documents.

Five tests added, each mutation-checked: dropping the generic tier,
setting roll-forward unconditionally, narrowing OSError back, and
deleting the DOTNET_ROOT line each fail exactly the test that covers
them. DOTNET_ROOT had no assertion before, so that line could be deleted
with the suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 11:05:27 -04:00
..

Constant-Time Analyzer (ct-analyzer)

A portable tool for detecting timing side-channel vulnerabilities in compiled cryptographic code. Analyzes assembly output from multiple compilers and architectures to detect instructions that could leak secret data through execution timing.

Background

Timing side-channel attacks exploit variations in execution time to extract secret information from cryptographic implementations. Common sources include:

  • Hardware division (DIV, IDIV): Execution time varies based on operand values
  • Floating-point operations (FDIV, FSQRT): Variable latency based on inputs
  • Conditional branches: Different execution paths have different timing

The infamous KyberSlash attack demonstrated how division instructions in post-quantum cryptographic implementations could be exploited to recover secret keys.

Features

  • Multi-language support: C, C++, Go, Rust, Swift, Java, Kotlin, C#, PHP, JavaScript, TypeScript, Python, Ruby
  • Multi-architecture support: x86_64, ARM64, ARM, RISC-V, PowerPC, s390x, i386
  • Multi-compiler support: GCC, Clang, Go compiler, Rustc, Swiftc
  • Bytecode support: Java/Kotlin (JVM), C# (CIL), PHP (VLD/opcache), JavaScript/TypeScript (V8 bytecode), Python (dis), Ruby (YARV)
  • Optimization-level testing: Test across O0-O3, Os, Oz
  • Multiple output formats: Text, JSON, GitHub Actions annotations
  • Cross-compilation: Analyze code for different target architectures

Quick Start

# Install
uv tool install .

# Analyze a C file
ct-analyzer crypto.c

Components

The rest of this README documents ct-analyzer, the CLI. Inside Claude Code the plugin offers two more entry points:

Component Name Purpose
Skill constant-time-analysis Triggers on its own when you write or review crypto code, or ask about a timing side channel. Invoke directly with /constant-time-analysis:constant-time-analysis. Adds the per-language guidance under skills/constant-time-analysis/references/ and judgment about triaging what the analyzer reports
Command /constant-time-analysis:ct-check One-shot run over a file — <source-file> [--warnings] [--json] [--arch <arch>]

Usage

Basic Analysis

ct-analyzer <source_file>

Options

Option Description
--arch, -a Target architecture (x86_64, arm64, arm, riscv64, ppc64le, s390x, i386)
--compiler, -c Compiler to use (gcc, clang, go, rustc)
--opt-level, -O Optimization level (O0, O1, O2, O3, Os, Oz) - default: O2
--warnings, -w Include conditional branch warnings
--func, -f Regex pattern to filter functions
--json Output JSON format
--github Output GitHub Actions annotations
--list-arch List supported architectures

Examples

# Test with different optimization levels
ct-analyzer --opt-level O0 crypto.c
ct-analyzer --opt-level O3 crypto.c

# Cross-compile for ARM64
ct-analyzer --arch arm64 crypto.c

# Include conditional branch warnings
ct-analyzer --warnings crypto.c

# Analyze specific functions
ct-analyzer --func 'decompose|sign' crypto.c

# JSON output for CI
ct-analyzer --json crypto.c

# Analyze Go code
ct-analyzer crypto.go

# Analyze Rust code
ct-analyzer crypto.rs

# Analyze PHP code (requires PHP with VLD extension or opcache)
ct-analyzer crypto.php

# Analyze TypeScript (transpiles to JS first)
ct-analyzer crypto.ts

# Analyze JavaScript (uses V8 bytecode analysis)
ct-analyzer crypto.js

# Analyze Python (uses dis module for bytecode disassembly)
ct-analyzer crypto.py

# Analyze Ruby (uses YARV instruction dump)
ct-analyzer crypto.rb

Detected Vulnerabilities

Error-Level (Must Fix)

Category x86_64 ARM64 RISC-V
Integer Division DIV, IDIV, DIVQ, IDIVQ UDIV, SDIV DIV, DIVU, REM, REMU
FP Division DIVSS, DIVSD, DIVPS, DIVPD FDIV FDIV.S, FDIV.D
Square Root SQRTSS, SQRTSD, SQRTPS, SQRTPD FSQRT FSQRT.S, FSQRT.D

Warning-Level (Review Needed)

Conditional branches that may leak timing if condition depends on secret data:

  • x86: JE, JNE, JZ, JNZ, JA, JB, JG, JL, etc.
  • ARM: BEQ, BNE, CBZ, CBNZ, TBZ, TBNZ
  • RISC-V: BEQ, BNE, BLT, BGE

Scripting Language Support

PHP Analysis

PHP analysis uses either the VLD extension (recommended) or opcache debug output:

Detected PHP Vulnerabilities:

Category Pattern Recommendation
Division ZEND_DIV, ZEND_MOD Use Barrett reduction
Cache timing chr(), ord() Use pack('C', $int) / unpack('C', $char)[1]
Table lookups bin2hex(), hex2bin(), base64_encode() Use constant-time alternatives
Array access FETCH_DIM_R (secret index) Use constant-time table lookup
Bit shifts ZEND_SL, ZEND_SR (secret amount) Mask shift amount
Variable encoding pack(), serialize(), json_encode() Use fixed-length output
Weak RNG rand(), mt_rand(), uniqid() Use random_int() / random_bytes()
String comparison strcmp(), === on secrets Use hash_equals()

Installation:

# Install VLD extension (recommended)
# Query latest version from PECL. POSIX ERE, not `grep -P`: PCRE mode is a GNU
# extension that stock macOS grep rejects, leaving VLD_VERSION empty.
VLD_VERSION=$(curl -fsS https://pecl.php.net/package/vld |
  grep -oE 'vld-[0-9]+(\.[0-9]+)*\.tgz' | head -1 | sed -E 's/^vld-//; s/\.tgz$//')
[ -n "$VLD_VERSION" ] && pecl install channel://pecl.php.net/vld-${VLD_VERSION}

# Or build from source (if PECL fails)
git clone https://github.com/derickr/vld.git && cd vld
phpize && ./configure && make && sudo make install

# Or use opcache (built-in, fallback)
# Enabled by default in PHP 7+

JavaScript/TypeScript Analysis

JavaScript analysis uses V8 bytecode via Node.js --print-bytecode. TypeScript files are automatically transpiled first.

Detected JS Vulnerabilities:

Category Pattern Recommendation
Division Div, Mod bytecodes Use constant-time multiply-shift
Array access LdaKeyedProperty (secret index) Use constant-time table lookup
Bit shifts ShiftLeft, ShiftRight (secret amount) Mask shift amount
Variable encoding TextEncoder, JSON.stringify(), btoa() Use fixed-length output
Weak RNG Math.random() Use crypto.getRandomValues() or crypto.randomBytes()
Variable latency Math.sqrt(), Math.pow() Avoid in crypto paths
String comparison === on secrets Use crypto.timingSafeEqual() (Node.js)
Early-exit search indexOf(), includes() Use constant-time comparison

Requirements:

# Node.js required
node --version

# TypeScript compiler (optional, for .ts files)
npm install -g typescript

Python Analysis

Python analysis uses the built-in dis module to analyze CPython bytecode.

Detected Python Vulnerabilities:

Category Pattern Recommendation
Division BINARY_OP 11 (/), BINARY_OP 6 (%) Use Barrett reduction or constant-time alternatives
Array access BINARY_SUBSCR (secret index) Use constant-time table lookup
Bit shifts BINARY_LSHIFT, BINARY_RSHIFT (secret amount) Mask shift amount
Variable encoding int.to_bytes(), json.dumps(), base64.b64encode() Use fixed-length output
Weak RNG random.random(), random.randint() Use secrets.token_bytes() / secrets.randbelow()
Variable latency math.sqrt(), math.pow() Avoid in crypto paths
String comparison == on secrets Use hmac.compare_digest()
Early-exit search .find(), .startswith() Use constant-time comparison

Requirements:

# Python 3.x required (built-in dis module)
uv run python --version

Ruby Analysis

Ruby analysis uses YARV (Yet Another Ruby VM) bytecode via ruby --dump=insns.

Detected Ruby Vulnerabilities:

Category Pattern Recommendation
Division opt_div, opt_mod Use constant-time alternatives
Array access opt_aref (secret index) Use constant-time table lookup
Bit shifts opt_lshift, opt_rshift (secret amount) Mask shift amount
Variable encoding pack(), to_json(), Base64.encode64() Use fixed-length output
Weak RNG rand(), Random.new Use SecureRandom.random_bytes()
Variable latency Math.sqrt() Avoid in crypto paths
String comparison == on secrets Use Rack::Utils.secure_compare() or OpenSSL
Early-exit search .include?(), .start_with?() Use constant-time comparison

Requirements:

# Ruby required (YARV is standard since Ruby 1.9)
ruby --version

Example Output

============================================================
Constant-Time Analysis Report
============================================================
Source: decompose.c
Architecture: arm64
Compiler: clang
Optimization: O2
Functions analyzed: 4
Instructions analyzed: 88

VIOLATIONS FOUND:
----------------------------------------
[ERROR] SDIV
  Function: decompose_vulnerable
  Reason: SDIV has early termination optimization; execution time depends on operand values

[ERROR] SDIV
  Function: use_hint_vulnerable
  Reason: SDIV has early termination optimization; execution time depends on operand values

----------------------------------------
Result: FAILED
Errors: 2, Warnings: 0

Fixing Violations

Replace Division with Barrett Reduction

// VULNERABLE
int32_t q = a / divisor;

// SAFE: Barrett reduction
// Precompute: mu = ceil(2^32 / divisor)
uint32_t q = (uint32_t)(((uint64_t)a * mu) >> 32);

Replace Branches with Constant-Time Selection

// VULNERABLE
if (secret) {
    result = a;
} else {
    result = b;
}

// SAFE: Constant-time selection
uint32_t mask = -(uint32_t)(secret != 0);
result = (a & mask) | (b & ~mask);

Replace Comparisons

// VULNERABLE
if (memcmp(a, b, len) == 0) { ... }

// SAFE: Use crypto/subtle or equivalent
if (subtle.ConstantTimeCompare(a, b) == 1) { ... }

Test Samples

The repository includes test samples demonstrating vulnerable and secure implementations:

  • ct_analyzer/tests/test_samples/decompose_vulnerable.c - Vulnerable C implementation
  • ct_analyzer/tests/test_samples/decompose_constant_time.c - Constant-time C implementation
  • ct_analyzer/tests/test_samples/decompose_vulnerable.go - Vulnerable Go implementation
  • ct_analyzer/tests/test_samples/decompose_vulnerable.rs - Vulnerable Rust implementation
  • ct_analyzer/tests/test_samples/vulnerable.php - Vulnerable PHP implementation
  • ct_analyzer/tests/test_samples/vulnerable.ts - Vulnerable TypeScript implementation
  • ct_analyzer/tests/test_samples/vulnerable.py - Vulnerable Python implementation
  • ct_analyzer/tests/test_samples/vulnerable.rb - Vulnerable Ruby implementation

These implement the Decompose and UseHint algorithms from ML-DSA (FIPS-204) as test cases.

CI Integration

GitHub Actions

name: Constant-Time Check

on: [push, pull_request]

jobs:
  ct-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          uv tool install .
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      - name: Check constant-time properties
        run: |
          ct-analyzer --github src/crypto/*.c

GitLab CI

ct-check:
  stage: test
  script:
    - uv tool install .
    - export PATH="$HOME/.local/bin:$PATH"
    - ct-analyzer --json src/crypto/*.c > ct-report.json
  artifacts:
    reports:
      codequality: ct-report.json

Limitations

  1. Compiler Output Analysis: Analyzes what the compiler produces, not runtime behavior. Cannot detect:

    • Cache timing attacks from memory access patterns
    • Microarchitectural side-channels (Spectre, etc.)
    • Processor-specific optimizations
  2. No Data Flow Analysis: Flags all dangerous instructions regardless of whether they operate on secret data. Manual review is needed to determine if flagged code handles secrets. This means false positives are expected - for example, division used in loop bounds with public constants will be flagged even though it's not a vulnerability.

  3. False Positive Verification: For each flagged violation, verify the operands:

    • If operands are compile-time constants or public parameters → likely false positive
    • If operands are derived from keys, plaintext, or secrets → true positive
    • See the SKILL.md documentation for detailed triage guidance
  4. Compiler Variations: Different compilers/versions may produce different assembly. Test with:

    • Multiple optimization levels
    • Multiple compilers
    • Target production architectures
  5. Scripting Languages: PHP, JavaScript/TypeScript, Python, and Ruby are supported via bytecode analysis.

Running Tests

uv run python ct_analyzer/tests/test_analyzer.py

References

Acknowledgments

Based on the test_ct utility created for ML-DSA.