Skip to content

Commit 745a8f7

Browse files
author
Jussi Kukkonen
authored
Merge pull request #1408 from jku/merge-ngclient
Merge ngclient: a new client library implementation
2 parents c0e6673 + ffff7f5 commit 745a8f7

File tree

12 files changed

+1839
-22
lines changed

12 files changed

+1839
-22
lines changed

tests/test_trusted_metadata_set.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import json
2+
import logging
3+
import os
4+
import shutil
5+
import sys
6+
import tempfile
7+
import unittest
8+
9+
from tuf import exceptions
10+
from tuf.api.metadata import Metadata
11+
from tuf.ngclient._internal.trusted_metadata_set import TrustedMetadataSet
12+
13+
from tests import utils
14+
15+
logger = logging.getLogger(__name__)
16+
17+
class TestTrustedMetadataSet(unittest.TestCase):
18+
19+
def test_update(self):
20+
repo_dir = os.path.join(os.getcwd(), 'repository_data', 'repository', 'metadata')
21+
22+
with open(os.path.join(repo_dir, "root.json"), "rb") as f:
23+
trusted_set = TrustedMetadataSet(f.read())
24+
trusted_set.root_update_finished()
25+
26+
with open(os.path.join(repo_dir, "timestamp.json"), "rb") as f:
27+
trusted_set.update_timestamp(f.read())
28+
with open(os.path.join(repo_dir, "snapshot.json"), "rb") as f:
29+
trusted_set.update_snapshot(f.read())
30+
with open(os.path.join(repo_dir, "targets.json"), "rb") as f:
31+
trusted_set.update_targets(f.read())
32+
with open(os.path.join(repo_dir, "role1.json"), "rb") as f:
33+
trusted_set.update_delegated_targets(f.read(), "role1", "targets")
34+
with open(os.path.join(repo_dir, "role2.json"), "rb") as f:
35+
trusted_set.update_delegated_targets(f.read(), "role2", "role1")
36+
37+
def test_out_of_order_ops(self):
38+
repo_dir = os.path.join(os.getcwd(), 'repository_data', 'repository', 'metadata')
39+
data={}
40+
for md in ["root", "timestamp", "snapshot", "targets", "role1"]:
41+
with open(os.path.join(repo_dir, f"{md}.json"), "rb") as f:
42+
data[md] = f.read()
43+
44+
trusted_set = TrustedMetadataSet(data["root"])
45+
46+
# Update timestamp before root is finished
47+
with self.assertRaises(RuntimeError):
48+
trusted_set.update_timestamp(data["timestamp"])
49+
50+
trusted_set.root_update_finished()
51+
with self.assertRaises(RuntimeError):
52+
trusted_set.root_update_finished()
53+
54+
# Update snapshot before timestamp
55+
with self.assertRaises(RuntimeError):
56+
trusted_set.update_snapshot(data["snapshot"])
57+
58+
trusted_set.update_timestamp(data["timestamp"])
59+
60+
# Update targets before snapshot
61+
with self.assertRaises(RuntimeError):
62+
trusted_set.update_targets(data["targets"])
63+
64+
trusted_set.update_snapshot(data["snapshot"])
65+
66+
#update timestamp after snapshot
67+
with self.assertRaises(RuntimeError):
68+
trusted_set.update_timestamp(data["timestamp"])
69+
70+
# Update delegated targets before targets
71+
with self.assertRaises(RuntimeError):
72+
trusted_set.update_delegated_targets(data["role1"], "role1", "targets")
73+
74+
trusted_set.update_targets(data["targets"])
75+
trusted_set.update_delegated_targets(data["role1"], "role1", "targets")
76+
77+
def test_update_with_invalid_json(self):
78+
repo_dir = os.path.join(os.getcwd(), 'repository_data', 'repository', 'metadata')
79+
data={}
80+
for md in ["root", "timestamp", "snapshot", "targets", "role1"]:
81+
with open(os.path.join(repo_dir, f"{md}.json"), "rb") as f:
82+
data[md] = f.read()
83+
84+
# root.json not a json file at all
85+
with self.assertRaises(exceptions.RepositoryError):
86+
TrustedMetadataSet(b"")
87+
# root.json is invalid
88+
root = Metadata.from_bytes(data["root"])
89+
root.signed.version += 1
90+
with self.assertRaises(exceptions.RepositoryError):
91+
TrustedMetadataSet(json.dumps(root.to_dict()).encode())
92+
93+
trusted_set = TrustedMetadataSet(data["root"])
94+
trusted_set.root_update_finished()
95+
96+
top_level_md = [
97+
(data["timestamp"], trusted_set.update_timestamp),
98+
(data["snapshot"], trusted_set.update_snapshot),
99+
(data["targets"], trusted_set.update_targets),
100+
]
101+
for metadata, update_func in top_level_md:
102+
# metadata is not json
103+
with self.assertRaises(exceptions.RepositoryError):
104+
update_func(b"")
105+
# metadata is invalid
106+
md = Metadata.from_bytes(metadata)
107+
md.signed.version += 1
108+
with self.assertRaises(exceptions.RepositoryError):
109+
update_func(json.dumps(md.to_dict()).encode())
110+
111+
# metadata is of wrong type
112+
with self.assertRaises(exceptions.RepositoryError):
113+
update_func(data["root"])
114+
115+
update_func(metadata)
116+
117+
118+
# TODO test updating over initial metadata (new keys, newer timestamp, etc)
119+
# TODO test the actual specification checks
120+
121+
122+
if __name__ == '__main__':
123+
utils.configure_test_logging(sys.argv)
124+
unittest.main()

tests/test_updater_ng.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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+
"""Test Updater class
7+
"""
8+
9+
import os
10+
import shutil
11+
import tempfile
12+
import logging
13+
import sys
14+
import unittest
15+
import tuf.unittest_toolbox as unittest_toolbox
16+
17+
from tests import utils
18+
from tuf import ngclient
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
class TestUpdater(unittest_toolbox.Modified_TestCase):
24+
25+
@classmethod
26+
def setUpClass(cls):
27+
# Create a temporary directory to store the repository, metadata, and target
28+
# files. 'temporary_directory' must be deleted in TearDownModule() so that
29+
# temporary files are always removed, even when exceptions occur.
30+
cls.temporary_directory = tempfile.mkdtemp(dir=os.getcwd())
31+
32+
# Needed because in some tests simple_server.py cannot be found.
33+
# The reason is that the current working directory
34+
# has been changed when executing a subprocess.
35+
cls.SIMPLE_SERVER_PATH = os.path.join(os.getcwd(), 'simple_server.py')
36+
37+
# Launch a SimpleHTTPServer (serves files in the current directory).
38+
# Test cases will request metadata and target files that have been
39+
# pre-generated in 'tuf/tests/repository_data', which will be served
40+
# by the SimpleHTTPServer launched here. The test cases of 'test_updater.py'
41+
# assume the pre-generated metadata files have a specific structure, such
42+
# as a delegated role 'targets/role1', three target files, five key files,
43+
# etc.
44+
cls.server_process_handler = utils.TestServerProcess(log=logger,
45+
server=cls.SIMPLE_SERVER_PATH)
46+
47+
48+
49+
@classmethod
50+
def tearDownClass(cls):
51+
# Cleans the resources and flush the logged lines (if any).
52+
cls.server_process_handler.clean()
53+
54+
# Remove the temporary repository directory, which should contain all the
55+
# metadata, targets, and key files generated for the test cases
56+
shutil.rmtree(cls.temporary_directory)
57+
58+
59+
60+
def setUp(self):
61+
# We are inheriting from custom class.
62+
unittest_toolbox.Modified_TestCase.setUp(self)
63+
64+
# Copy the original repository files provided in the test folder so that
65+
# any modifications made to repository files are restricted to the copies.
66+
# The 'repository_data' directory is expected to exist in 'tuf.tests/'.
67+
original_repository_files = os.path.join(os.getcwd(), 'repository_data')
68+
temporary_repository_root = \
69+
self.make_temp_directory(directory=self.temporary_directory)
70+
71+
# The original repository, keystore, and client directories will be copied
72+
# for each test case.
73+
original_repository = os.path.join(original_repository_files, 'repository')
74+
original_keystore = os.path.join(original_repository_files, 'keystore')
75+
original_client = os.path.join(original_repository_files, 'client', 'test_repository1', 'metadata', 'current')
76+
77+
# Save references to the often-needed client repository directories.
78+
# Test cases need these references to access metadata and target files.
79+
self.repository_directory = \
80+
os.path.join(temporary_repository_root, 'repository')
81+
self.keystore_directory = \
82+
os.path.join(temporary_repository_root, 'keystore')
83+
84+
self.client_directory = os.path.join(temporary_repository_root, 'client')
85+
86+
# Copy the original 'repository', 'client', and 'keystore' directories
87+
# to the temporary repository the test cases can use.
88+
shutil.copytree(original_repository, self.repository_directory)
89+
shutil.copytree(original_client, self.client_directory)
90+
shutil.copytree(original_keystore, self.keystore_directory)
91+
92+
# 'path/to/tmp/repository' -> 'localhost:8001/tmp/repository'.
93+
repository_basepath = self.repository_directory[len(os.getcwd()):]
94+
url_prefix = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
95+
+ str(self.server_process_handler.port) + repository_basepath
96+
97+
metadata_url = f"{url_prefix}/metadata/"
98+
targets_url = f"{url_prefix}/targets/"
99+
# Creating a repository instance. The test cases will use this client
100+
# updater to refresh metadata, fetch target files, etc.
101+
self.repository_updater = ngclient.Updater(self.client_directory,
102+
metadata_url,
103+
targets_url)
104+
105+
def tearDown(self):
106+
# We are inheriting from custom class.
107+
unittest_toolbox.Modified_TestCase.tearDown(self)
108+
109+
# Logs stdout and stderr from the sever subprocess.
110+
self.server_process_handler.flush_log()
111+
112+
def test_refresh(self):
113+
# All metadata is in local directory already
114+
self.repository_updater.refresh()
115+
116+
# Get targetinfo for 'file1.txt' listed in targets
117+
targetinfo1 = self.repository_updater.get_one_valid_targetinfo('file1.txt')
118+
# Get targetinfo for 'file3.txt' listed in the delegated role1
119+
targetinfo3= self.repository_updater.get_one_valid_targetinfo('file3.txt')
120+
121+
destination_directory = self.make_temp_directory()
122+
updated_targets = self.repository_updater.updated_targets([targetinfo1, targetinfo3],
123+
destination_directory)
124+
125+
self.assertListEqual(updated_targets, [targetinfo1, targetinfo3])
126+
127+
self.repository_updater.download_target(targetinfo1, destination_directory)
128+
updated_targets = self.repository_updater.updated_targets(updated_targets,
129+
destination_directory)
130+
131+
self.assertListEqual(updated_targets, [targetinfo3])
132+
133+
134+
self.repository_updater.download_target(targetinfo3, destination_directory)
135+
updated_targets = self.repository_updater.updated_targets(updated_targets,
136+
destination_directory)
137+
138+
self.assertListEqual(updated_targets, [])
139+
140+
def test_refresh_with_only_local_root(self):
141+
os.remove(os.path.join(self.client_directory, "timestamp.json"))
142+
os.remove(os.path.join(self.client_directory, "snapshot.json"))
143+
os.remove(os.path.join(self.client_directory, "targets.json"))
144+
os.remove(os.path.join(self.client_directory, "role1.json"))
145+
146+
self.repository_updater.refresh()
147+
148+
# Get targetinfo for 'file3.txt' listed in the delegated role1
149+
targetinfo3= self.repository_updater.get_one_valid_targetinfo('file3.txt')
150+
151+
if __name__ == '__main__':
152+
utils.configure_test_logging(sys.argv)
153+
unittest.main()

tox.ini

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ changedir = tests
1616
commands =
1717
python --version
1818
python -m coverage run aggregate_tests.py
19-
python -m coverage report -m --fail-under 97
19+
python -m coverage report -m --fail-under 97 --omit "{toxinidir}/tuf/ngclient/*"
2020

2121
deps =
2222
-r{toxinidir}/requirements-test.txt
@@ -43,13 +43,13 @@ changedir = {toxinidir}
4343
commands =
4444
# Use different configs for new (tuf/api/*) and legacy code
4545
# TODO: configure black and isort args in pyproject.toml (see #1161)
46-
black --check --diff --line-length 80 tuf/api
47-
isort --check --diff --line-length 80 --profile black -p tuf tuf/api
48-
pylint -j 0 tuf/api --rcfile=tuf/api/pylintrc
46+
black --check --diff --line-length 80 tuf/api tuf/ngclient
47+
isort --check --diff --line-length 80 --profile black -p tuf tuf/api tuf/ngclient
48+
pylint -j 0 tuf/api tuf/ngclient --rcfile=tuf/api/pylintrc
4949

5050
# NOTE: Contrary to what the pylint docs suggest, ignoring full paths does
5151
# work, unfortunately each subdirectory has to be ignored explicitly.
52-
pylint -j 0 tuf --ignore=tuf/api,tuf/api/serialization
52+
pylint -j 0 tuf --ignore=tuf/api,tuf/api/serialization,tuf/ngclient,tuf/ngclient/_internal
5353

5454
mypy
5555

tuf/exceptions.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ class UnsupportedAlgorithmError(Error):
7070
class LengthOrHashMismatchError(Error):
7171
"""Indicate an error while checking the length and hash values of an object"""
7272

73-
class BadHashError(Error):
73+
class RepositoryError(Error):
74+
"""Indicate an error with a repository's state, such as a missing file."""
75+
76+
class BadHashError(RepositoryError):
7477
"""Indicate an error while checking the value of a hash object."""
7578

7679
def __init__(self, expected_hash: str, observed_hash: str):
@@ -92,9 +95,6 @@ def __repr__(self) -> str:
9295
# self.__class__.__name__ + '(' + repr(self.expected_hash) + ', ' +
9396
# repr(self.observed_hash) + ')')
9497

95-
class BadVersionNumberError(Error):
96-
"""Indicate an error for metadata that contains an invalid version number."""
97-
9898

9999
class BadPasswordError(Error):
100100
"""Indicate an error after encountering an invalid password."""
@@ -104,8 +104,8 @@ class UnknownKeyError(Error):
104104
"""Indicate an error while verifying key-like objects (e.g., keyids)."""
105105

106106

107-
class RepositoryError(Error):
108-
"""Indicate an error with a repository's state, such as a missing file."""
107+
class BadVersionNumberError(RepositoryError):
108+
"""Indicate an error for metadata that contains an invalid version number."""
109109

110110

111111
class MissingLocalRepositoryError(RepositoryError):
@@ -120,35 +120,29 @@ class ForbiddenTargetError(RepositoryError):
120120
"""Indicate that a role signed for a target that it was not delegated to."""
121121

122122

123-
class ExpiredMetadataError(Error):
123+
class ExpiredMetadataError(RepositoryError):
124124
"""Indicate that a TUF Metadata file has expired."""
125125

126126

127127
class ReplayedMetadataError(RepositoryError):
128128
"""Indicate that some metadata has been replayed to the client."""
129129

130-
def __init__(self, metadata_role: str, previous_version: int, current_version: int):
130+
def __init__(self, metadata_role: str, downloaded_version: int, current_version: int):
131131
super(ReplayedMetadataError, self).__init__()
132132

133133
self.metadata_role = metadata_role
134-
self.previous_version = previous_version
134+
self.downloaded_version = downloaded_version
135135
self.current_version = current_version
136136

137137
def __str__(self) -> str:
138138
return (
139139
'Downloaded ' + repr(self.metadata_role) + ' is older (' +
140-
repr(self.previous_version) + ') than the version currently '
140+
repr(self.downloaded_version) + ') than the version currently '
141141
'installed (' + repr(self.current_version) + ').')
142142

143143
def __repr__(self) -> str:
144144
return self.__class__.__name__ + ' : ' + str(self)
145145

146-
# # Directly instance-reproducing:
147-
# return (
148-
# self.__class__.__name__ + '(' + repr(self.metadata_role) + ', ' +
149-
# repr(self.previous_version) + ', ' + repr(self.current_version) + ')')
150-
151-
152146

153147
class CryptoError(Error):
154148
"""Indicate any cryptography-related errors."""
@@ -250,7 +244,7 @@ class InvalidNameError(Error):
250244
"""Indicate an error while trying to validate any type of named object."""
251245

252246

253-
class UnsignedMetadataError(Error):
247+
class UnsignedMetadataError(RepositoryError):
254248
"""Indicate metadata object with insufficient threshold of signatures."""
255249

256250
# signable is not used but kept in method signature for backwards compat

0 commit comments

Comments
 (0)