Skip to content

Commit ec82eb8

Browse files
committed
Add auditor implementation
The auditor verifies all nodes in the snapshot merkle tree to check for rollback attacks. This PoC implementation re-uses a lot of functionality from the updater. It may be better to move some of the re-used functionality to a separate place (ie separating file downloading from update logic). The auditor implementation currently requires there to be snapshot metadata on the repository in order to iterate over all of the nodes. In the future, if the verification succeeds, the auditor should add a signature to timestamp metadata. Signed-off-by: Marina Moore <[email protected]>
1 parent d869cc4 commit ec82eb8

File tree

4 files changed

+507
-9
lines changed

4 files changed

+507
-9
lines changed

tests/test_auditor.py

+317
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
<Program Name>
5+
test_auditor.py
6+
7+
<Author>
8+
Marina Moore
9+
10+
<Started>
11+
January 29, 2021
12+
13+
<Copyright>
14+
See LICENSE-MIT OR LICENSE for licensing information.
15+
16+
<Purpose>
17+
'test-auditor.py' provides a collection of methods that test the public /
18+
non-public methods and functions of 'tuf.client.auditor.py'.
19+
20+
"""
21+
22+
import unittest
23+
import tempfile
24+
import os
25+
import logging
26+
import shutil
27+
28+
import tuf
29+
import tuf.exceptions
30+
import tuf.log
31+
import tuf.keydb
32+
import tuf.roledb
33+
import tuf.repository_tool as repo_tool
34+
import tuf.repository_lib as repo_lib
35+
import tuf.unittest_toolbox as unittest_toolbox
36+
import tuf.client.auditor as auditor
37+
38+
from tests import utils
39+
40+
import securesystemslib
41+
42+
logger = logging.getLogger(__name__)
43+
repo_tool.disable_console_log_messages()
44+
45+
46+
class TestAuditor(unittest_toolbox.Modified_TestCase):
47+
48+
@classmethod
49+
def setUpClass(cls):
50+
# setUpClass is called before tests in an individual class are executed.
51+
52+
# Create a temporary directory to store the repository, metadata, and target
53+
# files. 'temporary_directory' must be deleted in TearDownModule() so that
54+
# temporary files are always removed, even when exceptions occur.
55+
cls.temporary_directory = tempfile.mkdtemp(dir=os.getcwd())
56+
57+
# Needed because in some tests simple_server.py cannot be found.
58+
# The reason is that the current working directory
59+
# has been changed when executing a subprocess.
60+
cls.SIMPLE_SERVER_PATH = os.path.join(os.getcwd(), 'simple_server.py')
61+
62+
# Launch a SimpleHTTPServer (serves files in the current directory).
63+
# Test cases will request metadata and target files that have been
64+
# pre-generated in 'tuf/tests/repository_data', which will be served
65+
# by the SimpleHTTPServer launched here. The test cases of 'test_updater.py'
66+
# assume the pre-generated metadata files have a specific structure, such
67+
# as a delegated role 'targets/role1', three target files, five key files,
68+
# etc.
69+
cls.server_process_handler = utils.TestServerProcess(log=logger,
70+
server=cls.SIMPLE_SERVER_PATH)
71+
72+
73+
@classmethod
74+
def tearDownClass(cls):
75+
# Cleans the resources and flush the logged lines (if any).
76+
cls.server_process_handler.clean()
77+
78+
# Remove the temporary repository directory, which should contain all the
79+
# metadata, targets, and key files generated for the test cases
80+
shutil.rmtree(cls.temporary_directory)
81+
82+
83+
def setUp(self):
84+
# We are inheriting from custom class.
85+
unittest_toolbox.Modified_TestCase.setUp(self)
86+
87+
tuf.roledb.clear_roledb(clear_all=True)
88+
tuf.keydb.clear_keydb(clear_all=True)
89+
90+
self.repository_name = 'test_repository1'
91+
92+
# Copy the original repository files provided in the test folder so that
93+
# any modifications made to repository files are restricted to the copies.
94+
# The 'repository_data' directory is expected to exist in 'tuf.tests/'.
95+
original_repository_files = os.path.join(os.getcwd(), 'repository_data')
96+
temporary_repository_root = \
97+
self.make_temp_directory(directory=self.temporary_directory)
98+
99+
# The original repository, keystore, and client directories will be copied
100+
# for each test case.
101+
original_repository = os.path.join(original_repository_files, 'repository')
102+
original_keystore = os.path.join(original_repository_files, 'keystore')
103+
original_client = os.path.join(original_repository_files, 'client')
104+
105+
# Save references to the often-needed client repository directories.
106+
# Test cases need these references to access metadata and target files.
107+
self.repository_directory = \
108+
os.path.join(temporary_repository_root, 'repository')
109+
self.keystore_directory = \
110+
os.path.join(temporary_repository_root, 'keystore')
111+
112+
self.client_directory = os.path.join(temporary_repository_root,
113+
'client')
114+
self.client_metadata = os.path.join(self.client_directory,
115+
self.repository_name, 'metadata')
116+
self.client_metadata_current = os.path.join(self.client_metadata,
117+
'current')
118+
self.client_metadata_previous = os.path.join(self.client_metadata,
119+
'previous')
120+
121+
# Copy the original 'repository', 'client', and 'keystore' directories
122+
# to the temporary repository the test cases can use.
123+
shutil.copytree(original_repository, self.repository_directory)
124+
shutil.copytree(original_client, self.client_directory)
125+
shutil.copytree(original_keystore, self.keystore_directory)
126+
127+
# 'path/to/tmp/repository' -> 'localhost:8001/tmp/repository'.
128+
repository_basepath = self.repository_directory[len(os.getcwd()):]
129+
url_prefix = 'http://localhost:' \
130+
+ str(self.server_process_handler.port) + repository_basepath
131+
132+
# Setting 'tuf.settings.repository_directory' with the temporary client
133+
# directory copied from the original repository files.
134+
tuf.settings.repositories_directory = self.client_directory
135+
136+
# replace timestamp with a merkle timestamp
137+
merkle_timestamp = os.path.join(self.repository_directory, 'metadata', 'timestamp-merkle.json')
138+
timestamp = os.path.join(self.repository_directory, 'metadata', 'timestamp.json')
139+
shutil.move(merkle_timestamp, timestamp)
140+
141+
# Metadata role keys are needed by the test cases to make changes to the
142+
# repository (e.g., adding a new target file to 'targets.json' and then
143+
# requesting a refresh()).
144+
self.role_keys = _load_role_keys(self.keystore_directory)
145+
146+
# The repository must be rewritten with 'consistent_snapshot' set.
147+
repository = repo_tool.load_repository(self.repository_directory)
148+
149+
# Write metadata for all the top-level roles , since consistent snapshot
150+
# is now being set to true (i.e., the pre-generated repository isn't set
151+
# to support consistent snapshots. A new version of targets.json is needed
152+
# to ensure <digest>.filename target files are written to disk.
153+
repository.targets.load_signing_key(self.role_keys['targets']['private'])
154+
repository.root.load_signing_key(self.role_keys['root']['private'])
155+
repository.snapshot.load_signing_key(self.role_keys['snapshot']['private'])
156+
repository.timestamp.load_signing_key(self.role_keys['timestamp']['private'])
157+
158+
repository.mark_dirty(['targets', 'root', 'snapshot', 'timestamp'])
159+
repository.writeall(snapshot_merkle=True, consistent_snapshot=True)
160+
161+
# Move the staged metadata to the "live" metadata.
162+
shutil.rmtree(os.path.join(self.repository_directory, 'metadata'))
163+
shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'),
164+
os.path.join(self.repository_directory, 'metadata'))
165+
166+
self.repository_mirrors = {'mirror1': {'url_prefix': url_prefix,
167+
'metadata_path': 'metadata',
168+
'targets_path': 'targets'}}
169+
170+
171+
172+
173+
def tearDown(self):
174+
# We are inheriting from custom class.
175+
unittest_toolbox.Modified_TestCase.tearDown(self)
176+
tuf.roledb.clear_roledb(clear_all=True)
177+
tuf.keydb.clear_keydb(clear_all=True)
178+
179+
# Logs stdout and stderr from the sever subprocess.
180+
self.server_process_handler.flush_log()
181+
182+
183+
# UNIT TESTS.
184+
185+
def test_1__init_exceptions(self):
186+
# Invalid arguments
187+
self.assertRaises(securesystemslib.exceptions.FormatError, auditor.Auditor,
188+
5, self.repository_mirrors)
189+
self.assertRaises(securesystemslib.exceptions.FormatError, auditor.Auditor,
190+
self.repository_name, 5)
191+
192+
193+
194+
def test_2__verify_merkle_tree(self):
195+
repository_auditor = auditor.Auditor(self.repository_name, self.repository_mirrors)
196+
# skip version 1 as it was written without consistent snapshots
197+
repository_auditor.last_version_verified = 1
198+
199+
# The repository must be rewritten with 'consistent_snapshot' set.
200+
repository = repo_tool.load_repository(self.repository_directory)
201+
202+
# Write metadata for all the top-level roles , since consistent snapshot
203+
# is now being set to true (i.e., the pre-generated repository isn't set
204+
# to support consistent snapshots. A new version of targets.json is needed
205+
# to ensure <digest>.filename target files are written to disk.
206+
repository.targets.load_signing_key(self.role_keys['targets']['private'])
207+
repository.root.load_signing_key(self.role_keys['root']['private'])
208+
repository.snapshot.load_signing_key(self.role_keys['snapshot']['private'])
209+
repository.timestamp.load_signing_key(self.role_keys['timestamp']['private'])
210+
211+
repository.targets.add_target('file1.txt')
212+
213+
repository.mark_dirty(['targets', 'root', 'snapshot', 'timestamp'])
214+
repository.writeall(snapshot_merkle=True, consistent_snapshot=True)
215+
216+
# Move the staged metadata to the "live" metadata.
217+
shutil.rmtree(os.path.join(self.repository_directory, 'metadata'))
218+
shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'),
219+
os.path.join(self.repository_directory, 'metadata'))
220+
221+
222+
# Normal case, should not error
223+
repository_auditor.verify()
224+
225+
self.assertEqual(repository_auditor.version_info['role1.json'], 1)
226+
self.assertEqual(repository_auditor.version_info['targets.json'], 3)
227+
self.assertEqual(repository_auditor.last_version_verified, 3)
228+
229+
# modify targets
230+
repository.targets.add_target('file2.txt')
231+
232+
repository.targets.load_signing_key(self.role_keys['targets']['private'])
233+
repository.root.load_signing_key(self.role_keys['root']['private'])
234+
repository.snapshot.load_signing_key(self.role_keys['snapshot']['private'])
235+
repository.timestamp.load_signing_key(self.role_keys['timestamp']['private'])
236+
237+
238+
repository.mark_dirty(['targets', 'root', 'snapshot', 'timestamp'])
239+
repository.writeall(snapshot_merkle=True, consistent_snapshot=True)
240+
241+
# Move the staged metadata to the "live" metadata.
242+
shutil.rmtree(os.path.join(self.repository_directory, 'metadata'))
243+
shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'),
244+
os.path.join(self.repository_directory, 'metadata'))
245+
246+
repository_auditor.verify()
247+
248+
# Ensure the auditor checked the latest targets
249+
self.assertEqual(repository_auditor.version_info['targets.json'], 4)
250+
251+
# Test rollback attack detection
252+
repository_auditor.version_info['targets.json'] = 5
253+
repository_auditor.last_version_verified = 3
254+
255+
self.assertRaises(tuf.exceptions.RepositoryError, repository_auditor.verify)
256+
257+
258+
259+
260+
def _load_role_keys(keystore_directory):
261+
262+
# Populating 'self.role_keys' by importing the required public and private
263+
# keys of 'tuf/tests/repository_data/'. The role keys are needed when
264+
# modifying the remote repository used by the test cases in this unit test.
265+
266+
# The pre-generated key files in 'repository_data/keystore' are all encrypted with
267+
# a 'password' passphrase.
268+
EXPECTED_KEYFILE_PASSWORD = 'password'
269+
270+
# Store and return the cryptography keys of the top-level roles, including 1
271+
# delegated role.
272+
role_keys = {}
273+
274+
root_key_file = os.path.join(keystore_directory, 'root_key')
275+
targets_key_file = os.path.join(keystore_directory, 'targets_key')
276+
snapshot_key_file = os.path.join(keystore_directory, 'snapshot_key')
277+
timestamp_key_file = os.path.join(keystore_directory, 'timestamp_key')
278+
delegation_key_file = os.path.join(keystore_directory, 'delegation_key')
279+
280+
role_keys = {'root': {}, 'targets': {}, 'snapshot': {}, 'timestamp': {},
281+
'role1': {}}
282+
283+
# Import the top-level and delegated role public keys.
284+
role_keys['root']['public'] = \
285+
repo_tool.import_rsa_publickey_from_file(root_key_file+'.pub')
286+
role_keys['targets']['public'] = \
287+
repo_tool.import_ed25519_publickey_from_file(targets_key_file+'.pub')
288+
role_keys['snapshot']['public'] = \
289+
repo_tool.import_ed25519_publickey_from_file(snapshot_key_file+'.pub')
290+
role_keys['timestamp']['public'] = \
291+
repo_tool.import_ed25519_publickey_from_file(timestamp_key_file+'.pub')
292+
role_keys['role1']['public'] = \
293+
repo_tool.import_ed25519_publickey_from_file(delegation_key_file+'.pub')
294+
295+
# Import the private keys of the top-level and delegated roles.
296+
role_keys['root']['private'] = \
297+
repo_tool.import_rsa_privatekey_from_file(root_key_file,
298+
EXPECTED_KEYFILE_PASSWORD)
299+
role_keys['targets']['private'] = \
300+
repo_tool.import_ed25519_privatekey_from_file(targets_key_file,
301+
EXPECTED_KEYFILE_PASSWORD)
302+
role_keys['snapshot']['private'] = \
303+
repo_tool.import_ed25519_privatekey_from_file(snapshot_key_file,
304+
EXPECTED_KEYFILE_PASSWORD)
305+
role_keys['timestamp']['private'] = \
306+
repo_tool.import_ed25519_privatekey_from_file(timestamp_key_file,
307+
EXPECTED_KEYFILE_PASSWORD)
308+
role_keys['role1']['private'] = \
309+
repo_tool.import_ed25519_privatekey_from_file(delegation_key_file,
310+
EXPECTED_KEYFILE_PASSWORD)
311+
312+
return role_keys
313+
314+
315+
316+
if __name__ == '__main__':
317+
unittest.main()

tests/test_updater.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1791,15 +1791,15 @@ def test_snapshot_merkle(self):
17911791
repository_updater.refresh()
17921792

17931793
# Test verify merkle path
1794-
snapshot_info = repository_updater._verify_merkle_path('targets')
1794+
snapshot_info = repository_updater.verify_merkle_path('targets')
17951795
self.assertEqual(snapshot_info['version'], 1)
17961796

1797-
snapshot_info = repository_updater._verify_merkle_path('role1')
1797+
snapshot_info = repository_updater.verify_merkle_path('role1')
17981798
self.assertEqual(snapshot_info['version'], 1)
17991799

18001800
# verify merkle path with invalid role
18011801
self.assertRaises(tuf.exceptions.NoWorkingMirrorError,
1802-
repository_updater._verify_merkle_path, 'foo')
1802+
repository_updater.verify_merkle_path, 'foo')
18031803

18041804
# Test get_one_valid_targetinfo with snapshot merkle
18051805
repository_updater.get_one_valid_targetinfo('file1.txt')

0 commit comments

Comments
 (0)