Skip to content

Commit d9f2d9d

Browse files
author
Jussi Kukkonen
authored
Merge pull request #1707 from ivanayov/test_expired_metadata
Test expired metadata from cache
2 parents f2e80a8 + 8d4d9af commit d9f2d9d

3 files changed

Lines changed: 126 additions & 9 deletions

File tree

tests/repository_simulator.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@
4444
updater.refresh()
4545
"""
4646

47+
import datetime
4748
import logging
4849
import os
4950
import tempfile
5051
from dataclasses import dataclass, field
51-
from datetime import datetime, timedelta
5252
from typing import Dict, Iterator, List, Optional, Tuple
5353
from urllib import parse
5454

@@ -125,8 +125,10 @@ def __init__(self) -> None:
125125

126126
self.fetch_tracker = FetchTracker()
127127

128-
now = datetime.utcnow()
129-
self.safe_expiry = now.replace(microsecond=0) + timedelta(days=30)
128+
now = datetime.datetime.utcnow()
129+
self.safe_expiry = now.replace(microsecond=0) + datetime.timedelta(
130+
days=30
131+
)
130132

131133
self._initialize()
132134

tests/test_updater_top_level_update.py

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
"""Test ngclient Updater top-level metadata update workflow"""
77

88
import builtins
9+
import datetime
910
import os
1011
import sys
1112
import tempfile
1213
import unittest
13-
from datetime import datetime, timedelta
1414
from typing import Iterable, Optional
15-
from unittest.mock import MagicMock, call, patch
15+
from unittest.mock import MagicMock, Mock, call, patch
1616

1717
from tests import utils
1818
from tests.repository_simulator import RepositorySimulator
@@ -43,7 +43,9 @@ class TestRefresh(unittest.TestCase):
4343
# set dump_dir to trigger repository state dumps
4444
dump_dir: Optional[str] = None
4545

46-
past_datetime = datetime.utcnow().replace(microsecond=0) - timedelta(days=5)
46+
past_datetime = datetime.datetime.utcnow().replace(
47+
microsecond=0
48+
) - datetime.timedelta(days=5)
4749

4850
def setUp(self) -> None:
4951
# pylint: disable=consider-using-with
@@ -306,6 +308,81 @@ def test_new_timestamp_unsigned(self) -> None:
306308

307309
self._assert_files_exist([Root.type])
308310

311+
@patch.object(datetime, "datetime", wraps=datetime.datetime)
312+
def test_expired_timestamp_version_rollback(self, mock_time: Mock) -> None:
313+
"""Verifies that local timestamp is used in rollback checks even if it is expired.
314+
315+
The timestamp updates and rollback checks are performed
316+
with the following timing:
317+
- Timestamp v1 expiry set to day 7
318+
- First updater refresh performed on day 0
319+
- Repository publishes timestamp v2 on day 0
320+
- Timestamp v2 expiry set to day 21
321+
- Second updater refresh performed on day 18:
322+
assert that rollback check uses expired timestamp v1"""
323+
324+
now = datetime.datetime.utcnow()
325+
self.sim.timestamp.expires = now + datetime.timedelta(days=7)
326+
327+
self.sim.timestamp.version = 2
328+
329+
# Make a successful update of valid metadata which stores it in cache
330+
self._run_refresh()
331+
332+
self.sim.timestamp.expires = now + datetime.timedelta(days=21)
333+
334+
self.sim.timestamp.version = 1
335+
336+
mock_time.utcnow.return_value = (
337+
datetime.datetime.utcnow() + datetime.timedelta(days=18)
338+
)
339+
with patch("datetime.datetime", mock_time):
340+
# Check that a rollback protection is performed even if
341+
# local timestamp has expired
342+
with self.assertRaises(BadVersionNumberError):
343+
self._run_refresh()
344+
345+
self._assert_version_equals(Timestamp.type, 2)
346+
347+
@patch.object(datetime, "datetime", wraps=datetime.datetime)
348+
def test_expired_timestamp_snapshot_rollback(self, mock_time: Mock) -> None:
349+
"""Verifies that rollback protection is done even if local timestamp has expired.
350+
351+
The snapshot updates and rollback protection checks are performed
352+
with the following timing:
353+
- Timestamp v1 expiry set to day 7
354+
- Repository bumps snapshot to v3 on day 0
355+
- First updater refresh performed on day 0
356+
- Timestamp v2 expiry set to day 21
357+
- Second updater refresh performed on day 18:
358+
assert that rollback protection is done with expired timestamp v1"""
359+
360+
now = datetime.datetime.utcnow()
361+
self.sim.timestamp.expires = now + datetime.timedelta(days=7)
362+
363+
# Bump the snapshot version number to 3
364+
self.sim.update_snapshot()
365+
self.sim.update_snapshot()
366+
367+
# Make a successful update of valid metadata which stores it in cache
368+
self._run_refresh()
369+
370+
self.sim.snapshot.version = 1
371+
# Snapshot version number is set to 2, which is still less than 3
372+
self.sim.update_snapshot()
373+
self.sim.timestamp.expires = now + datetime.timedelta(days=21)
374+
375+
mock_time.utcnow.return_value = (
376+
datetime.datetime.utcnow() + datetime.timedelta(days=18)
377+
)
378+
with patch("datetime.datetime", mock_time):
379+
# Assert that rollback protection is done even if
380+
# local timestamp has expired
381+
with self.assertRaises(BadVersionNumberError):
382+
self._run_refresh()
383+
384+
self._assert_version_equals(Timestamp.type, 3)
385+
309386
def test_new_timestamp_version_rollback(self) -> None:
310387
# Check for a rollback attack
311388
self.sim.timestamp.version = 2
@@ -379,7 +456,7 @@ def test_new_snapshot_hash_mismatch(self) -> None:
379456

380457
# Modify snapshot contents without updating
381458
# timestamp's snapshot hash
382-
self.sim.snapshot.expires += timedelta(days=1)
459+
self.sim.snapshot.expires += datetime.timedelta(days=1)
383460
self.sim.snapshot.version += 1 # snapshot v2
384461
self.sim.timestamp.snapshot_meta.version = self.sim.snapshot.version
385462
self.sim.timestamp.version += 1 # timestamp v3
@@ -662,6 +739,44 @@ def test_load_metadata_from_cache(self, wrapped_open: MagicMock) -> None:
662739
expected_calls = [("root", 2), ("timestamp", None)]
663740
self.assertListEqual(self.sim.fetch_tracker.metadata, expected_calls)
664741

742+
@patch.object(datetime, "datetime", wraps=datetime.datetime)
743+
def test_expired_metadata(self, mock_time: Mock) -> None:
744+
"""Verifies that expired local timestamp/snapshot can be used for
745+
updating from remote.
746+
747+
The updates and verifications are performed with the following timing:
748+
- Timestamp v1 expiry set to day 7
749+
- First updater refresh performed on day 0
750+
- Repository bumps snapshot and targets to v2 on day 0
751+
- Timestamp v2 expiry set to day 21
752+
- Second updater refresh performed on day 18,
753+
it is successful and timestamp/snaphot final versions are v2"""
754+
755+
now = datetime.datetime.utcnow()
756+
self.sim.timestamp.expires = now + datetime.timedelta(days=7)
757+
758+
# Make a successful update of valid metadata which stores it in cache
759+
self._run_refresh()
760+
761+
self.sim.targets.version += 1
762+
self.sim.update_snapshot()
763+
self.sim.timestamp.expires = now + datetime.timedelta(days=21)
764+
765+
# Mocking time so that local timestam has expired
766+
# but the new timestamp has not
767+
mock_time.utcnow.return_value = (
768+
datetime.datetime.utcnow() + datetime.timedelta(days=18)
769+
)
770+
with patch("datetime.datetime", mock_time):
771+
self._run_refresh()
772+
773+
# Assert that the final version of timestamp/snapshot is version 2
774+
# which means a successful refresh is performed
775+
# with expired local metadata
776+
for role in ["timestamp", "snapshot", "targets"]:
777+
md = Metadata.from_file(f"{self.metadata_dir}/{role}.json")
778+
self.assertEqual(md.signed.version, 2)
779+
665780

666781
if __name__ == "__main__":
667782
if "--dump" in sys.argv:

tuf/ngclient/_internal/trusted_metadata_set.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@
5959
>>> trusted_set.update_snapshot(f.read())
6060
"""
6161

62+
import datetime
6263
import logging
6364
from collections import abc
64-
from datetime import datetime
6565
from typing import Dict, Iterator, Optional
6666

6767
from tuf.api import exceptions
@@ -91,7 +91,7 @@ def __init__(self, root_data: bytes):
9191
error type and content will contain more details.
9292
"""
9393
self._trusted_set: Dict[str, Metadata] = {}
94-
self.reference_time = datetime.utcnow()
94+
self.reference_time = datetime.datetime.utcnow()
9595

9696
# Load and validate the local root metadata. Valid initial trusted root
9797
# metadata is required

0 commit comments

Comments
 (0)