Skip to content

Commit a0019c6

Browse files
committed
Merge branch 'snapshot-merkle-tree' of github.com:mnm678/tuf into develop
2 parents 74b1549 + ec82eb8 commit a0019c6

14 files changed

Lines changed: 1283 additions & 108 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"leaf_contents": {
3+
"name": "role1",
4+
"version": 1
5+
},
6+
"merkle_path": {
7+
"0": "3bd2912d01accd816767dcde96a2b470dc5bb51cefe3b3aeb3aca7fdc1704d6b",
8+
"1": "70304860310d2c6f0a05f2ccbfb49a4a6d6d3c7a9ff9c93e0b91b2e0ab7fff97"
9+
},
10+
"path_directions": {
11+
"0": -1,
12+
"1": -1
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"leaf_contents": {
3+
"name": "role2",
4+
"version": 1
5+
},
6+
"merkle_path": {
7+
"0": "9a8cf4b3e3cf611d339867f295792c3105d3d8ebfcd559607f9528ba7511e52a",
8+
"1": "70304860310d2c6f0a05f2ccbfb49a4a6d6d3c7a9ff9c93e0b91b2e0ab7fff97"
9+
},
10+
"path_directions": {
11+
"0": 1,
12+
"1": -1
13+
}
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"leaf_contents": {
3+
"name": "targets",
4+
"version": 1
5+
},
6+
"merkle_path": {
7+
"0": "30e11c75a8fa88fd36cc2a4796c5c9f405c9ae52b7adf4180d1c351141e5037a"
8+
},
9+
"path_directions": {
10+
"0": 1
11+
}
12+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"signatures": [
3+
{
4+
"keyid": "8a1c4a3ac2d515dec982ba9910c5fd79b91ae57f625b9cff25d06bf0a61c1758",
5+
"sig": "1790a53390ab9928ba5c46e7a30a4e0348976e26f34d8cdd29ee11d644276dfc72e3fff6d1a7a913a42a1443cda12a738a3e4803818e970446a91e0e99f24601"
6+
}
7+
],
8+
"signed": {
9+
"_type": "timestamp",
10+
"expires": "2030-01-01T00:00:00Z",
11+
"merkle_root": "76eb3066cb278633fda18fa6e3ae33d783ff154e813e2752eb7bc8b65568a41b",
12+
"meta": {
13+
"snapshot.json": {
14+
"hashes": {
15+
"sha256": "8f88e2ba48b412c3843e9bb26e1b6f8fc9e98aceb0fbaa97ba37b4c98717d7ab",
16+
"sha512": "fe9ed4b709776cc24e877babc76928cd119c18a806f432650ef6a5c687b0b5411df3c7fb3b69eda1163db83e1ae24ee3e22c9152e548b04f0a0884ee65310a95"
17+
},
18+
"length": 515,
19+
"version": 1
20+
}
21+
},
22+
"spec_version": "1.0.0",
23+
"version": 1
24+
}
25+
}

tests/test_auditor.py

Lines changed: 317 additions & 0 deletions
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()

0 commit comments

Comments
 (0)