Skip to content

Commit 7d92672

Browse files
committed
ng client: support for prefix_targets_with_hash
Add support for prefixing targets with their hashes when downloading or using HASH.FILENAME.EXT as target names. The introduction of prefix_targets_with_hash was necessary, because there are use cases like Warehouse where you could use consistent_snapshot, but without adding a hash prefix to your targets. When prefix_targets_with_hash is set to True, target files conforming the format HASH.FILENAME.EXT will be downloaded from the server, but they will be saved on the client side without their hash prefixes or FILENAME.EXT. This makes sure the client won't understand the usage of prefix_targets_with_hash. Still, if you want to use HASH.FILENAME.EXT as target names when downloading, then additionally you need to provide consistent_snapshot set to True in your root.json. The reason is that the specification uses consistent_snapshot for the same purpose: "If consistent snapshots are not used (see § 6.2 Consistent snapshots), then the filename used to download the target file is of the fixed form FILENAME.EXT (e.g., foobar.tar.gz). Otherwise, the filename is of the form HASH.FILENAME.EXT (e.g., c14aeb4ac9f4a8fc0d83d12482b9197452f6adf3eb710e3b1e2b79e8d14cb681.foobar.tar.gz), where HASH is one of the hashes of the targets file listed in the targets metadata file found earlier in step § 5.6 Update the targets role. In either case, the client MUST write the file to non-volatile storage as FILENAME.EXT." The same behavior of using two flags is used in the legacy code when calling tuf.client.updater.download_target() in a repository using prefix_targets_with_hash and consistent_snapshot. See chapter 5.7.3: https://theupdateframework.github.io/specification/latest/index.html#fetch-target By default, prefix_targets_with_hash is set to true to make it easier to the user to provide uniquely identifiable targets file names by using consistent_snapshot set to True. Signed-off-by: Martin Vrachev <[email protected]>
1 parent 62d305a commit 7d92672

File tree

3 files changed

+102
-10
lines changed

3 files changed

+102
-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

+5
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@ class UpdaterConfig:
1515
timestamp_max_length: int = 16384 # bytes
1616
snapshot_max_length: int = 2000000 # bytes
1717
targets_max_length: int = 5000000 # bytes
18+
# We need this variable because there are use cases like Warehouse where
19+
# you could use consistent_snapshot, but without adding a hash prefix.
20+
# By default, prefix_targets_with_hash is set to true to use uniquely
21+
# identifiable targets file names for repositories.
22+
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)