
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의 연속 신뢰 체인 요구 사항을 위반합니다. 이로 인해 롤백 또는 혼합(mix-and-match) 공격이 발생할 수 있습니다. 이 경우 오래된(하지만 제대로 서명된) 루트가 최신인 것처럼 수락되어 폐기된 서명 키나 만료된 신뢰가 다시 도입될 수 있습니다.
tough/src/lib.rs의 루트 메타데이터에 대한 Tough의 업데이트 로직입니다. 취약한 구현은 새 루트의 버전이 이전 버전보다 낮지 않은지만 확인했으며, 다음 순차 버전이어야 한다고 요구하지 않았습니다. 이 누락은 클라이언트가 중간 루트 버전을 순서대로 다운로드해야 한다는 TUF 사양의 규칙(버전 건너뛰기 금지)을 위반합니다.취약한 구현: 버전 0.19.x 이하에서 Tough는 새 root.json을 다운로드한 후 서명을 검증했지만 버전 연속성을 엄격하게 적용하지 않았습니다. 새 루트 버전이 신뢰된 버전보다 크거나 같은 모든 값을 허용했습니다. 예를 들어, 현재 신뢰된 루트가 버전 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만 높을 것을 명시적으로 요구합니다([commit 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” 위임 대상(targets) 역할을 잘못 처리했습니다. TUF 저장소에서 위임은 terminating으로 표시될 수 있습니다. 즉, 대상 조회가 해당 위임에 도달했을 때 그곳에서 대상을 찾지 못하면 검색이 중단되어야 합니다(다른 우선순위가 낮은 위임으로 계속 진행되지 않아야 합니다). (여기 참조) 논리 오류로 인해 Tough는 이 경우 검색을 종료하지 못했습니다 – 중단해야 했음에도 후속 위임을 계속 검색했습니다. 이로 인해 낮은 우선순위의 위임을 제어하는 공격자가 제어해서는 안 되는 대상에 대한 콘텐츠를 제공할 수 있으며, 의도된 신뢰 경계를 우회할 수 있습니다.
tough/src/editor/targets.rs의 대상 조회 알고리즘(및 관련 위임 해석 코드). 취약한 코드는 위임의 terminating 플래그를 제대로 처리하지 못했습니다. 대상을 검색하기 위해 위임을 반복할 때 Tough는 현재 위임이 terminating으로 표시되고 일치 항목이 없더라도 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 역할의 연속성만 확인했으며, 스냅샷 메타데이터에 나열된 위임 역할들을 반복하며 유사한 검사를 수행하지 않았습니다.