2025 年 3 月,AWS Labs 的 Tough 库(TUF – The Update Framework 的 Rust 客户端实现)披露了多个安全漏洞。这些问题分别跟踪为 CVE-2025-2885、CVE-2025-2886、CVE-2025-2887 和 CVE-2025-2888,并已在 Tough 0.20.0 版本 中修复。安全工程师可以使用本文档了解每个漏洞的技术细节、代码库中的根本原因以及解决这些问题的补丁。强烈建议所有 Tough < 0.20.0 的用户升级到 v0.20.0 或更高版本。
Tough 在更新过程中未能按顺序验证 Root metadata 版本。控制仓库(或具备中间人能力)的攻击者可以提供带有意外版本号的根元数据文件,导致客户端获取并信任错误的版本。本质上,Tough 并未确保新根元数据的版本恰好比先前信任的版本大 1,这违反了 TUF 对连续信任链的要求。这可能导致 回滚或混搭攻击,即旧(但签名正确)的根被当作最新版本接受,从而可能重新引入已退役的签名密钥或过期的信任。
tough/src/lib.rs 中 Tough 对根元数据的更新逻辑。有漏洞的实现仅检查新根的版本不低于旧版本,而不是要求它为下一个顺序版本。这一遗漏违反了 TUF 规范中客户端必须按顺序下载中间根版本(不允许跳过版本)的规则。有漏洞的实现: 在 0.19.x 及更早版本中,下载新的 root.json 后,Tough 验证了签名,但并未严格执行版本连续性。它允许新根版本为任意 >= 受信任版本的值。例如,如果当前受信任的根是版本 N,Tough 会接受声称版本为 N+2 或更高(只要签名有效)的新根,从而跳过 N+1。下面的代码片段说明了修复前的检查:```rust
// (Prior to fix) Allow new root version to be >= old version – too permissive
ensure!(root.signed.version <= new_root.signed.version, error::OlderMetadataSnafu {
role: RoleType::Root,
current_version: root.signed.version,
new_version: new_root.signed.version
});
攻击者可以通过提供<b>一个版本号更高但实际上是旧密钥集的根元数据</b>来利用这一点。Tough 会将其视为更新并接受,进而[信任使用过期密钥签名的内容](https://github.com/advisories/GHSA-5vmp-m5v2-hx47#:~:text=The%20tough%20client%20will%20trust,with%20a%20previous%20root%20role)。
<b>修复实现:</b>补丁代码(Tough 0.20.0 中)明确要求新根版本的版本号恰好比旧版本大 1,参见 [提交 0eeb60a](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6)。它还确保新版本更高(防止相同或降低)并阻止任何大于 +1 的跳变:```rust
// (Fixed in v0.20.0) Enforce sequential root version update (new_version == old_version + 1)
ensure!(
root.signed.version < new_root.signed.version &&
root.signed.version.get() + 1 == new_root.signed.version.get(),
error::OlderMetadataSnafu {
role: RoleType::Root,
current_version: root.signed.version,
new_version: new_root.signed.version
}
);
此更改确保客户端按顺序(N, N+1, N+2, ...)下载并应用根元数据,而不会跳过。如果遇到版本不是恰好为 old_version+1 的根元数据文件,Tough 现在会将其视为潜在攻击并中止更新。
Tough 错误地处理了 TUF 所定义的 “终止”委派目标角色。在 TUF 仓库中,委派可以被标记为 terminating,这意味着如果目标查找到达该委派且未在其中找到目标,则搜索应停止(而不应继续到其他较低优先级的委派)。(参见此处)由于逻辑错误,Tough 未能终止搜索——即使本应停止,它仍会继续搜索后续委派。这可能允许控制较低优先级委派的攻击者为原本不应由其控制的目标提供内容,从而绕过预期的信任边界。
tough/src/editor/targets.rs 中的目标查找算法(以及相关的委派解析代码)。存在漏洞的代码没有正确处理委派上的 terminating 标志。在遍历委派查找目标时,即使当前委派被标记为 terminating 且没有匹配项,Tough 仍会继续到下一个委派,这与 TUF 规范相反。易受攻击的实现:在 Tough <0.20.0 中,find_target() 逻辑只是递归或循环遍历所有可能的委派角色,直到找到目标或全部耗尽。当遇到 terminating 角色时,它不会设置任何标志或跳出循环。旧行为伪代码如下:```rust
for role in delegation_chain {
if role.has_target(target) {
return target_metadata;
}
// Missing: if role is terminating and target not found, should break.
// Tough erroneously continues to next delegation.
}
这意味着,较低优先级的委托(在遇到其上方的终止委托后本应被忽略)仍可能被查询,并提供恶意目标文件
<b>修复后的实现:</b> 修补后的版本引入了一种机制来跟踪终止并适当地停止搜索。当遇到不含目标的终止委托时,Tough 现在会[立即跳出搜索循环](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a#:~:text=%2F%2F%20we%20encountered%20a%20terminating,so%20we%20stop%20iterating%20immediately)。在更新后的代码中,当命中终止角色时,会设置一个布尔标志(例如 `terminated`),并将其向调用栈上层传播。例如:```rust
// If a terminating delegation was reached (and we didn't find the target there), stop searching further
if role.terminating && !permissive {
// Mark that we encountered a terminating delegation
*terminated = true;
break;
}
此外,递归的 find_target 调用现在会携带一个 terminated 标志,以确保一旦发出终止信号,较高级别的循环中便不再考虑其他委托。Tough 的 RepositoryEditor::delegate_role 及相关结构也已更新,以存储 terminating 属性并将其传递给搜索逻辑。
break,导致未经授权的目标数据被纳入考虑。Tough 用于检测快照元数据中的回滚攻击的逻辑并不完整。具体而言,在更新 Snapshot 角色时,Tough 应验证所有先前见过的目标元数据(包括委托目标)在新快照中仍然存在,且未发生版本回退。(参见此处)Tough 确实对顶层 targets.json 执行了此检查,但未能对委托的目标元数据文件执行同样检查。这一缺陷可能允许攻击者在未被发现的情况下删除或回退仓库快照元数据中的委托目标文件,导致客户端将过期(或缺失)的委托目标文件当作最新版本接受。
tough/src/lib.rs 中的快照更新验证(加载/应用新 Snapshot 元数据的函数)。存在漏洞的代码只检查了快照中主 targets.json 角色的连续性,但未遍历快照元数据中列出的委托角色以执行类似检查。存在漏洞的实现: 在 Tough <0.20.0 中,客户端在获取新的 snapshot.json 后,会确保 root 和 snapshot 角色未被回滚,并专门确保 targets.json 仍然存在。它还会验证新快照中 targets.json 的版本是否 >= 先前版本。然而,如果快照包含委托目标(例如 projects.json、user.json 委托元数据),客户端不会验证这些内容。例如,原始代码大致如下:```rust
// Pseudo-code of original snapshot rollback check (simplified)
if let Some(old_targets_meta) = old_snapshot.meta.get("targets.json") {
let new_targets_meta = new_snapshot.meta.get("targets.json").unwrap();
ensure!(new_targets_meta.version >= old_targets_meta.version, ...);
}
// (No checks for delegated target roles like "projects.json", "user.json", etc.)
这意味着如果拥有仓库访问权限的攻击者<b>删除了某个委派元数据文件,或将其回滚到较旧版本</b>,Tough 的客户端将不会察觉——只要主要的 targets.json 仍然完好。客户端随后可能会从该委派中下载过时的目标,却不知道它本应作为回滚而被拒绝。
<b>已修复的实现:</b>版本 0.20.0 增加了对<b>快照元数据中列出的每个角色</b>的全面检查。新代码遍历旧快照元数据中的每个条目(包括所有委派的目标角色),并确保每个条目满足两点:(1) 该角色仍存在于新快照中,以及 (2) 其版本号未降低。如果任何角色在新快照中缺失,或其版本号低于之前,更新将被拒绝,视为潜在的回滚攻击。例如:```rust
for (name, old_meta) in &old_snapshot.signed.meta {
// 1. Role must appear in new snapshot
ensure!(
snapshot.signed.meta.contains_key(name),
error::SnapshotRoleMissingSnafu { role: name, old_version: old_snapshot.signed.version, new_version: snapshot.signed.version }
);
// 2. Role’s version must not decrease
let new_meta = snapshot.signed.meta.get(name).unwrap();
ensure!(
old_meta.version <= new_meta.version,
error::SnapshotRoleRollbackSnafu { role: name, old_role_version: old_meta.version, new_role_version: new_meta.version, … }
);
}
通过遍历 所有角色(name 代表每个元数据文件名,如 targets.json、delegated-role.json 等),客户端将捕获任何被删除或回退的委托目标元数据。如果某个角色消失或其版本回退,将触发 SnapshotRoleMissing 和 SnapshotRoleRollback 错误。(如果角色消失或其版本回退)。值得注意的是,Tough 现在还明确确保快照中至少包含 targets.json 条目(否则会抛出 SnapshotTargetsMetaMissing 错误),作为健全性检查。