diff --git a/tests/test_repository_tool.py b/tests/test_repository_tool.py index 28d093c5..59c50dde 100755 --- a/tests/test_repository_tool.py +++ b/tests/test_repository_tool.py @@ -1087,7 +1087,7 @@ def test_delegate(self): rolename = 'tuf' list_of_targets = [target1_filepath, target2_filepath] threshold = 1 - restricted_paths = [self.targets_directory] + restricted_paths = [self.targets_directory + '/*'] path_hash_prefixes = ['e3a3', '8fae', 'd543'] self.targets_object.delegate(rolename, public_keys, list_of_targets, @@ -1334,7 +1334,7 @@ def test_add_restricted_paths(self): restricted_path = os.path.join(self.targets_directory, 'tuf_files') os.mkdir(restricted_path) - restricted_paths = [restricted_path] + restricted_paths = [restricted_path + '/*'] self.targets_object.add_restricted_paths(restricted_paths, 'tuf') # Retrieve 'targets_object' roleinfo, and verify the roleinfo contains @@ -1344,7 +1344,7 @@ def test_add_restricted_paths(self): targets_object_roleinfo = tuf.roledb.get_roleinfo(self.targets_object.rolename) delegated_role = targets_object_roleinfo['delegations']['roles'][0] - self.assertEqual(['/tuf_files/'], delegated_role['paths']) + self.assertEqual(['/tuf_files/*'], delegated_role['paths']) # Test improperly formatted arguments. diff --git a/tests/test_updater.py b/tests/test_updater.py index 1a298d41..7210cbc5 100755 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -969,7 +969,7 @@ def test_6_target(self): # the metadata store) so that it can be found later. filepath, fileinfo = target_files.popitem() target_files[filepath] = fileinfo - + target_fileinfo = self.repository_updater.target(filepath) self.assertTrue(tuf.formats.TARGETFILE_SCHEMA.matches(target_fileinfo)) self.assertEqual(target_fileinfo['filepath'], filepath) @@ -982,6 +982,7 @@ def test_6_target(self): # Test updater.target() backtracking behavior (enabled by default.) targets_directory = os.path.join(self.repository_directory, 'targets') foo_directory = os.path.join(targets_directory, 'foo') + foo_pattern = os.path.join(foo_directory, 'foo*.tar.gz') os.makedirs(foo_directory) foo_package = os.path.join(foo_directory, 'foo1.1.tar.gz') @@ -993,10 +994,11 @@ def test_6_target(self): repository.targets.delegate('role2', [self.role_keys['targets']['public']], - [], restricted_paths=[foo_directory]) + [], restricted_paths=[foo_pattern]) repository.targets.delegate('role3', [self.role_keys['targets']['public']], - [foo_package], restricted_paths=[foo_directory]) + [foo_package], restricted_paths=[foo_pattern]) + repository.targets.load_signing_key(self.role_keys['targets']['private']) repository.targets('role2').load_signing_key(self.role_keys['targets']['private']) repository.targets('role3').load_signing_key(self.role_keys['targets']['private']) @@ -1026,9 +1028,9 @@ def test_6_target(self): # Ensure we delegate in trusted order (i.e., 'role2' has higher priority.) repository.targets.delegate('role2', [self.role_keys['targets']['public']], - [], backtrack=False, restricted_paths=[foo_directory]) + [], backtrack=False, restricted_paths=[foo_pattern]) repository.targets.delegate('role3', [self.role_keys['targets']['public']], - [foo_package], restricted_paths=[foo_directory]) + [foo_package], restricted_paths=[foo_pattern]) repository.targets('role2').load_signing_key(self.role_keys['targets']['private']) repository.targets('role3').load_signing_key(self.role_keys['targets']['private']) diff --git a/tests/test_util.py b/tests/test_util.py index 4d9a0454..bc625d53 100755 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -506,7 +506,7 @@ def test_C4_ensure_all_targets_allowed(self): ], "name": "targets/warehouse", "paths": [ - "/file1.txt", "/README.txt", '/warehouse/' + "/file*.txt", "/README.txt", '/warehouse/*' ], "threshold": 1 } @@ -540,7 +540,7 @@ def test_C4_ensure_all_targets_allowed(self): # Test for target file that is not allowed by the parent role. self.assertRaises(tuf.ForbiddenTargetError, tuf.util.ensure_all_targets_allowed, - 'targets/warehouse', ['file8.txt'], parent_delegations) + 'targets/warehouse', ['file1.zip'], parent_delegations) self.assertRaises(tuf.ForbiddenTargetError, tuf.util.ensure_all_targets_allowed, 'targets/warehouse', ['file1.txt', 'bad-README.txt'], diff --git a/tuf/client/updater.py b/tuf/client/updater.py index b2e0b253..6f0e7ce7 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -113,6 +113,7 @@ import shutil import time import random +import fnmatch import tuf import tuf.conf @@ -911,7 +912,7 @@ def verify_target_file(target_file_object): if self.consistent_snapshot: target_digest = random.choice(list(file_hashes.values())) dirname, basename = os.path.split(target_filepath) - target_filepath = os.path.join(dirname, target_digest+'.'+basename) + target_filepath = os.path.join(dirname, target_digest + '.' + basename) return self._get_file(target_filepath, verify_target_file, 'target', file_length, compression=None, @@ -2598,9 +2599,8 @@ def target(self, target_filepath): # Raise an exception if the target information could not be retrieved. if target is None: - message = target_filepath + ' not found.' logger.error(target_filepath + ' not found.') - raise tuf.UnknownTargetError(message) + raise tuf.UnknownTargetError(target_filepath + ' not found.') # Otherwise, return the found target. else: @@ -2835,13 +2835,17 @@ def _visit_child_role(self, child_role, target_filepath, parent_delegations): elif child_role_paths is not None: for child_role_path in child_role_paths: - # A child role path may be a filepath or directory. The child - # role 'child_role_name' is added if 'target_filepath' is located - # under 'child_role_path'. Explicit filepaths are also added. - prefix = os.path.commonprefix([target_filepath, child_role_path]) - if prefix == child_role_path: + # A child role path may be an explicit path or pattern (Unix + # shell-style wildcards). The child role 'child_role_name' is added if + # 'target_filepath' is equal or matches 'child_role_path'. Explicit + # filepaths are also added. + if fnmatch.fnmatch(target_filepath, child_role_path): child_role_is_relevant = True + else: + logger.debug('Target path' + repr(target_filepath) + ' does not' + ' match child role path ' + repr(child_role_path)) + else: # 'role_name' should have been validated when it was downloaded. # The 'paths' or 'path_hash_prefixes' fields should not be missing, diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index 2848d6a0..b8587d59 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -1728,22 +1728,22 @@ def target_files(self): - def add_restricted_paths(self, list_of_directory_paths, child_rolename): + def add_restricted_paths(self, restricted_paths, child_rolename): """ - Add 'list_of_directory_paths' to the restricted paths of 'child_rolename'. + Add 'restricted_paths' to the restricted paths of 'child_rolename'. The updater client verifies the target paths specified by child roles, and searches for targets by visiting these restricted paths. A child role may only provide targets specifically listed in the delegations field of the - parent, or a target that falls under a restricted path. + parent, or a target that matches a restricted path. >>> >>> >>> - list_of_directory_paths: - A list of directory paths 'child_rolename' should also be restricted to. + restricted_paths: + A list of paths that 'child_rolename' should be restricted to. child_rolename: The child delegation that requires an update to its restricted paths, @@ -1751,8 +1751,8 @@ def add_restricted_paths(self, list_of_directory_paths, child_rolename): 'unclaimed'). - tuf.Error, if a directory path in 'list_of_directory_paths' is not a - directory, or not under the repository's targets directory. If + tuf.Error, if a restricted path in 'restricted_paths' is not a string + path, doesn't live under the repository's targets directory, or if 'child_rolename' has not been delegated yet. @@ -1766,46 +1766,40 @@ def add_restricted_paths(self, list_of_directory_paths, child_rolename): # Ensure the arguments have the appropriate number of objects and object # types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if there is a mismatch. - tuf.formats.PATHS_SCHEMA.check_match(list_of_directory_paths) + tuf.formats.PATHS_SCHEMA.check_match(restricted_paths) tuf.formats.ROLENAME_SCHEMA.check_match(child_rolename) - # A list of verified paths to be added to the child role's entry in the - # parent's delegations. - directory_paths = [] + # A list of relative and verified paths to be added to the child role's + # entry in the parent's delegations. + relative_paths = [] # Ensure the 'child_rolename' has been delegated, otherwise it will not # have an entry in the parent role's delegations field. if not tuf.roledb.role_exists(child_rolename): raise tuf.Error(repr(child_rolename) + ' has not been delegated.') - # Are the paths in 'list_of_directory_paths' valid? - for directory_path in list_of_directory_paths: - directory_path = os.path.abspath(directory_path) - if not os.path.isdir(directory_path): - raise tuf.Error(repr(directory_path) + ' is not a directory.') - - # Are the paths in the repository's targets directory? Append a trailing - # path separator with os.path.join(path, ''). + for restricted_path in restricted_paths: + # Do the restricted paths fall under the repository's targets directory? + # Append a trailing path separator with os.path.join(path, ''). targets_directory = os.path.join(self._targets_directory, '') - directory_path = os.path.join(directory_path, '') - if not directory_path.startswith(targets_directory): - raise tuf.Error(repr(directory_path) + ' is not under the' + if not restricted_path.startswith(targets_directory): + raise tuf.Error(repr(restricted_path) + ' does not live under the' ' Repository\'s targets directory: ' + repr(self._targets_directory)) - directory_paths.append(directory_path[len(self._targets_directory):]) + relative_paths.append(restricted_path[len(self._targets_directory):]) # Get the current role's roleinfo, so that its delegations field can be # updated. roleinfo = tuf.roledb.get_roleinfo(self._rolename) - # Update the restricted paths of 'child_rolename'. + # Update the restricted paths of 'child_rolename' to add relative paths. for role in roleinfo['delegations']['roles']: if role['name'] == child_rolename: restricted_paths = role['paths'] - for directory_path in directory_paths: - if directory_path not in restricted_paths: - restricted_paths.append(directory_path) + for relative_path in relative_paths: + if relative_path not in restricted_paths: + restricted_paths.append(relative_path) tuf.roledb.update_roleinfo(self._rolename, roleinfo) @@ -2178,7 +2172,7 @@ def delegate(self, rolename, public_keys, list_of_targets, threshold=1, for target in list_of_targets: target = os.path.abspath(target) - if not target.startswith(self._targets_directory+os.sep): + if not target.startswith(self._targets_directory + os.sep): raise tuf.Error(repr(target) + ' is not under the Repository\'s' ' targets directory: ' + repr(self._targets_directory)) @@ -2190,13 +2184,11 @@ def delegate(self, rolename, public_keys, list_of_targets, threshold=1, if restricted_paths is not None: for path in restricted_paths: - path = os.path.abspath(path) + os.sep if not path.startswith(self._targets_directory + os.sep): raise tuf.Error(repr(path) + ' is not under the Repository\'s' ' targets directory: ' +repr(self._targets_directory)) # Append a trailing path separator with os.path.join(path, ''). - path = os.path.join(path, '') relative_restricted_paths.append(path[targets_directory_length:]) # Create a new Targets object for the 'rolename' delegation. An initial diff --git a/tuf/util.py b/tuf/util.py index 2156fc03..d6d73c65 100755 --- a/tuf/util.py +++ b/tuf/util.py @@ -32,6 +32,7 @@ import shutil import logging import tempfile +import fnmatch import tuf import tuf.hash @@ -584,13 +585,12 @@ def ensure_all_targets_allowed(rolename, list_of_targets, parent_delegations): function does not raise an exception if 'rolename' is 'targets'. Targets allowed are either exlicitly listed under the 'paths' field, or - implicitly exist under a subdirectory of a parent directory listed - under 'paths'. A parent role may delegate trust to all files under a - particular directory, including files in subdirectories, by simply - listing the directory (e.g., '/packages/source/Django/', the equivalent - of '/packages/source/Django/*'). Targets listed in hashed bins are - also validated (i.e., its calculated path hash prefix must be delegated - by the parent role). + match one of the patterns (i.e., Unix shell-style wildcards) listed there. + A parent role may delegate trust to all files under a particular directory, + including files in subdirectories by using wildcards (e.g., + '/packages/source/Django/*', '/packages/django*.tar.gzip). + Targets listed in hashed bins are also validated (i.e., its calculated path + hash prefix must be delegated by the parent role). TODO: Should the TUF spec restrict the repository to one particular algorithm when calcutating path hash prefixes (currently restricted to @@ -666,7 +666,7 @@ def ensure_all_targets_allowed(rolename, list_of_targets, parent_delegations): if allowed_child_path_hash_prefixes is not None: consistent = paths_are_consistent_with_hash_prefixes - # 'actual_child_tarets' (i.e., 'list_of_targets') should have lenth + # 'actual_child_targets' (i.e., 'list_of_targets') should have lenth # greater than zero due to the tuf.format check above. if not consistent(actual_child_targets, allowed_child_path_hash_prefixes): @@ -684,8 +684,7 @@ def ensure_all_targets_allowed(rolename, list_of_targets, parent_delegations): # different roles/developers. for child_target in actual_child_targets: for allowed_child_path in allowed_child_paths: - prefix = os.path.commonprefix([child_target, allowed_child_path]) - if prefix == allowed_child_path: + if fnmatch.fnmatch(child_target, allowed_child_path): break else: @@ -707,7 +706,7 @@ def ensure_all_targets_allowed(rolename, list_of_targets, parent_delegations): # 'rolename' child role. else: raise tuf.RepositoryError('The parent role has not delegated to '+\ - repr(rolename)+'.') + repr(rolename) + '.')