SKILL·8F83E8

constant-time-analysis

trailofbits
Updated 14 days ago
43 views
7,515
642
7,515
View on GitHub
Developmentgeneral

About

This skill detects timing side-channel vulnerabilities in cryptographic implementations across multiple languages by analyzing compiled code for variable-time operations. It helps developers identify risks like secret-dependent branches or divisions when implementing or reviewing crypto functions. Use it during security reviews of encryption, signing, or key derivation code to find constant-time violations.

Quick Install

Claude Code

Recommended
Primary
npx skills add trailofbits/skills -a claude-code
Plugin CommandAlternative
/plugin add https://github.com/trailofbits/skills
Git CloneAlternative
git clone https://github.com/trailofbits/skills.git ~/.claude/skills/constant-time-analysis

Copy and paste this command in Claude Code to install this skill

Documentation

Constant-Time Analysis

Compile the code, inspect the emitted assembly or bytecode for variable-time instructions, then decide which of the flagged operations actually touch secrets. The compilation step is mechanical; the triage step is the work.

When to Use

  • Implementing or reviewing a signature, encryption, KEM, or key derivation routine
  • Code applies / or % to a value derived from a key, plaintext, nonce, or token
  • The user mentions "constant-time", "timing attack", "side-channel", or "KyberSlash"
  • Reviewing functions named sign, verify, encrypt, decrypt, derive_key

When NOT to Use

  • Measuring timing variance on a running binary — use the constant-time-testing skill from the testing-handbook-skills plugin, which covers dudect and statistical approaches and may not be installed. This skill inspects compiler output statically and never executes the code under test.
  • Non-cryptographic code, or crypto code where every input is public
  • High-level API usage where a vetted library owns the constant-time guarantees
  • Cache and other microarchitectural side channels — the assembly view cannot see them

Language Routing

Read the guide for the target language before interpreting any findings; each one lists that language's dangerous instructions and the idiomatic constant-time replacements.

GuideLanguages
references/compiled.mdC, C++, Go, Rust
references/swift.mdSwift
references/vm-compiled.mdJava, C#
references/kotlin.mdKotlin
references/php.mdPHP
references/javascript.mdJavaScript, TypeScript
references/python.mdPython
references/ruby.mdRuby

Running the Analyzer

The analyzer takes one file and detects the language from its extension. Always pass --warnings:

uv run {baseDir}/ct_analyzer/analyzer.py --warnings <source_file>

Without it the analyzer reports only error-severity findings, which means division, modulo and weak RNG. Four detector families are warning severity and stay silent: secret-dependent branches, early-exit comparison (memcmp, strcmp, .equals, ==), table lookups indexed by a secret, and variable-time encoding. Early-exit comparison of an authentication tag is the most common timing bug in real code — Lucky Thirteen was exactly that — so a default run is quiet about the finding you are most likely to have.

FlagEffect
--warningsAdd the four warning-severity families above. Pass it every time
--func <regex>Restrict output to function names matching the regex
--jsonMachine-readable output
--githubGitHub Actions annotations
--arch <target>Target architecture (x86_64, arm64, riscv64, ...) — native languages only
--opt-level <level>Optimization level (O0 through O3, Os, Oz) — native languages only
--compiler <name>Override compiler choice (gcc, clang, go, rustc, swiftc)

Narrow a large file to the routines that handle secrets with a regex, for example --func 'sign|verify'.

Run natively compiled code (C, C++, Go, Rust, Swift) at more than one --arch and --opt-level. Division timing and branch lowering are architecture- and optimization-dependent: x86_64 IDIV and arm64 SDIV differ, and a cmov at -O2 can become a branch at -O0. A single clean run proves one configuration safe, not the code.

How --arch crosses depends on the toolchain. clang crosses with --target and needs no second compiler, but any source that includes libc headers also needs that target's C library headers — libc6-dev-riscv64-cross and friends — or it fails with bits/libc-header-start.h file not found. Go cross-builds through GOARCH, though go tool objdump has no riscv64 disassembler. A GNU cross toolchain is a separate binary, so gcc needs it named explicitly — --compiler x86_64-linux-gnu-gcc, --compiler riscv64-linux-gnu-gcc — and nothing is substituted for you, so the report always names the binary that ran. rustc needs the target's standard library (rustup target add), and Swift on Linux targets only the host. Compare against the toolchain that builds your product, not whichever cross build a distribution packages.

Re-run the whole sweep on the fix, across compilers, targets and every level including Os and Oz. Any fix that works by handing the compiler a constant divisor to strength-reduce is a fix only where the compiler chooses to cooperate, and that choice varies more than it looks. Replacing key_coef / (2 * gamma2) with a #defined divisor still emits a real divide here:

ToolchainLevels that emit a division
gcc riscv64O0 through Oz — every level
gcc arm64, gcc x86_64Os, Oz
clang arm64O0, Oz

Strength reduction is an optimizer courtesy, not a language guarantee. Prefer an explicit multiply-shift, and verify it against the original expression over the full input range rather than on sampled values — an off-by-a-power-of-two reciprocal matches for millions of inputs before it diverges.

Java, Kotlin, and C# compile to JVM/CIL bytecode. The analyzer reads that bytecode, so --arch and --opt-level do not apply and the JIT may still introduce variable-time native code the analyzer cannot see.

Per-language coverage limits

Coverage is not uniform, and the gaps change what a clean report means:

LanguageWhat the report does not cover
GoOnly symbols from the analyzed file. go build links the runtime in, and its divisions — all on public data — would otherwise dominate the findings
JavaScript, TypeScriptBytecode findings are restricted to functions the file declares by name, because V8 dumps node's internals the same way it dumps yours. Anonymous callbacks fall to the source scan. For TypeScript, bytecode findings name the function but carry no line, since V8's positions index the transpiled output
Python, Ruby, PHPBytecode reflects the interpreter that ran, not a JIT'd or alternative runtime
RustAnalyzed as a library unless the file declares fn main; private functions with no caller may be optimized away before analysis
SwiftTargets the host platform on Linux; iOS and macOS triples need an Apple toolchain

Since findings and silence both depend on the configuration, say which compiler, architecture, and optimization level produced a result when reporting it.

To sweep a directory, loop in the shell — the analyzer is a deterministic script, one invocation per file:

for f in src/crypto/*.c; do uv run {baseDir}/ct_analyzer/analyzer.py --warnings --json "$f"; done

Prerequisites

LanguageRequirement
C, C++, Go, Rustgcc/clang, go, rustc in PATH
SwiftXcode or Swift toolchain (swiftc)
Java / KotlinJDK (javac, javap); Kotlin also needs kotlinc
C#.NET SDK plus ilspycmd (dotnet tool install -g ilspycmd)
PHPPHP with the VLD extension or OPcache
JavaScript / TypeScriptNode.js
PythonPython 3.x
RubyRuby with --dump=insns support

On a "toolchain not found" error, see references/vm-compiled.md for JVM and .NET installation, macOS keg-only PATH configuration, and troubleshooting.

Interpreting Results

PASSED — no error-severity finding for the configuration you ran. Warnings do not affect it, so Result: PASSED alongside Warnings: 6 is normal and is not a clean result. Read the warning list before concluding anything.

FAILED — dangerous instructions found, reported per function:

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

Triaging Findings

The analyzer has no data flow analysis. It flags every dangerous instruction regardless of whether a secret reaches it, so a FAILED report is a worklist, not a verdict. Reporting the raw output as a set of vulnerabilities is the primary failure mode of this skill.

For each flagged instruction, read the source and answer one question: does an operand depend on secret data? Trace from the instruction's function back to the caller's inputs, then classify:

// FALSE POSITIVE: operands are a buffer length, already public from the ciphertext size
int num_blocks = data_len / 16;

// TRUE POSITIVE: dividend is a private-key coefficient; IDIV/SDIV leaks its magnitude
int32_t q = secret_coef / GAMMA2;
QuestionIf yes
Is the operand a compile-time constant?Likely false positive
Is the operand a public parameter — length, count, index bound?Likely false positive
Is the operand derived from a key, plaintext, nonce, or token?True positive
Can an attacker influence the operand's value?True positive

State the verdict and the data flow that justifies it for every flagged item. A finding you cannot trace to a secret is not a finding; say so explicitly rather than dropping it silently.

{baseDir}/ct_analyzer/tests/triage_samples/ holds a known-answer case per language: each fixture pairs a true positive with a false positive that the analyzer reports identically, and expectations.json records which is which and why. triage_c.c is the shortest example — the analyzer flags the division in both ct_high_bits and ct_block_count, and correct triage confirms the first and clears the second.

Weak-RNG and encoding findings ask a different question. For Math.random, mt_rand, random.randint, System.Random and base64_encode, no operand is secret, so "does an operand depend on a secret?" does not resolve them. Ask instead what the result is used for: seeding a nonce or key is a true positive, jittering a retry delay is not. These are reported by a regex scan over the source rather than from bytecode, so they are attributed to <source> with a line number instead of to the enclosing function — except in PHP, where they carry the function.

Comparison and lookup findings have their own question, and their own fix. For an early-exit comparison, ask whether either side is secret: comparing an authentication tag, MAC, or password hash is a true positive, comparing a public protocol header is not. For a table lookup, ask whether the index is secret — the array's contents do not matter, only what selects the element. Both are exploitable as written, so a confirmed one needs the language's constant-time primitive rather than a rewrite of the loop:

LanguageConstant-time comparison
C, C++CRYPTO_memcmp (OpenSSL) or sodium_memcmp
Gocrypto/subtle.ConstantTimeCompare
Rustthe subtle crate's ConstantTimeEq
Java, KotlinMessageDigest.isEqual
C#CryptographicOperations.FixedTimeEquals
PHPhash_equals
Pythonhmac.compare_digest
RubyOpenSSL.secure_compare
JavaScript, TypeScriptcrypto.timingSafeEqual

A secret-indexed lookup has no drop-in replacement: it needs a bit-sliced or arithmetic formulation that touches every element, which is why AES S-box tables are the classic case. Encoding a secret through a table — base64_encode, bin2hex, chr/ord — is the same problem in a library, and paragonie/constant_time_encoding is the reference fix for PHP.

Limitations

  1. Static only — reads assembly and bytecode, never runtime behavior. Cache timing and other microarchitectural channels are invisible.
  2. No data flow analysis — see triage above.
  3. Configuration-specific — a different compiler, optimization level, architecture, or runtime version can emit different instructions from identical source.

Real-World Impact

  • KyberSlash (2023) — division instructions in ML-KEM implementations allowed key recovery
  • Lucky Thirteen (2013) — timing differences in CBC padding validation enabled plaintext recovery
  • RSA timing attacks — early implementations leaked private key bits through division timing

References

GitHub Repository

trailofbits/skills
Path: plugins/constant-time-analysis/skills/constant-time-analysis
0
agent-skills
FAQ

Frequently asked questions

What is the constant-time-analysis skill?

constant-time-analysis is a Claude Skill by trailofbits. Skills package instructions and resources that Claude loads on demand, so Claude can perform constant-time-analysis-related tasks without extra prompting.

How do I install constant-time-analysis?

Use the install commands on this page: add constant-time-analysis to Claude Code as a plugin, or clone its repository into your skills directory, then restart Claude so it picks up the skill.

What category does constant-time-analysis belong to?

constant-time-analysis is in the Development category, tagged general.

Is constant-time-analysis free to use?

Yes. constant-time-analysis is listed on AIMCP and free to install.

Related Skills

qmd
Development

qmd is a local search and indexing CLI tool that enables developers to index and search through local files using hybrid search combining BM25, vector embeddings, and reranking. It supports both command-line usage and MCP (Model Context Protocol) mode for integration with Claude. The tool uses Ollama for embeddings and stores indexes locally, making it ideal for searching documentation or codebases directly from the terminal.

View skill
subagent-driven-development
Development

This skill executes implementation plans by dispatching a fresh subagent for each independent task, with code review between tasks. It enables fast iteration while maintaining quality gates through this review process. Use it when working on mostly independent tasks within the same session to ensure continuous progress with built-in quality checks.

View skill
mcporter
Development

The mcporter skill enables developers to manage and call Model Context Protocol (MCP) servers directly from Claude. It provides commands to list available servers, call their tools with arguments, and handle authentication and daemon lifecycle. Use this skill for integrating and testing MCP server functionality in your development workflow.

View skill
adk-deployment-specialist
Development

This skill deploys and orchestrates Vertex AI ADK agents using A2A protocol, managing AgentCard discovery, task submission, and supporting tools like Code Execution Sandbox and Memory Bank. It enables building multi-agent systems with sequential, parallel, or loop orchestration patterns in Python, Java, or Go. Use it when asked to deploy ADK agents or orchestrate agent workflows on Google Cloud.

View skill