Skip to content

Commit b515997

Browse files
author
Jussi Kukkonen
committed
ngclient: Allow limited use of expired timestamp/snapshot
While this is not explicitly said in the spec, the intention is that expired timestamp and snapshot should be used for rollback protection checks on newer timestamp/snapshot (but not for anything else). Move the expiry checks to the "next" metadata update: timestamp expiry is checked when snapshot is loaded, and snapshot expiry is checked when targets is loaded. Signed-off-by: Jussi Kukkonen <[email protected]>
1 parent 0911972 commit b515997

File tree

2 files changed

+32
-7
lines changed

2 files changed

+32
-7
lines changed

tests/test_trusted_metadata_set.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,13 @@ def test_update_timestamp_expired(self):
268268
def timestamp_expired_modifier(timestamp: Timestamp) -> None:
269269
timestamp.expires = datetime(1970, 1, 1)
270270

271+
# intermediate timestamp is allowed to be expired
271272
timestamp = self.modify_metadata("timestamp", timestamp_expired_modifier)
273+
self.trusted_set.update_timestamp(timestamp)
274+
275+
# update snapshot to trigger final timestamp expiry check
272276
with self.assertRaises(exceptions.ExpiredMetadataError):
273-
self.trusted_set.update_timestamp(timestamp)
277+
self.trusted_set.update_snapshot(self.metadata["snapshot"])
274278

275279
def test_update_snapshot_length_or_hash_mismatch(self):
276280
def modify_snapshot_length(timestamp: Timestamp) -> None:
@@ -328,13 +332,16 @@ def version_meta_modifier(snapshot: Snapshot) -> None:
328332

329333
def test_update_snapshot_expired_new_snapshot(self):
330334
self._root_updated_and_update_timestamp(self.metadata["timestamp"])
331-
# new_snapshot has expired
332335
def snapshot_expired_modifier(snapshot: Snapshot) -> None:
333336
snapshot.expires = datetime(1970, 1, 1)
334337

338+
# intermediate snapshot is allowed to be expired
335339
snapshot = self.modify_metadata("snapshot", snapshot_expired_modifier)
340+
self.trusted_set.update_snapshot(snapshot)
341+
342+
# update targets to trigger final snapshot expiry check
336343
with self.assertRaises(exceptions.ExpiredMetadataError):
337-
self.trusted_set.update_snapshot(snapshot)
344+
self.trusted_set.update_targets(self.metadata["targets"])
338345

339346
def test_update_targets_no_meta_in_snapshot(self):
340347
def no_meta_modifier(snapshot: Snapshot) -> None:

tuf/ngclient/_internal/trusted_metadata_set.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ def update_root(self, data: bytes):
178178
def update_timestamp(self, data: bytes):
179179
"""Verifies and loads 'data' as new timestamp metadata.
180180
181+
Note that an expired intermediate timestamp is considered valid so it
182+
can be used for rollback checks on newer, final timestamp. Expiry is
183+
only checked for the final timestamp in update_snapshot().
184+
181185
Args:
182186
data: unverified new timestamp metadata as bytes
183187
@@ -227,15 +231,19 @@ def update_timestamp(self, data: bytes):
227231
self.timestamp.signed.meta["snapshot.json"].version,
228232
)
229233

230-
if new_timestamp.signed.is_expired(self.reference_time):
231-
raise exceptions.ExpiredMetadataError("New timestamp is expired")
234+
# expiry not checked to allow old timestamp to be used for rollback
235+
# protection of new timestamp: expiry is checked in update_snapshot()
232236

233237
self._trusted_set["timestamp"] = new_timestamp
234238
logger.debug("Updated timestamp")
235239

236240
def update_snapshot(self, data: bytes):
237241
"""Verifies and loads 'data' as new snapshot metadata.
238242
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().
246+
239247
Args:
240248
data: unverified new snapshot metadata as bytes
241249
@@ -250,6 +258,11 @@ def update_snapshot(self, data: bytes):
250258
raise RuntimeError("Cannot update snapshot after targets")
251259
logger.debug("Updating snapshot")
252260

261+
# Local timestamp was allowed to be expired to allow for rollback
262+
# checks on new timestamp but now timestamp must not be expired
263+
if self.timestamp.signed.is_expired(self.reference_time):
264+
raise exceptions.ExpiredMetadataError("timestamp.json is expired")
265+
253266
meta = self.timestamp.signed.meta["snapshot.json"]
254267

255268
# Verify against the hashes in timestamp, if any
@@ -301,8 +314,8 @@ def update_snapshot(self, data: bytes):
301314
f"{new_fileinfo.version}, got {fileinfo.version}."
302315
)
303316

304-
if new_snapshot.signed.is_expired(self.reference_time):
305-
raise exceptions.ExpiredMetadataError("New snapshot is expired")
317+
# expiry not checked to allow old snapshot to be used for rollback
318+
# protection of new snapshot: expiry is checked in update_targets()
306319

307320
self._trusted_set["snapshot"] = new_snapshot
308321
logger.debug("Updated snapshot")
@@ -336,6 +349,11 @@ def update_delegated_targets(
336349
if self.snapshot is None:
337350
raise RuntimeError("Cannot load targets before snapshot")
338351

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")
356+
339357
delegator: Optional[Metadata] = self.get(delegator_name)
340358
if delegator is None:
341359
raise RuntimeError("Cannot load targets before delegator")

0 commit comments

Comments
 (0)