XEY LABS — MYTHIC / 002 EN/ ID/ 中文/ 日本語/ 한국어 ← BUILD LOG 项目 REPO ↗ GITHUB ↗

Mythic 加固。

有一周,我们停下构建 Mythic ,转而尝试攻破它 —— 对线上服务器做黑盒测试,黑盒不够的地方再用 白盒确认。这一周产出了十二项发现。每一项都已修复、由回归测试钉住, 并在我们宣布完成之前用原始复现重新验证。本篇还记录了同一轮中落地的 两个 M1 路线图事项:密钥轮换和 Redis 存储。

零停机密钥轮换

直到本周,Mythic 用单一 Ed25519 密钥签署所有决策令牌;轮换意味着 重启,并使所有在途令牌失效。服务器现在持有一个只含一名活跃签名者 的密钥环 (ADR-0006)。 轮换基于时间并在进程内运行 (MYTHIC_KEY_MAX_AGE,默认 720h);退役密钥在 MYTHIC_KEY_RETENTION(默认 24h)内继续出现在 JWKS 中 —— 故意长于它们签发的任何令牌的可能寿命,因此在途令牌在轮换 中途永远不会中断:

密钥库是以原子方式写入的 JSON 文件;旧版 32 字节种子文件仍可加载, 并在首次保存时迁移。源站继续在本地验证令牌,按 kid 声明选择密钥 —— 无需协调调用。

超越单节点的存储

内存存储把 mythicd 绑在单节点上 (ADR-0009)。 MYTHIC_STORE=redis 选择 Redis 后端:一次性挑战用 GETDEL(原子取删),压力计数器用一个 Lua 脚本裁剪的 有序集合。两个后端都实现滑动窗口计数器,堵住了固定窗口的边界突发。 Store 接口现在返回错误:连不上的 Redis 拒绝启动, 运行时的中断在 /v1 上表现为 503 store_unavailable,而 /healthz 保持对负载均衡器开放。一套共享的契约测试同时跑在两个后端 (memory 和 miniredis)上,让它们的语义无法漂移。

白纸黑字记一个 caveat:共享的是存储,不是签名密钥。节点 A 的令牌 无法用节点 B 的 JWKS 验证。多节点令牌验证需要共享密钥库 (KMS/HSM)—— M5,本次改动未解决。

十二项发现,分组呈现

每行:攻击揭示了什么,然后改了什么。每项修复都有回归测试; 每项修复都用原始复现重新验证过。

领域原因修复
传输与限流
限流预算 预算在验证之后才消耗 —— 非法 JSON 和未知 site key 的洪水未被限流(600 请求,225 req/s,零 429)。 预算现在在验证之前计数。
IPv6 压力键 限流器按 /128 键控 IPv6 —— 一个 /64 控制了 2⁶⁴ 个身份(单个网段 5,000 请求,零命中)。 计数器按 /64 聚合 IPv6(ADR-0007)。
连接超时 没有 body/read/idle 超时 —— 慢客户端无限期占用连接。 已设置 ReadTimeout、WriteTimeout、IdleTimeout。
挑战经济学
求解时间下限 下限按 50k H/s 的 worker 建模;实测 crypto.subtle worker 约 400k H/s —— 诚实客户端被误判。 下限现为诚实求解时间的低分位数(2^d / (2M × 32)):最坏情况误报 ≤ ~6%,GPU 级答案约 96% 触发(ADR-0008)。
延迟验证 客户端只需推迟验证越过时钟下限 —— 下限抓不住耐心。 已在威胁模型中记为行为层(M2)的职责,而非下限的。
令牌与配置
令牌 TTL “令牌活得不会比挑战更久”这条文档化的不变式此前未被强制 —— 从 5 秒挑战签发了 1 小时令牌。 TokenTTL 现在钳制到 ChallengeTTL。
分值钳制 恶意调参(MYTHIC_FAST_SOLVE_PENALTY=500)把 500 的风险分值泄漏进签名令牌。 分值在引擎边界钳制到 0–100;超过 100 的惩罚拒绝启动。
配置校验 负的 MYTHIC_BASE_DIFFICULTY 会发出任意 nonce 都能满足的挑战。 配置在启动时校验,越界值拒绝服务。
Base64 规范形式 Go 的 base64 解码器会静默丢弃尾字符中未使用的低位比特,导致不同的令牌字符串解码出相同的字节并通过验证 —— 破坏了字符串层面的令牌身份。篡改抵抗变异套件在首次运行中抓到。 VerifyToken 强制规范 base64:解码后重新编码必须与输入完全一致,否则拒绝令牌。
状态处理
密钥库并发 共享同一密钥库的两个进程在同一临时文件上冲突,或静默分裂签名密钥。 临时文件名按进程唯一;保存拒绝覆盖自加载后被修改的文件。
JSON 尾部字节 JSON 值之后的尾部字节被静默忽略。 尾部字节现在被拒绝。
计数器饱和 滑动窗口计数器每个命中存一条 —— 包括被限流的。来自同一身份的一百万次命中花费约 16.4 MB 堆内存和 595 秒 CPU。 计数器在 IPLimit+1 处饱和:同样的洪水只花 +0.00 MB 和 714 ms。

分值钳制这项修复值得展示,因为它处在信任边界上 —— 最终被签名进令牌的值:

// The score is a 0-100 contract (token claim `risk`,
          // dashboards, tuning docs). Hostile tuning must clamp
          // here, not leak 500 into tokens (red-team H3).
          if score > 100 {
              score = 100
          }
          if score < 0 {
              score = 0
          }

验证 —— 以及仍未解决的问题

每项发现都有复现;每项修复都有回归测试 (go test ./... -race,外加 SDK 类型检查和测试), 每项修复都用原始复现重新验证。仍未解决的都记在威胁模型里, 而不是记在我们脑子里:推迟验证的耐心求解者(M2,行为信号)、 使用全新身份的分布式攻击(此后已部分被 M1 网络指纹缓解 —— 见 003)、以及按节点划分的密钥库 (M5,共享 KMS/HSM)。

— XEY LABS