domains / security / constant-time-crypto-review / constant-time-analysis
The trailofbits copy is the more complete and authoritative procedure, but the sickn33 copy adds material constraints and setup steps that the first lacks, so crowning one would hide a real alternative. The trailofbits copy mandates --warnings, explains why the default run is quiet about the most common bug, and gives detailed cross-architecture/optimization-level guidance including a table of when strength reduction fails. The sickn33 copy adds a prerequisites table, macOS PATH setup for keg-only Java/.NET, and a decision flowchart, but it presents --warnings as optional and omits the critical warning about default runs missing early-exit comparisons. These are complementary, not merely redundant.
| Repository | Path |
|---|---|
| trailofbits/skills | plugins/constant-time-analysis/skills/constant-time-analysis/SKILL.md |
| sickn33/agentic-awesome-skills | plugins/agentic-awesome-skills-claude/skills/constant-time-analysis/SKILL.md |
| Role | Repository | Path |
|---|---|---|
| peer | trailofbits/skills | plugins/constant-time-analysis/skills/constant-time-analysis/SKILL.md |
| peer | sickn33/agentic-awesome-skills | plugins/agentic-awesome-skills-claude/skills/constant-time-analysis/SKILL.md |
| adapter | sickn33/agentic-awesome-skills | plugins/agentic-awesome-skills/skills/constant-time-analysis/SKILL.md |
| adapter | sickn33/agentic-awesome-skills | skills/constant-time-analysis/SKILL.md |
# 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, which covers dudect and statistical approaches. 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.
| Guide | Languages |
| ----- | --------- |
| references/compiled.md | C, C++, Go, Rust |
| references/swift.md | Swift |
| references/vm-compiled.md | Java, C# |
| references/kotlin.md | Kotlin |
| references/php.md | PHP |
| references/javascript.md | JavaScript, TypeScript |
| references/python.md | Python |
| references/ruby.md | Ruby |
## Prerequisites
| Language | Requirements |
| -------- | ------------ |
| C, C++, Go, Rust | Compiler in PATH (`gcc`/`clang`, `go`, `rustc`) |
| Swift | Xcode or Swift toolchain (`swiftc` in PATH) |
| Java | JDK with `javac` and `javap` in PATH |
| Kotlin | Kotlin compiler (`kotlinc`) + JDK (`javap`) in PATH |
| C# | .NET SDK + `ilspycmd` (`dotnet tool install -g ilspycmd`) |
| PHP | PHP with VLD extension or OPcache |
| JavaScript/TypeScript | Node.js in PATH |
| Python | Python 3.x in PATH |
| Ruby | Ruby with `--dump=insns` support |
**macOS users**: Homebrew installs Java and .NET as "keg-only". You must add them to your PATH:
```bash
# For Java (add to ~/.zshrc)
export PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH"
# For .NET tools (add to ~/.zshrc)
export PATH="$HOME/.dotnet/tools:$PATH"
```
## Running the Analyzer
The analyzer takes one file and detects the language from its extension. **Always pass `--warnings`:**
```bash
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.
| Flag | Effect |
| ---- | ------ |
| `--warnings` | Add the four warning-severity families above. Pass it every time |
| `--func <regex>` | Restrict output to function names matching the regex |
| `--json` | Machine-readable output |
| `--github` | GitHub 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 `#define`d divisor still emits a real divide here:
| Toolchain | Levels that emit a division |
| --------- | --------------------------- |
| gcc riscv64 | `O0` through `Oz` — every level |
| gcc arm64, gcc x86_64 | `Os`, `Oz` |
| clang arm64 | `O0`, `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.