* 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>
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 implementationct_analyzer/tests/test_samples/decompose_constant_time.c- Constant-time C implementationct_analyzer/tests/test_samples/decompose_vulnerable.go- Vulnerable Go implementationct_analyzer/tests/test_samples/decompose_vulnerable.rs- Vulnerable Rust implementationct_analyzer/tests/test_samples/vulnerable.php- Vulnerable PHP implementationct_analyzer/tests/test_samples/vulnerable.ts- Vulnerable TypeScript implementationct_analyzer/tests/test_samples/vulnerable.py- Vulnerable Python implementationct_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
-
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
-
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.
-
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
-
Compiler Variations: Different compilers/versions may produce different assembly. Test with:
- Multiple optimization levels
- Multiple compilers
- Target production architectures
-
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.