casablanque-code/sidecheck
GitHub: casablanque-code/sidecheck
sidecheck 是一款 CLI 工具,通过严谨的统计学方法检测 HTTP 端点是否存在远程计时侧信道泄漏,帮助开发者发现非恒定时间比较导致的安全隐患。
Stars: 2 | Forks: 0
# sidecheck
你怎么知道你的密码比较真的是常量时间的?
测一下就知道了。
`sidecheck` 是一个 CLI 工具,用于审计你自己的 HTTP endpoint 是否存在远程
计时侧信道(timing side-channels)—— 这是一类漏洞:当使用 `==` 比较密钥时,
攻击者可以通过测量响应时间,逐字符地恢复出密钥,而不需要对整个密钥进行暴力破解。
它是作为测量仪器而非漏洞利用工具构建的:它会告诉你是否存在可测量的计时信道
及其置信度,而不是直接告诉你“这是你的密码”。
## 为什么这很重要
AI 辅助(“凭感觉”式)编程让这类 bug 变得更为常见 ——
LLM 能稳定地写出可以通过测试的认证比较代码,但它们并不是常量时间的。
如果你的登录 endpoint 是使用 Claude/Copilot/Cursor 编写的,
且从未经过安全审查,那么大概率没有人检查过这一点。
## 方法论
在网络测量中进行简单的平均值/中位数分析是不可靠的 —— 相比于你试图检测的
CPU 级别信号,网络抖动要大上好几个数量级。`sidecheck` 采用了 Crosby,
Wallach & Riedi 在《Opportunities and Limits of Remote Timing Attacks》
(ACM TISSEC, 2009)中提出的方法论:
- 网络噪声只能**增加**延迟,绝不会减少延迟,因此样本的低百分位数
(例如 p10)所携带的噪声远小于平均值甚至原始最小值
- **box test**(盒形图检验)会比较两类请求(例如“正确前缀”与“错误前缀”)的低百分位数
- 置信区间通过 bootstrap 重采样计算 —— 无需假设网络延迟呈正态分布
- 请求顺序在交替块(interleaved blocks)中进行随机化(绝不采用“先全 A,后全 B”的方式),这样一天中的时间漂移或服务器预热就不会导致结果产生偏差
- 在完整运行之前,试点批次(pilot batch)会估计网络抖动,并报告检测给定大小的泄漏实际需要多少样本 —— 如果网络对于目标信号来说太嘈杂,它会如实说明情况,而不是盲目猜测
## 用法
简单模式 —— 提供你的真实密钥,它会自动生成一个长度匹配的错误值:
```
sidecheck check https://myapp.local/login \
--header X-API-Key \
--secret "my-real-api-key-do-not-share"
```
样本大小会通过快速试运行自动选取 —— 你不需要预先猜测 `--samples`。支持以下三种注入点:
```
# HTTP header (API keys, tokens)
sidecheck check https://myapp.local/api --header X-API-Key --secret "..."
# query parameter (旧版 token-in-URL endpoints)
sidecheck check https://myapp.local/api --query token --secret "..."
# JSON POST body field (典型 web login forms)
sidecheck check https://myapp.local/login --json-field password --secret "..."
```
大多数真实的登录 endpoint 在请求体中需要的不仅仅是被测试的字段 ——
还需要一个 `username`/`email`,以便后端在进行密码比较之前进行查找。使用 `--json-body` 将其余的请求体作为模板提供;`--json-field` 仍然是每次请求时在两个比较值之间进行替换的字段:
```
sidecheck check https://myapp.local/login \
--json-field password \
--json-body '{"username": "admin"}' \
--secret "..."
```
高级模式 —— 完全控制两个比较的值(例如,测试特定的猜测前缀,而不是完整的密钥):
```
sidecheck check https://myapp.local/login \
--header X-API-Key \
--value-a "0000000000000000000000000" \
--value-b "correct-se0000000000000000" \
--samples 5000
```
```
────────────────────────────────────────────────
sidecheck timing report
────────────────────────────────────────────────
target https://myapp.local/login
field header X-API-Key
samples/class 12480
network jitter 1.80 ms
⚠ timing leak detected
estimated leak 31.4 μs
bootstrap confidence 95.0% (of the measured difference being non-zero)
this endpoint responds measurably differently depending on
input correctness. an attacker can exploit this to recover
secrets character-by-character instead of brute-forcing them.
fix: use a constant-time comparison instead of == on secret
bytes (e.g. the `subtle` crate in Rust, `crypto/subtle` in Go,
`hmac.compare_digest` in Python).
────────────────────────────────────────────────
sidecheck cannot prove the absence of a timing leak — only detect a
statistically significant one under the tested conditions. A clean
result here is not a safety guarantee.
generated by sidecheck 0.1.0 · seed 4891023741 (rerun with --seed 4891023741 to reproduce request order)
```
## 先检查测量是否可行
在花时间进行 `check` 之前,先确认网络路径是否能够进行有意义的测量:
```
sidecheck doctor https://myapp.local/login
```
```
────────────────────────────────────────────────
sidecheck doctor
────────────────────────────────────────────────
target https://myapp.local/login
samples 300
median RTT: 8.2 ms
RTT jitter: 0.42 ms (low)
packet loss: 0.0%
recommended samples: ~2765881 (to reliably detect a ~1μs leak, the
rough scale of a real == vs constant-time bug)
environment quality: GOOD
────────────────────────────────────────────────
this path looks suitable for timing measurement. proceed with `sidecheck check`.
```
请注意,即使对于“GOOD”(良好)质量的路径,上面推荐的样本计数也很大 —— `GOOD` 的意思是路径*足够稳定,使得该估计是有意义的*,而不是说运行速度会很快。在 0.42ms 的抖动背后发现约 1μs 的泄漏确实非常困难;正是这个差距导致了这里的估计值达到数百万。`check` 会自动调整其运行规模,除非你传递 `--force` 参数,否则它将拒绝继续超过 `--max-samples`(默认 200,000)——见下文。
`check` 本身已经在其试点批次中对此进行了估计,并且默认情况下会拒绝运行不可行的完整测量(见下文)—— `doctor` 适用于你想预先获取该结果,而尚未确定特定字段/密钥的情况,例如,在决定是否值得测试只能通过公共互联网访问的目标之前。
## 处理密钥
`--secret` 很方便,但它会显示在 `ps aux` 和你的 shell 历史记录中 —— 这对于一次性的测试密钥来说没问题,但不适用于真实的密钥。建议使用:
```
# 从环境变量
sidecheck check https://myapp.local/login --header X-API-Key --secret-env API_KEY
# 从 stdin 管道传入 (例如来自 password manager)
pass show myapp/api-key | sidecheck check https://myapp.local/login --header X-API-Key --secret-stdin
```
`sidecheck` 本身绝不会将你的密钥发送到你指定测试目标以外的任何地方,也绝不会将其记录到磁盘上。它**不**尝试从你的 shell 历史记录文件中清除 `--secret` —— 作为子进程,没有可靠且可移植的方法可以做到这一点(shell 会将历史记录保留在内存中,直到退出时才写入文件,并且格式在 bash/zsh/fish 之间有所不同)。请使用 `--secret-env`/`--secret-stdin`,而不是事后尝试清理。
## 可重现性
每次运行都会选取(或通过 `--seed` 接收)一个种子,该种子决定了请求的交替顺序和生成的错误值。它会打印在报告和 JSON 输出中 —— 通过 `--seed` 将其传回可以重现完全相同的请求序列,例如在调试奇怪的结果时。请注意,使用 `--repeat` 时,`--seed` 重现的是*整个运行序列*,而不是独立的单次运行 —— 每次重复都会继续从相同的 RNG 流中提取数据,而不是重新启动。
## 检查单次运行的估计是否可信
单次运行的 `estimated leak`(估计泄漏)只是一个点估计值;它无法告诉你如果再次运行,该数值是否看起来相似。`--repeat N` 会将完整的试点+测量循环运行 N 次,并总结估计值和显著性判断实际波动的程度:
```
sidecheck check https://myapp.local/login --header X-API-Key --secret-env API_KEY --repeat 5
```
```
────────────────────────────────────────────────
stability summary across 5 runs
────────────────────────────────────────────────
significant in 5/5 runs
estimated leak mean 12.23 ms · range [12.20 ms, 12.25 ms] · std dev 21.8 μs
✓ consistently significant with a stable magnitude across runs.
```
结论优先考虑显著性判断本身是否一致(0/N 或 N/N),而不是点估计值的原始方差 —— 当确实不存在泄漏时,平均值接近零,任何微小的绝对波动在除以约零的情况下都会看起来像是巨大的*相对*不稳定性,这只是除以约零带来的统计学假象,并不是真正的问题。混合的显著性(有些运行报告有泄漏,有些没有)才是真正值得怀疑的情况 —— 这通常意味着该效应正好处于样本量能够解析的边缘。
使用 `--output-csv`/`--report` 时,每次重复都会生成自己的文件(`report-run1.json`, `report-run2.json`, ...),而不是将同一个文件覆盖 N 次。
## 当 sidecheck 拒绝运行时
如果试点批次估计,相对于检测到的效应,网络太嘈杂,无法在 `--max-samples`(默认 200,000)内达到显著性水平,`sidecheck check` 会在主运行**之前**停止,而不是默默地花费数分钟到数小时去获取一个几乎可以肯定是模糊不清的结果。它会解释信噪比和预计需要的实际耗时。此时的选项有:
- 从较低延迟的有利位置进行测试(与目标位于同一局域网/数据中心,或直接在服务器本机上通过 `127.0.0.1` 测试)—— `sidecheck doctor` 会在你决定之前确认这是否真的更好
- 如果你明白运行结果可能无法得出明确结论,但仍想要数据,请传递 `--force`
- 如果你愿意等待更长时间,请提高 `--max-samples`
显式设置 `--samples` 会绕过此关卡(你已经做出了决定),但仍会打印相同的时间/耗时估计作为提醒。
完整的参数参考:`sidecheck check --help` / `sidecheck doctor --help`
—— 像 `--pilot-samples`、`--block-size`、`--confidence` 和 `--percentile` 这样的调整参数都在那里有详细说明,而不是在这里重复,因为在文字描述中重复它们正是文档悄然过时的原因。
## 用于 CI / 自动化的报告
```
sidecheck check https://myapp.local/login --header X-API-Key --secret-env API_KEY \
--report report.json --output-csv raw.csv
```
`report.json` 包含了 sidecheck 的版本、seed(种子)和时间戳以及结论 —— 一个来自 `v0.1.0` 的报告不应该与一个来自未来版本(具有改进的统计功能)的报告受到同等程度的信任。
## 自我验证
`test-fixture/test_fixture.py` 是一个小型的参考服务器,包含一个故意留下漏洞的 `/vulnerable` endpoint 和一个使用 `hmac.compare_digest` 的安全 `/safe` endpoint。在将其用于真实目标之前,请使用它来确认 `sidecheck` 是否能正确标记出存在漏洞的 endpoint,并对安全的 endpoint 保持静默:
```
python3 test-fixture/test_fixture.py &
sidecheck check http://127.0.0.1:8000/vulnerable --header X-API-Key --secret "correct-secret-key-123456"
sidecheck check http://127.0.0.1:8000/safe --header X-API-Key --secret "correct-secret-key-123456"
```
## 局限性
**sidecheck 无法证明不存在计时泄漏。** 一个干净的结果只是意味着在*测试条件下*(此样本量、此网络路径、此百分位数)未发现具有统计学显著性的差异 —— 并不代表该 endpoint 是安全的。更小的泄漏、更嘈杂的网络或不同的代码路径仍然可能隐藏真实的问题。请将肯定的结果视为存在 bug 的有力证据;将否定的结果视为“此处未发现任何异常”,而不是安全证书。
报告中的 `bootstrap confidence` 是围绕测量差异的 bootstrap 重采样区间的置信水平 —— 它回答的是“我们有多确定这个特定的差异不仅仅是噪声”,而不是“该服务器存在漏洞的概率”,更不是经典假设检验意义上的 p 值。
## 状态
`v0.1`,1.0 之前的版本 —— 可能会有粗糙的边缘。已端到端工作并验证的功能包括:
检测(`check`)、飞行前诊断(`doctor`)、用于流水线验证的 Python 放大夹具(fixture),以及两个非放大的参考夹具(`realistic-fixtures/`,Go 和 Node),确认了对于短密钥,真实的(非人为放大的)`==`/`===` 泄漏通常*无法*通过 HTTP 可靠地检测到 —— 真实请求的噪底,即使在回环地址上,也可能超过纳秒级的 CPU 泄漏。这是该方法固有的客观限制,而不是一个 bug(参见上面的局限性)。
尚未完成:FastAPI/Express/Actix/Axum/Spring 的参考目标(目前只有 Go/Node),用于寻找 HTTP 可检测交叉点长度的长密钥扫描,端到端实际检测逻辑的 CI 覆盖(目前的 CI 只运行 fmt/clippy/unit-tests,而不是针对夹具的真实检查流程),以及 crates.io 发布。接下来的重大功能是 SSH/TLS 密钥熵审计(通过跨你自有集群的批量 GCD 进行共享素数因子检测),这独立于计时分析。
## 安装
```
cargo install --locked sidecheck
```
`--locked` 在这里很重要:如果没有它,`cargo install` 会针对 crates.io 上今天最新的版本重新解析依赖项,这可能会引入需要 2024 版 Rust edition 的间接依赖(transitive crate)—— 你会看到类似 `feature edition2024 is required` 的错误。`--locked` 会使用提交到本仓库的 `Cargo.lock`,已知该文件是可以成功构建的。
我们还声明了 `rust-version = "1.85"`(参见 `Cargo.toml`,edition2024 在该版本中稳定)—— 在 Cargo 1.85+ 上,这使得依赖项解析本身具备 MSRV 感知能力,因此即使是全新且不带 `--locked` 的 `cargo install` 也会避免引入比我们声明的 edition 更新的间接版本。这种感知能力在低于 1.85 的 Cargo 版本(例如某些 LTS 发行版默认提供的版本)上是不存在的,这正是为什么上面仍然推荐双保险地使用 `--locked` 的原因 —— 如果你的系统 Cargo 版本早于 1.85,请通过 [rustup](https://rustup.rs) 进行更新。
## 构建
```
cargo build --release
./target/release/sidecheck check --help
```
## 许可证
MIT
标签:HTTP测试, 侧信道分析, 可视化界面, 文档结构分析, 时间攻击检测