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 错误),作为健全性检查。
Tough 错误地处理了 Timestamp 角色中检测到的回滚。TUF 中的 Timestamp 角色会定期签署最新的快照元数据版本,以帮助客户端检测是否看到一个较旧的快照(即回滚)。Tough 确实对时间戳中包含的快照版本执行了回滚检查,但仅在将新的时间戳元数据缓存到本地之后才执行。如果检测到回滚,Tough 会拒绝更新,但已经将无效时间戳作为“最新”版本持久化在其缓存中。这意味着恶意时间戳(带有过时的快照版本)可能污染客户端的缓存。随后,合法的更新在 Tough 看来可能像是回滚(因为缓存中的时间戳所指示的快照版本高于新版本),从而阻止有效的更新。
tough/src/lib.rs 中的时间戳更新逻辑(加载新 timestamp.json 的函数)。核心问题是一个 操作顺序错误:Tough 在完全验证内部快照版本不早于之前所见版本之前,就已用新的 Timestamp 元数据更新了本地缓存/状态。存在漏洞的实现: 在 Tough <0.20.0 中,当获取到新的 timestamp.json 时,客户端会解析并将其写入缓存(将其标记为当前受信任的时间戳),然后对快照版本执行回滚检查。如果新时间戳中的快照版本低于先前记录的快照版本(表示回滚尝试),Tough 会记录错误并拒绝该更新周期 – 但缓存中已经保存了“坏”时间戳。在该错误路径中,缓存条目不会被移除。因此,客户端对“受信任”时间戳的记录可能变成这个过时的记录。
例如,假设最后已知的快照版本为 5。攻击者可以提供一份 Timestamp 元数据(带有更高的时间戳版本号),该元数据签署的是快照版本 4。Tough 会接受该时间戳文件(将其缓存),然后发现快照 4 < 5 并在更新过程中报错退出 – 但现在其缓存显示“最新时间戳指示快照 4”。当接下来出现正确的时间戳(快照 5 或 6)时,Tough 会将快照 5 与缓存的快照 4 视为又一次回滚(因为它错误地将缓存中的快照版本 4 作为基线),从而连有效更新也拒绝。AWS 公告描述了这一循环:“客户端会缓存时间戳元数据,尽管在检测到回滚时该元数据已被正确拒绝……导致 tough 随后无法消费有效更新。”
修复后的实现: 此修复确保在缓存新时间戳之前执行回滚检查,并添加了对时间戳内容的更严格验证。在 Tough 0.20.0 中,修改了 load_timestamp() 函数,强制要求在完成更新前验证 Timestamp 元数据格式正确且其快照版本不低于之前受信任的快照版本。具体来说,代码现在检查:(a) 时间戳元数据只有一个条目(必须仅针对 snapshot.json),(b) 该条目存在且可解析,以及 (c) 内部的快照版本 >= 旧的快照版本。(参见 此处)只有通过这些验证后,新时间戳才被视为受信任。关键的新增逻辑如下所示:```rust
// Ensure the timestamp meta contains exactly one entry (the snapshot)
ensure!(timestamp.signed.meta.len() == 1, error::TimestampMetaLengthSnafu { … });
let snapshot_meta = timestamp.signed.meta.get("snapshot.json");
ensure!(snapshot_meta.is_some(), error::MissingSnapshotMetaSnafu { … });
// If we have a previously trusted timestamp (old_timestamp): if let Some(old_timestamp) = old_timestamp_opt { // Check that the snapshot version in the new timestamp >= old snapshot version let old_snapshot_meta = old_timestamp.signed.meta.get("snapshot.json").unwrap(); ensure!( old_snapshot_meta.version <= snapshot_meta.unwrap().version, error::OlderSnapshotInTimestampSnafu { // details: new snapshot vs old snapshot versions snapshot_new: snapshot_meta.unwrap().version, snapshot_old: old_snapshot_meta.version, timestamp_new: timestamp.signed.version, timestamp_old: old_timestamp.signed.version } ); }
通过此更改,如果新时间戳的快照版本低于之前见过的快照版本,`ensure!` 将在新时间戳被保存为受信任<b>之前</b>失败。将引发错误 `OlderSnapshotInTimestamp` 以中止更新。因此,当检测到回滚时,Tough 会将旧的(正确的)时间戳保留在缓存中,而错误的时间戳永远不会被缓存为受信任。这防止了被拒绝的时间戳更新污染未来更新的情况。
- <b>根本原因:</b> <b>不当的更新顺序</b>(CWE-367:检查时间与使用时间竞争条件,在元数据验证的上下文中)。Tough 对时间戳中快照回滚的检查发生在错误的时间点——即在副作用(缓存)已经发生之后。此外,未完全验证时间戳内容(长度)使逻辑变得脆弱
- <b>影响:</b> 恶意时间戳(具有较低的快照版本)可能暂时欺骗客户端,导致其存储错误状态。这会导致更新机制中的拒绝服务:客户端此后会将真实更新视为无效(错误的回滚检测)。不会直接执行代码或窃取数据,但<b>持续更新</b>失败可能同样危险(例如,阻止安全补丁应用)。(参见[此处](https://github.com/advisories/GHSA-76g3-38jv-wxh4#:~:text=If%20the%20tough%20client%20successfully,client%20from%20consuming%20valid%20updates))
- <b>修复方案:</b> 升级到 tough <b>0.20.0+</b>。已修补的版本可以安全地处理时间戳回滚事件。如果观察到因该错误导致的更新失败,则可能需要在用修复后的客户端重试更新之前<b>清除缓存的元数据</b>(以移除任何被污染的时间戳)。作为一般实践,客户端应始终在信任或缓存元数据之前对其进行验证——此问题恰恰凸显了这一原则。
参考文献:
- [AWS 安全公告 AWS-2025-007](https://aws.amazon.com/security/security-bulletins/AWS-2025-007/) – <i>tough 的问题,0.20.0 之前的版本(多个 CVE)</i>
- [GitHub 公告数据库中关于 CVE-2025-2885 的条目](https://github.com/advisories/GHSA-5vmp-m5v2-hx47)
- [GitHub 公告数据库中关于 CVE-2025-2886 的条目](https://github.com/advisories/GHSA-v4wr-j3w6-mxqc)
- [GitHub 公告数据库中关于 CVE-2025-2887 的条目](https://github.com/advisories/GHSA-q6r9-r9pw-4cf7)
- [GitHub 公告数据库中关于 CVE-2025-2888 的条目](https://github.com/advisories/GHSA-76g3-38jv-wxh4)
- Tough v0.20.0 补丁提交:
- 根版本修复提交 | [`awslabs/tough@0eeb60a`](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6)
- 终止委派修复提交 | [`awslabs/tough@598111f`](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a)
- 快照回滚修复提交 | [`awslabs/tough@3345151`](https://github.com/awslabs/tough/commit/3345151a87c358d1ce43aeb7e8b3ebea5ebdbab4)
- 时间戳回滚修复提交 | [`awslabs/tough@9b400e1`](https://github.com/awslabs/tough/commit/9b400e1c8b7d6b9ab8009104fa7fe5884db05f18)