Skip to content

Commit e3b789c

Browse files
author
Jussi Kukkonen
committed
ngclient: allow limited use of wrong snapshot version
Spec does not explicitly say so but the intent is that a snapshot metadata can be trusted for rollback protection checks of newer snapshots even if current snapshot version does not match the version in current timestamp meta. Only do the snapshot version check for the "final" snapshot by doing it when targets is updated. Improve test names and comments. Signed-off-by: Jussi Kukkonen <[email protected]>
1 parent b515997 commit e3b789c

File tree

2 files changed

+62
-33
lines changed

2 files changed

+62
-33
lines changed

tests/test_trusted_metadata_set.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -300,33 +300,34 @@ def timestamp_version_modifier(timestamp: Timestamp) -> None:
300300

301301
timestamp = self.modify_metadata("timestamp", timestamp_version_modifier)
302302
self._root_updated_and_update_timestamp(timestamp)
303-
# new_snapshot.version != trusted timestamp.meta["snapshot"].version
304-
def snapshot_version_modifier(snapshot: Snapshot) -> None:
305-
snapshot.version = 3
306303

307-
snapshot = self.modify_metadata("snapshot", snapshot_version_modifier)
304+
#intermediate snapshot is allowed to not match meta version
305+
self.trusted_set.update_snapshot(self.metadata["snapshot"])
306+
307+
# final snapshot must match meta version
308308
with self.assertRaises(exceptions.BadVersionNumberError):
309-
self.trusted_set.update_snapshot(snapshot)
309+
self.trusted_set.update_targets(self.metadata["targets"])
310+
310311

311-
def test_update_snapshot_after_successful_update_new_snapshot_no_meta(self):
312+
def test_update_snapshot_file_removed_from_meta(self):
312313
self._update_all_besides_targets(self.metadata["timestamp"])
313-
# Test removing a meta_file in new_snapshot compared to the old snapshot
314-
def no_meta_modifier(snapshot: Snapshot) -> None:
315-
snapshot.meta = {}
314+
def remove_file_from_meta(snapshot: Snapshot) -> None:
315+
del snapshot.meta["targets.json"]
316316

317-
snapshot = self.modify_metadata("snapshot", no_meta_modifier)
317+
# Test removing a meta_file in new_snapshot compared to the old snapshot
318+
snapshot = self.modify_metadata("snapshot", remove_file_from_meta)
318319
with self.assertRaises(exceptions.RepositoryError):
319320
self.trusted_set.update_snapshot(snapshot)
320321

321-
def test_update_snapshot_after_succesfull_update_new_snapshot_meta_version_different(self):
322+
def test_update_snapshot_meta_version_decreases(self):
322323
self._root_updated_and_update_timestamp(self.metadata["timestamp"])
323-
# snapshot.meta["project1"].version != new_snapshot.meta["project1"].version
324+
324325
def version_meta_modifier(snapshot: Snapshot) -> None:
325-
for metafile_path in snapshot.meta.keys():
326-
snapshot.meta[metafile_path].version += 1
326+
snapshot.meta["targets.json"].version += 1
327327

328328
snapshot = self.modify_metadata("snapshot", version_meta_modifier)
329329
self.trusted_set.update_snapshot(snapshot)
330+
330331
with self.assertRaises(exceptions.BadVersionNumberError):
331332
self.trusted_set.update_snapshot(self.metadata["snapshot"])
332333

@@ -343,6 +344,26 @@ def snapshot_expired_modifier(snapshot: Snapshot) -> None:
343344
with self.assertRaises(exceptions.ExpiredMetadataError):
344345
self.trusted_set.update_targets(self.metadata["targets"])
345346

347+
def test_update_snapshot_successful_rollback_checks(self):
348+
def meta_version_bump(timestamp: Timestamp) -> None:
349+
timestamp.meta["snapshot.json"].version += 1
350+
351+
def version_bump(snapshot: Snapshot) -> None:
352+
snapshot.version += 1
353+
354+
# load a "local" timestamp, then update to newer one:
355+
self.trusted_set.update_timestamp(self.metadata["timestamp"])
356+
new_timestamp = self.modify_metadata("timestamp", meta_version_bump)
357+
self.trusted_set.update_timestamp(new_timestamp)
358+
359+
# load a "local" snapshot, then update to newer one:
360+
self.trusted_set.update_snapshot(self.metadata["snapshot"])
361+
new_snapshot = self.modify_metadata("snapshot", version_bump)
362+
self.trusted_set.update_snapshot(new_snapshot)
363+
364+
# update targets to trigger final snapshot meta version check
365+
self.trusted_set.update_targets(self.metadata["targets"])
366+
346367
def test_update_targets_no_meta_in_snapshot(self):
347368
def no_meta_modifier(snapshot: Snapshot) -> None:
348369
snapshot.meta = {}

tuf/ngclient/_internal/trusted_metadata_set.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,11 @@ def update_timestamp(self, data: bytes):
240240
def update_snapshot(self, data: bytes):
241241
"""Verifies and loads 'data' as new snapshot metadata.
242242
243-
Note that an expired intermediate snapshot is considered valid so it
244-
can be used for rollback checks on newer, final snapshot. Expiry is
245-
only checked for the final snapshot in update_delegated_targets().
243+
Note that intermediate snapshot is considered valid even if it is
244+
expired or the version does not match the timestamp meta version. This
245+
means the intermediate snapshot can be used for rollback checks on
246+
newer, final snapshot. Expiry and meta version are only checked for
247+
the final snapshot in update_delegated_targets().
246248
247249
Args:
248250
data: unverified new snapshot metadata as bytes
@@ -285,18 +287,10 @@ def update_snapshot(self, data: bytes):
285287

286288
self.root.verify_delegate("snapshot", new_snapshot)
287289

288-
if (
289-
new_snapshot.signed.version
290-
!= self.timestamp.signed.meta["snapshot.json"].version
291-
):
292-
raise exceptions.BadVersionNumberError(
293-
f"Expected snapshot version "
294-
f"{self.timestamp.signed.meta['snapshot.json'].version}, "
295-
f"got {new_snapshot.signed.version}"
296-
)
290+
# version not checked against meta version to allow old snapshot to be
291+
# used in rollback protection: it is checked when targets is updated
297292

298-
# If an existing trusted snapshot is updated,
299-
# check for a rollback attack
293+
# If an existing trusted snapshot is updated, check for rollback attack
300294
if self.snapshot is not None:
301295
for filename, fileinfo in self.snapshot.signed.meta.items():
302296
new_fileinfo = new_snapshot.signed.meta.get(filename)
@@ -315,11 +309,25 @@ def update_snapshot(self, data: bytes):
315309
)
316310

317311
# expiry not checked to allow old snapshot to be used for rollback
318-
# protection of new snapshot: expiry is checked in update_targets()
312+
# protection of new snapshot: it is checked when targets is updated
319313

320314
self._trusted_set["snapshot"] = new_snapshot
321315
logger.debug("Updated snapshot")
322316

317+
def _check_final_snapshot(self):
318+
if self.snapshot.signed.is_expired(self.reference_time):
319+
raise exceptions.ExpiredMetadataError("snapshot.json is expired")
320+
321+
if (
322+
self.snapshot.signed.version
323+
!= self.timestamp.signed.meta["snapshot.json"].version
324+
):
325+
raise exceptions.BadVersionNumberError(
326+
f"Expected snapshot version "
327+
f"{self.timestamp.signed.meta['snapshot.json'].version}, "
328+
f"got {self.snapshot.signed.version}"
329+
)
330+
323331
def update_targets(self, data: bytes):
324332
"""Verifies and loads 'data' as new top-level targets metadata.
325333
@@ -349,10 +357,10 @@ def update_delegated_targets(
349357
if self.snapshot is None:
350358
raise RuntimeError("Cannot load targets before snapshot")
351359

352-
# Local snapshot was allowed to be expired to allow for rollback
353-
# checks on new snapshot but now snapshot must not be expired
354-
if self.snapshot.signed.is_expired(self.reference_time):
355-
raise exceptions.ExpiredMetadataError("snapshot.json is expired")
360+
# Local snapshot was allowed to be expired and to not match meta
361+
# version to allow for rollback checks on new snapshot but now
362+
# snapshot must not be expired and must match meta version
363+
self._check_final_snapshot()
356364

357365
delegator: Optional[Metadata] = self.get(delegator_name)
358366
if delegator is None:

0 commit comments

Comments
 (0)