
Problème avec tough, versions antérieures à 0.20.0 (CVEs multiples)
En mars 2025, plusieurs vulnérabilités de sécurité ont été divulguées dans la bibliothèque Tough d’AWS Labs (une implémentation cliente en Rust de TUF – The Update Framework). Elles sont référencées sous les identifiants CVE-2025-2885, CVE-2025-2886, CVE-2025-2887 et CVE-2025-2888, et ont été corrigées dans Tough version 0.20.0. Les ingénieurs sécurité peuvent utiliser ce document pour comprendre les détails techniques de chaque vulnérabilité, la cause racine dans le code, ainsi que les correctifs qui les résolvent. Tous les utilisateurs de Tough < 0.20.0 sont vivement invités à passer à la v0.20.0 ou à une version ultérieure.
Tough ne parvenait pas à valider en séquence la version des Root metadata lors de la mise à jour. Un attaquant contrôlant un dépôt (ou disposant d’une capacité d’homme du milieu) pourrait fournir un fichier de métadonnées racine avec un numéro de version inattendu, amenant le client à récupérer une mauvaise version et à lui faire confiance. En substance, Tough ne garantissait pas que la version d’une nouvelle métadonnée racine soit exactement supérieure de un à la version précédemment approuvée, ce qui viole l’exigence de TUF d’une chaîne de confiance continue. Cela pourrait conduire à des attaques par rollback ou par mix-and-match, où une ancienne racine (mais correctement signée) est acceptée comme s’il s’agissait de la dernière, ce qui pourrait .
tough/src/lib.rs. L’implémentation vulnérable vérifiait uniquement que la version de la nouvelle racine n’était pas inférieure à l’ancienne, au lieu d’exiger qu’elle soit la version suivante dans la séquence. Cette omission enfreint la règle de la spécification TUF selon laquelle les clients doivent télécharger les versions racine intermédiaires dans l’ordre (sans sauter de version).Implémentation vulnérable : Dans les versions 0.19.x et antérieures, après avoir téléchargé un nouveau root.json, Tough vérifiait les signatures mais n’appliquait pas strictement la continuité des versions. Il permettait que la nouvelle version de la racine soit toute valeur >= la version approuvée. Par exemple, si la racine approuvée actuelle était la version N, Tough acceptait une nouvelle racine prétendant être la version N+2 ou supérieure (tant que les signatures étaient valides), sans tenir compte de N+1. L’extrait de code ci-dessous illustre la vérification effectuée avant le correctif :```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
});
Un attaquant pourrait exploiter cela en présentant <b>une métadonnée root de version supérieure qui est en réalité un ancien ensemble de clés</b>. Tough l’accepterait, pensant qu’il s’agit d’une mise à jour, et [ferait confiance à un contenu signé avec des clés obsolètes](https://github.com/advisories/GHSA-5vmp-m5v2-hx47#:~:text=The%20tough%20client%20will%20trust,with%20a%20previous%20root%20role).
<b>Implémentation corrigée :</b> Le code corrigé (dans Tough 0.20.0) exige explicitement que la version de la nouvelle métadonnée root soit exactement supérieure d’un à l’ancienne version, voir [commit 0eeb60a](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6). Il garantit également que la nouvelle version est supérieure (empêche l’égalité ou la diminution) et empêche tout saut supérieur à +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
}
);
Ce changement garantit que le client télécharge et applique les métadonnées racine dans l’ordre (N, N+1, N+2, ...) sans en sauter. Si un fichier de métadonnées racine est rencontré avec une version qui n’est pas exactement old_version+1, Tough le traite désormais comme une attaque potentielle et interrompt la mise à jour.
Tough a mal géré les rôles de cibles délégués « terminants » tels que définis par TUF. Dans un dépôt TUF, une délégation peut être marquée terminante, ce qui signifie que si une recherche de cible atteint cette délégation et que la cible n’y est pas trouvée, la recherche doit s’arrêter (et ne pas continuer vers d’autres délégations de priorité inférieure). (voir ici) En raison d’une erreur logique, Tough ne parvenait pas à terminer la recherche dans ce cas – il continuait à chercher dans les délégations suivantes alors qu’il aurait dû s’arrêter. Cela pourrait permettre à un attaquant contrôlant une délégation de priorité inférieure de fournir du contenu pour des cibles qu’il ne devrait pas contrôler, contournant les frontières de confiance prévues.
tough/src/editor/targets.rs (et le code de résolution des délégations associé). Le code vulnérable ne gérait pas correctement le drapeau terminating sur les délégations. Lors de l’itération sur les délégations pour rechercher une cible, Tough passait à la délégation suivante même si la délégation courante était marquée comme terminante et ne contenait aucune correspondance, contrairement à la spécification TUF.Implémentation vulnérable : Dans Tough <0.20.0, la logique find_target() parcourait simplement, de manière récursive ou itérative, tous les rôles délégués possibles jusqu’à ce qu’une cible soit trouvée ou que tous soient épuisés. Elle ne levait aucun drapeau et ne sortait pas de la boucle lorsqu’elle rencontrait un rôle terminant. Pseudo-code de l’ancien comportement :```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.
}
Cela signifie qu'un délégué de priorité inférieure (qui devrait être ignoré après une délégation terminale au-dessus de lui) pourrait encore être consulté et fournir un fichier cible malveillant
<b>Implémentation corrigée :</b> La version corrigée introduit un mécanisme pour suivre la terminaison et arrêter la recherche de manière appropriée. Lorsqu'une délégation terminale est rencontrée et ne contient pas la cible, Tough [sort immédiatement de la boucle de recherche](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a#:~:text=%2F%2F%20we%20encountered%20a%20terminating,so%20we%20stop%20iterating%20immediately). Dans le code mis à jour, un indicateur booléen (par ex. `terminated`) est défini lorsqu'un rôle terminal est atteint, et propagé dans la pile d'appels. Par exemple :```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;
}
De plus, les appels récursifs find_target portent désormais un indicateur terminated pour garantir qu’une fois la terminaison signalée, aucune autre délégation n’est envisagée dans les boucles de niveau supérieur. Les structures RepositoryEditor::delegate_role de Tough et les structures associées ont également été mises à jour pour stocker l’attribut terminating et le transmettre dans la logique de recherche.
break sur une délégation terminale permettait que des données de cibles non autorisées soient prises en compte.La logique de Tough pour détecter les attaques de rollback dans les métadonnées de snapshot était incomplète. Plus précisément, lors de la mise à jour du rôle Snapshot, Tough devrait vérifier que toutes les métadonnées de cibles déjà vues (y compris les cibles déléguées) sont toujours présentes et ne sont pas rétrogradées en version dans le nouveau snapshot. (voir ici) Tough appliquait bien cette vérification pour le targets.json de niveau supérieur, mais ne le faisait pas pour les fichiers de métadonnées de cibles déléguées. Cette faille pouvait permettre à un attaquant de supprimer ou de restaurer une version antérieure d’un fichier de cible déléguée dans les métadonnées de snapshot du dépôt sans être détecté, ce qui amenait le client à accepter un fichier de cible déléguée obsolète (ou manquant) comme s’il était à jour.
tough/src/lib.rs (fonction qui charge/applique les nouvelles métadonnées de snapshot). Le code vulnérable ne vérifiait que la continuité du rôle principal targets.json dans le snapshot, mais n’itérait pas sur les rôles délégués listés dans les métadonnées de snapshot pour effectuer des vérifications similaires.Implémentation vulnérable : Dans Tough <0.20.0, après avoir récupéré un nouveau snapshot.json, le client s’assurait que les rôles root et snapshot n’étaient pas régressés, et il s’assurait spécifiquement que targets.json était toujours présent. Il vérifiait également que la version de targets.json dans le nouveau snapshot était >= la version précédente. Cependant, si le snapshot contenait des cibles déléguées (par exemple, des métadonnées déléguées projects.json, user.json), le client ne les vérifiait pas. Par exemple, à l’origine, le code faisait quelque chose comme :```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.)
Cela signifie que si un attaquant disposant d'un accès au référentiel <b>supprimait un fichier de métadonnées délégué ou le ramenait à une version antérieure</b>, le client de Tough ne le remarquerait pas – tant que le fichier targets.json principal restait intact. Le client pourrait alors télécharger une cible obsolète depuis cette délégation, sans savoir qu'elle aurait dû être rejetée comme une régression.
<b>Implémentation corrigée:</b> la version 0.20.0 ajoute des vérifications exhaustives pour <b>chaque rôle listé dans les métadonnées de snapshot</b>. Le nouveau code parcourt chaque entrée des métadonnées de l'ancien snapshot (y compris tous les rôles de cibles délégués) et garantit deux choses pour chacune : (1) que le rôle existe toujours dans le nouveau snapshot, et (2) que sa version n'a pas diminué. Si un rôle manque dans le nouveau snapshot ou a un numéro de version inférieur à celui d'avant, la mise à jour est rejetée comme une potentielle attaque de rollback. Par exemple:```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, … }
);
}
By looping through all roles (name represents each metadata filename like targets.json, delegated-role.json, etc.), the client will catch if any delegated target metadata was removed or reverted. The errors SnapshotRoleMissing and SnapshotRoleRollback will trigger if a role disappeared or its version went backwards. Notably, Tough now also explicitly ensures that the snapshot contains at least the targets.json entry (otherwise it errors with SnapshotTargetsMetaMissing) as a sanity check.
Tough incorrectly handled a detected rollback in the Timestamp role. The Timestamp role in TUF periodically signs the latest snapshot metadata version to help clients detect if they are seeing an older snapshot (a rollback). Tough did perform the rollback check on the snapshot version contained in the timestamp, but only after caching the new timestamp metadata locally. If a rollback was detected, Tough would reject the update but had already persisted the invalid timestamp as the “latest” in its cache. This meant a malicious timestamp (with an outdated snapshot version) could poison the client’s cache. Subsequent legitimate updates could then appear to Tough as rollbacks (since the cache held a timestamp indicating a higher snapshot version than the new one), blocking valid updates.
tough/src/lib.rs (function that loads the new timestamp.json). The core issue was an order-of-operations bug: Tough updated its local cache/state with the new Timestamp metadata before fully validating that the snapshot version inside wasn’t older than what it had seen before.Vulnerable Implementation: In Tough <0.20.0, when a new timestamp.json was fetched, the client would parse and write it to the cache (marking it as the current trusted timestamp) and then perform the rollback check on the snapshot version. If the snapshot version in the new timestamp was lower than the previously recorded snapshot version (indicating a rollback attempt), Tough would log an error and reject that update cycle – but the cache already held the “bad” timestamp. There was no removal of the cached entry in that error path. Thus, the client’s record of the “trusted” timestamp could become this outdated one.
For example, suppose the last known snapshot version was 5. An attacker could provide a Timestamp metadata (with a higher timestamp version number) that signs snapshot version 4. Tough would accept the timestamp file (caching it), then notice snapshot 4 < 5 and error out of the update – but now its cache says “latest timestamp indicates snapshot 4”. When a correct timestamp (snapshot 5 or 6) comes next, Tough sees snapshot 5 vs cached snapshot 4 as another rollback (since it erroneously trusts the cache’s snapshot version 4 as baseline), and thus rejects even the valid update. The AWS bulletin describes this cycle: “the client caches timestamp metadata despite it being correctly rejected when a rollback was detected… causing tough to subsequently fail to consume valid updates.”
Fixed Implementation: The fix ensures that rollback checks occur before caching the new timestamp, and adds stricter validation of the timestamp contents. In Tough 0.20.0, the load_timestamp() function was modified to enforce that the Timestamp metadata is well-formed and that its snapshot version is not less than the previously trusted snapshot version before concluding the update. Specifically, the code now checks: (a) the timestamp metadata has exactly one entry (must only be for snapshot.json), (b) that entry exists and is parsed, and (c) the snapshot version inside is >= the older snapshot version. (see here). Only after these validations pass is the new timestamp considered trusted. The critical added logic is illustrated below:```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 } ); }
Avec ce changement, si la version de snapshot du nouveau timestamp est inférieure à la version de snapshot précédemment observée, le `ensure!` échoue <b>avant</b> que le nouveau timestamp ne soit enregistré comme étant de confiance. L'erreur `OlderSnapshotInTimestamp` est levée pour interrompre la mise à jour. Par conséquent, Tough conserve l'ancien timestamp (correct) dans le cache lorsqu'un rollback est détecté, et le mauvais timestamp n'est jamais mis en cache comme étant de confiance. Cela empêche le scénario dans lequel une mise à jour de timestamp rejetée pollue les mises à jour futures.
- <b>Cause racine :</b> Une <b>séquence de mise à jour incorrecte</b> (CWE-367 : Time-of-check Time-of-use Race Condition, dans le contexte de la validation des métadonnées). La vérification par Tough du rollback de snapshot dans le timestamp était effectuée au mauvais moment, après que des effets de bord (mise en cache) s'étaient produits. De plus, la validation incomplète du contenu du timestamp (longueur) rendait la logique fragile.
- <b>Impact :</b> Un timestamp malveillant (avec une version de snapshot inférieure) pourrait temporairement tromper le client, l'amenant à stocker un mauvais état. Cela conduit à un déni de service dans le mécanisme de mise à jour : le client considérerait ensuite les mises à jour légitimes comme invalides (fausse détection de rollback). Pas d'exécution de code directe ni de vol de données, mais un <b>échec persistant de la mise à jour</b> peut être tout aussi dangereux (par exemple, en empêchant l'application de correctifs de sécurité). (voir [ici](https://github.com/advisories/GHSA-76g3-38jv-wxh4#:~:text=If%20the%20tough%20client%20successfully,client%20from%20consuming%20valid%20updates))
- <b>Remédiation :</b> Mettez à jour tough vers <b>0.20.0+</b>. La version corrigée gère les événements de rollback de timestamp en toute sécurité. Si un échec de mise à jour dû à ce bug a été observé, il peut être nécessaire de <b>vider les métadonnées en cache</b> (pour supprimer tout timestamp empoisonné) avant de réessayer les mises à jour avec le client corrigé. En règle générale, les clients doivent toujours vérifier les métadonnées avant de leur faire confiance ou de les mettre en cache – cette faille souligne ce principe.
References:
- [AWS Security Bulletin AWS-2025-007](https://aws.amazon.com/security/security-bulletins/AWS-2025-007/) – <i>Problème avec tough, versions antérieures à 0.20.0 (plusieurs CVE)</i>
- [Entrées de la base de données GitHub Advisory pour CVE-2025-2885](https://github.com/advisories/GHSA-5vmp-m5v2-hx47)
- [Entrées de la base de données GitHub Advisory pour CVE-2025-2886](https://github.com/advisories/GHSA-v4wr-j3w6-mxqc)
- [Entrées de la base de données GitHub Advisory pour CVE-2025-2887](https://github.com/advisories/GHSA-q6r9-r9pw-4cf7)
- [Entrées de la base de données GitHub Advisory pour CVE-2025-2888](https://github.com/advisories/GHSA-76g3-38jv-wxh4)
- Commits de correctifs de Tough v0.20.0 :
- Commit de correctif de la version racine | [`awslabs/tough@0eeb60a`](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6)
- Commit de correctif des délégations terminales | [`awslabs/tough@598111f`](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a)
- Commit de correctif du rollback de snapshot | [`awslabs/tough@3345151`](https://github.com/awslabs/tough/commit/3345151a87c358d1ce43aeb7e8b3ebea5ebdbab4)
- Commit de correctif du rollback de timestamp | [`awslabs/tough@9b400e1`](https://github.com/awslabs/tough/commit/9b400e1c8b7d6b9ab8009104fa7fe5884db05f18)