
Problema com tough, versões anteriores a 0.20.0 (Múltiplas CVEs)
Em março de 2025, múltiplas vulnerabilidades de segurança foram divulgadas na biblioteca Tough da AWS Labs (uma implementação cliente em Rust do TUF – The Update Framework). Esses problemas são rastreados sob CVE-2025-2885, CVE-2025-2886, CVE-2025-2887 e CVE-2025-2888, e foram corrigidos na versão 0.20.0 do Tough. Engenheiros de segurança podem usar este documento para entender os detalhes técnicos de cada vulnerabilidade, a causa raiz no código e os patches que as resolvem. Todos os usuários do Tough < 0.20.0 são fortemente aconselhados a atualizar para v0.20.0 ou posterior.
O Tough não validava a versão do Root metadata em sequência durante a atualização. Um atacante que controlasse um repositório (ou com capacidade de man-in-the-middle) poderia fornecer um arquivo de metadados root com um número de versão inesperado, fazendo com que o cliente buscasse e confiasse em uma versão errada. Em essência, o Tough não garantia que a versão dos novos metadados root fosse exatamente uma unidade maior do que a versão anteriormente confiável, violando o requisito do TUF de uma cadeia contínua de confiança. Isso poderia levar a ataques de rollback ou mix-and-match, em que um root antigo (mas devidamente assinado) é aceito como se fosse o mais recente, potencialmente reintroduzindo chaves de assinatura aposentadas ou confiança expirada.
tough/src/lib.rs. A implementação vulnerável apenas verificava se a versão do novo root não era menor que a anterior, em vez de exigir que fosse a próxima versão sequencial. Essa omissão viola a regra da especificação do TUF de que os clientes devem baixar as versões intermediárias do root em ordem (sem pular versões).Implementação Vulnerável: Na versão 0.19.x e anteriores, após baixar um novo root.json, o Tough verificava as assinaturas, mas não aplicava estritamente a continuidade da versão. Ele permitia que a versão do novo root fosse qualquer valor >= à versão confiável. Por exemplo, se o root confiável atual fosse a versão N, o Tough aceitaria um novo root com versão N+2 ou superior (desde que as assinaturas fossem válidas), pulando a N+1. O trecho de código abaixo ilustra a verificação anterior à correção:```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
});
Um atacante poderia explorar isso apresentando <b>metadados de root de versão mais alta que na verdade são um conjunto de chaves mais antigo</b>. O Tough aceitaria, pensando que é uma atualização, e [confiaria em conteúdo assinado com chaves desatualizadas](https://github.com/advisories/GHSA-5vmp-m5v2-hx47#:~:text=The%20tough%20client%20will%20trust,with%20a%20previous%20root%20role).
<b>Implementação Corrigida:</b> O código corrigido (no Tough 0.20.0) exige explicitamente que a versão do novo root seja exatamente uma unidade maior que a versão antiga, veja o [commit 0eeb60a](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6). Ele também garante que a nova versão seja mais alta (evita igualdade ou decremento) e impede qualquer salto maior que +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
}
);
Esta alteração garante que o cliente baixe e aplique os metadados raiz em ordem (N, N+1, N+2, ...) sem pular. Se um arquivo de metadados raiz for encontrado com uma versão que não seja exatamente old_version+1, o Tough agora trata isso como um possível ataque e aborta a atualização.
O Tough tratou incorretamente os papéis de alvos delegados “terminantes” conforme definidos pelo TUF. Em um repositório TUF, uma delegação pode ser marcada como terminante, o que significa que, se uma busca por alvo alcançar essa delegação e o alvo não for encontrado nela, a busca deve parar (e não continuar para outras delegações de prioridade mais baixa). (veja aqui) Devido a um erro de lógica, o Tough falhou em encerrar a busca nesse caso – ele continuava buscando em delegações subsequentes mesmo quando deveria ter parado. Isso poderia permitir que um invasor que controla uma delegação de prioridade mais baixa servisse conteúdo para alvos que não deveria controlar, contornando os limites de confiança pretendidos.
tough/src/editor/targets.rs (e o código relacionado de resolução de delegações). O código vulnerável não tratava corretamente a flag terminating nas delegações. Ao iterar pelas delegações em busca de um alvo, o Tough prosseguia para a próxima delegação mesmo se a atual estivesse marcada como terminante e não tivesse correspondência, contrariando a especificação do TUF.Implementação vulnerável: No Tough <0.20.0, a lógica de find_target() simplesmente fazia recursão ou percorria todos os papéis delegados possíveis até que um alvo fosse encontrado ou todos fossem esgotados. Ela não definia nenhuma flag nem interrompia o loop ao encontrar um papel terminante. Pseudocódigo do comportamento antigo:```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.
}
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
<b>Implementação corrigida:</b> A versão corrigida introduz um mecanismo para rastrear a terminação e interromper a busca adequadamente. Quando uma delegação terminadora é encontrada e não contém o destino, Tough agora [interrompe o loop de busca imediatamente](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a#:~:text=%2F%2F%20we%20encountered%20a%20terminating,so%20we%20stop%20iterating%20immediately). No código atualizado, uma flag booleana (por exemplo, `terminated`) é definida quando um papel de terminação é atingido e propagada pela pilha de chamadas. Por exemplo:```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;
}
Além disso, as chamadas recursivas a find_target agora carregam um sinalizador terminated para garantir que, uma vez que a terminação seja sinalizada, nenhuma outra delegação seja considerada nos loops de nível superior. O RepositoryEditor::delegate_role do Tough e as estruturas relacionadas também foram atualizados para armazenar o atributo terminating e passá-lo pela lógica de busca.
break em uma delegação terminante permitia que dados de alvos não autorizados fossem considerados.A lógica do Tough para detectar ataques de rollback em metadados de snapshot estava incompleta. Especificamente, ao atualizar o papel de Snapshot, o Tough deve verificar que todos os metadados de alvos vistos anteriormente (incluindo alvos delegados) ainda estão presentes e não foram revertidos para versões anteriores no novo snapshot. (ver aqui) O Tough realmente aplicava isso para o targets.json de nível superior, mas falhava ao fazer o mesmo para arquivos de metadados de alvos delegados. Essa lacuna poderia permitir que um atacante removesse ou revertesse um arquivo de alvo delegado nos metadados de snapshot do repositório sem detecção, fazendo o cliente aceitar um arquivo de alvo delegado desatualizado (ou ausente) como se estivesse atualizado.
tough/src/lib.rs (função que carrega/aplica novos metadados de Snapshot). O código vulnerável apenas verificava a continuidade do papel principal targets.json no snapshot, mas não iterava sobre os papéis delegados listados nos metadados de snapshot para realizar verificações semelhantes.Implementação Vulnerável: No Tough <0.20.0, após recuperar um novo snapshot.json, o cliente garantia que os papéis root e snapshot não sofreram rollback, e especificamente garantia que targets.json ainda estava presente. Também verificava que a versão de targets.json no novo snapshot era >= à versão anterior. No entanto, se o snapshot contivesse alvos delegados (por exemplo, metadados delegados projects.json, user.json), o cliente não os verificava. Por exemplo, originalmente o código fazia algo como:```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.)
Isto significa que, se um atacante com acesso ao repositório <b>removesse um arquivo de metadados delegado ou o revertesse para uma versão mais antiga</b>, o cliente do Tough não notaria – desde que o targets.json primário estivesse intacto. O cliente poderia então baixar um alvo desatualizado dessa delegação, sem saber que deveria ter sido rejeitado como um rollback.
<b>Implementação corrigida:</b> A versão 0.20.0 adiciona verificações abrangentes para <b>cada função listada nos metadados do snapshot</b>. O novo código itera por cada entrada nos metadados do snapshot antigo (incluindo todas as funções de alvos delegados) e garante duas coisas para cada uma: (1) que a função ainda exista no novo snapshot e (2) que sua versão não tenha diminuído. Se qualquer função estiver ausente no novo snapshot ou tiver um número de versão inferior ao anterior, a atualização é rejeitada como um potencial ataque de rollback. Por exemplo:```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 representa cada nome de arquivo de metadados como targets.json, delegated-role.json, etc.), o cliente detectará se algum metadado de alvo delegado foi removido ou revertido. Os erros SnapshotRoleMissing e SnapshotRoleRollback serão acionados se um papel desaparecer ou sua versão retroceder. Notavelmente, o Tough agora também garante explicitamente que o snapshot contenha pelo menos a entrada targets.json (caso contrário, ele gera erro com SnapshotTargetsMetaMissing) como uma verificação de sanidade.
O Tough tratou incorretamente um rollback detectado no papel Timestamp. O papel Timestamp no TUF assina periodicamente a versão mais recente dos metadados de snapshot para ajudar os clientes a detectar se estão vendo um snapshot antigo (um rollback). O Tough de fato realizava a verificação de rollback na versão do snapshot contida no timestamp, mas somente após armazenar em cache os novos metadados do timestamp localmente. Se um rollback fosse detectado, o Tough rejeitaria a atualização, mas já teria persistido o timestamp inválido como o "mais recente" em seu cache. Isso significava que um timestamp malicioso (com uma versão de snapshot desatualizada) poderia envenenar o cache do cliente. Atualizações legítimas subsequentes poderiam então parecer rollbacks para o Tough (já que o cache continha um timestamp indicando uma versão de snapshot mais alta do que a nova), bloqueando atualizações válidas.
tough/src/lib.rs (função que carrega o novo timestamp.json). O problema central era um bug de ordem de operações: o Tough atualizava seu cache/estado local com os novos metadados do Timestamp antes de validar completamente que a versão do snapshot contida neles não era mais antiga do que a que ele já havia visto.Implementação vulnerável: No Tough <0.20.0, quando um novo timestamp.json era buscado, o cliente fazia o parse e o gravava no cache (marcando-o como o timestamp confiável atual) e então realizava a verificação de rollback na versão do snapshot. Se a versão do snapshot no novo timestamp fosse menor do que a versão de snapshot registrada anteriormente (indicando uma tentativa de rollback), o Tough registraria um erro e rejeitaria aquele ciclo de atualização – mas o cache já continha o timestamp "inválido". Não havia remoção da entrada em cache nesse caminho de erro. Assim, o registro do cliente do timestamp "confiável" poderia se tornar esse desatualizado.
Por exemplo, suponha que a última versão conhecida do snapshot fosse 5. Um invasor poderia fornecer metadados de Timestamp (com um número de versão de timestamp mais alto) que assinam a versão 4 do snapshot. O Tough aceitaria o arquivo de timestamp (armazenando-o em cache) e então notaria que snapshot 4 < 5 e abortaria a atualização com erro – mas agora seu cache diz "o timestamp mais recente indica snapshot 4". Quando um timestamp correto (snapshot 5 ou 6) chegar em seguida, o Tough enxerga snapshot 5 versus snapshot 4 no cache como outro rollback (já que erroneamente confia na versão de snapshot 4 do cache como referência) e, portanto, rejeita até a atualização válida. O boletim da AWS descreve esse ciclo: "o cliente armazena em cache os metadados de timestamp apesar de eles serem rejeitados corretamente quando um rollback era detectado... fazendo com que o tough subsequentemente falhe ao consumir atualizações válidas."
Implementação corrigida: A correção garante que as verificações de rollback ocorram antes do cacheamento do novo timestamp e adiciona validação mais rigorosa do conteúdo do timestamp. No Tough 0.20.0, a função load_timestamp() foi modificada para garantir que os metadados do Timestamp sejam bem formados e que sua versão de snapshot não seja menor do que a versão de snapshot anteriormente confiável antes de concluir a atualização. Especificamente, o código agora verifica: (a) se os metadados do timestamp possuem exatamente uma entrada (devem ser apenas para snapshot.json), (b) se essa entrada existe e é analisada, e (c) se a versão do snapshot neles é >= à versão mais antiga do snapshot. (veja aqui) Somente após essas validações passarem, o novo timestamp é considerado confiável. A lógica crítica adicionada é ilustrada abaixo:```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 } ); }
Com esta alteração, se a versão de snapshot do novo timestamp for inferior à versão de snapshot vista anteriormente, o `ensure!` falhará <b>antes</b> de o novo timestamp ser salvo como confiável. O erro `OlderSnapshotInTimestamp` é gerado para abortar a atualização. Como resultado, o Tough manterá o timestamp antigo (correto) em cache quando um rollback for detectado, e o timestamp ruim nunca será armazenado em cache como confiável. Isso evita o cenário em que uma atualização de timestamp rejeitada contamina atualizações futuras.
- <b>Causa raiz:</b> Uma <b>sequência de atualização inadequada</b> (CWE-367: Time-of-check Time-of-use Race Condition, no contexto de validação de metadados). A verificação de rollback de snapshot no timestamp feita pelo Tough ocorreu no momento errado, depois que os efeitos colaterais (cache) já haviam ocorrido. Além disso, não validar completamente o conteúdo do timestamp (comprimento) tornou a lógica frágil
- <b>Impacto:</b> Um timestamp malicioso (com uma versão de snapshot inferior) poderia enganar temporariamente o cliente, fazendo com que ele armazenasse um estado ruim. Isso leva a uma negação de serviço no mecanismo de atualização: o cliente passaria a tratar atualizações genuínas como inválidas (detecção falsa de rollback). Não há execução direta de código ou roubo de dados, mas a falha de <b>atualização persistente</b> pode ser igualmente perigosa (por exemplo, impedindo a aplicação de patches de segurança). (veja [aqui](https://github.com/advisories/GHSA-76g3-38jv-wxh4#:~:text=If%20the%20tough%20client%20successfully,client%20from%20consuming%20valid%20updates))
- <b>Remediação:</b> Atualize para o tough <b>0.20.0+</b> A versão corrigida lida com eventos de rollback de timestamp com segurança. Se uma falha de atualização devido a esse bug foi observada, pode ser necessário <b>limpar os metadados em cache</b> (para remover qualquer timestamp envenenado) antes de repetir as atualizações com o cliente corrigido. Como prática geral, os clientes devem sempre verificar os metadados antes de confiá-los ou armazená-los em cache – esse problema reforça esse princípio.
Referências:
- [AWS Security Bulletin AWS-2025-007](https://aws.amazon.com/security/security-bulletins/AWS-2025-007/) – <i>Problema com o tough, versões anteriores a 0.20.0 (Múltiplos CVEs)</i>
- [Entradas do GitHub Advisory Database para CVE-2025-2885](https://github.com/advisories/GHSA-5vmp-m5v2-hx47)
- [Entradas do GitHub Advisory Database para CVE-2025-2886](https://github.com/advisories/GHSA-v4wr-j3w6-mxqc)
- [Entradas do GitHub Advisory Database para CVE-2025-2887](https://github.com/advisories/GHSA-q6r9-r9pw-4cf7)
- [Entradas do GitHub Advisory Database para CVE-2025-2888](https://github.com/advisories/GHSA-76g3-38jv-wxh4)
- Commits do patch do Tough v0.20.0:
- Commit de correção da versão raiz | [`awslabs/tough@0eeb60a`](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6)
- Commit de correção de delegações de terminação | [`awslabs/tough@598111f`](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a)
- Commit de correção de rollback de snapshot | [`awslabs/tough@3345151`](https://github.com/awslabs/tough/commit/3345151a87c358d1ce43aeb7e8b3ebea5ebdbab4)
- Commit de correção de rollback de timestamp | [`awslabs/tough@9b400e1`](https://github.com/awslabs/tough/commit/9b400e1c8b7d6b9ab8009104fa7fe5884db05f18)