Skip to content

Commit 7901687

Browse files
author
Jussi Kukkonen
authored
Merge pull request #1501 from MVrachev/ngclient-support-consistent-snapshot
ng client: support for prefix_targets_with_hash when downloading targets
2 parents 25993d6 + 1b392bd commit 7901687

File tree

3 files changed

+115
-10
lines changed

3 files changed

+115
-10
lines changed

tests/test_updater_ng.py

+87-7
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
import tuf.unittest_toolbox as unittest_toolbox
1616

1717
from tests import utils
18+
from tuf.api.metadata import Metadata
1819
from tuf import ngclient
20+
from securesystemslib.signer import SSlibSigner
21+
from securesystemslib.interface import import_rsa_privatekey_from_file
1922

2023
logger = logging.getLogger(__name__)
2124

@@ -94,13 +97,13 @@ def setUp(self):
9497
url_prefix = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
9598
+ str(self.server_process_handler.port) + repository_basepath
9699

97-
metadata_url = f"{url_prefix}/metadata/"
98-
targets_url = f"{url_prefix}/targets/"
100+
self.metadata_url = f"{url_prefix}/metadata/"
101+
self.targets_url = f"{url_prefix}/targets/"
99102
# Creating a repository instance. The test cases will use this client
100103
# updater to refresh metadata, fetch target files, etc.
101104
self.repository_updater = ngclient.Updater(self.client_directory,
102-
metadata_url,
103-
targets_url)
105+
self.metadata_url,
106+
self.targets_url)
104107

105108
def tearDown(self):
106109
# We are inheriting from custom class.
@@ -109,14 +112,91 @@ def tearDown(self):
109112
# Logs stdout and stderr from the sever subprocess.
110113
self.server_process_handler.flush_log()
111114

115+
def _create_consistent_target(self, targetname: str, target_hash:str) -> None:
116+
"""Create consistent targets copies of their non-consistent counterparts
117+
inside the repository directory.
118+
119+
Args:
120+
targetname: A string denoting the name of the target file.
121+
target_hash: A string denoting the hash of the target.
122+
123+
"""
124+
consistent_target_name = f"{target_hash}.{targetname}"
125+
source_path = os.path.join(self.repository_directory, "targets", targetname)
126+
destination_path = os.path.join(
127+
self.repository_directory, "targets", consistent_target_name
128+
)
129+
shutil.copy(source_path, destination_path)
130+
131+
132+
def _make_root_file_with_consistent_snapshot_true(self) -> None:
133+
"""Swap the existing root file inside the client directory with a new root
134+
file where the consistent_snapshot is set to true."""
135+
root_path = os.path.join(self.client_directory, "root.json")
136+
root = Metadata.from_file(root_path)
137+
root.signed.consistent_snapshot = True
138+
root_key_path = os.path.join(self.keystore_directory, "root_key")
139+
root_key_dict = import_rsa_privatekey_from_file(
140+
root_key_path, password="password"
141+
)
142+
root_signer = SSlibSigner(root_key_dict)
143+
root.sign(root_signer)
144+
# Remove the old root file and replace it with the newer root file.
145+
os.remove(root_path)
146+
root.to_file(root_path)
147+
148+
149+
def test_refresh_on_consistent_targets(self):
150+
# Generate a new root file where consistent_snapshot is set to true and
151+
# replace the old root metadata file with it.
152+
self._make_root_file_with_consistent_snapshot_true()
153+
self.repository_updater = ngclient.Updater(self.client_directory,
154+
self.metadata_url,
155+
self.targets_url)
156+
# All metadata is in local directory already
157+
self.repository_updater.refresh()
158+
159+
# Get targetinfo for "file1.txt" listed in targets
160+
targetinfo1 = self.repository_updater.get_one_valid_targetinfo("file1.txt")
161+
# Get targetinfo for "file3.txt" listed in the delegated role1
162+
targetinfo3 = self.repository_updater.get_one_valid_targetinfo("file3.txt")
163+
164+
# Create consistent targets with file path HASH.FILENAME.EXT
165+
target1_hash = list(targetinfo1["fileinfo"].hashes.values())[0]
166+
target3_hash = list(targetinfo3["fileinfo"].hashes.values())[0]
167+
self._create_consistent_target("file1.txt", target1_hash)
168+
self._create_consistent_target("file3.txt", target3_hash)
169+
170+
destination_directory = self.make_temp_directory()
171+
updated_targets = self.repository_updater.updated_targets(
172+
[targetinfo1, targetinfo3], destination_directory
173+
)
174+
175+
self.assertListEqual(updated_targets, [targetinfo1, targetinfo3])
176+
self.repository_updater.download_target(targetinfo1, destination_directory)
177+
updated_targets = self.repository_updater.updated_targets(
178+
updated_targets, destination_directory
179+
)
180+
181+
self.assertListEqual(updated_targets, [targetinfo3])
182+
183+
self.repository_updater.download_target(targetinfo3, destination_directory)
184+
updated_targets = self.repository_updater.updated_targets(
185+
updated_targets, destination_directory
186+
)
187+
188+
self.assertListEqual(updated_targets, [])
189+
112190
def test_refresh(self):
191+
# Test refresh without consistent targets - targets without hash prefixes.
192+
113193
# All metadata is in local directory already
114194
self.repository_updater.refresh()
115195

116196
# Get targetinfo for 'file1.txt' listed in targets
117-
targetinfo1 = self.repository_updater.get_one_valid_targetinfo('file1.txt')
197+
targetinfo1 = self.repository_updater.get_one_valid_targetinfo("file1.txt")
118198
# Get targetinfo for 'file3.txt' listed in the delegated role1
119-
targetinfo3= self.repository_updater.get_one_valid_targetinfo('file3.txt')
199+
targetinfo3 = self.repository_updater.get_one_valid_targetinfo("file3.txt")
120200

121201
destination_directory = self.make_temp_directory()
122202
updated_targets = self.repository_updater.updated_targets([targetinfo1, targetinfo3],
@@ -146,7 +226,7 @@ def test_refresh_with_only_local_root(self):
146226
self.repository_updater.refresh()
147227

148228
# Get targetinfo for 'file3.txt' listed in the delegated role1
149-
targetinfo3= self.repository_updater.get_one_valid_targetinfo('file3.txt')
229+
targetinfo3 = self.repository_updater.get_one_valid_targetinfo('file3.txt')
150230

151231
if __name__ == '__main__':
152232
utils.configure_test_logging(sys.argv)

tuf/ngclient/config.py

+18
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,27 @@
99

1010
@dataclass
1111
class UpdaterConfig:
12+
"""Used to store Updater configuration.
13+
14+
Arguments:
15+
max_root_rotations: The maximum number of root rotations.
16+
max_delegations: The maximum number of delegations.
17+
root_max_length: The maxmimum length of a root metadata file.
18+
timestamp_max_length: The maximum length of a timestamp metadata file.
19+
snapshot_max_length: The maximum length of a snapshot metadata file.
20+
targets_max_length: The maximum length of a targets metadata file.
21+
prefix_targets_with_hash: When consistent snapshots are used
22+
(see https://theupdateframework.github.io/specification/latest/#consistent-snapshots), #pylint: disable=line-too-long
23+
target download URLs are formed by prefixing the filename with a
24+
hash digest of file content by default. This can be overridden by
25+
setting prefix_targets_with_hash to False.
26+
27+
"""
28+
1229
max_root_rotations: int = 32
1330
max_delegations: int = 32
1431
root_max_length: int = 512000 # bytes
1532
timestamp_max_length: int = 16384 # bytes
1633
snapshot_max_length: int = 2000000 # bytes
1734
targets_max_length: int = 5000000 # bytes
35+
prefix_targets_with_hash: bool = True

tuf/ngclient/updater.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,12 @@ def download_target(
252252
else:
253253
target_base_url = _ensure_trailing_slash(target_base_url)
254254

255-
target_filepath = targetinfo["filepath"]
256255
target_fileinfo: "TargetFile" = targetinfo["fileinfo"]
256+
target_filepath = targetinfo["filepath"]
257+
consistent_snapshot = self._trusted_set.root.signed.consistent_snapshot
258+
if consistent_snapshot and self.config.prefix_targets_with_hash:
259+
hashes = list(target_fileinfo.hashes.values())
260+
target_filepath = f"{hashes[0]}.{target_filepath}"
257261
full_url = parse.urljoin(target_base_url, target_filepath)
258262

259263
with self._fetcher.download_file(
@@ -266,8 +270,11 @@ def download_target(
266270
f"{target_filepath} length or hashes do not match"
267271
) from e
268272

269-
filepath = os.path.join(destination_directory, target_filepath)
270-
sslib_util.persist_temp_file(target_file, filepath)
273+
# Store the target file name without the HASH prefix.
274+
local_filepath = os.path.join(
275+
destination_directory, targetinfo["filepath"]
276+
)
277+
sslib_util.persist_temp_file(target_file, local_filepath)
271278

272279
def _download_metadata(
273280
self, rolename: str, length: int, version: Optional[int] = None

0 commit comments

Comments
 (0)