From 5d889370102ff9e8fd850c7d43105665bea7fbc2 Mon Sep 17 00:00:00 2001 From: Erick Tryzelaar Date: Tue, 7 Jul 2026 12:58:31 +0000 Subject: [PATCH 1/3] fix clippy lint --- interop-tests/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/interop-tests/src/lib.rs b/interop-tests/src/lib.rs index 59a8fe5..7e98866 100644 --- a/interop-tests/src/lib.rs +++ b/interop-tests/src/lib.rs @@ -264,7 +264,6 @@ pub async fn generate_repos( add_target(&mut repo, &keys, 0, consistent_snapshot).await; // Queue up a series of key rotations - let mut i: u8 = 1; let rotations = [ Some(Role::Root), Some(Role::Targets), @@ -272,7 +271,7 @@ pub async fn generate_repos( Some(Role::Timestamp), None, ]; - for r in rotations.iter() { + for (i, r) in (1_u8..).zip(rotations.iter()) { // Initialize new repo and copy the files from the previous step. let dir_i = Path::new(dir).join(i.to_string()); let mut repo = FileSystemRepositoryBuilder::new(dir_i) @@ -306,7 +305,6 @@ pub async fn generate_repos( ) .await; add_target(&mut repo, &keys, i, consistent_snapshot).await; - i += 1; } Ok(()) } From 1847be7a5d00260655f475c404747e54bf3643ef Mon Sep 17 00:00:00 2001 From: Erick Tryzelaar Date: Mon, 6 Jul 2026 19:34:49 +0000 Subject: [PATCH 2/3] When updating snapshots, verify targets are valid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implements [TUF-1.0.5 §5.3.3.2], which states: > The version number of the targets metadata file, and all > delegated targets metadata files (if any), in the trusted snapshot metadata > file, if any, MUST be less than or equal to its version number in the new > snapshot metadata file. Furthermore, any targets metadata filename that was > listed in the trusted snapshot metadata file, if any, MUST continue to be > listed in the new snapshot metadata file. If any of these conditions are > not met, discard the new snapshot metadadata file, abort the update cycle, > and report the failure. Furthermore, it adds three tests to: * make sure rollbacks are prevented * that trusted target roles cannot be removed in a normal update. * that we can remove a target role with a root roll. Fixes #295. [TUF-1.0.5 §5.3.3.2]: https://github.com/theupdateframework/specification/blob/39c80de07407bd4251ad823976dc4116a4b05043/tuf-spec.md?plain=1#L1210 --- tuf/src/database.rs | 257 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 246 insertions(+), 11 deletions(-) diff --git a/tuf/src/database.rs b/tuf/src/database.rs index 6e2e5c5..e676b56 100644 --- a/tuf/src/database.rs +++ b/tuf/src/database.rs @@ -426,16 +426,6 @@ impl Database { // FIXME(#294): Implement this section. - ///////////////////////////////////////// - // FIXME(#297): forgetting the trusted snapshot here is not part of the spec. Do we need to - // do it? - - if let Some(trusted_snapshot) = &self.trusted_snapshot { - if trusted_snapshot.version() != new_timestamp.snapshot().version() { - self.trusted_snapshot = None; - } - } - ///////////////////////////////////////// // TUF-1.0.5 §5.2.3: // @@ -570,7 +560,24 @@ impl Database { // metadata file. If any of these conditions are not met, discard the new snapshot // metadadata file, abort the update cycle, and report the failure. - // FIXME(#295): Implement this section. + if let Some(trusted_snapshot) = &self.trusted_snapshot { + for (role, trusted_description) in trusted_snapshot.meta().iter() { + let new_description = new_snapshot.meta().get(role).ok_or_else(|| { + Error::MissingMetadataDescription { + parent_role: MetadataPath::snapshot(), + child_role: role.clone(), + } + })?; + + if new_description.version() < trusted_description.version() { + return Err(Error::AttemptedMetadataRollBack { + role: role.clone(), + trusted_version: trusted_description.version(), + new_version: new_description.version(), + }); + } + } + } ///////////////////////////////////////// // TUF-1.0.5 §5.3.4: @@ -1474,6 +1481,234 @@ mod test { assert!(tuf.update_snapshot(&now, &raw_snapshot).is_err()); } + #[test] + fn bad_snapshot_update_rollback_targets() { + let now = Utc::now(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let signed_targets_v2 = TargetsMetadataBuilder::new() + .version(2) + .signed::(&KEYS[2]) + .unwrap(); + + let snapshot_v1 = SnapshotMetadataBuilder::new() + .version(1) + .insert_metadata(&signed_targets_v2, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[1]) + .unwrap(); + let raw_snapshot_v1 = snapshot_v1.to_raw().unwrap(); + + let raw_timestamp_v1 = + TimestampMetadataBuilder::from_snapshot(&snapshot_v1, &[HashAlgorithm::Sha256]) + .unwrap() + .version(1) + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp_v1).unwrap(); + tuf.update_snapshot(&now, &raw_snapshot_v1).unwrap(); + + let signed_targets_v1 = TargetsMetadataBuilder::new() + .version(1) + .signed::(&KEYS[2]) + .unwrap(); + + let snapshot_v2 = SnapshotMetadataBuilder::new() + .version(2) + .insert_metadata(&signed_targets_v1, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[1]) + .unwrap(); + let raw_snapshot_v2 = snapshot_v2.to_raw().unwrap(); + + let raw_timestamp_v2 = + TimestampMetadataBuilder::from_snapshot(&snapshot_v2, &[HashAlgorithm::Sha256]) + .unwrap() + .version(2) + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp_v2).unwrap(); + + assert_matches!( + tuf.update_snapshot(&now, &raw_snapshot_v2), + Err(Error::AttemptedMetadataRollBack { role, trusted_version: 2, new_version: 1 }) + if role == MetadataPath::targets() + ); + } + + #[test] + fn bad_snapshot_update_missing_targets() { + let now = Utc::now(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let signed_targets = TargetsMetadataBuilder::new() + .version(1) + .signed::(&KEYS[2]) + .unwrap(); + + let snapshot_v1 = SnapshotMetadataBuilder::new() + .version(1) + .insert_metadata(&signed_targets, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[1]) + .unwrap(); + let raw_snapshot_v1 = snapshot_v1.to_raw().unwrap(); + + let raw_timestamp_v1 = + TimestampMetadataBuilder::from_snapshot(&snapshot_v1, &[HashAlgorithm::Sha256]) + .unwrap() + .version(1) + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp_v1).unwrap(); + tuf.update_snapshot(&now, &raw_snapshot_v1).unwrap(); + + let snapshot_v2 = SnapshotMetadataBuilder::new() + .version(2) + .signed::(&KEYS[1]) + .unwrap(); + let raw_snapshot_v2 = snapshot_v2.to_raw().unwrap(); + + let raw_timestamp_v2 = + TimestampMetadataBuilder::from_snapshot(&snapshot_v2, &[HashAlgorithm::Sha256]) + .unwrap() + .version(2) + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp_v2).unwrap(); + + assert_matches!( + tuf.update_snapshot(&now, &raw_snapshot_v2), + Err(Error::MissingMetadataDescription { parent_role, child_role }) + if parent_role == MetadataPath::snapshot() && child_role == MetadataPath::targets() + ); + } + + #[test] + fn good_snapshot_update_remove_role_after_root_update() { + let now = Utc::now(); + + let raw_root_v1 = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root_v1).unwrap(); + + let signed_targets = TargetsMetadataBuilder::new() + .version(1) + .signed::(&KEYS[2]) + .unwrap(); + + let snapshot_v1 = SnapshotMetadataBuilder::new() + .version(1) + .insert_metadata(&signed_targets, &[HashAlgorithm::Sha256]) + .unwrap() + .insert_metadata_with_path("delegation", &signed_targets, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[1]) + .unwrap(); + let raw_snapshot_v1 = snapshot_v1.to_raw().unwrap(); + + let raw_timestamp_v1 = + TimestampMetadataBuilder::from_snapshot(&snapshot_v1, &[HashAlgorithm::Sha256]) + .unwrap() + .version(1) + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp_v1).unwrap(); + tuf.update_snapshot(&now, &raw_snapshot_v1).unwrap(); + + assert!( + tuf.trusted_snapshot() + .unwrap() + .meta() + .contains_key(&MetadataPath::new("delegation").unwrap()) + ); + + let root_v2 = RootMetadataBuilder::new() + .version(2) + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[0].public().clone()) + .targets_key(KEYS[2].public().clone()) + .timestamp_key(KEYS[3].public().clone()) + .signed::(&KEYS[0]) + .unwrap(); + let raw_root_v2 = root_v2.to_raw().unwrap(); + + tuf.update_root(&raw_root_v2).unwrap(); + + let snapshot_v2 = SnapshotMetadataBuilder::new() + .version(2) + .insert_metadata(&signed_targets, &[HashAlgorithm::Sha256]) + .unwrap() + .signed::(&KEYS[0]) + .unwrap(); + let raw_snapshot_v2 = snapshot_v2.to_raw().unwrap(); + + let raw_timestamp_v2 = + TimestampMetadataBuilder::from_snapshot(&snapshot_v2, &[HashAlgorithm::Sha256]) + .unwrap() + .version(2) + .signed::(&KEYS[3]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp_v2).unwrap(); + + assert_matches!(tuf.update_snapshot(&now, &raw_snapshot_v2), Ok(true)); + + assert!( + !tuf.trusted_snapshot() + .unwrap() + .meta() + .contains_key(&MetadataPath::new("delegation").unwrap()) + ); + } + #[test] fn good_targets_update() { let now = Utc::now(); From 7e690f2774714fc629a682783ef96595cad40578 Mon Sep 17 00:00:00 2001 From: Erick Tryzelaar Date: Mon, 6 Jul 2026 20:03:02 +0000 Subject: [PATCH 3/3] When updating timestamp, make sure we aren't rolling back snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implements [TUF-1.0.5 §5.2.2.2], which states: > The version number of the targets metadata file, and all > delegated targets metadata files (if any), in the trusted snapshot metadata > file, if any, MUST be less than or equal to its version number in the new > snapshot metadata file. Furthermore, any targets metadata filename that was > listed in the trusted snapshot metadata file, if any, MUST continue to be > listed in the new snapshot metadata file. If any of these conditions are > not met, discard the new snapshot metadadata file, abort the update cycle, > and report the failure. Furthermore, it adds three tests to: * make sure rollbacks are prevented * that trusted target roles cannot be removed in a normal update. * that we can remove a target role with a root roll. Fixes #294. [TUF-1.0.5 §5.2.2.2]: https://github.com/theupdateframework/specification/blob/39c80de07407bd4251ad823976dc4116a4b05043/tuf-spec.md?plain=1#L1163 --- tuf/src/database.rs | 63 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/tuf/src/database.rs b/tuf/src/database.rs index e676b56..8dd6c8e 100644 --- a/tuf/src/database.rs +++ b/tuf/src/database.rs @@ -424,7 +424,15 @@ impl Database { // new timestamp metadata file. If not, discard the new timestamp metadadata file, // abort the update cycle, and report the failure. - // FIXME(#294): Implement this section. + if let Some(trusted_timestamp) = &self.trusted_timestamp { + if new_timestamp.snapshot().version() < trusted_timestamp.snapshot().version() { + return Err(Error::AttemptedMetadataRollBack { + role: MetadataPath::snapshot(), + trusted_version: trusted_timestamp.snapshot().version(), + new_version: new_timestamp.snapshot().version(), + }); + } + } ///////////////////////////////////////// // TUF-1.0.5 §5.2.3: @@ -1370,6 +1378,59 @@ mod test { assert!(tuf.update_timestamp(&now, &raw_timestamp).is_err()) } + #[test] + fn bad_timestamp_update_rollback_snapshot() { + let now = Utc::now(); + + let raw_root = RootMetadataBuilder::new() + .root_key(KEYS[0].public().clone()) + .snapshot_key(KEYS[1].public().clone()) + .targets_key(KEYS[1].public().clone()) + .timestamp_key(KEYS[1].public().clone()) + .signed::(&KEYS[0]) + .unwrap() + .to_raw() + .unwrap(); + + let mut tuf = Database::from_trusted_root(&raw_root).unwrap(); + + let snapshot_v2 = SnapshotMetadataBuilder::new() + .version(2) + .signed::(&KEYS[1]) + .unwrap(); + + let raw_timestamp_v1 = + TimestampMetadataBuilder::from_snapshot(&snapshot_v2, &[HashAlgorithm::Sha256]) + .unwrap() + .version(1) + .signed::(&KEYS[1]) + .unwrap() + .to_raw() + .unwrap(); + + tuf.update_timestamp(&now, &raw_timestamp_v1).unwrap(); + + let snapshot_v1 = SnapshotMetadataBuilder::new() + .version(1) + .signed::(&KEYS[1]) + .unwrap(); + + let raw_timestamp_v2 = + TimestampMetadataBuilder::from_snapshot(&snapshot_v1, &[HashAlgorithm::Sha256]) + .unwrap() + .version(2) + .signed::(&KEYS[1]) + .unwrap() + .to_raw() + .unwrap(); + + assert_matches!( + tuf.update_timestamp(&now, &raw_timestamp_v2), + Err(Error::AttemptedMetadataRollBack { role, trusted_version: 2, new_version: 1 }) + if role == MetadataPath::snapshot() + ); + } + #[test] fn good_snapshot_update() { let now = Utc::now();