Skip to content

Commit d7cc4bf

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 f9493d6 commit d7cc4bf

File tree

2 files changed

+57
-30
lines changed

2 files changed

+57
-30
lines changed

tests/test_trusted_metadata_set.py

+35-14
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

+22-16
Original file line numberDiff line numberDiff line change
@@ -277,18 +277,10 @@ def update_snapshot(self, data: bytes):
277277

278278
self.root.verify_delegate("snapshot", new_snapshot)
279279

280-
if (
281-
new_snapshot.signed.version
282-
!= self.timestamp.signed.meta["snapshot.json"].version
283-
):
284-
raise exceptions.BadVersionNumberError(
285-
f"Expected snapshot version "
286-
f"{self.timestamp.signed.meta['snapshot.json'].version}, "
287-
f"got {new_snapshot.signed.version}"
288-
)
280+
# version not checked against meta version to allow old snapshot to be
281+
# used in rollback protection: it is checked when targets is updated
289282

290-
# If an existing trusted snapshot is updated,
291-
# check for a rollback attack
283+
# If an existing trusted snapshot is updated, check for rollback attack
292284
if self.snapshot is not None:
293285
for filename, fileinfo in self.snapshot.signed.meta.items():
294286
new_fileinfo = new_snapshot.signed.meta.get(filename)
@@ -307,11 +299,25 @@ def update_snapshot(self, data: bytes):
307299
)
308300

309301
# expiry not checked to allow old snapshot to be used for rollback
310-
# protection of new snapshot: expiry is checked in update_targets()
302+
# protection of new snapshot: it is checked when targets is updated
311303

312304
self._trusted_set["snapshot"] = new_snapshot
313305
logger.debug("Updated snapshot")
314306

307+
def _check_final_snapshot(self):
308+
if self.snapshot.signed.is_expired(self.reference_time):
309+
raise exceptions.ExpiredMetadataError("Final snapshot.json is expired")
310+
311+
if (
312+
self.snapshot.signed.version
313+
!= self.timestamp.signed.meta["snapshot.json"].version
314+
):
315+
raise exceptions.BadVersionNumberError(
316+
f"Expected snapshot version "
317+
f"{self.timestamp.signed.meta['snapshot.json'].version}, "
318+
f"got {self.snapshot.signed.version}"
319+
)
320+
315321
def update_targets(self, data: bytes):
316322
"""Verifies and loads 'data' as new top-level targets metadata.
317323
@@ -341,10 +347,10 @@ def update_delegated_targets(
341347
if self.snapshot is None:
342348
raise RuntimeError("Cannot load targets before snapshot")
343349

344-
# Local snapshot was allowed to be expired to allow for rollback
345-
# checks on new snapshot but now snapshot must not be expired
346-
if self.snapshot.signed.is_expired(self.reference_time):
347-
raise exceptions.ExpiredMetadataError("Final snapshot.json is expired")
350+
# Local snapshot was allowed to be expired and to not match meta
351+
# version to allow for rollback checks on new snapshot but now
352+
# snapshot must not be expired and must match meta version
353+
self._check_final_snapshot()
348354

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

0 commit comments

Comments
 (0)