git:20260114.695119c to git:20260803.9ea55c5

113 added, 147 removed. Audit A to A.

---
name: constant-time-analysis
description: Detects timing side-channel vulnerabilities in cryptographic code. Use when implementing or reviewing crypto code, encountering division on secrets, secret-dependent branches, or constant-time programming questions in C, C++, Go, Rust, Swift, Java, Kotlin, C#, PHP, JavaScript, TypeScript, Python, or Ruby.
+ allowed-tools: Bash Read Grep Glob
+ effort: medium
---
# Constant-Time Analysis
- Analyze cryptographic code to detect operations that leak secret data through execution timing variations.
+ 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
- ```text
- User writing crypto code? ──yes──> Use this skill
- │
- no
- │
- v
- User asking about timing attacks? ──yes──> Use this skill
- │
- no
- │
- v
- Code handles secret keys/tokens? ──yes──> Use this skill
- │
- no
- │
- v
- Skip this skill
- ```
-
- **Concrete triggers:**
-
- - User implements signature, encryption, or key derivation
- - Code contains `/` or `%` operators on secret-derived values
- - User mentions "constant-time", "timing attack", "side-channel", "KyberSlash"
+ - 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
- - Non-cryptographic code (business logic, UI, etc.)
- - Public data processing where timing leaks don't matter
- - Code that doesn't handle secrets, keys, or authentication tokens
- - High-level API usage where timing is handled by the library
+ - **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 Selection
+ ## Language Routing
- Based on the file extension or language context, refer to the appropriate guide:
+ 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.
- | Language | File Extensions | Guide |
- | ---------- | --------------------------------- | -------------------------------------------------------- |
- | C, C++ | `.c`, `.h`, `.cpp`, `.cc`, `.hpp` | [references/compiled.md](references/compiled.md) |
- | Go | `.go` | [references/compiled.md](references/compiled.md) |
- | Rust | `.rs` | [references/compiled.md](references/compiled.md) |
- | Swift | `.swift` | [references/swift.md](references/swift.md) |
- | Java | `.java` | [references/vm-compiled.md](references/vm-compiled.md) |
- | Kotlin | `.kt`, `.kts` | [references/kotlin.md](references/kotlin.md) |
- | C# | `.cs` | [references/vm-compiled.md](references/vm-compiled.md) |
- | PHP | `.php` | [references/php.md](references/php.md) |
- | JavaScript | `.js`, `.mjs`, `.cjs` | [references/javascript.md](references/javascript.md) |
- | TypeScript | `.ts`, `.tsx` | [references/javascript.md](references/javascript.md) |
- | Python | `.py` | [references/python.md](references/python.md) |
- | Ruby | `.rb` | [references/ruby.md](references/ruby.md) |
+ | Guide | Languages |
+ | ----- | --------- |
+ | [references/compiled.md](references/compiled.md) | C, C++, Go, Rust |
+ | [references/swift.md](references/swift.md) | Swift |
+ | [references/vm-compiled.md](references/vm-compiled.md) | Java, C# |
+ | [references/kotlin.md](references/kotlin.md) | Kotlin |
+ | [references/php.md](references/php.md) | PHP |
+ | [references/javascript.md](references/javascript.md) | JavaScript, TypeScript |
+ | [references/python.md](references/python.md) | Python |
+ | [references/ruby.md](references/ruby.md) | Ruby |
- ## Quick Start
+ ## Running the Analyzer
- ```bash
- # Analyze any supported file type
- uv run {baseDir}/ct_analyzer/analyzer.py <source_file>
+ The analyzer takes one file and detects the language from its extension. **Always pass `--warnings`:**
- # Include conditional branch warnings
+ ```bash
uv run {baseDir}/ct_analyzer/analyzer.py --warnings <source_file>
-
- # Filter to specific functions
- uv run {baseDir}/ct_analyzer/analyzer.py --func 'sign|verify' <source_file>
-
- # JSON output for CI
- uv run {baseDir}/ct_analyzer/analyzer.py --json <source_file>
```
- ### Native Compiled Languages Only (C, C++, Go, Rust)
-
- ```bash
- # Cross-architecture testing (RECOMMENDED)
- uv run {baseDir}/ct_analyzer/analyzer.py --arch x86_64 crypto.c
- uv run {baseDir}/ct_analyzer/analyzer.py --arch arm64 crypto.c
-
- # Multiple optimization levels
- uv run {baseDir}/ct_analyzer/analyzer.py --opt-level O0 crypto.c
- uv run {baseDir}/ct_analyzer/analyzer.py --opt-level O3 crypto.c
- ```
+ 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.
- ### VM-Compiled Languages (Java, Kotlin, C#)
+ | 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`) |
- ```bash
- # Analyze Java bytecode
- uv run {baseDir}/ct_analyzer/analyzer.py CryptoUtils.java
+ Narrow a large file to the routines that handle secrets with a regex, for example `--func 'sign|verify'`.
- # Analyze Kotlin bytecode (Android/JVM)
- uv run {baseDir}/ct_analyzer/analyzer.py CryptoUtils.kt
+ **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.
- # Analyze C# IL
- uv run {baseDir}/ct_analyzer/analyzer.py CryptoUtils.cs
- ```
+ **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.
- Note: Java, Kotlin, and C# compile to bytecode (JVM/CIL) that runs on a virtual machine with JIT compilation. The analyzer examines the bytecode directly, not the JIT-compiled native code. The `--arch` and `--opt-level` flags do not apply to these languages.
+ **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:
- ### Swift (iOS/macOS)
+ | Toolchain | Levels that emit a division |
+ | --------- | --------------------------- |
+ | gcc riscv64 | `O0` through `Oz` — every level |
+ | gcc arm64, gcc x86_64 | `Os`, `Oz` |
+ | clang arm64 | `O0`, `Oz` |
- ```bash
- # Analyze Swift for native architecture
- uv run {baseDir}/ct_analyzer/analyzer.py crypto.swift
+ 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.
- # Analyze for specific architecture (iOS devices)
- uv run {baseDir}/ct_analyzer/analyzer.py --arch arm64 crypto.swift
+ 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.
- # Analyze with different optimization levels
- uv run {baseDir}/ct_analyzer/analyzer.py --opt-level O0 crypto.swift
- ```
+ ### Per-language coverage limits
- Note: Swift compiles to native code like C/C++/Go/Rust, so it uses assembly-level analysis and supports `--arch` and `--opt-level` flags.
+ Coverage is not uniform, and the gaps change what a clean report means:
- ### Prerequisites
+ | Language | What the report does not cover |
+ | -------- | ------------------------------ |
+ | Go | Only symbols from the analyzed file. `go build` links the runtime in, and its divisions — all on public data — would otherwise dominate the findings |
+ | JavaScript, TypeScript | Bytecode 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, PHP | Bytecode reflects the interpreter that ran, not a JIT'd or alternative runtime |
+ | Rust | Analyzed as a library unless the file declares `fn main`; private functions with no caller may be optimized away before analysis |
+ | Swift | Targets the host platform on Linux; iOS and macOS triples need an Apple toolchain |
- | 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 |
+ Since findings and silence both depend on the configuration, say which compiler, architecture, and optimization level produced a result when reporting it.
- **macOS users**: Homebrew installs Java and .NET as "keg-only". You must add them to your PATH:
+ To sweep a directory, loop in the shell — the analyzer is a deterministic script, one invocation per file:
```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"
+ for f in src/crypto/*.c; do uv run {baseDir}/ct_analyzer/analyzer.py --warnings --json "$f"; done
```
- See [references/vm-compiled.md](references/vm-compiled.md) for detailed setup instructions and troubleshooting.
+ ### Prerequisites
- ## Quick Reference
+ | Language | Requirement |
+ | -------- | ----------- |
+ | C, C++, Go, Rust | `gcc`/`clang`, `go`, `rustc` in PATH |
+ | Swift | Xcode or Swift toolchain (`swiftc`) |
+ | Java / Kotlin | JDK (`javac`, `javap`); Kotlin also needs `kotlinc` |
+ | C# | .NET SDK plus `ilspycmd` (`dotnet tool install -g ilspycmd`) |
+ | PHP | PHP with the VLD extension or OPcache |
+ | JavaScript / TypeScript | Node.js |
+ | Python | Python 3.x |
+ | Ruby | Ruby with `--dump=insns` support |
- | Problem | Detection | Fix |
- | ---------------------- | ------------------------------- | -------------------------------------------- |
- | Division on secrets | DIV, IDIV, SDIV, UDIV | Barrett reduction or multiply-by-inverse |
- | Branch on secrets | JE, JNE, BEQ, BNE | Constant-time selection (cmov, bit masking) |
- | Secret comparison | Early-exit memcmp | Use `crypto/subtle` or constant-time compare |
- | Weak RNG | rand(), mt_rand, Math.random | Use crypto-secure RNG |
- | Table lookup by secret | Array subscript on secret index | Bit-sliced lookups |
+ On a "toolchain not found" error, see [references/vm-compiled.md](references/vm-compiled.md) for JVM and .NET installation, macOS keg-only PATH configuration, and troubleshooting.
## Interpreting Results
- **PASSED** - No variable-time operations detected.
+ **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. Example:
+ **FAILED** — dangerous instructions found, reported per function:
```text
[ERROR] SDIV
Function: decompose_vulnerable
Reason: SDIV has early termination optimization; execution time depends on operand values
```
- ## Verifying Results (Avoiding False Positives)
-
- **CRITICAL**: Not every flagged operation is a vulnerability. The tool has no data flow analysis - it flags ALL potentially dangerous operations regardless of whether they involve secrets.
+ ## Triaging Findings
- For each flagged violation, ask: **Does this operation's input depend on secret data?**
+ **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.
- 1. **Identify the secret inputs** to the function (private keys, plaintext, signatures, tokens)
+ 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:
- 2. **Trace data flow** from the flagged instruction back to inputs
+ ```c
+ // FALSE POSITIVE: operands are a buffer length, already public from the ciphertext size
+ int num_blocks = data_len / 16;
- 3. **Common false positive patterns**:
+ // TRUE POSITIVE: dividend is a private-key coefficient; IDIV/SDIV leaks its magnitude
+ int32_t q = secret_coef / GAMMA2;
+ ```
- ```c
- // FALSE POSITIVE: Division uses public constant, not secret
- int num_blocks = data_len / 16; // data_len is length, not content
+ | Question | If 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** |
- // TRUE POSITIVE: Division involves secret-derived value
- int32_t q = secret_coef / GAMMA2; // secret_coef from private key
- ```
+ 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.
- 4. **Document your analysis** for each flagged item
+ `{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.
- ### Quick Triage Questions
+ **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.
- | Question | If Yes | If No |
- | ------------------------------------------------- | --------------------- | --------------------- |
- | Is the operand a compile-time constant? | Likely false positive | Continue |
- | Is the operand a public parameter (length, count)?| Likely false positive | Continue |
- | Is the operand derived from key/plaintext/secret? | **TRUE POSITIVE** | Likely false positive |
- | Can an attacker influence the operand value? | **TRUE POSITIVE** | Likely false positive |
+ **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:
- ## Limitations
+ | Language | Constant-time comparison |
+ | -------- | ------------------------ |
+ | C, C++ | `CRYPTO_memcmp` (OpenSSL) or `sodium_memcmp` |
+ | Go | `crypto/subtle.ConstantTimeCompare` |
+ | Rust | the `subtle` crate's `ConstantTimeEq` |
+ | Java, Kotlin | `MessageDigest.isEqual` |
+ | C# | `CryptographicOperations.FixedTimeEquals` |
+ | PHP | `hash_equals` |
+ | Python | `hmac.compare_digest` |
+ | Ruby | `OpenSSL.secure_compare` |
+ | JavaScript, TypeScript | `crypto.timingSafeEqual` |
- 1. **Static Analysis Only**: Analyzes assembly/bytecode, not runtime behavior. Cannot detect cache timing or microarchitectural side-channels.
+ 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.
- 2. **No Data Flow Analysis**: Flags all dangerous operations regardless of whether they process secrets. Manual review required.
+ ## Limitations
- 3. **Compiler/Runtime Variations**: Different compilers, optimization levels, and runtime versions may produce different output.
+ 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 post-quantum 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
+ - **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
- - [Cryptocoding Guidelines](https://github.com/veorq/cryptocoding) - Defensive coding for crypto
- - [KyberSlash](https://kyberslash.cr.yp.to/) - Division timing in post-quantum crypto
- - [BearSSL Constant-Time](https://www.bearssl.org/constanttime.html) - Practical constant-time techniques
+ - [Cryptocoding Guidelines](https://github.com/veorq/cryptocoding) — defensive coding for crypto
+ - [KyberSlash](https://kyberslash.cr.yp.to/) — division timing in post-quantum crypto
+ - [BearSSL Constant-Time](https://www.bearssl.org/constanttime.html) — practical constant-time techniques