
Problema con tough, versiones anteriores a 0.20.0 (Múltiples CVEs)
En marzo de 2025 se revelaron múltiples vulnerabilidades de seguridad en la librería Tough de AWS Labs (una implementación cliente en Rust de TUF – The Update Framework). Estos problemas están registrados como CVE-2025-2885, CVE-2025-2886, CVE-2025-2887 y CVE-2025-2888, y se corrigieron en Tough versión 0.20.0. Los ingenieros de seguridad pueden usar este documento para comprender los detalles técnicos de cada vulnerabilidad, la causa raíz en el código y los parches que las resuelven. Se recomienda encarecidamente a todos los usuarios de Tough < 0.20.0 que actualicen a v0.20.0 o superior.
Tough no validaba la versión de Root metadata de forma secuencial durante la actualización. Un atacante que controlara un repositorio (o con capacidad de man-in-the-middle) podría suministrar un archivo de metadatos root con un número de versión inesperado, lo que haría que el cliente obtuviera y confiara en una versión incorrecta. En esencia, Tough no garantizaba que la versión de un nuevo root metadata fuera exactamente una unidad mayor que la versión previamente confiable, violando el requisito de TUF de una cadena de confianza continua. Esto podría dar lugar a ataques de rollback o mix-and-match, en los que una root antigua (pero correctamente firmada) se acepta como si fuera la más reciente, lo que podría reintroducir claves de firma retiradas o confianza caducada.
tough/src/lib.rs. La implementación vulnerable solo comprobaba que la versión de la nueva root no fuera inferior a la anterior, en lugar de exigir que fuera la siguiente versión secuencial. Esta omisión rompe la regla de la especificación TUF según la cual los clientes deben descargar las versiones intermedias de root en orden (sin saltarse versiones).Implementación vulnerable: En la versión 0.19.x y anteriores, tras descargar un nuevo root.json, Tough verificaba las firmas pero no aplicaba estrictamente la continuidad de versiones. Permitía que la nueva versión de root fuera cualquier valor >= la versión confiable. Por ejemplo, si la root confiable actual era la versión N, Tough aceptaría una nueva root que afirmara ser la versión N+2 o superior (siempre que las firmas fueran válidas), omitiendo la N+1. El siguiente fragmento de código ilustra la comprobación anterior a la corrección:```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 atacante podría explotar esto presentando <b>metadatos raíz de una versión superior que en realidad son un conjunto de claves antiguo</b>. Tough los aceptaría, pensando que es una actualización, y [confiaría en contenido firmado con claves obsoletas](https://github.com/advisories/GHSA-5vmp-m5v2-hx47#:~:text=The%20tough%20client%20will%20trust,with%20a%20previous%20root%20role).
<b>Implementación corregida:</b> El código parcheado (en Tough 0.20.0) exige explícitamente que la versión de la nueva raíz sea exactamente una mayor que la versión anterior, consulte [commit 0eeb60a](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6). También garantiza que la nueva versión sea mayor (evita la igualdad o la disminución) y previene cualquier salto mayor 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
}
);
Este cambio garantiza que el cliente descargue y aplique los metadatos raíz en orden (N, N+1, N+2, ...) sin omitir ninguno. Si se encuentra un archivo de metadatos raíz con una versión que no sea exactamente old_version+1, Tough ahora lo trata como un posible ataque y aborta la actualización.
Tough gestionó incorrectamente los roles de objetivos delegados “terminantes” según lo definido por TUF. En un repositorio TUF, una delegación puede marcarse como terminante, lo que significa que si una búsqueda de objetivo llega a esa delegación y el objetivo no se encuentra allí, la búsqueda debe detenerse (y no continuar con otras delegaciones de menor prioridad). (ver aquí) Debido a un error de lógica, Tough no logró terminar la búsqueda en este caso: continuaba buscando en delegaciones posteriores incluso cuando debería haberse detenido. Esto podría permitir que un atacante que controle una delegación de menor prioridad sirva contenido para objetivos que no debería controlar, evadiendo los límites de confianza previstos.
tough/src/editor/targets.rs (y el código relacionado de resolución de delegaciones). El código vulnerable no manejaba correctamente el indicador terminating en las delegaciones. Al iterar sobre las delegaciones en busca de un objetivo, Tough pasaba a la siguiente delegación incluso si la actual estaba marcada como terminante y no había coincidencia, en contra de la especificación TUF.Implementación vulnerable: En Tough <0.20.0, la lógica de find_target() simplemente recurría o recorría todos los roles delegados posibles hasta encontrar un objetivo o agotarlos todos. No establecía ningún indicador ni salía del bucle al encontrar un rol terminante. Pseudocódigo del comportamiento anterior:```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.
}
Esto significa que un delegado de menor prioridad (que debería ignorarse después de una delegación terminante por encima de él) aún podría ser consultado y suministrar un archivo de destino malicioso
<b>Implementación corregida:</b> La versión corregida introduce un mecanismo para rastrear la terminación y detener la búsqueda de forma adecuada. Cuando se encuentra una delegación terminante y no contiene el objetivo, Tough ahora [sale del bucle de búsqueda inmediatamente](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a#:~:text=%2F%2F%20we%20encountered%20a%20terminating,so%20we%20stop%20iterating%20immediately). En el código actualizado, se establece una marca booleana (p. ej., `terminated`) cuando se alcanza un rol terminante, y se propaga hacia arriba en la pila de llamadas. Por ejemplo:```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;
}
Adicionalmente, las llamadas recursivas a find_target ahora llevan una marca terminated para garantizar que, una vez que se señala la terminación, no se consideren otras delegaciones en los bucles de nivel superior. Las estructuras RepositoryEditor::delegate_role y relacionadas de Tough también se actualizaron para almacenar el atributo terminating y propagarlo a través de la lógica de búsqueda.
break en una delegación terminante permitía que se consideraran datos de targets no autorizados.La lógica de Tough para detectar ataques de rollback en los metadatos de snapshot era incompleta. Específicamente, al actualizar el rol Snapshot, Tough debería verificar que todos los metadatos de targets vistos previamente (incluidos los targets delegados) sigan presentes y no tengan versiones anteriores en el nuevo snapshot. (ver aquí) Tough sí aplicaba esto para el targets.json de nivel superior, pero no lo hacía para los archivos de metadatos de targets delegados. Esta brecha podría permitir que un atacante eliminara o revirtiera un archivo de target delegado en los metadatos de snapshot del repositorio sin ser detectado, lo que provocaba que el cliente aceptara un archivo de target delegado desactualizado (o faltante) como si estuviera actualizado.
tough/src/lib.rs (función que carga/aplica los nuevos metadatos de Snapshot). El código vulnerable solo comprobaba la continuidad del rol principal targets.json en el snapshot, pero no iteraba sobre los roles delegados listados en los metadatos de snapshot para realizar comprobaciones similares.Implementación vulnerable: En Tough <0.20.0, después de recuperar un nuevo snapshot.json, el cliente se aseguraba de que los roles root y snapshot no se revirtieran, y específicamente se aseguraba de que targets.json siguiera presente. También verificaba que la versión de targets.json en el nuevo snapshot fuera >= la versión anterior. Sin embargo, si el snapshot contenía targets delegados (p. ej., metadatos delegados projects.json, user.json), el cliente no los verificaba. Por ejemplo, originalmente el código hacía 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.)
Esto significa que si un atacante con acceso al repositorio <b>eliminaba un archivo de metadatos delegados o lo revertía a una versión anterior</b>, el cliente de Tough no lo notaría – siempre que el targets.json principal estuviera intacto. El cliente podría entonces descargar un target obsoleto de esa delegación, sin saber que debería haber sido rechazado como un rollback.
<b>Implementación corregida:</b> La versión 0.20.0 añade comprobaciones exhaustivas para <b>cada rol listado en los metadatos del snapshot</b>. El nuevo código itera sobre cada entrada en los metadatos del snapshot anterior (incluidos todos los roles de targets delegados) y garantiza dos cosas para cada uno: (1) que el rol siga existiendo en el nuevo snapshot, y (2) que su versión no haya disminuido. Si algún rol falta en el nuevo snapshot o tiene un número de versión inferior al anterior, la actualización se rechaza como un posible ataque de rollback. Por ejemplo:```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, … }
);
}
Al recorrer todos los roles (el nombre representa cada nombre de archivo de metadatos como targets.json, delegated-role.json, etc.), el cliente detectará si se eliminó o revirtió algún metadato de targets delegados. Los errores SnapshotRoleMissing y SnapshotRoleRollback se activarán si un rol desapareció o su versión retrocedió. Notablemente, Tough ahora también garantiza explícitamente que el snapshot contenga al menos la entrada targets.json (de lo contrario, devuelve un error con SnapshotTargetsMetaMissing) como comprobación de cordura.
Tough manejó incorrectamente un rollback detectado en el rol Timestamp. El rol Timestamp en TUF firma periódicamente la última versión de metadatos del snapshot para ayudar a los clientes a detectar si están viendo un snapshot más antiguo (un rollback). Tough sí realizaba la comprobación de rollback en la versión del snapshot contenida en el timestamp, pero solo después de almacenar en caché localmente los nuevos metadatos del timestamp. Si se detectaba un rollback, Tough rechazaba la actualización pero ya había persistido el timestamp inválido como el “más reciente” en su caché. Esto significaba que un timestamp malicioso (con una versión de snapshot desactualizada) podía envenenar la caché del cliente. Las actualizaciones legítimas posteriores podrían entonces parecerle a Tough rollbacks (ya que la caché contenía un timestamp que indicaba una versión de snapshot más alta que la nueva), bloqueando actualizaciones válidas.
tough/src/lib.rs (función que carga el nuevo timestamp.json). El problema central era un error de orden de operaciones: Tough actualizaba su caché/estado local con los nuevos metadatos de Timestamp antes de validar completamente que la versión del snapshot en su interior no fuera anterior a la que ya había visto antes.Implementación vulnerable: En Tough <0.20.0, cuando se obtenía un nuevo timestamp.json, el cliente lo analizaba y lo escribía en la caché (marcándolo como el timestamp de confianza actual) y luego realizaba la comprobación de rollback en la versión del snapshot. Si la versión del snapshot en el nuevo timestamp era inferior a la versión de snapshot registrada anteriormente (lo que indicaba un intento de rollback), Tough registraba un error y rechazaba ese ciclo de actualización – pero la caché ya contenía el timestamp “malo”. No se eliminaba la entrada de la caché en esa ruta de error. Por lo tanto, el registro del cliente del timestamp “de confianza” podía convertirse en este obsoleto.
Por ejemplo, supongamos que la última versión de snapshot conocida era 5. Un atacante podría proporcionar un metadato de Timestamp (con un número de versión de timestamp más alto) que firma la versión de snapshot 4. Tough aceptaría el archivo de timestamp (almacenándolo en caché), luego notaría que snapshot 4 < 5 y fallaría la actualización – pero ahora su caché dice “el último timestamp indica snapshot 4”. Cuando llegue a continuación un timestamp correcto (snapshot 5 o 6), Tough verá snapshot 5 frente al snapshot 4 en caché como otro rollback (ya que erróneamente confía en la versión de snapshot 4 de la caché como referencia) y, por tanto, rechazará incluso la actualización válida. El boletín de AWS describe este ciclo: “el cliente almacena en caché los metadatos del timestamp a pesar de que se rechazan correctamente cuando se detecta un rollback… lo que hace que tough posteriormente no pueda consumir actualizaciones válidas.”
Implementación corregida: La corrección garantiza que las comprobaciones de rollback ocurran antes de almacenar en caché el nuevo timestamp y añade una validación más estricta del contenido del timestamp. En Tough 0.20.0, la función load_timestamp() se modificó para garantizar que los metadatos del Timestamp estén bien formados y que su versión de snapshot no sea inferior a la versión de snapshot previamente confiable antes de concluir la actualización. Específicamente, el código ahora verifica: (a) que los metadatos del timestamp tengan exactamente una entrada (debe ser solo para snapshot.json), (b) que esa entrada exista y se analice, y (c) que la versión de snapshot en su interior sea >= a la versión de snapshot anterior. (ver aquí) Solo después de que estas validaciones pasen, el nuevo timestamp se considera confiable. La lógica crítica añadida se ilustra a continuación:```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 } ); }
Con este cambio, si la versión de snapshot del nuevo timestamp es inferior a la versión de snapshot vista anteriormente, el `ensure!` fallará <b>antes</b> de que el nuevo timestamp se guarde como confiable. Se lanza el error `OlderSnapshotInTimestamp` para abortar la actualización. Como resultado, Tough conservará el timestamp anterior (correcto) en la caché cuando se detecte un rollback, y el timestamp malicioso nunca se guardará en caché como confiable. Esto evita el escenario en el que una actualización de timestamp rechazada contamina futuras actualizaciones.
- <b>Causa raíz:</b> Una <b>secuencia de actualización incorrecta</b> (CWE-367: Time-of-check Time-of-use Race Condition, en el contexto de validación de metadatos). La verificación de Tough para detectar rollback de snapshot en el timestamp se realizaba en el momento equivocado, después de que se hubieran producido efectos secundarios (almacenamiento en caché). Además, no validar completamente el contenido del timestamp (longitud) hacía que la lógica fuera frágil.
- <b>Impacto:</b> Un timestamp malicioso (con una versión de snapshot inferior) podría engañar temporalmente al cliente, haciendo que este almacene un estado incorrecto. Esto provoca una denegación de servicio en el mecanismo de actualización: el cliente pasaría a tratar las actualizaciones legítimas como inválidas (detección falsa de rollback). No hay ejecución directa de código ni robo de datos, pero un <b>fallo persistente de actualización</b> puede ser igual de peligroso (por ejemplo, impidiendo que se apliquen parches de seguridad). (ver [aquí](https://github.com/advisories/GHSA-76g3-38jv-wxh4#:~:text=If%20the%20tough%20client%20successfully,client%20from%20consuming%20valid%20updates))
- <b>Remediación:</b> Actualice a tough <b>0.20.0+</b>. La versión corregida maneja los eventos de rollback de timestamp de forma segura. Si se observó un fallo de actualización debido a este error, puede ser necesario <b>limpiar los metadatos en caché</b> (para eliminar cualquier timestamp envenenado) antes de reintentar las actualizaciones con el cliente corregido. Como práctica general, los clientes siempre deben verificar los metadatos antes de confiar en ellos o almacenarlos en caché; este problema subraya ese principio.
Referencias:
- [AWS Security Bulletin AWS-2025-007](https://aws.amazon.com/security/security-bulletins/AWS-2025-007/) – <i>Problema con tough, versiones anteriores a 0.20.0 (Múltiples CVEs)</i>
- [Entradas de la base de datos de avisos de GitHub para CVE-2025-2885](https://github.com/advisories/GHSA-5vmp-m5v2-hx47)
- [Entradas de la base de datos de avisos de GitHub para CVE-2025-2886](https://github.com/advisories/GHSA-v4wr-j3w6-mxqc)
- [Entradas de la base de datos de avisos de GitHub para CVE-2025-2887](https://github.com/advisories/GHSA-q6r9-r9pw-4cf7)
- [Entradas de la base de datos de avisos de GitHub para CVE-2025-2888](https://github.com/advisories/GHSA-76g3-38jv-wxh4)
- Commits del parche de Tough v0.20.0:
- Commit de corrección de versión raíz | [`awslabs/tough@0eeb60a`](https://github.com/awslabs/tough/commit/0eeb60aefe27f00b65730634b788a1aafb8bf3c6)
- Commit de corrección de delegaciones terminales | [`awslabs/tough@598111f`](https://github.com/awslabs/tough/commit/598111f88105a707ee68b0fa06c52da7176ea96a)
- Commit de corrección de rollback de snapshot | [`awslabs/tough@3345151`](https://github.com/awslabs/tough/commit/3345151a87c358d1ce43aeb7e8b3ebea5ebdbab4)
- Commit de corrección de rollback de timestamp | [`awslabs/tough@9b400e1`](https://github.com/awslabs/tough/commit/9b400e1c8b7d6b9ab8009104fa7fe5884db05f18)