
Issue with tough, versions prior to 0.20.0 (Multiple CVEs)
In March 2025, multiple security vulnerabilities were disclosed in AWS Labs’ Tough library (a Rust client implementation of TUF – The Update Framework). These issues are tracked under CVE-2025-2885, CVE-2025-2886, CVE-2025-2887 and CVE-2025-2888, and were fixed in Tough version 0.20.0. Security engineers can use this document to understand each vulnerability’s technical details, root cause in the codebase, and the patches that resolve them. All users of Tough < 0.20.0 are strongly advised to upgrade to v0.20.0 or later.
Tough failed to validate the Root metadata version in sequence during update. An attacker controlling a repository (or with man-in-the-middle ability) could supply a root metadata file with an unexpected version number, causing the client to fetch and trust a wrong version. In essence, Tough did not ensure that a new root metadata’s version was exactly one greater than the previously trusted version, violating TUF’s requirement for a continuous chain of trust. This could lead to rollback or mix-and-match attacks, where an old (but properly signed) root is accepted as if it were the latest, potentially re-introducing retired signing keys or expired trust.
tough/src/lib.rs. The vulnerable implementation only checked that the new root’s version was not less than the old one, instead of requiring it to be the next sequential version. This omission breaks the TUF spec’s rule that clients must download intermediate root versions in order (no version skipping).Vulnerable Implementation: In version 0.19.x and earlier, after downloading a new root.json, Tough verified signatures but did not strictly enforce the version continuity. It permitted the new root version to be any value >= the trusted version. For example, if the current trusted root was version N, Tough would accept a new root claiming version N+2 or higher (as long as signatures were valid), skipping N+1. The code snippet below illustrates the check prior to the fix:
// (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
});
An attacker could exploit this by presenting a higher-version root metadata that is actually an older key set. Tough would accept it, thinking it’s an update, and trust content signed with outdated keys.
Fixed Implementation: The patched code (in Tough 0.20.0) explicitly requires the new root’s version to be exactly one greater than the old version, see commit 0eeb60a. It also ensures the new version is higher (prevents equality or decrease) and prevents any jump larger than +1:
// (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
}
);
This change ensures the client downloads and applies root metadata in order (N, N+1, N+2, ...) without skipping. If a root metadata file is encountered with a version not exactly old_version+1, Tough now treats it as a potential attack and aborts the update.
Tough mishandled “terminating” delegated targets roles as defined by TUF. In a TUF repository, a delegation can be marked terminating, meaning that if a target lookup reaches that delegation and the target is not found there, the search should stop (and not continue to other lower-priority delegations). (see here) Due to a logic error, Tough failed to terminate the search in this case – it would continue searching subsequent delegations even when it should have stopped. This could allow an attacker controlling a lower-priority delegation to serve content for targets they shouldn’t control, bypassing the intended trust boundaries.
tough/src/editor/targets.rs (and related delegation resolution code). The vulnerable code did not properly handle the terminating flag on delegations. When iterating through delegations in search of a target, Tough would proceed to the next delegation even if the current one was marked terminating and had no match, contrary to the TUF spec.Vulnerable Implementation: In Tough <0.20.0, the find_target() logic simply recursed or looped through all possible delegated roles until a target was found or all were exhausted. It did not set any flag or break out when encountering a terminating role. Pseudocode of the old behavior:
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.
}
This means a lower-priority delegate (which should be ignored after a terminating delegation above it) could still be consulted and supply a malicious target file
Fixed Implementation: The patched version introduces a mechanism to track termination and stop the search appropriately. When a terminating delegation is encountered and does not contain the target, Tough now breaks out of the search loop immediately. In the updated code, a boolean flag (e.g. terminated) is set when a terminating role is hit, and propagated up the call stack. For example:
// 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;
}