
tough のバージョン 0.20.0 より前の問題(複数の CVE)
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は署名を検証しましたが、バージョンの連続性を厳密に強制していませんでした。Toughはを許可していました。たとえば、現在信頼されているルートがバージョンNの場合、ToughはバージョンN+1をスキップして、N+2以上のバージョンを主張する新しいルートを(署名が有効である限り)受け入れていました。以下のコードスニペットは、修正前のチェックを示しています。```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 で定義されている「終端(terminating)」委任ターゲットロールを誤って処理していました。TUF リポジトリでは、委任を 終端(terminating) としてマークできます。つまり、ターゲットのルックアップがその委任に到達し、そこでターゲットが見つからなかった場合、検索は停止する(他の優先度の低い委任に続行しない)必要があります。(こちら を参照)論理エラーのため、Tough はこの場合検索を終了できず、停止すべき状況でも後続の委任を検索し続けていました。これにより、優先度の低い委任を制御する攻撃者が、本来制御すべきでないターゲットのコンテンツを配信でき、意図された信頼境界を迂回する可能性がありました。
tough/src/editor/targets.rs のターゲット検索アルゴリズム(および関連する委任解決コード)。脆弱なコードは、委任の terminating フラグを適切に処理していませんでした。Tough は、ターゲットを探して委任を反復処理する際、現在の委任が終端とマークされていて一致しなかった場合でも、TUF 仕様に反して次の委任に進んでいました。脆弱な実装: Tough <0.20.0 では、find_target() ロジックは、ターゲットが見つかるかすべての委任ロールを探索し尽くすまで、単純にすべての可能な委任ロールを再帰的またはループで探索していました。終端ロールに遭遇しても、フラグを設定したりループを抜けたりしませんでした。旧動作の擬似コード:```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>に対する包括的なチェックが追加されました。新しいコードは、古いスナップショットのメタデータ内の各エントリ(すべての委任ターゲットロールを含む)を反復処理し、それぞれについて次の2点を確認します: (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 だったとします。攻撃者は、スナップショットバージョン 4 に署名する Timestamp メタデータ(より高いタイムスタンプバージョン番号を持つ)を提供できます。Tough はタイムスタンプファイルを受け入れ(キャッシュし)、その後スナップショット 4 < 5 に気づいて更新をエラー終了します – しかし、この時点でキャッシュには「最新のタイムスタンプはスナップショット 4 を示す」と記録されています。次に正しいタイムスタンプ(スナップショット 5 または 6)が届くと、Tough はキャッシュされたスナップショット 4 とスナップショット 5 を比較し、別のロールバックとみなします(キャッシュのスナップショットバージョン 4 をベースラインとして誤って信頼しているため)、その結果、正当な更新さえも拒否します。AWS の速報はこのサイクルを次のように説明しています: 「クライアントは、ロールバックが検出されたときに正しく拒否されたにもかかわらず、タイムスタンプメタデータをキャッシュし…その結果 tough は後続の正当な更新を消費できなくなる。」
修正された実装: この修正により、ロールバックチェックが新しいタイムスタンプのキャッシュ前に行われることが保証され、タイムスタンプ内容のより厳格な検証が追加されました。Tough 0.20.0 では、load_timestamp() 関数が変更され、更新を完了する前に、Timestamp メタデータが適切に形成され、そのスナップショットバージョンが以前に信頼されたスナップショットバージョン以上であることを強制するようになりました。具体的には、コードは現在次のことをチェックします: (a) タイムスタンプメタデータがちょうど 1 つのエントリを持つこと(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: メタデータ検証の文脈における Time-of-check Time-of-use 競合状態)。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 Security Bulletin AWS-2025-007](https://aws.amazon.com/security/security-bulletins/AWS-2025-007/) – <i>tough の 0.20.0 より前のバージョンに関する問題(複数の CVE)</i>
- [GitHub Advisory Database の CVE-2025-2885 エントリ](https://github.com/advisories/GHSA-5vmp-m5v2-hx47)
- [GitHub Advisory Database の CVE-2025-2886 エントリ](https://github.com/advisories/GHSA-v4wr-j3w6-mxqc)
- [GitHub Advisory Database の CVE-2025-2887 エントリ](https://github.com/advisories/GHSA-q6r9-r9pw-4cf7)
- [GitHub Advisory Database の 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)