Skip to content

Commit 6565365

Browse files
committed
ngtests: Add top-level-roles update tests
Add ngclient/updater tests following the top-level-roles metadata update from the specification (Detailed client workflow) using RepositorySimulator. Signed-off-by: Teodora Sechkova <[email protected]>
1 parent 2206fc9 commit 6565365

File tree

1 file changed

+361
-0
lines changed

1 file changed

+361
-0
lines changed
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2021, New York University and the TUF contributors
4+
# SPDX-License-Identifier: MIT OR Apache-2.0
5+
6+
"""TODO
7+
"""
8+
import os
9+
import sys
10+
import tempfile
11+
import unittest
12+
from datetime import datetime, timedelta
13+
14+
from securesystemslib import hash as sslib_hash
15+
16+
from tests import utils
17+
from tests.repository_simulator import RepositorySimulator
18+
from tuf.api.metadata import Metadata
19+
from tuf.exceptions import (
20+
BadVersionNumberError,
21+
ExpiredMetadataError,
22+
ReplayedMetadataError,
23+
RepositoryError,
24+
UnsignedMetadataError,
25+
)
26+
from tuf.ngclient import Updater
27+
from tuf.ngclient.config import UpdaterConfig
28+
29+
30+
class TestRefresh(unittest.TestCase):
31+
"""Test update of top-level metadata following
32+
'Detailed client workflow' in the specification."""
33+
34+
def setUp(self):
35+
self.temp_dir = tempfile.TemporaryDirectory()
36+
self.metadata_dir = os.path.join(self.temp_dir.name, "metadata")
37+
self.targets_dir = os.path.join(self.temp_dir.name, "targets")
38+
os.mkdir(self.metadata_dir)
39+
os.mkdir(self.targets_dir)
40+
41+
self.sim = RepositorySimulator()
42+
# Add one more root version to repository so that
43+
# refresh() always updates from local trusted root (v1) to
44+
# remote root (v2)
45+
self.sim.root.version += 1
46+
self.sim.publish_root()
47+
48+
with open(os.path.join(self.metadata_dir, "root.json"), "bw") as f:
49+
root = self.sim.download_bytes(
50+
"https://example.com/metadata/1.root.json", 100000
51+
)
52+
f.write(root)
53+
54+
def tearDown(self):
55+
self.temp_dir.cleanup()
56+
57+
def _run_refresh(self) -> Updater:
58+
updater = Updater(
59+
self.metadata_dir,
60+
"https://example.com/metadata/",
61+
"https://example.com/targets/",
62+
self.sim,
63+
)
64+
updater.refresh()
65+
return updater
66+
67+
def test_first_time_refresh(self):
68+
# Metadata dir contains only the mandatory initial root.json
69+
metadata_files = os.listdir(self.metadata_dir)
70+
self.assertListEqual(metadata_files, ["root.json"])
71+
72+
self._run_refresh()
73+
74+
# Top-level metadata can be found in metadata dir
75+
metadata_files_after_refresh = os.listdir(self.metadata_dir)
76+
self.assertListEqual(
77+
metadata_files_after_refresh,
78+
["root.json", "timestamp.json", "targets.json", "snapshot.json"],
79+
)
80+
81+
def test_trusted_root_os_error(self):
82+
os.remove(os.path.join(self.metadata_dir, "root.json"))
83+
with self.assertRaises(OSError):
84+
self._run_refresh()
85+
86+
def test_trusted_root_expired(self):
87+
# Local trusted root is expired
88+
root_path = os.path.join(self.metadata_dir, "root.json")
89+
md_root = Metadata.from_file(root_path)
90+
md_root.signed.expires = datetime.utcnow().replace(
91+
microsecond=0
92+
) - timedelta(days=5)
93+
for signer in self.sim.signers["root"]:
94+
md_root.sign(signer)
95+
md_root.to_file(root_path)
96+
97+
# The expiration of the trusted root metadata file does not lead
98+
# to failure in the update workflow and root is successfully updated
99+
# to a valid version.
100+
self._run_refresh()
101+
102+
md_root = Metadata.from_file(root_path)
103+
self.assertEqual(md_root.signed.version, self.sim.root.version)
104+
105+
def test_trusted_root_unsigned(self):
106+
# Local trusted root is not signed
107+
root_path = os.path.join(self.metadata_dir, "root.json")
108+
md_root = Metadata.from_file(root_path)
109+
md_root.signatures.clear()
110+
md_root.to_file(root_path)
111+
112+
with self.assertRaises(UnsignedMetadataError):
113+
self._run_refresh()
114+
115+
def test_max_root_rotations(self):
116+
# Root must stop looking for new versions after Y number of
117+
# intermediate files were downloaded.
118+
119+
# Create some big number of root files in the repository
120+
highest_repo_root_version = UpdaterConfig.max_root_rotations + 10
121+
for version in range(
122+
self.sim.root.version + 1, highest_repo_root_version
123+
):
124+
self.sim.root.version = version
125+
self.sim.publish_root()
126+
127+
root_path = os.path.join(self.metadata_dir, "root.json")
128+
md_root = Metadata.from_file(root_path)
129+
initial_root_version = md_root.signed.version
130+
131+
self._run_refresh()
132+
133+
# Asserts that root version was increased with no more than 'max_root_rotations'
134+
md_root = Metadata.from_file(root_path)
135+
self.assertEqual(
136+
md_root.signed.version,
137+
initial_root_version + UpdaterConfig.max_root_rotations,
138+
)
139+
140+
def test_intermediate_root_incorrectly_signed(self):
141+
# Check for an arbitrary software attack
142+
143+
# Intermediate root v3 is unsigned
144+
self.sim.root.version += 1
145+
root_signers = self.sim.signers["root"]
146+
self.sim.signers["root"].clear()
147+
self.sim.publish_root()
148+
149+
# Final root v4 is correctly signed
150+
self.sim.root.version += 1
151+
self.sim.signers["root"] = root_signers
152+
self.sim.publish_root()
153+
154+
# Incorrectly signed intermediate root is detected
155+
with self.assertRaises(UnsignedMetadataError):
156+
self._run_refresh()
157+
158+
def test_intermediate_root_expired(self):
159+
# The expiration of the new (intermediate) root metadata file
160+
# does not matter yet
161+
162+
# Intermediate root v3 is expired
163+
self.sim.root.expires = datetime.utcnow().replace(
164+
microsecond=0
165+
) - timedelta(days=5)
166+
self.sim.root.version += 1
167+
self.sim.publish_root()
168+
169+
# Final root v4 is up to date
170+
self.sim.root.expires = datetime.utcnow().replace(
171+
microsecond=0
172+
) + timedelta(days=5)
173+
self.sim.root.version += 1
174+
self.sim.publish_root()
175+
176+
self._run_refresh()
177+
md_root = Metadata.from_file(
178+
os.path.join(self.metadata_dir, "root.json")
179+
)
180+
self.assertEqual(md_root.signed.version, self.sim.root.version)
181+
182+
def test_final_root_incorrectly_signed(self):
183+
# Check for an arbitrary software attack
184+
self.sim.root.version += 1
185+
self.sim.signers["root"].clear()
186+
self.sim.publish_root()
187+
188+
with self.assertRaises(UnsignedMetadataError):
189+
self._run_refresh()
190+
191+
def test_new_root_same_version(self):
192+
# Check for a rollback_attack
193+
# Repository serves a root file with the same version as previous
194+
self.sim.publish_root()
195+
with self.assertRaises(ReplayedMetadataError):
196+
self._run_refresh()
197+
198+
def test_new_root_nonconsecutive_version(self):
199+
# Repository serves non-consecutive root version
200+
self.sim.root.version += 2
201+
self.sim.publish_root()
202+
with self.assertRaises(ReplayedMetadataError):
203+
self._run_refresh()
204+
205+
def test_final_root_expired(self):
206+
# Check for a freeze attack
207+
# Final root is expired
208+
self.sim.root.expires = datetime.utcnow().replace(
209+
microsecond=0
210+
) - timedelta(days=5)
211+
self.sim.root.version += 1
212+
self.sim.publish_root()
213+
214+
with self.assertRaises(ExpiredMetadataError):
215+
self._run_refresh()
216+
217+
def test_new_timestamp_unsigned(self):
218+
# Check for an arbitrary software attack
219+
self.sim.signers["timestamp"].clear()
220+
with self.assertRaises(UnsignedMetadataError):
221+
self._run_refresh()
222+
223+
def test_new_timestamp_version_rollback(self):
224+
# Check for a rollback attack.
225+
self.sim.timestamp.version = 2
226+
self._run_refresh()
227+
228+
self.sim.timestamp.version = 1
229+
with self.assertRaises(ReplayedMetadataError):
230+
self._run_refresh()
231+
232+
def test_new_timestamp_snapshot_rollback(self):
233+
# Check for a rollback attack.
234+
self._run_refresh()
235+
236+
self.sim.snapshot.version = 2
237+
self.sim.update_timestamp()
238+
self._run_refresh()
239+
240+
# Snapshot meta version is smaller than previous
241+
self.sim.timestamp.snapshot_meta.version = 1
242+
self.sim.timestamp.version += 1
243+
244+
with self.assertRaises(ReplayedMetadataError):
245+
self._run_refresh()
246+
247+
def test_new_timestamp_expired(self):
248+
# Check for a freeze attack
249+
self.sim.timestamp.expires = datetime.utcnow().replace(
250+
microsecond=0
251+
) - timedelta(days=5)
252+
self.sim.update_timestamp()
253+
254+
with self.assertRaises(ExpiredMetadataError):
255+
self._run_refresh()
256+
257+
def test_new_snapshot_hash_mismatch(self):
258+
# Check against timestamp role’s snapshot hash
259+
260+
# Add snapshot hash to timestamp and update
261+
self.sim.compute_metafile_hashes_length = True
262+
self.sim.update_timestamp()
263+
self._run_refresh()
264+
265+
# Modify the snapshot contents without updating
266+
# timestamp's snapshot hash
267+
self.sim.compute_metafile_hashes_length = False
268+
self.sim.update_snapshot()
269+
270+
# Hash mismatch error
271+
with self.assertRaises(RepositoryError):
272+
self._run_refresh()
273+
274+
def test_new_snapshot_unsigned(self):
275+
# Check for an arbitrary software attack
276+
self.sim.signers["snapshot"].clear()
277+
with self.assertRaises(UnsignedMetadataError):
278+
self._run_refresh()
279+
280+
# TODO: RepositorySimulator works always with consistent snapshot
281+
# enabled which forces the client to look for the snapshot version
282+
# written in timestamp (which leads to "Unknown snapshot version").
283+
# This fails the test for a snapshot version mismatch.
284+
285+
# def test_new_snapshot_version_mismatch(self):
286+
# # Check against timestamp role’s snapshot version
287+
288+
# # Increase snapshot version without updating
289+
# # timestamp's snapshot version
290+
# self.sim.snapshot.version += 1
291+
# with self.assertRaises(BadVersionNumberError):
292+
# self._run_refresh()
293+
294+
def test_new_snapshot_version_rollback(self):
295+
self.sim.snapshot.version = 2
296+
self.sim.update_timestamp()
297+
self._run_refresh()
298+
299+
self.sim.snapshot.version = 1
300+
self.sim.update_timestamp()
301+
302+
with self.assertRaises(ReplayedMetadataError):
303+
self._run_refresh()
304+
305+
def test_new_snapshot_expired(self):
306+
# Check for a freeze attack.
307+
self.sim.snapshot.expires = datetime.utcnow().replace(
308+
microsecond=0
309+
) - timedelta(days=5)
310+
self.sim.update_snapshot()
311+
312+
with self.assertRaises(ExpiredMetadataError):
313+
self._run_refresh()
314+
315+
def test_new_targets_hash_mismatch(self):
316+
# Check against snapshot role’s targets hash
317+
self.sim.compute_metafile_hashes_length = True
318+
self.sim.update_snapshot()
319+
self._run_refresh()
320+
321+
# Modify targets contents without updating
322+
# snapshot's targets hash
323+
self.sim.targets.version += 1
324+
self.sim.compute_metafile_hashes_length = False
325+
self.sim.update_snapshot()
326+
327+
with self.assertRaises(RepositoryError):
328+
self._run_refresh()
329+
330+
def test_new_targets_unsigned(self):
331+
# Check for an arbitrary software attack
332+
self.sim.signers["targets"].clear()
333+
with self.assertRaises(UnsignedMetadataError):
334+
self._run_refresh()
335+
336+
# TODO: RepositorySimulator works always with consistent snapshot
337+
# enabled which forces the client to look for the targets version
338+
# written in snapshot (which leads to "Unknown targets version").
339+
# This fails the test for a targets version mismatch.
340+
341+
# def test_new_targets_version_mismatch(self):
342+
# # Check against snapshot role’s targets version
343+
# self.sim.targets.version += 1
344+
# with self.assertRaises(BadVersionNumberError):
345+
# self._run_refresh()
346+
347+
def test_new_targets_expired(self):
348+
# Check for a freeze attack.
349+
self.sim.targets.expires = datetime.utcnow().replace(
350+
microsecond=0
351+
) - timedelta(days=5)
352+
self.sim.update_snapshot()
353+
354+
with self.assertRaises(ExpiredMetadataError):
355+
self._run_refresh()
356+
357+
358+
if __name__ == "__main__":
359+
360+
utils.configure_test_logging(sys.argv)
361+
unittest.main()

0 commit comments

Comments
 (0)