
Problem mit tough, Versionen vor 0.20.0 (mehrere CVEs)
Im März 2025 wurden mehrere Sicherheitslücken in der Tough-Bibliothek von AWS Labs (einer Rust-Client-Implementierung von TUF – The Update Framework) bekannt gegeben. Diese Probleme werden unter CVE-2025-2885, CVE-2025-2886, CVE-2025-2887 und CVE-2025-2888 geführt und wurden in Tough Version 0.20.0 behoben. Sicherheitsingenieure können dieses Dokument verwenden, um die technischen Details jeder Schwachstelle, die Grundursache in der Codebasis und die Patches zu verstehen, die sie beheben. Allen Benutzern von Tough < 0.20.0 wird dringend empfohlen, auf v0.20.0 oder höher zu aktualisieren.
Tough hat es versäumt, die Version der Root metadata während des Updates in der richtigen Reihenfolge zu validieren. Ein Angreifer, der ein Repository kontrolliert (oder über Man-in-the-Middle-Fähigkeiten verfügt), könnte eine Root-Metadatendatei mit einer unerwarteten Versionsnummer bereitstellen, wodurch der Client eine falsche Version abruft und ihr vertraut. Im Wesentlichen hat Tough nicht sichergestellt, dass die Version einer neuen Root-Metadaten um genau eins höher war als die zuvor vertrauenswürdige Version, was gegen die Anforderung von TUF an eine kontinuierliche Vertrauenskette verstößt. Dies könnte zu Rollback- oder Mix-and-Match-Angriffen führen, bei denen eine alte (aber ordnungsgemäß signierte) Root als die neueste akzeptiert wird, was möglicherweise ausgemusterte Signaturschlüssel oder abgelaufenes Vertrauen wieder einführt.
tough/src/lib.rs. Die anfällige Implementierung prüfte nur, ob die Version der neuen Root nicht kleiner war als die alte, anstatt zu verlangen, dass es sich um die nächste sequenzielle Version handelt. Diese Auslassung verstößt gegen die Regel der TUF-Spezifikation, dass Clients Zwischenversionen der Root in der richtigen Reihenfolge herunterladen müssen (kein Überspringen von Versionen).Anfällige Implementierung: In Version 0.19.x und früher verifizierte Tough nach dem Herunterladen einer neuen root.json zwar Signaturen, setzte die Versionskontinuität jedoch nicht strikt durch. Es erlaubte der neuen Root-Version, einen beliebigen Wert >= der vertrauenswürdigen Version anzunehmen. Wenn beispielsweise die aktuell vertrauenswürdige Root Version N war, akzeptierte Tough eine neue Root mit Version N+2 oder höher (solange die Signaturen gültig waren) und übersprang dabei N+1. Der folgende Codeausschnitt zeigt die Prüfung vor der Korrektur:```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
});
Ein Angreifer könnte dies ausnutzen, indem er <b>Root-Metadaten mit einer höheren Version präsentiert, die tatsächlich ein älterer Schlüsselsatz sind</b>. Tough würde dies akzeptieren, in der Annahme, es sei ein Update, und [Inhalten vertrauen, die mit veralteten Schlüsseln signiert wurden](https://github.com/advisories/GHSA-5vmp-m5v2-hx47#:~:text=The%20tough%20client%20will%20trust,with%20a%20previous%20root%20role).
<b>Korrigierte Implementierung:</b> Der gepatchte Code (in Tough 0.20.0) verlangt ausdrücklich, dass die Version der neuen Root-Metadaten genau eins größer ist als die alte Version, siehe [commit 0eeb60a](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6). Er stellt außerdem sicher, dass die neue Version höher ist (verhindert Gleichheit oder Verringerung) und verhindert jeden Sprung größer als +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
}
);
Dieser Wechsel stellt sicher, dass der Client Root-Metadaten in der Reihenfolge (N, N+1, N+2, ...) herunterlädt und anwendet, ohne sie zu überspringen. Wenn eine Root-Metadaten-Datei mit einer Version angetroffen wird, die nicht exakt old_version+1 ist, behandelt Tough dies nun als potenziellen Angriff und bricht das Update ab.
Tough behandelte die von TUF definierten „terminierenden“ delegierten Targets-Rollen falsch. In einem TUF-Repository kann eine Delegation als terminierend markiert werden, was bedeutet, dass die Suche gestoppt werden soll, wenn eine Target-Suche diese Delegation erreicht und das Target dort nicht gefunden wird (und nicht mit anderen Delegationen niedrigerer Priorität fortgesetzt werden soll). (siehe hier) Aufgrund eines Logikfehlers beendete Tough die Suche in diesem Fall nicht – es durchsuchte weiterhin nachfolgende Delegationen, obwohl es hätte aufhören müssen. Dies könnte es einem Angreifer, der eine Delegation mit niedrigerer Priorität kontrolliert, ermöglichen, Inhalte für Targets auszuliefern, die er nicht kontrollieren dürfte, und damit die vorgesehenen Vertrauensgrenzen umgehen.
tough/src/editor/targets.rs (und der zugehörige Code zur Delegationsauflösung). Der anfällige Code behandelte das Flag terminating bei Delegationen nicht korrekt. Beim Durchlaufen der Delegationen auf der Suche nach einem Target fuhr Tough mit der nächsten Delegation fort, selbst wenn die aktuelle als terminierend markiert war und keinen Treffer ergab – entgegen der TUF-Spezifikation.Anfällige Implementierung: In Tough <0.20.0 durchlief die find_target()-Logik einfach rekursiv oder in einer Schleife alle möglichen delegierten Rollen, bis ein Target gefunden oder alle erschöpft waren. Es wurde kein Flag gesetzt und kein Ausstieg ausgeführt, wenn eine terminierende Rolle angetroffen wurde. Pseudocode des alten Verhaltens:```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.
}
Das bedeutet, dass ein Delegate mit niedrigerer Priorität (das nach einer terminierenden Delegation darüber hätte ignoriert werden sollen) dennoch konsultiert werden und eine schädliche Zieldatei liefern könnte.
<b>Behobene Implementierung:</b> Die gepatchte Version führt einen Mechanismus ein, um die Termination zu verfolgen und die Suche entsprechend zu stoppen. Wenn eine terminierende Delegation angetroffen wird und das Ziel nicht enthält, bricht Tough jetzt [sofort aus der Suchschleife aus](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a#:~:text=%2F%2F%20we%20encountered%20a%20terminating,so%20we%20stop%20iterating%20immediately). Im aktualisierten Code wird ein boolesches Flag (z. B. `terminated`) gesetzt, wenn eine terminierende Rolle erreicht wird, und den Aufrufstapel hinauf propagiert. Zum Beispiel:```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;
}
Zusätzlich tragen die rekursiven find_target-Aufrufe nun ein terminated-Flag, um sicherzustellen, dass nach einem Terminierungssignal keine anderen Delegationen in übergeordneten Schleifen berücksichtigt werden. Toughs RepositoryEditor::delegate_role und zugehörige Strukturen wurden ebenfalls aktualisiert, um das Attribut terminating zu speichern und es durch die Suchlogik zu reichen.
break bei einer terminierenden Delegation, dass nicht autorisierte Zieldaten berücksichtigt wurden.Toughs Logik zur Erkennung von Rollback-Angriffen in Snapshot-Metadaten war unvollständig. Konkret sollte Tough beim Aktualisieren der Snapshot-Rolle überprüfen, dass alle zuvor gesehenen Targets-Metadaten (einschließlich delegierter Targets) im neuen Snapshot weiterhin vorhanden und nicht auf eine ältere Version zurückgesetzt sind. (siehe hier) Tough setzte dies zwar für die oberste Ebene targets.json durch, versäumte es jedoch bei delegierten Target-Metadatendateien. Diese Lücke könnte es einem Angreifer ermöglichen, eine delegierte Target-Datei in den Snapshot-Metadaten des Repositorys unbemerkt zu entfernen oder zurückzusetzen, wodurch der Client eine veraltete (oder fehlende) delegierte Target-Datei als aktuell akzeptiert.
tough/src/lib.rs (Funktion, die neue Snapshot-Metadaten lädt/anwendet). Der anfällige Code prüfte nur die Kontinuität der Hauptrolle targets.json im Snapshot, iterierte jedoch nicht über delegierte Rollen, die in den Snapshot-Metadaten aufgeführt sind, um ähnliche Prüfungen durchzuführen.Anfällige Implementierung: In Tough <0.20.0 stellte der Client nach dem Abruf einer neuen snapshot.json sicher, dass die Rollen root und snapshot nicht zurückgesetzt wurden, und stellte insbesondere sicher, dass targets.json weiterhin vorhanden war. Außerdem prüfte er, ob die Version von targets.json im neuen Snapshot >= der vorherigen Version war. Wenn der Snapshot jedoch delegierte Targets enthielt (z. B. delegierte Metadaten projects.json, user.json), prüfte der Client diese nicht. Beispielsweise führte der Code ursprünglich etwa Folgendes aus:```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.)
Das bedeutet: Wenn ein Angreifer mit Repository-Zugriff <b>eine delegierte Metadatendatei entfernt oder auf eine ältere Version zurückgesetzt hat</b>, würde Toughs Client das nicht bemerken – solange die primäre targets.json intakt war. Der Client könnte daraufhin eine veraltete Zieldatei aus dieser Delegation herunterladen, ohne zu wissen, dass sie als Rollback hätte abgelehnt werden müssen.
<b>Behobene Implementierung:</b> Version 0.20.0 fügt umfassende Prüfungen für <b>jede in den Snapshot-Metadaten aufgeführte Rolle</b> hinzu. Der neue Code iteriert durch jeden Eintrag in den Metadaten des alten Snapshots (einschließlich aller delegierten Targets-Rollen) und stellt für jeden zwei Dinge sicher: (1) dass die Rolle im neuen Snapshot weiterhin existiert, und (2) dass ihre Version nicht gesunken ist. Wenn eine Rolle im neuen Snapshot fehlt oder eine niedrigere Versionsnummer als zuvor aufweist, wird das Update als potenzieller Rollback-Angriff abgelehnt. Zum Beispiel:```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, … }
);
}
Indem der Client alle Rollen durchläuft (der Name steht für jeden Metadaten-Dateinamen wie targets.json, delegated-role.json usw.), erkennt er, wenn Metadaten delegierter Ziele entfernt oder zurückgesetzt wurden. Die Fehler SnapshotRoleMissing und SnapshotRoleRollback werden ausgelöst, wenn eine Rolle verschwunden ist oder ihre Version zurückgegangen ist. Bemerkenswert ist, dass Tough nun auch explizit sicherstellt, dass der Snapshot mindestens den Eintrag targets.json enthält (andernfalls wird der Fehler SnapshotTargetsMetaMissing ausgelöst), als Plausibilitätsprüfung.
Tough behandelte ein erkanntes Rollback in der Timestamp-Rolle falsch. Die Timestamp-Rolle in TUF signiert regelmäßig die neueste Snapshot-Metadatenversion, um Clients dabei zu helfen zu erkennen, ob sie einen älteren Snapshot (ein Rollback) sehen. Tough führte zwar die Rollback-Überprüfung für die im Timestamp enthaltene Snapshot-Version durch, jedoch erst nachdem die neuen Timestamp-Metadaten lokal zwischengespeichert wurden. Wenn ein Rollback erkannt wurde, lehnte Tough das Update ab, hatte aber den ungültigen Timestamp bereits als „neuesten“ in seinem Cache gespeichert. Das bedeutete, dass ein bösartiger Timestamp (mit einer veralteten Snapshot-Version) den Cache des Clients vergiften konnte. Nachfolgende legitime Updates konnten Tough dann wie Rollbacks erscheinen (da der Cache einen Timestamp mit einer höheren Snapshot-Version als der neue enthielt), wodurch gültige Updates blockiert wurden.
tough/src/lib.rs (Funktion, die die neue timestamp.json lädt). Das Kernproblem war ein Fehler in der Reihenfolge der Operationen: Tough aktualisierte seinen lokalen Cache/Status mit den neuen Timestamp-Metadaten, bevor es vollständig validierte, dass die darin enthaltene Snapshot-Version nicht älter war als die zuvor gesehene.Anfällige Implementierung: In Tough <0.20.0 analysierte und schrieb der Client eine neue timestamp.json in den Cache (wodurch sie als aktueller vertrauenswürdiger Timestamp markiert wurde) und führte dann die Rollback-Überprüfung auf die Snapshot-Version durch. Wenn die Snapshot-Version im neuen Timestamp niedriger war als die zuvor aufgezeichnete Snapshot-Version (was auf einen Rollback-Versuch hindeutete), protokollierte Tough einen Fehler und lehnte diesen Update-Zyklus ab – aber der Cache enthielt bereits den „schlechten“ Timestamp. In diesem Fehlerpfad wurde der zwischengespeicherte Eintrag nicht entfernt. Dadurch konnte der Datensatz des Clients für den „vertrauenswürdigen“ Timestamp dieser veraltete werden.
Angenommen, die zuletzt bekannte Snapshot-Version war 5. Ein Angreifer könnte Timestamp-Metadaten (mit einer höheren Timestamp-Versionsnummer) bereitstellen, die Snapshot-Version 4 signieren. Tough würde die Timestamp-Datei akzeptieren (und sie zwischenspeichern), dann feststellen, dass Snapshot 4 < 5 ist, und den Update-Vorgang mit einem Fehler abbrechen – aber sein Cache sagt nun „neuester Timestamp zeigt auf Snapshot 4“. Wenn als Nächstes ein korrekter Timestamp (Snapshot 5 oder 6) kommt, sieht Tough Snapshot 5 im Vergleich zum zwischengespeicherten Snapshot 4 als weiteres Rollback (da es fälschlicherweise der Cache-Version 4 als Basis vertraut) und lehnt folglich sogar das gültige Update ab. Das AWS-Bulletin beschreibt diesen Kreislauf: „Der Client cached Timestamp-Metadaten, obwohl sie bei Erkennung eines Rollbacks korrekt abgelehnt wurden … was dazu führt, dass Tough anschließend keine gültigen Updates mehr konsumieren kann.“
Behobene Implementierung: Der Fix stellt sicher, dass Rollback-Prüfungen vor dem Zwischenspeichern des neuen Timestamps erfolgen, und fügt eine strengere Validierung des Timestamp-Inhalts hinzu. In Tough 0.20.0 wurde die Funktion load_timestamp() so geändert, dass vor Abschluss des Updates erzwungen wird, dass die Timestamp-Metadaten wohlgeformt sind und ihre Snapshot-Version nicht kleiner ist als die zuvor vertrauenswürdige Snapshot-Version. Konkret prüft der Code jetzt: (a) Die Timestamp-Metadaten haben genau einen Eintrag (der nur für snapshot.json sein darf), (b) dieser Eintrag existiert und wird geparst, und (c) die darin enthaltene Snapshot-Version ist >= der älteren Snapshot-Version. (siehe hier) Erst wenn diese Validierungen bestanden sind, gilt der neue Timestamp als vertrauenswürdig. Die kritische neue Logik wird unten veranschaulicht:```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 } ); }
With this change, if the new timestamp’s snapshot version is lower than the previously seen snapshot version, the `ensure!` will fail <b>before</b> the new timestamp is saved as trusted. The error `OlderSnapshotInTimestamp` is raised to abort the update. As a result, Tough will keep the old (correct) timestamp in cache when a rollback is detected, and the bad timestamp is never cached as trusted. This prevents the scenario where a rejected timestamp update pollutes future updates.
- <b>Grundursache:</b> Eine <b>unsachgemäße Update-Sequenz</b> (CWE-367: Time-of-check Time-of-use Race Condition, im Kontext der Metadatenvalidierung). Toughs Prüfung auf Snapshot-Rollback im Timestamp wurde zum falschen Zeitpunkt durchgeführt, nachdem Seiteneffekte (Caching) aufgetreten waren. Zudem machte die unvollständige Validierung der Timestamp-Inhalte (Länge) die Logik brüchig.
- <b>Auswirkungen:</b> Ein bösartiger Timestamp (mit einer niedrigeren Snapshot-Version) könnte den Client vorübergehend täuschen und dazu führen, dass er einen schlechten Zustand speichert. Dies führt zu einem Denial of Service im Update-Mechanismus: Der Client würde danach echte Updates als ungültig behandeln (falsche Rollback-Erkennung). Keine direkte Codeausführung oder Datendiebstahl, aber ein <b>dauerhaft fehlschlagendes Update</b> kann genauso gefährlich sein (z. B. wenn dadurch Sicherheitspatches nicht angewendet werden können). (siehe [hier](https://github.com/advisories/GHSA-76g3-38jv-wxh4#:~:text=If%20the%20tough%20client%20successfully,client%20from%20consuming%20valid%20updates))
- <b>Abhilfe:</b> Aktualisieren Sie auf tough <b>0.20.0+</b>. Die gepatchte Version behandelt Timestamp-Rollback-Ereignisse sicher. Wenn ein durch diesen Fehler verursachter Aktualisierungsfehler beobachtet wurde, kann es erforderlich sein, die <b>zwischengespeicherten Metadaten zu löschen</b> (um einen vergifteten Timestamp zu entfernen), bevor Updates mit dem korrigierten Client erneut versucht werden. Als allgemeine Praxis sollten Clients Metadaten immer validieren, bevor sie ihnen vertrauen oder sie zwischenspeichern – dieses Problem unterstreicht diesen Grundsatz.
Referenzen:
- [AWS Security Bulletin AWS-2025-007](https://aws.amazon.com/security/security-bulletins/AWS-2025-007/) – <i>Problem mit tough, Versionen vor 0.20.0 (Mehrere CVEs)</i>
- [GitHub-Advisory-Database-Einträge für CVE-2025-2885](https://github.com/advisories/GHSA-5vmp-m5v2-hx47)
- [GitHub-Advisory-Database-Einträge für CVE-2025-2886](https://github.com/advisories/GHSA-v4wr-j3w6-mxqc)
- [GitHub-Advisory-Database-Einträge für CVE-2025-2887](https://github.com/advisories/GHSA-q6r9-r9pw-4cf7)
- [GitHub-Advisory-Database-Einträge für CVE-2025-2888](https://github.com/advisories/GHSA-76g3-38jv-wxh4)
- Patch-Commits für Tough v0.20.0:
- Commit zur Korrektur der Root-Version | [`awslabs/tough@0eeb60a`](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6)
- Commit zur Korrektur abschließender Delegationen | [`awslabs/tough@598111f`](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a)
- Commit zur Korrektur des Snapshot-Rollbacks | [`awslabs/tough@3345151`](https://github.com/awslabs/tough/commit/3345151a87c358d1ce43aeb7e8b3ebea5ebdbab4)
- Commit zur Korrektur des Timestamp-Rollbacks | [`awslabs/tough@9b400e1`](https://github.com/awslabs/tough/commit/9b400e1c8b7d6b9ab8009104fa7fe5884db05f18)