From 3fc1e59000e91162293eb39a2213036cda84cdd6 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 22 Sep 2015 07:31:30 -0400 Subject: [PATCH 01/30] Initial implementation of stored version numbers in snapshot.json --- tests/test_repository_lib.py | 8 +++- tuf/formats.py | 35 +++++++++++----- tuf/repository_lib.py | 81 ++++++++++++++++++++++++++++++------ 3 files changed, 99 insertions(+), 25 deletions(-) diff --git a/tests/test_repository_lib.py b/tests/test_repository_lib.py index 5c5b79b0..1c6e64a4 100755 --- a/tests/test_repository_lib.py +++ b/tests/test_repository_lib.py @@ -547,7 +547,7 @@ def test_generate_targets_metadata(self): - + """ def test_generate_snapshot_metadata(self): # Test normal case. temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) @@ -562,6 +562,10 @@ def test_generate_snapshot_metadata(self): repo_lib.TARGETS_FILENAME) version = 1 expiration_date = '1985-10-21T13:20:00Z' + + # TODO: + root_filename = 'root' + targets_filename = 'targets' snapshot_metadata = \ repo_lib.generate_snapshot_metadata(metadata_directory, version, @@ -590,7 +594,7 @@ def test_generate_snapshot_metadata(self): self.assertRaises(tuf.FormatError, repo_lib.generate_snapshot_metadata, metadata_directory, version, expiration_date, root_filename, targets_filename, 3) - + """ def test_generate_timestamp_metadata(self): diff --git a/tuf/formats.py b/tuf/formats.py index 66d4d4a0..b0f5dbce 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -255,7 +255,7 @@ keyid = KEYID_SCHEMA, keyval = KEYVAL_SCHEMA) -# Info that describes both metadata and target files. +# Information that describes both metadata and target files. # This schema allows the storage of multiple hashes for the same file # (e.g., sha256 and sha512 may be computed for the same file and stored). FILEINFO_SCHEMA = SCHEMA.Object( @@ -264,6 +264,21 @@ hashes = HASHDICT_SCHEMA, custom = SCHEMA.Optional(SCHEMA.Object())) +# Version information included in "snapshot.json" for each role available on +# the TUF repository. The 'FILEINFO_SCHEMA' object was previously listed in +# the snapshot role, but was switched to this object format to reduce the +# amount of metadata that needs to be downloaded. Listing version numbers in +# "snapshot.json" also prevents rollback attacks for roles that clients have +# not downloaded. +VERSIONINFO_SCHEMA = SCHEMA.Object( + object_name = 'VERSIONINFO_SCHEMA', + version = METADATAVERSION_SCHEMA) + +# +VERSIONDICT_SCHEMA = SCHEMA.DictOf( + key_schema = RELPATH_SCHEMA, + value_schema = VERSIONINFO_SCHEMA) + # A dict holding the information for a particular file. The keys hold the # relative file path and the values the relevant file information. FILEDICT_SCHEMA = SCHEMA.DictOf( @@ -279,9 +294,9 @@ # A list of TARGETFILE_SCHEMA. TARGETFILES_SCHEMA = SCHEMA.ListOf(TARGETFILE_SCHEMA) -# A single signature of an object. Indicates the signature, the id of the +# A single signature of an object. Indicates the signature, the ID of the # signing key, and the signing method. -# I debated making the signature schema not contain the key id and instead have +# I debated making the signature schema not contain the key ID and instead have # the signatures of a file be a dictionary with the key being the keyid and the # value being the signature schema without the keyid. That would be under # the argument that a key should only be able to sign a file once. However, @@ -451,7 +466,7 @@ _type = SCHEMA.String('Snapshot'), version = METADATAVERSION_SCHEMA, expires = ISO8601_DATETIME_SCHEMA, - meta = FILEDICT_SCHEMA) + meta = VERSIONDICT_SCHEMA) # Timestamp role: indicates the latest version of the snapshot file. TIMESTAMP_SCHEMA = SCHEMA.Object( @@ -630,11 +645,11 @@ def make_metadata(version, expiration_date, keydict, roledict, class SnapshotFile(MetaFile): - def __init__(self, version, expires, filedict): + def __init__(self, version, expires, versiondict): self.info = {} self.info['version'] = version self.info['expires'] = expires - self.info['meta'] = filedict + self.info['meta'] = versiondict @staticmethod @@ -645,17 +660,17 @@ def from_metadata(object): version = object['version'] expires = object['expires'] - filedict = object['meta'] + versiondict = object['meta'] - return SnapshotFile(version, expires, filedict) + return SnapshotFile(version, expires, versiondict) @staticmethod - def make_metadata(version, expiration_date, filedict): + def make_metadata(version, expiration_date, versiondict): result = {'_type' : 'Snapshot'} result['version'] = version result['expires'] = expiration_date - result['meta'] = filedict + result['meta'] = versiondict # Is 'result' a Snapshot metadata file? # Raise 'tuf.FormatError' if not. diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index a7f1a79c..9b550bed 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -143,8 +143,8 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, TARGETS_EXPIRES_WARN_SECONDS) elif rolename == 'snapshot': - root_filename = filenames['root'] - targets_filename = filenames['targets'] + root_filename = 'root' + targets_filename = 'targets' metadata = generate_snapshot_metadata(metadata_directory, roleinfo['version'], roleinfo['expires'], root_filename, @@ -179,12 +179,26 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, status = tuf.sig.get_signature_status(temp_signable, rolename) if len(status['good_sigs']) == 0: metadata['version'] = metadata['version'] + 1 + if rolename != 'snapshot': + print('write metadata: ' + rolename) + #roleinfo = tuf.roledb.get_roleinfo(rolename + METADATA_EXTENSION) + roleinfo = tuf.roledb.get_roleinfo(rolename) + roleinfo['version'] = roleinfo['version'] + 1 + tuf.roledb.update_roleinfo(rolename, roleinfo) + #tuf.roledb.update_roleinfo(rolename + METADATA_EXTENSION, roleinfo) signable = sign_metadata(metadata, roleinfo['signing_keyids'], metadata_filename) # non-partial write() else: if tuf.sig.verify(signable, rolename) and not roleinfo['partial_loaded']: metadata['version'] = metadata['version'] + 1 + if rolename != 'snapshot': + print('write metadata: ' + rolename) + print(repr(tuf.roledb.get_rolenames())) + #roleinfo = tuf.roledb.get_roleinfo(rolename + METADATA_EXTENSION) + roleinfo = tuf.roledb.get_roleinfo(rolename) + tuf.roledb.update_roleinfo(rolename, roleinfo) + #tuf.roledb.update_roleinfo(rolename + METADATA_EXTENSION, roleinfo) signable = sign_metadata(metadata, roleinfo['signing_keyids'], metadata_filename) @@ -1265,6 +1279,47 @@ def get_metadata_fileinfo(filename, custom=None): +def get_metadata_versioninfo(rolename): + """ + + Retrieve the version information of 'rolename'. The object returned + conforms to 'tuf.formats.VERSIONINFO_SCHEMA'. The information + generated for 'rolename' is stored in 'snapshot.json'. + The versioninfo object returned has the form: + + versioninfo = {'version': 14} + + + rolename: + The metadata role whose version number is needed. It must exist. + + + tuf.FormatError, if 'rolename' is improperly formatted. + + tuf.UnknownRoleError, if 'rolename' does not exist. + + + None. + + + A dictionary conformant to 'tuf.formats.VERSIONINFO_SCHEMA'. This + dictionary contains the version number of 'rolename'. + """ + + # Does 'rolename' have the correct format? + # Ensure the arguments have the appropriate number of objects and object + # types, and that all dict keys are properly named. + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + + roleinfo = tuf.roledb.get_roleinfo(rolename) + + versioninfo = {'version': roleinfo['version']} + + return versioninfo + + + + def get_target_hash(target_filepath): """ @@ -1596,9 +1651,9 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date, # Retrieve the fileinfo of 'root.json' and 'targets.json'. This file # information includes data such as file length, hashes of the file, etc. - filedict = {} - filedict[ROOT_FILENAME] = get_metadata_fileinfo(root_filename) - filedict[TARGETS_FILENAME] = get_metadata_fileinfo(targets_filename) + versiondict = {} + versiondict[ROOT_FILENAME] = get_metadata_versioninfo(root_filename) + versiondict[TARGETS_FILENAME] = get_metadata_versioninfo(targets_filename) # Add compressed versions of the 'targets.json' and 'root.json' metadata, # if they exist. @@ -1607,13 +1662,13 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date, compressed_targets_filename = targets_filename+extension # If the compressed versions of the root and targets metadata is found, - # add their file attributes to 'filedict'. + # add their file attributes to 'versiondict'. if os.path.exists(compressed_root_filename): - filedict[ROOT_FILENAME+extension] = \ - get_metadata_fileinfo(compressed_root_filename) + versiondict[ROOT_FILENAME+extension] = \ + get_metadata_versioninfo(compressed_root_filename) if os.path.exists(compressed_targets_filename): - filedict[TARGETS_FILENAME+extension] = \ - get_metadata_fileinfo(compressed_targets_filename) + versiondict[TARGETS_FILENAME+extension] = \ + get_metadata_versioninfo(compressed_targets_filename) # Walk the 'targets/' directory and generate the fileinfo of all the role # files found. This information is stored in the 'meta' field of the snapshot @@ -1643,12 +1698,12 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date, # Obsolete role files may still be found. Ensure only roles loaded # in the roledb are included in the snapshot metadata. if tuf.roledb.role_exists(rolename): - filedict[metadata_name] = get_metadata_fileinfo(metadata_path) + versiondict[metadata_name] = get_metadata_versioninfo(rolename) # Generate the snapshot metadata object. snapshot_metadata = tuf.formats.SnapshotFile.make_metadata(version, - expiration_date, - filedict) + expiration_date, + versiondict) return snapshot_metadata From e3847026c96d6bd0b493a195fc2f4cd5fc96a074 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 22 Sep 2015 10:02:40 -0400 Subject: [PATCH 02/30] Fix issue where incorrect version number is added to versiondict in snapshot.json --- tuf/repository_lib.py | 9 +-------- tuf/repository_tool.py | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index 9b550bed..b21c44df 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -180,12 +180,9 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, if len(status['good_sigs']) == 0: metadata['version'] = metadata['version'] + 1 if rolename != 'snapshot': - print('write metadata: ' + rolename) - #roleinfo = tuf.roledb.get_roleinfo(rolename + METADATA_EXTENSION) roleinfo = tuf.roledb.get_roleinfo(rolename) roleinfo['version'] = roleinfo['version'] + 1 tuf.roledb.update_roleinfo(rolename, roleinfo) - #tuf.roledb.update_roleinfo(rolename + METADATA_EXTENSION, roleinfo) signable = sign_metadata(metadata, roleinfo['signing_keyids'], metadata_filename) # non-partial write() @@ -193,12 +190,9 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, if tuf.sig.verify(signable, rolename) and not roleinfo['partial_loaded']: metadata['version'] = metadata['version'] + 1 if rolename != 'snapshot': - print('write metadata: ' + rolename) - print(repr(tuf.roledb.get_rolenames())) - #roleinfo = tuf.roledb.get_roleinfo(rolename + METADATA_EXTENSION) roleinfo = tuf.roledb.get_roleinfo(rolename) + roleinfo['version'] = roleinfo['version'] + 1 tuf.roledb.update_roleinfo(rolename, roleinfo) - #tuf.roledb.update_roleinfo(rolename + METADATA_EXTENSION, roleinfo) signable = sign_metadata(metadata, roleinfo['signing_keyids'], metadata_filename) @@ -1312,7 +1306,6 @@ def get_metadata_versioninfo(rolename): tuf.formats.ROLENAME_SCHEMA.check_match(rolename) roleinfo = tuf.roledb.get_roleinfo(rolename) - versioninfo = {'version': roleinfo['version']} return versioninfo diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index ec90046c..bf9e4542 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -2021,7 +2021,7 @@ def delegate(self, rolename, public_keys, list_of_targets, threshold=1, full_rolename = self._rolename + '/' + rolename if tuf.roledb.role_exists(full_rolename): - raise tuf.Error(repr(full_rolename) + ' already delegated.') + raise tuf.Error(repr(rolename) + ' already delegated.') # Keep track of the valid keyids (added to the new Targets object) and their # keydicts (added to this Targets delegations). From 89dfda1f1141086c15ed38170c2db9c8cc537f23 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Thu, 24 Sep 2015 10:52:14 -0400 Subject: [PATCH 03/30] self.fileinfo ---> self.versioninfo --- tuf/client/updater.py | 104 ++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index bb4eb375..4a341828 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -160,12 +160,10 @@ class Updater(object): self.metadata_directory: The directory where trusted metadata is stored. - self.fileinfo: - A cache of lengths and hashes of stored metadata files. + self.versioninfo: + A cache of version numbers of stored metadata files. - Example: {'root.json': {'length': 13323, - 'hashes': {'sha256': dbfac345..}}, - ...} + Example: {'root.json': {'version': 128}, ...} self.mirrors: The repository mirrors from which metadata and targets are available. @@ -299,10 +297,11 @@ def __init__(self, updater_name, repository_mirrors): # Store the previously trusted/verified metadata. self.metadata['previous'] = {} - # Store the file information of all the metadata files. The dict keys are - # paths, the dict values fileinfo data. This information can help determine - # whether a metadata file has changed and so needs to be re-downloaded. - self.fileinfo = {} + # Store the version numbers of all metadata files. The dict keys are + # paths, the dict values versioninfo data. This information can help + # determine whether a metadata file has changed and so needs to be + # re-downloaded. + self.versioninfo = {} # Store the location of the client's metadata directory. self.metadata_directory = {} @@ -1522,8 +1521,8 @@ def _update_metadata_if_changed(self, metadata_role, # Simply return if the file has not changed, according to the metadata # about the uncompressed file provided by the referenced metadata. - if not self._fileinfo_has_changed(uncompressed_metadata_filename, - uncompressed_fileinfo): + if not self._versioninfo_has_changed(uncompressed_metadata_filename, + uncompressed_fileinfo): logger.info(repr(uncompressed_metadata_filename)+' up-to-date.') return @@ -1561,65 +1560,69 @@ def _update_metadata_if_changed(self, metadata_role, - def _fileinfo_has_changed(self, metadata_filename, new_fileinfo): + def _versioninfo_has_changed(self, metadata_filename, new_versioninfo): """ - Non-public method that determines whether the current fileinfo of - 'metadata_filename' differs from 'new_fileinfo'. The 'new_fileinfo' + Non-public method that determines whether the current versioninfo of + 'metadata_filename' differs from 'new_versioninfo'. The 'new_versioninfo' argument should be extracted from the latest copy of the metadata that references 'metadata_filename'. Example: 'root.json' would be referenced by 'snapshot.json'. - 'new_fileinfo' should only be 'None' if this is for updating 'root.json' - without having 'snapshot.json' available. + 'new_versioninfo' should only be 'None' if this is for updating + 'root.json' without having 'snapshot.json' available. metadadata_filename: The metadata filename for the role. For the 'root' role, 'metadata_filename' would be 'root.json'. - new_fileinfo: + new_versioninfo: A dict object representing the new file information for - 'metadata_filename'. 'new_fileinfo' may be 'None' when + 'metadata_filename'. 'new_versioninfo' may be 'None' when updating 'root' without having 'snapshot' available. This - dict conforms to 'tuf.formats.FILEINFO_SCHEMA' and has + dict conforms to 'tuf.formats.VERSIONINFO_SCHEMA' and has the form: - {'length': 23423 - 'hashes': {'sha256': adfbc32343..}} + + {'version': 288} None. - If there is no fileinfo currently loaded for 'metada_filename', + If there is no versioninfo currently loaded for 'metada_filename', try to load it. - Boolean. True if the fileinfo has changed, false otherwise. + Boolean. True if the versioninfo has increased, false otherwise. """ - - # If there is no fileinfo currently stored for 'metadata_filename', - # try to load the file, calculate the fileinfo, and store it. - if metadata_filename not in self.fileinfo: - self._update_fileinfo(metadata_filename) + + # If there is no versioninfo currently stored for 'metadata_filename', + # try to load the file, calculate the versioninfo, and store it. + if metadata_filename not in self.versioninfo: + self._update_versioninfo(metadata_filename) - # Return true if there is no fileinfo for 'metadata_filename'. - # 'metadata_filename' is not in the 'self.fileinfo' store + # Return true if there is no versioninfo for 'metadata_filename'. + # 'metadata_filename' is not in the 'self.versioninfo' store # and it doesn't exist in the 'current' metadata location. - if self.fileinfo[metadata_filename] is None: + if self.versioninfo[metadata_filename] is None: return True - current_fileinfo = self.fileinfo[metadata_filename] + current_versioninfo = self.versioninfo[metadata_filename] - if current_fileinfo['length'] != new_fileinfo['length']: + if new_versioninfo['version'] > current_versioninfo['version']: return True + + else: + return False # Now compare hashes. Note that the reason we can't just do a simple - # equality check on the fileinfo dicts is that we want to support the + # equality check on the versioninfo dicts is that we want to support the # case where the hash algorithms listed in the metadata have changed # without having that result in considering all files as needing to be # updated, or not all hash algorithms listed can be calculated on the # specific client. + """ for algorithm, hash_value in six.iteritems(new_fileinfo['hashes']): # We're only looking for a single match. This isn't a security # check, we just want to prevent unnecessary downloads. @@ -1628,18 +1631,18 @@ def _fileinfo_has_changed(self, metadata_filename, new_fileinfo): return False return True + """ - - def _update_fileinfo(self, metadata_filename): + def _update_versioninfo(self, metadata_filename): """ - Non-public method that updates the 'self.fileinfo' entry for the metadata - belonging to 'metadata_filename'. If the 'current' metadata for - 'metadata_filename' cannot be loaded, set its fileinfo' to 'None' to - signal that it is not in the 'self.fileinfo' AND it also doesn't exist + Non-public method that updates the 'self.versioninfo' entry for the + metadata belonging to 'metadata_filename'. If the current metadata for + 'metadata_filename' cannot be loaded, set its 'versioninfo' to 'None' to + signal that it is not in 'self.versioninfo' AND it also doesn't exist locally. @@ -1651,30 +1654,31 @@ def _update_fileinfo(self, metadata_filename): None. - The file details of 'metadata_filename' is calculated and - stored in 'self.fileinfo'. + The version number of 'metadata_filename' is calculated and + stored in its corresponding entry in 'self.versioninfo'. None. """ # In case we delayed loading the metadata and didn't do it in - # __init__ (such as with delegated metadata), then get the file + # __init__ (such as with delegated metadata), then get the version # info now. # Save the path to the current metadata file for 'metadata_filename'. current_filepath = os.path.join(self.metadata_directory['current'], metadata_filename) - # If the path is invalid, simply return and leave fileinfo unset. + # If the path is invalid, simply return and leave versioninfo unset. if not os.path.exists(current_filepath): - self.fileinfo[metadata_filename] = None + self.versioninfo[metadata_filename] = None return - # Extract the file information from the actual file and save it - # to the fileinfo store. - file_length, hashes = tuf.util.get_file_details(current_filepath) - metadata_fileinfo = tuf.formats.make_fileinfo(file_length, hashes) - self.fileinfo[metadata_filename] = metadata_fileinfo + # Extract the version information from the trusted snapshot role and save + # it to the fileinfo store. + trusted_versioninfo = \ + self.metadata['current']['snapshot']['meta'][metadata_filename] + + self.versioninfo[metadata_filename] = trusted_versioninfo From ed1f217022f38bc36f1d25b8e380bcea9f0db12c Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Thu, 15 Oct 2015 09:49:32 -0400 Subject: [PATCH 04/30] Implement changes for _update_metadata() --- tuf/__init__.py | 7 ++ tuf/client/updater.py | 192 +++++++++++++++++++++++++++++++----------- tuf/conf.py | 17 +++- tuf/formats.py | 53 ++++++++++-- tuf/repository_lib.py | 30 ++++--- 5 files changed, 227 insertions(+), 72 deletions(-) diff --git a/tuf/__init__.py b/tuf/__init__.py index 4add6e45..dd159764 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -106,6 +106,13 @@ def __str__(self): +class BadVersionNumberError(Error): + """Indicate an error after fetching metadata that contains an invalid version""" + + + + + class BadPasswordError(Error): """Indicate an error after encountering an invalid password.""" pass diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 4a341828..435efd54 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -602,20 +602,14 @@ def refresh(self, unsafely_update_root_if_necessary=True): # The timestamp role does not have signed metadata about it; otherwise we # would need an infinite regress of metadata. Therefore, we use some - # default, sane metadata about it. - DEFAULT_TIMESTAMP_FILEINFO = { - 'hashes': {}, - 'length': tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH - } + # default, sane length for its metadata. + DEFAULT_TIMESTAMP_FILELENGTH = tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH # The Root role may be updated without knowing its hash if top-level # metadata cannot be safely downloaded (e.g., keys may have been revoked, # thus requiring a new Root file that includes the updated keys) and # 'unsafely_update_root_if_necessary' is True. - DEFAULT_ROOT_FILEINFO = { - 'hashes': {}, - 'length': tuf.conf.DEFAULT_ROOT_REQUIRED_LENGTH - } + DEFAULT_ROOT_FILELENGTH = tuf.conf.DEFAULT_ROOT_REQUIRED_LENGTH # Update the top-level metadata. The _update_metadata_if_changed() and # _update_metadata() calls below do NOT perform an update if there @@ -640,7 +634,7 @@ def refresh(self, unsafely_update_root_if_necessary=True): message = \ 'Expired Root metadata was loaded from disk. Try to update it now.' logger.info(message) - self._update_metadata('root', DEFAULT_ROOT_FILEINFO) + self._update_metadata('root', DEFAULT_ROOT_FILELENGTH) # The caller explicitly requested not to unsafely fetch an expired Root. else: @@ -650,7 +644,7 @@ def refresh(self, unsafely_update_root_if_necessary=True): # Use default but sane information for timestamp metadata, and do not # require strict checks on its required length. try: - self._update_metadata('timestamp', DEFAULT_TIMESTAMP_FILEINFO) + self._update_metadata('timestamp', DEFAULT_TIMESTAMP_FILELENGTH) self._update_metadata_if_changed('snapshot', referenced_metadata='timestamp') self._update_metadata_if_changed('root') @@ -662,7 +656,7 @@ def refresh(self, unsafely_update_root_if_necessary=True): 'update the Root metadata.' logger.info(message) - self._update_metadata('root', DEFAULT_ROOT_FILEINFO) + self._update_metadata('root', DEFAULT_ROOT_FILELENGTH) self.refresh(unsafely_update_root_if_necessary=False) else: @@ -1045,6 +1039,127 @@ def unsafely_verify_compressed_metadata_file(metadata_file_object): + def _get_metadata_file(self, metadata_role, remote_filename, + upperbound_filelength, expected_version, compression): + """ + + Non-public method that tries downloading, up to a certain length, a + metadata or target file from a list of known mirrors. As soon as the first + valid copy of the file is found, the rest of the mirrors will be skipped. + + + filepath: + The relative metadata or target filepath. + + verify_file_function: + A callable function that expects a 'tuf.util.TempFile' file-like object + and raises an exception if the file is invalid. Target files and + uncompressed versions of metadata may be verified with + 'verify_file_function'. + + file_type: + Type of data needed for download, must correspond to one of the strings + in the list ['meta', 'target']. 'meta' for metadata file type or + 'target' for target file type. It should correspond to the + 'tuf.formats.NAME_SCHEMA' format. + + file_length: + The expected length, or upper bound, of the target or metadata file to + be downloaded. + + compression: + The name of the compression algorithm (e.g., 'gzip'), if the metadata + file is compressed. + + verify_compressed_file_function: + If compression is specified, in the case of metadata files, this + callable function may be set to perform verification of the compressed + version of the metadata file. Decompressed metadata is also verified. + + download_safely: + A boolean switch to toggle safe or unsafe download of the file. + + + tuf.NoWorkingMirrorError: + The metadata could not be fetched. This is raised only when all known + mirrors failed to provide a valid copy of the desired metadata file. + + + The file is downloaded from all known repository mirrors in the worst + case. If a valid copy of the file is found, it is stored in a temporary + file and returned. + + + A 'tuf.util.TempFile' file-like object containing the metadata or target. + """ + + file_mirrors = tuf.mirrors.get_list_of_mirrors('meta', remote_filename, + self.mirrors) + # file_mirror (URL): error (Exception) + file_mirror_errors = {} + file_object = None + + for file_mirror in file_mirrors: + try: + file_object = tuf.download.unsafe_download(file_mirror, + upperbound_filelength) + + if compression is not None: + logger.info('Decompressing ' + str(file_mirror)) + file_object.decompress_temp_file_object(compression) + + else: + logger.info('Not decompressing ' + str(file_mirror)) + + # Verify 'file_object' according to the callable function. + # 'file_object' is also verified if decompressed above (i.e., the + # uncompressed version). + + # Verify that the version number is the version that was requested. + current_version = \ + self.metadata['current'][metadata_role]['signed']['version'] + + metadata_signable = \ + tuf.util.load_json_string(file_object.read().decode('utf-8')) + + # If the version number is unspecified, ensure that the version number + # downloaded is greater than the currently trusted version number. + version_downloaded = metadata_signable['signed']['version'] + if expected_version is None: + if version_downloaded <= expected_version: + message = \ + 'Downloaded version number: ' + repr(version_downloaded) + '.' \ + ' Version number MUST be greater than: ' + repr(expected_version) + raise tuf.BadVersionNumberError(message) + + else: + if version_downloaded != expected_version: + message = \ + 'Downloaded version number: ' + repr(version_downloaded) + '.' \ + ' Version number MUST be: ' + repr(expected_version) + raise tuf.BadVersionNumberError(message) + + except Exception as exception: + # Remember the error from this mirror, and "reset" the target file. + logger.exception('Update failed from ' + file_mirror + '.') + file_mirror_errors[file_mirror] = exception + file_object = None + + else: + break + + if file_object: + return file_object + + else: + logger.exception('Failed to update {0} from all mirrors: {1}'.format( + filepath, file_mirror_errors)) + raise tuf.NoWorkingMirrorError(file_mirror_errors) + + + + + def _safely_get_metadata_file(self, metadata_role, metadata_filepath, uncompressed_fileinfo, compression=None, compressed_fileinfo=None): @@ -1233,8 +1348,8 @@ def _get_file(self, filepath, verify_file_function, file_type, - def _update_metadata(self, metadata_role, uncompressed_fileinfo, - compression=None, compressed_fileinfo=None): + def _update_metadata(self, metadata_role, upperbound_filelength, version=None, + compression=None): """ Non-public method that downloads, verifies, and 'installs' the metadata @@ -1306,45 +1421,27 @@ def _update_metadata(self, metadata_role, uncompressed_fileinfo, # # Some metadata (presently timestamp) will be downloaded "unsafely", in the # sense that we can only estimate its true length and know nothing about - # its hashes. This is because not all metadata will have other metadata + # its version. This is because not all metadata will have other metadata # for it; otherwise we will have an infinite regress of metadata signing # for each other. In this case, we will download the metadata up to the - # best length we can get for it, not check its hashes, but perform the rest - # of the checks (e.g signature verification). + # best length we can get for it, not request a specific version, but + # perform the rest of the checks (e.g., signature verification). # # Note also that we presently support decompression of only "safe" # metadata, but this is easily extend to "unsafe" metadata as well as # "safe" targets. + + remote_filename = metadata_filename - if metadata_role == 'timestamp': - metadata_file_object = \ - self._unsafely_get_metadata_file(metadata_role, metadata_filename, - uncompressed_fileinfo, - compression, compressed_fileinfo) + if self.consistent_snapshot: + filename_version = str(version) - elif metadata_role == 'root' and not len(uncompressed_fileinfo['hashes']): - metadata_file_object = \ - self._unsafely_get_metadata_file(metadata_role, metadata_filename, - uncompressed_fileinfo, - compression, compressed_fileinfo) + dirname, basename = os.path.split(remote_filename) + remote_filename = os.path.join(dirname, filename_digest + '.' + basename) - else: - remote_filename = metadata_filename - if self.consistent_snapshot: - if compression: - filename_digest = \ - random.choice(list(compressed_fileinfo['hashes'].values())) - - else: - filename_digest = \ - random.choice(list(uncompressed_fileinfo['hashes'].values())) - dirname, basename = os.path.split(remote_filename) - remote_filename = os.path.join(dirname, filename_digest+'.'+basename) - - metadata_file_object = \ - self._safely_get_metadata_file(metadata_role, remote_filename, - uncompressed_fileinfo, - compression, compressed_fileinfo) + metadata_file_object = \ + self._get_metadata_file(metadata_role, remote_filename, + upperbound_filelength, version, compression) # The metadata has been verified. Move the metadata file into place. # First, move the 'current' metadata file to the 'previous' directory @@ -1366,7 +1463,8 @@ def _update_metadata(self, metadata_role, uncompressed_fileinfo, # Next, move the verified updated metadata file to the 'current' directory. # Note that the 'move' method comes from tuf.util's TempFile class. # 'metadata_file_object' is an instance of tuf.util.TempFile. - metadata_signable = tuf.util.load_json_string(metadata_file_object.read().decode('utf-8')) + metadata_signable = \ + tuf.util.load_json_string(metadata_file_object.read().decode('utf-8')) if compression == 'gzip': current_uncompressed_filepath = \ os.path.join(self.metadata_directory['current'], @@ -1388,7 +1486,7 @@ def _update_metadata(self, metadata_role, uncompressed_fileinfo, # key and role info for the top-level roles if 'metadata_role' is root. # Rebuilding the the key and role info is required if the newly-installed # root metadata has revoked keys or updated any top-level role information. - logger.debug('Updated '+repr(current_filepath)+'.') + logger.debug('Updated ' + repr(current_filepath) + '.') self.metadata['previous'][metadata_role] = current_metadata_object self.metadata['current'][metadata_role] = updated_metadata_object self._update_fileinfo(uncompressed_metadata_filename) @@ -1472,7 +1570,7 @@ def _update_metadata_if_changed(self, metadata_role, raise tuf.RepositoryError(message) # The referenced metadata has been loaded. Extract the new - # fileinfo for 'metadata_role' from it. + # versioninfo for 'metadata_role' from it. else: message = repr(metadata_role)+' referenced in '+\ repr(referenced_metadata)+'. '+repr(metadata_role)+' may be updated.' diff --git a/tuf/conf.py b/tuf/conf.py index 1f347603..0421abe8 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -50,12 +50,21 @@ # default but sane upper bound for the number of bytes required to download it. DEFAULT_TIMESTAMP_REQUIRED_LENGTH = 16384 #bytes -# The Root role may be updated without knowing its hash if top-level metadata -# cannot be safely downloaded (e.g., keys may have been revoked, thus requiring -# a new Root file that includes the updated keys). Set a default upper bound -# for the maximum total bytes that may be downloaded for Root metadata. +# The Root role may be updated without knowing its version if top-level +# metadata cannot be safely downloaded (e.g., keys may have been revoked, thus +# requiring a new Root file that includes the updated keys). Set a default +# upper bound for the maximum total bytes that may be downloaded for Root +# metadata. DEFAULT_ROOT_REQUIRED_LENGTH = 512000 #bytes +# Set a default but sane upper bound for the number of bytes required to +# download Snapshot metadata. +DEFAULT_SNAPSHOT_REQUIRED_LENGTH = 2000000 #bytes + +# Set a default but sane upper bound for the number of bytes required to +# download Targets metadata. +DEFAULT_TARGETS_REQUIRED_LENGTH = 5000000 #bytes + # Set a timeout value in seconds (float) for non-blocking socket operations. SOCKET_TIMEOUT = 2 #seconds diff --git a/tuf/formats.py b/tuf/formats.py index b0f5dbce..ed498ad2 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -474,7 +474,7 @@ _type = SCHEMA.String('Timestamp'), version = METADATAVERSION_SCHEMA, expires = ISO8601_DATETIME_SCHEMA, - meta = FILEDICT_SCHEMA) + meta = VERSIONDICT_SCHEMA) # project.cfg file: stores information about the project in a json dictionary PROJECT_CFG_SCHEMA = SCHEMA.Object( @@ -562,11 +562,11 @@ def __getattr__(self, name): class TimestampFile(MetaFile): - def __init__(self, version, expires, filedict): + def __init__(self, version, expires, versiondict): self.info = {} self.info['version'] = version self.info['expires'] = expires - self.info['meta'] = filedict + self.info['meta'] = versiondict @staticmethod @@ -577,17 +577,17 @@ def from_metadata(object): version = object['version'] expires = object['expires'] - filedict = object['meta'] + versiondict = object['meta'] - return TimestampFile(version, expires, filedict) + return TimestampFile(version, expires, versiondict) @staticmethod - def make_metadata(version, expiration_date, filedict): + def make_metadata(version, expiration_date, versiondict): result = {'_type' : 'Timestamp'} result['version'] = version result['expires'] = expiration_date - result['meta'] = filedict + result['meta'] = versiondict # Is 'result' a Timestamp metadata file? # Raise 'tuf.FormatError' if not. @@ -1008,6 +1008,45 @@ def make_fileinfo(length, hashes, custom=None): + +def make_versioninfo(version_number): + """ + + Create a dictionary conformant to 'VERSIONINFO_SCHEMA'. + This dict describes both metadata and target files. + + + length: + An integer representing the size of the file. + + hashes: + A dict of hashes in 'HASHDICT_SCHEMA' format, which has the form: + {'sha256': 123df8a9b12, 'sha512': 324324dfc121, ...} + + + tuf.FormatError, if the 'VERSIONINFO_SCHEMA' to be returned + does not have the correct format. + + + If any of the arguments are incorrectly formatted, the dict + returned will be checked for formatting errors, and if found, + will raise a 'tuf.FormatError' exception. + + + A dictionary conformant to 'VERSIONINFO_SCHEMA', ile + information of a metadata or target file. + """ + + versioninfo = {'version' : version_number} + + # Raise 'tuf.FormatError' if 'versioninfo' is improperly formatted. + VERSIONINFO_SCHEMA.check_match(versioninfo) + + return versioninfo + + + + def make_role_metadata(keyids, threshold, name=None, paths=None, path_hash_prefixes=None): """ diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index b21c44df..0229d50a 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -179,20 +179,18 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, status = tuf.sig.get_signature_status(temp_signable, rolename) if len(status['good_sigs']) == 0: metadata['version'] = metadata['version'] + 1 - if rolename != 'snapshot': - roleinfo = tuf.roledb.get_roleinfo(rolename) - roleinfo['version'] = roleinfo['version'] + 1 - tuf.roledb.update_roleinfo(rolename, roleinfo) + roleinfo = tuf.roledb.get_roleinfo(rolename) + roleinfo['version'] = roleinfo['version'] + 1 + tuf.roledb.update_roleinfo(rolename, roleinfo) signable = sign_metadata(metadata, roleinfo['signing_keyids'], metadata_filename) # non-partial write() else: if tuf.sig.verify(signable, rolename) and not roleinfo['partial_loaded']: metadata['version'] = metadata['version'] + 1 - if rolename != 'snapshot': - roleinfo = tuf.roledb.get_roleinfo(rolename) - roleinfo['version'] = roleinfo['version'] + 1 - tuf.roledb.update_roleinfo(rolename, roleinfo) + roleinfo = tuf.roledb.get_roleinfo(rolename) + roleinfo['version'] = roleinfo['version'] + 1 + tuf.roledb.update_roleinfo(rolename, roleinfo) signable = sign_metadata(metadata, roleinfo['signing_keyids'], metadata_filename) @@ -1751,13 +1749,16 @@ def generate_timestamp_metadata(snapshot_filename, version, tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(expiration_date) tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) - # Retrieve the fileinfo of the snapshot metadata file. - # This file information contains hashes, file length, custom data, etc. - fileinfo = {} - fileinfo[SNAPSHOT_FILENAME] = get_metadata_fileinfo(snapshot_filename) + # Retrieve the versioninfo of the Snapshot metadata file. + versioninfo = {} + versioninfo[SNAPSHOT_FILENAME] = get_metadata_versioninfo('snapshot') - # Save the fileinfo of the compressed versions of 'timestamp.json' + # Save the versioninfo of the compressed versions of 'timestamp.json' # in 'fileinfo'. Log the files included in 'fileinfo'. + # TODO: Since version numbers are now stored, the version numbers of + # compressed roles do not change and can thus be excluded. Remove this + # after testing. + """ for file_extension in compressions: if not len(file_extension): continue @@ -1772,11 +1773,12 @@ def generate_timestamp_metadata(snapshot_filename, version, else: logger.info('Including fileinfo about ' + repr(compressed_filename)) fileinfo[SNAPSHOT_FILENAME + '.' + file_extension] = compressed_fileinfo + """ # Generate the timestamp metadata object. timestamp_metadata = tuf.formats.TimestampFile.make_metadata(version, expiration_date, - fileinfo) + versioninfo) return timestamp_metadata From 7bbdd87117c815b364098cf418019156d3e78def Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Thu, 15 Oct 2015 09:52:14 -0400 Subject: [PATCH 05/30] Update the unit test for updater.py after implementing versioninfo --- tests/test_updater.py | 83 ++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/tests/test_updater.py b/tests/test_updater.py index 40b005b0..1b82d5ec 100755 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -349,33 +349,40 @@ def test_1__rebuild_key_and_role_db(self): - def test_1__update_fileinfo(self): + def test_1__update_versioninfo(self): # Tests - # Verify that the 'self.fileinfo' dictionary is empty (its starts off empty - # and is only populated if _update_fileinfo() is called. - fileinfo_dict = self.repository_updater.fileinfo - self.assertEqual(len(fileinfo_dict), 0) + # Verify that the 'self.versioninfo' dictionary is empty (its starts off + # empty and is only populated if _update_versioninfo() is called. + versioninfo_dict = self.repository_updater.versioninfo + self.assertEqual(len(versioninfo_dict), 0) - # Load the fileinfo of the top-level root role. This populates the - # 'self.fileinfo' dictionary. - self.repository_updater._update_fileinfo('root.json') - self.assertEqual(len(fileinfo_dict), 1) - self.assertTrue(tuf.formats.FILEDICT_SCHEMA.matches(fileinfo_dict)) - root_filepath = os.path.join(self.client_metadata_current, 'root.json') - length, hashes = tuf.util.get_file_details(root_filepath) - root_fileinfo = tuf.formats.make_fileinfo(length, hashes) - self.assertTrue('root.json' in fileinfo_dict) - self.assertEqual(fileinfo_dict['root.json'], root_fileinfo) + # Load the versioninfo of the top-level Root role. This action populates + # the 'self.versioninfo' dictionary. + self.repository_updater._update_versioninfo('root.json') + self.assertEqual(len(versioninfo_dict), 1) + self.assertTrue(tuf.formats.VERSIONDICT_SCHEMA.matches(versioninfo_dict)) + + # The Snapshot role stores the version numbers of all the roles available + # on the repository. Load Snapshot to extract root's version number + # and compare it against the one loaded by 'self.repository_updater'. + snapshot_filepath = os.path.join(self.client_metadata_current, 'snapshot.json') + snapshot_signable = tuf.util.load_json_file(snapshot_filepath) + root_versioninfo = snapshot_signable['signed']['meta']['root.json'] + + # Verify that the manually loaded version number of root.json matches + # the one loaded by the updater object. + self.assertTrue('root.json' in versioninfo_dict) + self.assertEqual(versioninfo_dict['root.json'], root_versioninfo) - # Verify that 'self.fileinfo' is incremented if another role is updated. - self.repository_updater._update_fileinfo('targets.json') - self.assertEqual(len(fileinfo_dict), 2) + # Verify that 'self.versioninfo' is incremented if another role is updated. + self.repository_updater._update_versioninfo('targets.json') + self.assertEqual(len(versioninfo_dict), 2) - # Verify that 'self.fileinfo' is inremented if a non-existent role is - # requested, and has its fileinfo entry set to 'None'. - self.repository_updater._update_fileinfo('bad_role.json') - self.assertEqual(len(fileinfo_dict), 3) - self.assertEqual(fileinfo_dict['bad_role.json'], None) + # Verify that 'self.versioninfo' is inremented if a non-existent role is + # requested, and has its versioninfo entry set to 'None'. + self.repository_updater._update_versioninfo('bad_role.json') + self.assertEqual(len(versioninfo_dict), 3) + self.assertEqual(versioninfo_dict['bad_role.json'], None) @@ -458,24 +465,20 @@ def test_2__import_delegations(self): - def test_2__fileinfo_has_changed(self): - # Verify that the method returns 'False' if file info was not changed. - root_filepath = os.path.join(self.client_metadata_current, 'root.json') - length, hashes = tuf.util.get_file_details(root_filepath) - root_fileinfo = tuf.formats.make_fileinfo(length, hashes) - self.assertFalse(self.repository_updater._fileinfo_has_changed('root.json', - root_fileinfo)) + def test_2__versioninfo_has_changed(self): + # Verify that the method returns 'False' if a versioninfo was not changed. + snapshot_filepath = os.path.join(self.client_metadata_current, 'snapshot.json') + snapshot_signable = tuf.util.load_json_file(snapshot_filepath) + root_versioninfo = snapshot_signable['signed']['meta']['root.json'] + + self.assertFalse(self.repository_updater._versioninfo_has_changed('root.json', + root_versioninfo)) + + # Verify that the method returns 'True' if Root's version number changes. + root_versioninfo['version'] = 8 + self.assertTrue(self.repository_updater._versioninfo_has_changed('root.json', + root_versioninfo)) - # Verify that the method returns 'True' if length or hashes were changed. - new_length = 8 - new_root_fileinfo = tuf.formats.make_fileinfo(new_length, hashes) - self.assertTrue(self.repository_updater._fileinfo_has_changed('root.json', - new_root_fileinfo)) - # Hashes were changed. - new_hashes = {'sha256': self.random_string()} - new_root_fileinfo = tuf.formats.make_fileinfo(length, new_hashes) - self.assertTrue(self.repository_updater._fileinfo_has_changed('root.json', - new_root_fileinfo)) From d993ccf35ebf0acb1cb2db6a482d037c39f483cd Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Thu, 15 Oct 2015 09:53:54 -0400 Subject: [PATCH 06/30] Generate a repository for the unit tests that contains the changes to the snapshot role --- .../client/metadata/current/root.json | Bin 3756 -> 3756 bytes .../client/metadata/current/root.json.gz | Bin 2054 -> 2053 bytes .../client/metadata/current/snapshot.json | Bin 1531 -> 906 bytes .../client/metadata/current/snapshot.json.gz | Bin 800 -> 549 bytes .../client/metadata/current/targets.json | Bin 2014 -> 2014 bytes .../client/metadata/current/targets.json.gz | Bin 1237 -> 1239 bytes .../metadata/current/targets/role1.json | Bin 974 -> 974 bytes .../client/metadata/current/timestamp.json | Bin 1078 -> 817 bytes .../client/metadata/current/timestamp.json.gz | Bin 652 -> 530 bytes .../client/metadata/previous/root.json | Bin 3756 -> 3756 bytes .../client/metadata/previous/root.json.gz | Bin 2054 -> 2053 bytes .../client/metadata/previous/snapshot.json | Bin 1531 -> 906 bytes .../client/metadata/previous/snapshot.json.gz | Bin 800 -> 549 bytes .../client/metadata/previous/targets.json | Bin 2014 -> 2014 bytes .../client/metadata/previous/targets.json.gz | Bin 1237 -> 1239 bytes .../metadata/previous/targets/role1.json | Bin 974 -> 974 bytes .../client/metadata/previous/timestamp.json | Bin 1078 -> 817 bytes .../metadata/previous/timestamp.json.gz | Bin 652 -> 530 bytes .../repository/metadata.staged/root.json | Bin 3756 -> 3756 bytes .../repository/metadata.staged/root.json.gz | Bin 2054 -> 2053 bytes .../repository/metadata.staged/snapshot.json | Bin 1531 -> 906 bytes .../metadata.staged/snapshot.json.gz | Bin 800 -> 549 bytes .../repository/metadata.staged/targets.json | Bin 2014 -> 2014 bytes .../metadata.staged/targets.json.gz | Bin 1237 -> 1239 bytes .../metadata.staged/targets/role1.json | Bin 974 -> 974 bytes .../repository/metadata.staged/timestamp.json | Bin 1078 -> 817 bytes .../metadata.staged/timestamp.json.gz | Bin 652 -> 530 bytes .../repository/metadata/root.json | Bin 3756 -> 3756 bytes .../repository/metadata/root.json.gz | Bin 2054 -> 2053 bytes .../repository/metadata/snapshot.json | Bin 1531 -> 907 bytes .../repository/metadata/snapshot.json.gz | Bin 800 -> 549 bytes .../repository/metadata/targets.json | Bin 2014 -> 2014 bytes .../repository/metadata/targets.json.gz | Bin 1237 -> 1239 bytes .../repository/metadata/targets/role1.json | Bin 974 -> 974 bytes .../repository/metadata/timestamp.json | Bin 1078 -> 817 bytes .../repository/metadata/timestamp.json.gz | Bin 652 -> 530 bytes 36 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/repository_data/client/metadata/current/root.json b/tests/repository_data/client/metadata/current/root.json index f4c8ad4215bd5b6c6ed65c19f1b71eb5214574e8..1f79ec7140a51b4b5f9026a51f1ac7f695c17d22 100644 GIT binary patch delta 528 zcmWlW$&DC52m~d|Pp7bQ3czrNGhAtG1S>g^{W(F8|Ikzy-yh!}f4)AoSi>VaEkqnl zyL0Qp76t982y+n`NjkBvM%a=!73<(7)nKB66l}XLP^UNPtX?r!Ro^;`2FW7q*rtW0{bkz5i*|49I L)qlT!{QmV12H%m; delta 528 zcmWkr$&DC52$P~b)50n(a1Kxo$I!-Buu=-iFAZk)hrk%y{Qda*@#pI^4WZM*F{%JX z=|Um|9;8|%rz27`)(Dx#gQPhTV`jYN>M+7Ie5ryVR1RA!n%#s9CTia!Gne1;20;%G z^E(Qc5+ln9_1ZV4N7EXks$ zYi0s=;LdsE){o1Ja7>u1$=g5gq!K-4DdD=&h2FexFmi|uS=DDxm(|vhCBG<&u#5{|+)%AGn( diff --git a/tests/repository_data/client/metadata/current/root.json.gz b/tests/repository_data/client/metadata/current/root.json.gz index 655ba429bd4da8a4219271745e1e650d76e64024..2b282a01ed791f9a004cca540da47ccce78b23f3 100644 GIT binary patch literal 2053 zcmV+g2>SOQiwFRyxD!?a|IJs~j^j2KeebU*)T@aXpaCAx7Hh+#q?#EJ5f6BX7>YHb6d*`4$Mx#hsErHcYAnR!%7$<}nqL3#nrwqY} zD5)sa9+Ylhff3Tc;~GYu#VWFfGHZmj1ab^9rjSZ(1SCQcY)!!cbFdV8n>f8X1O=0hUD?BEcBKC<2jH`-4z1x!}D5w z3F0(feBI;am1qlr6AvexD9@7+x>Oizc_F`u{5g7+`(IbRyk-}*kM+l@?>|1ZrXTcQhSOR%sK0cY%je)a?FB(C*j4+r zO>){Mu`h0*Rvk54)#{*L9r5bNe0|e#&&{e>oZ83j;)dyY(3!@AUOBw4(|zNdPxjfv zn|I$)*_vcoADC`%sy!O`kdRb8=AA)V`sO@ZZmyzLKAK}(RRv3jpGjzrvx z&>1rARxb4o6W(UEmpQqu`pCRbwhR&7AI%?ORf_J-uk07H)4L?2$~xtc(Ryc$+N8L8 z4O8a+?Bd<|44$@+_T#}sYu{guXUlMQ>dR$$ZI}0AmbQl3hFM-?^>u1(e44j&u}HGh zZM?5%_v3MKz}sxUVi%K;&IQ4GIl_bahHceFS5~{aUuFw?;Qh<7n66V6##!3&v*sC8 zH*vNl8yeQoTcMApC39Hs^alN4&%r_7%jzhp+>`kxZUazRp;N0~xW89(V&md%nFQSl z9rs#~RmqML&TqZ*bhkX@#rRTIcKeoA{mXro1&ItMj&6yTk+0jZx?88-=&#F40u~V> z6>_D-`I2A?MaWuLN&tWqawQ_R;QMC??gS(dQ7_A z3=HF2;OImHQE$abt9HaLO?U;~=Tz)3XhG(q)}6okU6CA)aI{e45-VJ~h#U(Ggt|1C|@Bq^_=leC=~w(z*CI{7s19NC+w(_nh&ojq`? zCD_l}>vX)T@LS7{x6`Jpf8x#3#U3xzxz;{f75KN=+n^Q&<$#pIrsiF0$yupWZMJJG zKG@MgyiS+PB54Lv4=dp`&m@d5b(#+PJ1VyU>8H)z8P)Jx)|FG;93HTjBj$uG zhs&R*%l!#g|<%?h9G%Dn1QDuz%FdeL}x*q`c;;ohIkDeYcMc1uPbow3}4 zdb5+%{Qa_b-h`((mYetX15B>;RF&E7ql}hKx{7)jG+`U&q41~iIRYKr-DZ>B9PVwL zO;mr>=*Fwi7h}}irj6d76c-uD))uVO>0ffIPhX|=njiP(w`MCpKKr%bv=T?Zyng&o zism!Uk6%T1-nve3d)$BYlkj(9|6Sww?|^)BQ~0NVoMpZ-=i(1=e6!vEIUMo96lZzJ j{s_spI>diK@~dF3CNJV+`eowtr_cWcs^#wQtPTJGJiHwJ literal 2054 zcmV+h2>JIPiwFQaf!kC9|IJs~uH!ZmeebWhxUWudxrs~Oyvnk?TVBUOKvJSD%a&!y zn-uiF5AW^Hf&nIzOb;+IWI?E+ONUj5r^-KndU-A4T`I4)JStv)c=^YtmzSUI_vJN- z$~d%^S1LGFlrqgQl~`d$7>oc0+7KNvDXC@=2ptSCaw1AJq7)!T6=N#G%ptGeeZ)IO z*Zuh;;B;1>&FbHeXS1&^i{RZZ83bA~!J$wJK;obXNC*U^5Gex@N0bm1hD<{sCBq>^ zjtqqe8DrQe?r;zij6xhCB`^>Up_pk0G8Rc71x847LrEA~SU5BS0UhEHLu11C6%bPd zpd&d)7PW%}hC~U?8ITS}QgUXrV%Ra**jUO$N@}c#LK1PxZB7J1hEpk|6Gn<)2DC7U z7|11eq_i*40-BK8;5L^=bA@2Y2*yeyu6Yzn2O*0Kae@^S5_7FNA;NB(Au0@aLPYEe zM!16@BG7hbS>JT6- z31m^o?Dn)$0uU2vO9ZVYvcFN#5eN%gAX_>?Q3zy60LIdJwPg7G>E*}ozLGL}XUjVo zUw^umS@c1j^ZEKgnpb^Ji@3N(>Gh`~m07X3jvro(JQmT1KYC{I7fQb(OV;lJv{wMY z490GL}IIm+UJP!SZ_y8?h?2WTXXc5Gs7@@!qXwA&*>YsM`h zBgK9R^bz(Kz^CRee`>!W*#(w){dk|5VPD?7CPE;Qx5YFiezXs3-X)rC)3^TK zi~_nYyYoR;%nloWz*mdz97g+R5Z9@Q&IPHTi-8Ya!wxwhs8gA^5tO^j0`8q{+Fw0K zz?-jEu}7ljI(Sq*Y3|LWadL6X?YMj7@-p9DCL51Sa~;tkUlo~kontm^zd5s zNxiX6KZ?^Hw*IwVuOGGj{C@xZMclqwKL7u;Vm;_=WYn6B^Zlfl9*}t06pZ(=>egCk zM?8zbo@m;y-I_Zl@NRRIx%1czQPiF78FKV+vMhtT6#a)=JFcWZxFwU?Hsz4frawhJ zr?}%*RORmChM#-^FZ;4rmP~Yx!_9QD4i=Z8TvzvA^(+=?XPoVr;SJW@rpCmVWiJ=2 zB)dGO$7c3CpI0Zc&yE{*ixS7bA=s=YWVGC|y}Iez=FoJjY-LV-csm#KZOVc;OMN$M zUqO8rXM1Nyg9dsl^f~It95(&IXy_d|ILT*KpCq+svfRZz0BRd_=`;&_yxKDp7jNst z>(A(P&?z?+J4?8H46gIT`ji*bTU9$8J6a8I&rRkf(wo_IOSBGM-GlYRHpP>_PAdsm z=r~d#+vBV**>-?JWQ^e)0NXIR5}_4hO(IA`tM}R%33=!UX-P#iaQ+6gx^(c<4+o-C z&ieffjN^xA(}_l+*@=@*<4kOs$Od$mR2*+;`gSOH$M%o z7(5yYakJhwoo;IU(J|BgyiK_y-V9wG$x2-ty|YoC`&hh<8lhK>oXXoZ@U4+tmAW(* zho0i210BWNbiFQ;wkP$t7R>WZ!uZyt>1cSMa_>3Aw0*du26@Z+a<1Fs6TwMx?FCX* zX<~+()kxYrq=Q4cTk|__t6{&9xpd72m(AnR-;hTtgHhx(GK}zgD5Aw^xH5CJy9Q{` zI4|4JduOptJO71RN#Z!ficm;2rgjptY{sy!vAAC}8g7L5{4X7{hrJ7~iw6zNuDO|t##!%NA!FG|1$)cTvW>498I_fr0 zVBHJWeZ3b|w|47##i=ZB)i9^KGaBBvsXLxcAM5rQlYZ}by5v-dDmlzn4<>@QDc`_?%lkRg6cf#v5n`} zJ5|+m>nCuai@SSIo+Un&r*@4m4pm;aB^6^RM}u&>I2|v|a(r|bOG^9qianBvuQQe# z&}{oj!#%DC*IjUlW4Y`0N-(?AOI>D<_cmI!=_VWy(1tyj2g04l*AV!mzt3ieB|Mrq zo2lWX)sHuUE2gNuPg{edQ{1E{JA1HA=YL79K7FOuYkodPzYSaY`D0%DjVp2Z%ioXx zNz?p@^W#_2UH5hp+@Fu{^CbKo*?%`U{yQMwJQV&ZAXl01qHFO7IKEl#{~V6^6ctx_ k%Kiw+w<^ScK=LbL?onRE=k$x?^QX`M1kFh{a;y#j03PZ1RsaA1 diff --git a/tests/repository_data/client/metadata/current/snapshot.json b/tests/repository_data/client/metadata/current/snapshot.json index d528363e3c87d3e72f9ebd5ce2ffa7bb487f4c5e..99f5fa0e8c89e5d73de3bb3104b68732484e3d17 100644 GIT binary patch literal 906 zcmb7?L5>_Z3`O^Iibkztn-oQf^3FFf(pfnO0+OQaN#f~2cPFr6%-zH4u@_z$22=%< z#QOaFdSC7i+vWcKgI@nV$7Oqc`2Fs1_>#|I`#V10&)nJA7Xpw73mcIlS}l=p0}x{H zqRSa%4PC2LQJh|fdzbDQ*0*17dLQHZ;dB%H_4wv^eDmV%@%Z&99qf&I5i2gmCM&f7 zD`?$EEE^HCsdt~9icCgzg8(jlNSY%k5*2DCRSyZ6Jo_{-reP}5b}QVG!#ldyuF@ge zoD3CG3uc6(^sZb7Q(yzAWX^`D7&^UaDME9mjJRX~Jc&491ew)2G(cmh4ufH>WQ}@N za*d`T5`D32ljT_42G`(7Yi4alp#eFex-PER+v2(Aid4>$iIt&@!bP}oGjJ=EBc-L% zMkZZ4bJ1{Y)TvcnxW`k4cC%DV85o(N6T9UniboDT$xyrNTu=u^nKq?s(=jQA3>=X+ zZr&_o)(VsvQTCLU2bebwIUlab&OpXFP}tILz|u2??o`^e^dy=Qav1})Vj;{>NrrW9 zqafC`#$MxINJ?8)mrva;IN)4%_*3y1xADa(ei-bJ+fi^X2}wfjg&9&$M3k{64OipQ(Ly UKK&E;gFp5W_YU8;eY$)4ANNr8Hvj+t literal 1531 zcmb7^(Qafl42JLf6r*#^E_Q6k$=>Z7Aj%a8Av(GU{6$5G>77A zXkSx+#{hGw91v_WCoxcW_D<7kF9W<1_>$J6iISMlDcoa(6%dI&97pNq1DL6J>u8x{ zX@xbrMru^|QR;{Vvyd$YlU(f$Ct6qHSr{|kUZCnE1T4HdgkYZSR5Gu1lwRxNz21T; zQ);XE2M3}6sK_|fN!Z)$y-})wAkjsA4Qka)Wx2#$Z3a)6A|>m`1vL8*)?6V$OD!un zr~*TUf|=9;Tbc|CuC3X!hRZQbywClvc%?VOk=AiqlI8A~DMnV83{K0|Tvo!U8kd?! zC7o^Nh^3Qi@5ZpCWFB#&7Hid_#nsg`g+wZBKv{tcPmU#h_FX+{C3Q|0E4E(e=<2{k z&|plKwN}&tomB;qMGJV0c0_I;?D2d&U9RA4@yFx+ zGqzg+f;WKqd2!Jf^ZE4f_sU^A+WB}z|B8JkJ0Gn`-LqkV zrKMLBGD?F>?^=5j(9)%O1t2uvW4c*S%lCEid}mICXfG&By7GV6KE%V_<(P3vrS3b= z8~rvHfr&UT$i=rVzN^?=}D|9E!nH|ZrNPcR4Q5L==>$QeW z+D8Vfkh{wo1F;d}j$i8|$JJ^Q2{uQ9xb|5=ZDp^|p)0bBUr>g51^+ed>!;I)z}MRb zIjlxw$^Hna+3zi+O;1ahJm%H%C37}J#n9#k?4zEn=HrTHn;{yN;7LK3Ju5!)pc>j-WJa_S0r<`OsouL z6fVMzn}J)Q94RH0Hqz}ZVBtz}4b3q*x zW!jXkO~<4dGH^t`xOua*Su0R_M0uyQEMUGkk=G7Qg$<1cSsLm7Ko_AqY0-o8G?9+A_dK3zYZ zZ};GA@z>MCOKi82A$SXz>qmfh_x00DQ|3-zd5+uVe7=46`Fei*2RUwk#^w5O2XF_T nUm|(a%g4A~|4;Atm-Fv||8e5*dg|YW=fm^gi|g!#iUR-uwUrN1 literal 800 zcmV+*1K<1~iwFQaf!kC9|CN;AZe%qK#_#(SqjTL|?AVTzz1ufHlq(QIcH*>!rL8&x zVp;Xw@tKxC5~@^=&KW6A#{Tl<7k{}pY{%Q{J3IaQ7{~3y;rolj;Y)sp?dSM>J2SG$ zSO`F^NfReBMCTIu>i~@eMzJ*n+UmAe;bu4o_c65Tz;N9z&-s3d)6M-k;Wt+wudY76 z`}*qYWhxcy@v@>h6kkL8ngToqm`mk=V3RqCfx5GInpS%m;FZ9av>r{A#C%TS9wV%P zNc7=2N;eh$M=Y3yY%!Q*v^Sh+U5RI5%zXO>RVN`};ng7o zb8V-Rb*-cHS{Lv8EtoQ;wwix%APRtrj6+|P|# zZ_YWM$65Nuiqt(R7Fb$(H6f!kxb&{ICjl*8nr8q)a~#vndRo56$#cw{2+?-V_;mSP z=u_NXpKdZQsnmRTjP`>#c@O+FJ&zkh{wo1F;d}j$i8|+tq3k2{uQ9xb|K_ZRK5`Ls#T2enlDP z5&T!#SC99f0^e>MWV0HLCGST-&HLU$+Vr%9$+K^ah=4LHlA`S0b&MD~wQ#l=oBWEn euFtMryv%LC#pChztO+kN{PZV9HXS(o1pojj8=U6= diff --git a/tests/repository_data/client/metadata/current/targets.json b/tests/repository_data/client/metadata/current/targets.json index b1e493ae253ef61fafda4b55a3074ec794d15abd..4cf4c3534b5837a5b21e7142933678f12e7f7bf7 100644 GIT binary patch delta 527 zcmWlW*^LlD3_~Rl-`h%#$<+oOOpa+|8vu!3=x1vod`606Kj-V?>*LSwPYpXl>L)~a zs6qT7g-ZfDl7hchM(Ya0EE+Aix4Y~j{ zkKSf;d=g(eUw%hSuB#MFm&R08^E3_z z)y@jNjd)J3fZLwRWV^~ffKF;f3{t^LGW!Cx5M}WU3S$czcHIemarFLoe#5aor)BOiA!X!&a{I(xd4$6!hV=VH5G0OUT-JP zxSIn>tatpg9A6ebUsIEoH#H@-be}pPh1M)=*wZO1yNCA*xeG4ZQ8Q2-E+1lkM9yuK zPl28>2*;s5TCuU2jD=28j|T_o)h7z~c$A#N0?q?5gT<3N5P^Em&Jfh0?R*(A-gZ$~ z@UaBJ4)4onIld0T+@^}DPTiIn6N?4Jkk&i0Nd}Y}IjS3e2CZ>g&*f&gvbCIPs&9?wz*$I{u+jz^CH&r*0uezLveP5G||&OL*^l}v67&Rs8n8z2DvMib6Au=m;N~_mA>OsDnG$y6mg~O zh>lV^&lJ}#1*<)aETv=&AyiT0ZDia90aQgIY)Hvb6Qy;)wFpF(KkoERpcqbf3On7b z+s!^*cDrv|h+w;}tQL|fBdnAhSR~R2%B+!|P(zs_CPl?jw^funt!zwzMeVqP5F+&? z_^3_bR8mCcLS$a4=pz@B07ODV;v~q#Z8Tca#1u<}tF-mcQRYcZCTK~F1fn^YjyuPr za6(cCOafT2Jfd>0rFY&@Cj^aFTJ#g0Pu_4Tg7?x`7YPPKGDs{TG(!_5+MPEVat{+8OvXb%?!2_xpwmb6pI~i$`QseaK&hvFWv-q ztUdf7bp;VoC{F})41r_5LlB-BBpn6mHV{4Dz&qEyL8#BH0vj&M(u95_>i~i^-BFQYgYzO&%>wD znO&1gc2zsgvWt`K%4FO9lR*Q&YuR%nUU&LApER#ddx@~lY&m$y*Zo>gY|aY^!+Nvn zRo>Dcmh-f3f=ar!XrEM1QStSqUNx3in{{m7&4=sz#@&6ZI&uBH{*XV5cwAJPD}VfW zc6VIAJngS;e0FhDtZ&Y=n~k#5d6@Bfd{(u_nd|qPxh^&5yLAA8f&#>pUT)Nru2>c4*<}zGQ7-2`;k#*gp*H%;(blX=iPn3uh2Ibhw9wfqdR+t8V$=uF`c}tw5`n78O$fh zvbWkUD<$>__i->gDuZ-gl0$-yQi6NI2F47><*LkZ3q0f$iEv0}6W| zmzGg#G55Ho!C9P}1f-&nJdT*)to=7g_UJ7!`<$#`zKz&EWv@rCKLAm^H-X*<004$p BWpw}m literal 1237 zcmV;`1SRNxib9B}H3lXm=m?;Li_CfBi3cYI zMKGM}=m`c(Wt4_ea1^AlLho zAf8a^nGQk<%>o7|lbK0ux!~4HmXHypHB}0Qx5$NvoLdnzx~M3RT2Lu~@IaJ@6xBJw zJhNQ-D3BrJ(K9ImTJVy1=BaRm0wPOkbqXl}Q-q`>01u#o6-F=yF)>jz;YRJKWC3h2 zf+QHFBZ?+)59otaiK7FAgo<)Ooen9*S~{R!X>OUangLJeFeqX~5Y)&-%BXY@AZVUK zN|ub2U{VShj7Am-)mUay1}}({hH)k|n$l2|X%SX9!BRIS9;3+WAdR023*iYbVO$awIybNmCuVhMZU z?w8jv{b^UJ&R1?a@ul6T@>6S^HM^C|L9N|9sk9n*yTymR(`?q7U$bs)^!Pk}oSfMW zsbyD<(=59<$*xRR?w^cW>31W0ZiVaKFy~M0tJ6UwtT$hc9`enwF%aAH0{ytzZabBC z^oP|VZrZ4#ZY;Ye_0#0|`cki3tE=rMwC@(<&3)_czEgjKVcvYmpG7z>YVEZ<{(5$I z+`K#;u5Vm+aZ_w=&NR#?+3CX1c{4n#M(YfQ!*;HV?OKZu`DD3V-km?>e%P9Y>EpNg z=$1C!B0Ote8%(p8apS66&juaozHQG3H>5TY7yYk;*=g%!>C(C%>z-ZJ)mg(ruW@Jlt=U~7vmRV5py{8|bU44~-KUp%m-mPH?L%JR zd^S0su}M~3^omB4Zm&<)$Km1<=EoCsYs8bUxl;Pi#}0lwpM`$U-12#6)M<5w+m~@- zG1@fi*;Q71$alqQbnCy@tGX}l>(Sx;x}RK&+4L|rD+jGO@Q=kJz0yvxSm8UdOu|p| z|3jdAM_+zK$l+-x(PG~x$0a^`5bGR@@h{+PyiVvlg2_eymP%P}tBom!58Nwu?;&ZM~(Wtxm>t{7{WBvB9msew}+OnDm+m!^?O z(@QA9DMw*+S{Ee1uYsnRkBV{f%jvti?})HpiTnp7A(=q8TiiegMRm+)Zizd^D`Z-v>{WQ~h5WBZc59=-kp;3Eze-Uk2x$y;=C diff --git a/tests/repository_data/client/metadata/current/targets/role1.json b/tests/repository_data/client/metadata/current/targets/role1.json index 263d9895c5408c48daeb5831acbcb7ea4d84983b..7dfe6f87faada4a8fb0fbbf655732985da4da3f0 100644 GIT binary patch delta 528 zcmWlV$%z;+3`I!@Ogpl6A5)6fHl~7s^pZZNra0(g8QEX&eSds^{Q3F}SuyI#9h2fT zY}ze5dNs~Lr*jl@k6^ne?g}_)ly^g~x=*DsFf!$z7@#oakhcj&KtdN9hJ(tIpq64v1jK^U4_ou{mU}x K@7K@YKmGw*=9K6F delta 528 zcmWlW*=-mw3`5bPK-SS=9scYnI;=G^L4fYkJu*{O_{%a#kAD) z@pf8I4~2drt&3%Y`F+7Jll7_Hm?%IoMolyUP}O3yl&3Dw=-OkBq->dTZWAnNZpNF6 z(anQOl|`xRTgInng`3s;M2KNAd^WvdDR(%OvKzYx+%hi_$T%aGW1Q?IAfsaDXNO>m zqvuQk5YcSu%)Q!hf?l&DDm1b_hy;@x9iC%0Zq#Gbenj}w)jNG9+ZH=w*|A+$T*Pvg zyoWB-eWbZE~Pr1n{Qs5*10(=~WNSxmE6 zv8)<(#o{2m8cj3bUS0HV<8*U>5q!UYyWhXPez)KMZDoS9Q6X+p5zWdNP*cvPTXOPzwfmgBT%e3IEoT_FFQ>TdfLKaPh>0nV^yirw=W+y5KSSI>U{)al!~ literal 1078 zcma)*%Z}Ve3`O_z6%DOpgRBRO^v-_}xV=gc1SE?!-A?8KcRPq<%)f`NaTZww2v8CP zmUMaR^61;$VY{5~ANBg>b6mD}hoA2bhi`cuw%_9Gd1htP)_ZGSX7ftSg(gsl*cecz zNeo9=qfIKTS=B~T!H~!5ZK~S#!>;!b*VEH3_~H2eczpln$K&z)RyufXG^B^s=ma%| zW*~Mc?u%yCPGd@K2HDc3_S)Kj&Q^65gHm+GQs|YOtf-Gc#C?z}MO(#(PhB_^XCrFT z#w2% z17Xe#D;0Gp&DBj&1jI`b8L>*U!3;FYf{8*9EQN~8EKA#z&QhrjAAu>be4(q)I!QsD zwTMO?b#jju+AI1jeX$a!dWAy30U73ykjR-3bTrOfN^>I%2l8ozvZy!AsPbWkX`pEh zQI6_?v-(Y-Fn%+z|PAeu&FeAD(w|w)pe;{L1ZC zpu!u#Tpt16?dzx4ml8bvE|j{D`g}P(UEhAXJU#wXJ!~hvoOb?q_Os*Vq@~lX<+L34 zs7yRuJctY4fUxBm&`?h{hU9>i7f35Xk-=EWGXs^5^>(T}*Zvb6w%_CN{(4GX%#q!8 zUN`>j>+Stt|I1p2n%#WH3PA{Ao(XKQkpzatYy$zoyG}{UCo`@5KPHliTKg|+46r-P T+wN}rBR*fwdt7PxpA=*op1l`Ui;Z351ZHICMd6SKS3s zRr~K`x8;E396U~D#>%{Imi4gv{%FTv&v969mM@!Sd7Wojf5ywMPh>SJg{b9D4Xa45 zJ=IWk=9If@aDz0Mdd*@JHL8lqL3kAEX1-os_#WbT|8ybzw7uPKZ?8XYx9_9r;EX5Y zA{o&vwFY^_)E$fpwbBS?7tvx;iaF|ZlY>h%&w?C98}x!jqIBt1lIJKf2^*nQ(ZpI^ zH9{J3wWM6Nj+C@fVZdb3qvbYNL`}VFqTD-Y;@&!T2(ap5G@7g7$g`Q6QU)Mq%9fh} zU@@@30>!09RA^bHiH{6vic3{X0DAUC5EU8-$n&x?SVG?tl5OgI}E96~G zMcFDr$AQyX9(jymy(P_5@-PZ;9w-s_m2JIJc0S!d>gBgDao#@cf86c&-}2sXzr@$m%*v*% z_tw13=9QQWO`s65F`!J77>=+;n^ah{s*R+AA&=GDRJHBy#(Rj%@#zNq{_yc|`1tO- z!{Kcy6}&bY(!*+Wf|{Tih+T^NqFJ@mm{OZTwsfhzwl<)%Rb9oP6kV|tdL<_->SGXb zALL5WR`KCe7Y@bQh?=x9$z7zc#pcN90E58RTwzuLkphH=`5GZr3r}w1aBb%v6($tn zY+M`ITvkkIIbuY#%9iIqm@~~vq7J3Gx+#i)cqt+yR%tevfks&{Q3!&iP;r@MX`9kn zDz)JwFagUKy85h>6x3OZXw*?B_h_NLqR-M7D{;~*6ao%NGlzsk&V-<&ar#o48(BDz zPa~8?yL>yO>2m9R1cihuZW7IMP=31h^?U_;&d8)te|4d!j(Ozy};AeQX_Tw z63=o)ZgWw=(ivLQq^6B&t z=Zii(-;%S%AJ3=P+-?ObyaUYZ6TpYt^~39?44%G$lKZI7=i}4m{pa)3wuHb2YHJXWsiwAMRYancS8Z^{18$)uy$_u2Gph#n^iUluY?#rj`H4 mL^4rp|0Rt9ZpwCVYuoSf<$Su?m6Bg}FMk1xp&!jQ1ONaJLs;(s diff --git a/tests/repository_data/client/metadata/previous/root.json b/tests/repository_data/client/metadata/previous/root.json index f4c8ad4215bd5b6c6ed65c19f1b71eb5214574e8..1f79ec7140a51b4b5f9026a51f1ac7f695c17d22 100644 GIT binary patch delta 528 zcmWlW$&DC52m~d|Pp7bQ3czrNGhAtG1S>g^{W(F8|Ikzy-yh!}f4)AoSi>VaEkqnl zyL0Qp76t982y+n`NjkBvM%a=!73<(7)nKB66l}XLP^UNPtX?r!Ro^;`2FW7q*rtW0{bkz5i*|49I L)qlT!{QmV12H%m; delta 528 zcmWkr$&DC52$P~b)50n(a1Kxo$I!-Buu=-iFAZk)hrk%y{Qda*@#pI^4WZM*F{%JX z=|Um|9;8|%rz27`)(Dx#gQPhTV`jYN>M+7Ie5ryVR1RA!n%#s9CTia!Gne1;20;%G z^E(Qc5+ln9_1ZV4N7EXks$ zYi0s=;LdsE){o1Ja7>u1$=g5gq!K-4DdD=&h2FexFmi|uS=DDxm(|vhCBG<&u#5{|+)%AGn( diff --git a/tests/repository_data/client/metadata/previous/root.json.gz b/tests/repository_data/client/metadata/previous/root.json.gz index 655ba429bd4da8a4219271745e1e650d76e64024..2b282a01ed791f9a004cca540da47ccce78b23f3 100644 GIT binary patch literal 2053 zcmV+g2>SOQiwFRyxD!?a|IJs~j^j2KeebU*)T@aXpaCAx7Hh+#q?#EJ5f6BX7>YHb6d*`4$Mx#hsErHcYAnR!%7$<}nqL3#nrwqY} zD5)sa9+Ylhff3Tc;~GYu#VWFfGHZmj1ab^9rjSZ(1SCQcY)!!cbFdV8n>f8X1O=0hUD?BEcBKC<2jH`-4z1x!}D5w z3F0(feBI;am1qlr6AvexD9@7+x>Oizc_F`u{5g7+`(IbRyk-}*kM+l@?>|1ZrXTcQhSOR%sK0cY%je)a?FB(C*j4+r zO>){Mu`h0*Rvk54)#{*L9r5bNe0|e#&&{e>oZ83j;)dyY(3!@AUOBw4(|zNdPxjfv zn|I$)*_vcoADC`%sy!O`kdRb8=AA)V`sO@ZZmyzLKAK}(RRv3jpGjzrvx z&>1rARxb4o6W(UEmpQqu`pCRbwhR&7AI%?ORf_J-uk07H)4L?2$~xtc(Ryc$+N8L8 z4O8a+?Bd<|44$@+_T#}sYu{guXUlMQ>dR$$ZI}0AmbQl3hFM-?^>u1(e44j&u}HGh zZM?5%_v3MKz}sxUVi%K;&IQ4GIl_bahHceFS5~{aUuFw?;Qh<7n66V6##!3&v*sC8 zH*vNl8yeQoTcMApC39Hs^alN4&%r_7%jzhp+>`kxZUazRp;N0~xW89(V&md%nFQSl z9rs#~RmqML&TqZ*bhkX@#rRTIcKeoA{mXro1&ItMj&6yTk+0jZx?88-=&#F40u~V> z6>_D-`I2A?MaWuLN&tWqawQ_R;QMC??gS(dQ7_A z3=HF2;OImHQE$abt9HaLO?U;~=Tz)3XhG(q)}6okU6CA)aI{e45-VJ~h#U(Ggt|1C|@Bq^_=leC=~w(z*CI{7s19NC+w(_nh&ojq`? zCD_l}>vX)T@LS7{x6`Jpf8x#3#U3xzxz;{f75KN=+n^Q&<$#pIrsiF0$yupWZMJJG zKG@MgyiS+PB54Lv4=dp`&m@d5b(#+PJ1VyU>8H)z8P)Jx)|FG;93HTjBj$uG zhs&R*%l!#g|<%?h9G%Dn1QDuz%FdeL}x*q`c;;ohIkDeYcMc1uPbow3}4 zdb5+%{Qa_b-h`((mYetX15B>;RF&E7ql}hKx{7)jG+`U&q41~iIRYKr-DZ>B9PVwL zO;mr>=*Fwi7h}}irj6d76c-uD))uVO>0ffIPhX|=njiP(w`MCpKKr%bv=T?Zyng&o zism!Uk6%T1-nve3d)$BYlkj(9|6Sww?|^)BQ~0NVoMpZ-=i(1=e6!vEIUMo96lZzJ j{s_spI>diK@~dF3CNJV+`eowtr_cWcs^#wQtPTJGJiHwJ literal 2054 zcmV+h2>JIPiwFQaf!kC9|IJs~uH!ZmeebWhxUWudxrs~Oyvnk?TVBUOKvJSD%a&!y zn-uiF5AW^Hf&nIzOb;+IWI?E+ONUj5r^-KndU-A4T`I4)JStv)c=^YtmzSUI_vJN- z$~d%^S1LGFlrqgQl~`d$7>oc0+7KNvDXC@=2ptSCaw1AJq7)!T6=N#G%ptGeeZ)IO z*Zuh;;B;1>&FbHeXS1&^i{RZZ83bA~!J$wJK;obXNC*U^5Gex@N0bm1hD<{sCBq>^ zjtqqe8DrQe?r;zij6xhCB`^>Up_pk0G8Rc71x847LrEA~SU5BS0UhEHLu11C6%bPd zpd&d)7PW%}hC~U?8ITS}QgUXrV%Ra**jUO$N@}c#LK1PxZB7J1hEpk|6Gn<)2DC7U z7|11eq_i*40-BK8;5L^=bA@2Y2*yeyu6Yzn2O*0Kae@^S5_7FNA;NB(Au0@aLPYEe zM!16@BG7hbS>JT6- z31m^o?Dn)$0uU2vO9ZVYvcFN#5eN%gAX_>?Q3zy60LIdJwPg7G>E*}ozLGL}XUjVo zUw^umS@c1j^ZEKgnpb^Ji@3N(>Gh`~m07X3jvro(JQmT1KYC{I7fQb(OV;lJv{wMY z490GL}IIm+UJP!SZ_y8?h?2WTXXc5Gs7@@!qXwA&*>YsM`h zBgK9R^bz(Kz^CRee`>!W*#(w){dk|5VPD?7CPE;Qx5YFiezXs3-X)rC)3^TK zi~_nYyYoR;%nloWz*mdz97g+R5Z9@Q&IPHTi-8Ya!wxwhs8gA^5tO^j0`8q{+Fw0K zz?-jEu}7ljI(Sq*Y3|LWadL6X?YMj7@-p9DCL51Sa~;tkUlo~kontm^zd5s zNxiX6KZ?^Hw*IwVuOGGj{C@xZMclqwKL7u;Vm;_=WYn6B^Zlfl9*}t06pZ(=>egCk zM?8zbo@m;y-I_Zl@NRRIx%1czQPiF78FKV+vMhtT6#a)=JFcWZxFwU?Hsz4frawhJ zr?}%*RORmChM#-^FZ;4rmP~Yx!_9QD4i=Z8TvzvA^(+=?XPoVr;SJW@rpCmVWiJ=2 zB)dGO$7c3CpI0Zc&yE{*ixS7bA=s=YWVGC|y}Iez=FoJjY-LV-csm#KZOVc;OMN$M zUqO8rXM1Nyg9dsl^f~It95(&IXy_d|ILT*KpCq+svfRZz0BRd_=`;&_yxKDp7jNst z>(A(P&?z?+J4?8H46gIT`ji*bTU9$8J6a8I&rRkf(wo_IOSBGM-GlYRHpP>_PAdsm z=r~d#+vBV**>-?JWQ^e)0NXIR5}_4hO(IA`tM}R%33=!UX-P#iaQ+6gx^(c<4+o-C z&ieffjN^xA(}_l+*@=@*<4kOs$Od$mR2*+;`gSOH$M%o z7(5yYakJhwoo;IU(J|BgyiK_y-V9wG$x2-ty|YoC`&hh<8lhK>oXXoZ@U4+tmAW(* zho0i210BWNbiFQ;wkP$t7R>WZ!uZyt>1cSMa_>3Aw0*du26@Z+a<1Fs6TwMx?FCX* zX<~+()kxYrq=Q4cTk|__t6{&9xpd72m(AnR-;hTtgHhx(GK}zgD5Aw^xH5CJy9Q{` zI4|4JduOptJO71RN#Z!ficm;2rgjptY{sy!vAAC}8g7L5{4X7{hrJ7~iw6zNuDO|t##!%NA!FG|1$)cTvW>498I_fr0 zVBHJWeZ3b|w|47##i=ZB)i9^KGaBBvsXLxcAM5rQlYZ}by5v-dDmlzn4<>@QDc`_?%lkRg6cf#v5n`} zJ5|+m>nCuai@SSIo+Un&r*@4m4pm;aB^6^RM}u&>I2|v|a(r|bOG^9qianBvuQQe# z&}{oj!#%DC*IjUlW4Y`0N-(?AOI>D<_cmI!=_VWy(1tyj2g04l*AV!mzt3ieB|Mrq zo2lWX)sHuUE2gNuPg{edQ{1E{JA1HA=YL79K7FOuYkodPzYSaY`D0%DjVp2Z%ioXx zNz?p@^W#_2UH5hp+@Fu{^CbKo*?%`U{yQMwJQV&ZAXl01qHFO7IKEl#{~V6^6ctx_ k%Kiw+w<^ScK=LbL?onRE=k$x?^QX`M1kFh{a;y#j03PZ1RsaA1 diff --git a/tests/repository_data/client/metadata/previous/snapshot.json b/tests/repository_data/client/metadata/previous/snapshot.json index d528363e3c87d3e72f9ebd5ce2ffa7bb487f4c5e..99f5fa0e8c89e5d73de3bb3104b68732484e3d17 100644 GIT binary patch literal 906 zcmb7?L5>_Z3`O^Iibkztn-oQf^3FFf(pfnO0+OQaN#f~2cPFr6%-zH4u@_z$22=%< z#QOaFdSC7i+vWcKgI@nV$7Oqc`2Fs1_>#|I`#V10&)nJA7Xpw73mcIlS}l=p0}x{H zqRSa%4PC2LQJh|fdzbDQ*0*17dLQHZ;dB%H_4wv^eDmV%@%Z&99qf&I5i2gmCM&f7 zD`?$EEE^HCsdt~9icCgzg8(jlNSY%k5*2DCRSyZ6Jo_{-reP}5b}QVG!#ldyuF@ge zoD3CG3uc6(^sZb7Q(yzAWX^`D7&^UaDME9mjJRX~Jc&491ew)2G(cmh4ufH>WQ}@N za*d`T5`D32ljT_42G`(7Yi4alp#eFex-PER+v2(Aid4>$iIt&@!bP}oGjJ=EBc-L% zMkZZ4bJ1{Y)TvcnxW`k4cC%DV85o(N6T9UniboDT$xyrNTu=u^nKq?s(=jQA3>=X+ zZr&_o)(VsvQTCLU2bebwIUlab&OpXFP}tILz|u2??o`^e^dy=Qav1})Vj;{>NrrW9 zqafC`#$MxINJ?8)mrva;IN)4%_*3y1xADa(ei-bJ+fi^X2}wfjg&9&$M3k{64OipQ(Ly UKK&E;gFp5W_YU8;eY$)4ANNr8Hvj+t literal 1531 zcmb7^(Qafl42JLf6r*#^E_Q6k$=>Z7Aj%a8Av(GU{6$5G>77A zXkSx+#{hGw91v_WCoxcW_D<7kF9W<1_>$J6iISMlDcoa(6%dI&97pNq1DL6J>u8x{ zX@xbrMru^|QR;{Vvyd$YlU(f$Ct6qHSr{|kUZCnE1T4HdgkYZSR5Gu1lwRxNz21T; zQ);XE2M3}6sK_|fN!Z)$y-})wAkjsA4Qka)Wx2#$Z3a)6A|>m`1vL8*)?6V$OD!un zr~*TUf|=9;Tbc|CuC3X!hRZQbywClvc%?VOk=AiqlI8A~DMnV83{K0|Tvo!U8kd?! zC7o^Nh^3Qi@5ZpCWFB#&7Hid_#nsg`g+wZBKv{tcPmU#h_FX+{C3Q|0E4E(e=<2{k z&|plKwN}&tomB;qMGJV0c0_I;?D2d&U9RA4@yFx+ zGqzg+f;WKqd2!Jf^ZE4f_sU^A+WB}z|B8JkJ0Gn`-LqkV zrKMLBGD?F>?^=5j(9)%O1t2uvW4c*S%lCEid}mICXfG&By7GV6KE%V_<(P3vrS3b= z8~rvHfr&UT$i=rVzN^?=}D|9E!nH|ZrNPcR4Q5L==>$QeW z+D8Vfkh{wo1F;d}j$i8|$JJ^Q2{uQ9xb|5=ZDp^|p)0bBUr>g51^+ed>!;I)z}MRb zIjlxw$^Hna+3zi+O;1ahJm%H%C37}J#n9#k?4zEn=HrTHn;{yN;7LK3Ju5!)pc>j-WJa_S0r<`OsouL z6fVMzn}J)Q94RH0Hqz}ZVBtz}4b3q*x zW!jXkO~<4dGH^t`xOua*Su0R_M0uyQEMUGkk=G7Qg$<1cSsLm7Ko_AqY0-o8G?9+A_dK3zYZ zZ};GA@z>MCOKi82A$SXz>qmfh_x00DQ|3-zd5+uVe7=46`Fei*2RUwk#^w5O2XF_T nUm|(a%g4A~|4;Atm-Fv||8e5*dg|YW=fm^gi|g!#iUR-uwUrN1 literal 800 zcmV+*1K<1~iwFQaf!kC9|CN;AZe%qK#_#(SqjTL|?AVTzz1ufHlq(QIcH*>!rL8&x zVp;Xw@tKxC5~@^=&KW6A#{Tl<7k{}pY{%Q{J3IaQ7{~3y;rolj;Y)sp?dSM>J2SG$ zSO`F^NfReBMCTIu>i~@eMzJ*n+UmAe;bu4o_c65Tz;N9z&-s3d)6M-k;Wt+wudY76 z`}*qYWhxcy@v@>h6kkL8ngToqm`mk=V3RqCfx5GInpS%m;FZ9av>r{A#C%TS9wV%P zNc7=2N;eh$M=Y3yY%!Q*v^Sh+U5RI5%zXO>RVN`};ng7o zb8V-Rb*-cHS{Lv8EtoQ;wwix%APRtrj6+|P|# zZ_YWM$65Nuiqt(R7Fb$(H6f!kxb&{ICjl*8nr8q)a~#vndRo56$#cw{2+?-V_;mSP z=u_NXpKdZQsnmRTjP`>#c@O+FJ&zkh{wo1F;d}j$i8|+tq3k2{uQ9xb|K_ZRK5`Ls#T2enlDP z5&T!#SC99f0^e>MWV0HLCGST-&HLU$+Vr%9$+K^ah=4LHlA`S0b&MD~wQ#l=oBWEn euFtMryv%LC#pChztO+kN{PZV9HXS(o1pojj8=U6= diff --git a/tests/repository_data/client/metadata/previous/targets.json b/tests/repository_data/client/metadata/previous/targets.json index b1e493ae253ef61fafda4b55a3074ec794d15abd..4cf4c3534b5837a5b21e7142933678f12e7f7bf7 100644 GIT binary patch delta 527 zcmWlW*^LlD3_~Rl-`h%#$<+oOOpa+|8vu!3=x1vod`606Kj-V?>*LSwPYpXl>L)~a zs6qT7g-ZfDl7hchM(Ya0EE+Aix4Y~j{ zkKSf;d=g(eUw%hSuB#MFm&R08^E3_z z)y@jNjd)J3fZLwRWV^~ffKF;f3{t^LGW!Cx5M}WU3S$czcHIemarFLoe#5aor)BOiA!X!&a{I(xd4$6!hV=VH5G0OUT-JP zxSIn>tatpg9A6ebUsIEoH#H@-be}pPh1M)=*wZO1yNCA*xeG4ZQ8Q2-E+1lkM9yuK zPl28>2*;s5TCuU2jD=28j|T_o)h7z~c$A#N0?q?5gT<3N5P^Em&Jfh0?R*(A-gZ$~ z@UaBJ4)4onIld0T+@^}DPTiIn6N?4Jkk&i0Nd}Y}IjS3e2CZ>g&*f&gvbCIPs&9?wz*$I{u+jz^CH&r*0uezLveP5G||&OL*^l}v67&Rs8n8z2DvMib6Au=m;N~_mA>OsDnG$y6mg~O zh>lV^&lJ}#1*<)aETv=&AyiT0ZDia90aQgIY)Hvb6Qy;)wFpF(KkoERpcqbf3On7b z+s!^*cDrv|h+w;}tQL|fBdnAhSR~R2%B+!|P(zs_CPl?jw^funt!zwzMeVqP5F+&? z_^3_bR8mCcLS$a4=pz@B07ODV;v~q#Z8Tca#1u<}tF-mcQRYcZCTK~F1fn^YjyuPr za6(cCOafT2Jfd>0rFY&@Cj^aFTJ#g0Pu_4Tg7?x`7YPPKGDs{TG(!_5+MPEVat{+8OvXb%?!2_xpwmb6pI~i$`QseaK&hvFWv-q ztUdf7bp;VoC{F})41r_5LlB-BBpn6mHV{4Dz&qEyL8#BH0vj&M(u95_>i~i^-BFQYgYzO&%>wD znO&1gc2zsgvWt`K%4FO9lR*Q&YuR%nUU&LApER#ddx@~lY&m$y*Zo>gY|aY^!+Nvn zRo>Dcmh-f3f=ar!XrEM1QStSqUNx3in{{m7&4=sz#@&6ZI&uBH{*XV5cwAJPD}VfW zc6VIAJngS;e0FhDtZ&Y=n~k#5d6@Bfd{(u_nd|qPxh^&5yLAA8f&#>pUT)Nru2>c4*<}zGQ7-2`;k#*gp*H%;(blX=iPn3uh2Ibhw9wfqdR+t8V$=uF`c}tw5`n78O$fh zvbWkUD<$>__i->gDuZ-gl0$-yQi6NI2F47><*LkZ3q0f$iEv0}6W| zmzGg#G55Ho!C9P}1f-&nJdT*)to=7g_UJ7!`<$#`zKz&EWv@rCKLAm^H-X*<004$p BWpw}m literal 1237 zcmV;`1SRNxib9B}H3lXm=m?;Li_CfBi3cYI zMKGM}=m`c(Wt4_ea1^AlLho zAf8a^nGQk<%>o7|lbK0ux!~4HmXHypHB}0Qx5$NvoLdnzx~M3RT2Lu~@IaJ@6xBJw zJhNQ-D3BrJ(K9ImTJVy1=BaRm0wPOkbqXl}Q-q`>01u#o6-F=yF)>jz;YRJKWC3h2 zf+QHFBZ?+)59otaiK7FAgo<)Ooen9*S~{R!X>OUangLJeFeqX~5Y)&-%BXY@AZVUK zN|ub2U{VShj7Am-)mUay1}}({hH)k|n$l2|X%SX9!BRIS9;3+WAdR023*iYbVO$awIybNmCuVhMZU z?w8jv{b^UJ&R1?a@ul6T@>6S^HM^C|L9N|9sk9n*yTymR(`?q7U$bs)^!Pk}oSfMW zsbyD<(=59<$*xRR?w^cW>31W0ZiVaKFy~M0tJ6UwtT$hc9`enwF%aAH0{ytzZabBC z^oP|VZrZ4#ZY;Ye_0#0|`cki3tE=rMwC@(<&3)_czEgjKVcvYmpG7z>YVEZ<{(5$I z+`K#;u5Vm+aZ_w=&NR#?+3CX1c{4n#M(YfQ!*;HV?OKZu`DD3V-km?>e%P9Y>EpNg z=$1C!B0Ote8%(p8apS66&juaozHQG3H>5TY7yYk;*=g%!>C(C%>z-ZJ)mg(ruW@Jlt=U~7vmRV5py{8|bU44~-KUp%m-mPH?L%JR zd^S0su}M~3^omB4Zm&<)$Km1<=EoCsYs8bUxl;Pi#}0lwpM`$U-12#6)M<5w+m~@- zG1@fi*;Q71$alqQbnCy@tGX}l>(Sx;x}RK&+4L|rD+jGO@Q=kJz0yvxSm8UdOu|p| z|3jdAM_+zK$l+-x(PG~x$0a^`5bGR@@h{+PyiVvlg2_eymP%P}tBom!58Nwu?;&ZM~(Wtxm>t{7{WBvB9msew}+OnDm+m!^?O z(@QA9DMw*+S{Ee1uYsnRkBV{f%jvti?})HpiTnp7A(=q8TiiegMRm+)Zizd^D`Z-v>{WQ~h5WBZc59=-kp;3Eze-Uk2x$y;=C diff --git a/tests/repository_data/client/metadata/previous/targets/role1.json b/tests/repository_data/client/metadata/previous/targets/role1.json index 263d9895c5408c48daeb5831acbcb7ea4d84983b..7dfe6f87faada4a8fb0fbbf655732985da4da3f0 100644 GIT binary patch delta 528 zcmWlV$%z;+3`I!@Ogpl6A5)6fHl~7s^pZZNra0(g8QEX&eSds^{Q3F}SuyI#9h2fT zY}ze5dNs~Lr*jl@k6^ne?g}_)ly^g~x=*DsFf!$z7@#oakhcj&KtdN9hJ(tIpq64v1jK^U4_ou{mU}x K@7K@YKmGw*=9K6F delta 528 zcmWlW*=-mw3`5bPK-SS=9scYnI;=G^L4fYkJu*{O_{%a#kAD) z@pf8I4~2drt&3%Y`F+7Jll7_Hm?%IoMolyUP}O3yl&3Dw=-OkBq->dTZWAnNZpNF6 z(anQOl|`xRTgInng`3s;M2KNAd^WvdDR(%OvKzYx+%hi_$T%aGW1Q?IAfsaDXNO>m zqvuQk5YcSu%)Q!hf?l&DDm1b_hy;@x9iC%0Zq#Gbenj}w)jNG9+ZH=w*|A+$T*Pvg zyoWB-eWbZE~Pr1n{Qs5*10(=~WNSxmE6 zv8)<(#o{2m8cj3bUS0HV<8*U>5q!UYyWhXPez)KMZDoS9Q6X+p5zWdNP*cvPTXOPzwfmgBT%e3IEoT_FFQ>TdfLKaPh>0nV^yirw=W+y5KSSI>U{)al!~ literal 1078 zcma)*%Z}Ve3`O_z6%DOpgRBRO^v-_}xV=gc1SE?!-A?8KcRPq<%)f`NaTZww2v8CP zmUMaR^61;$VY{5~ANBg>b6mD}hoA2bhi`cuw%_9Gd1htP)_ZGSX7ftSg(gsl*cecz zNeo9=qfIKTS=B~T!H~!5ZK~S#!>;!b*VEH3_~H2eczpln$K&z)RyufXG^B^s=ma%| zW*~Mc?u%yCPGd@K2HDc3_S)Kj&Q^65gHm+GQs|YOtf-Gc#C?z}MO(#(PhB_^XCrFT z#w2% z17Xe#D;0Gp&DBj&1jI`b8L>*U!3;FYf{8*9EQN~8EKA#z&QhrjAAu>be4(q)I!QsD zwTMO?b#jju+AI1jeX$a!dWAy30U73ykjR-3bTrOfN^>I%2l8ozvZy!AsPbWkX`pEh zQI6_?v-(Y-Fn%+z|PAeu&FeAD(w|w)pe;{L1ZC zpu!u#Tpt16?dzx4ml8bvE|j{D`g}P(UEhAXJU#wXJ!~hvoOb?q_Os*Vq@~lX<+L34 zs7yRuJctY4fUxBm&`?h{hU9>i7f35Xk-=EWGXs^5^>(T}*Zvb6w%_CN{(4GX%#q!8 zUN`>j>+Stt|I1p2n%#WH3PA{Ao(XKQkpzatYy$zoyG}{UCo`@5KPHliTKg|+46r-P T+wN}rBR*fwdt7PxpA=*op1l`Ui;Z351ZHICMd6SKS3s zRr~K`x8;E396U~D#>%{Imi4gv{%FTv&v969mM@!Sd7Wojf5ywMPh>SJg{b9D4Xa45 zJ=IWk=9If@aDz0Mdd*@JHL8lqL3kAEX1-os_#WbT|8ybzw7uPKZ?8XYx9_9r;EX5Y zA{o&vwFY^_)E$fpwbBS?7tvx;iaF|ZlY>h%&w?C98}x!jqIBt1lIJKf2^*nQ(ZpI^ zH9{J3wWM6Nj+C@fVZdb3qvbYNL`}VFqTD-Y;@&!T2(ap5G@7g7$g`Q6QU)Mq%9fh} zU@@@30>!09RA^bHiH{6vic3{X0DAUC5EU8-$n&x?SVG?tl5OgI}E96~G zMcFDr$AQyX9(jymy(P_5@-PZ;9w-s_m2JIJc0S!d>gBgDao#@cf86c&-}2sXzr@$m%*v*% z_tw13=9QQWO`s65F`!J77>=+;n^ah{s*R+AA&=GDRJHBy#(Rj%@#zNq{_yc|`1tO- z!{Kcy6}&bY(!*+Wf|{Tih+T^NqFJ@mm{OZTwsfhzwl<)%Rb9oP6kV|tdL<_->SGXb zALL5WR`KCe7Y@bQh?=x9$z7zc#pcN90E58RTwzuLkphH=`5GZr3r}w1aBb%v6($tn zY+M`ITvkkIIbuY#%9iIqm@~~vq7J3Gx+#i)cqt+yR%tevfks&{Q3!&iP;r@MX`9kn zDz)JwFagUKy85h>6x3OZXw*?B_h_NLqR-M7D{;~*6ao%NGlzsk&V-<&ar#o48(BDz zPa~8?yL>yO>2m9R1cihuZW7IMP=31h^?U_;&d8)te|4d!j(Ozy};AeQX_Tw z63=o)ZgWw=(ivLQq^6B&t z=Zii(-;%S%AJ3=P+-?ObyaUYZ6TpYt^~39?44%G$lKZI7=i}4m{pa)3wuHb2YHJXWsiwAMRYancS8Z^{18$)uy$_u2Gph#n^iUluY?#rj`H4 mL^4rp|0Rt9ZpwCVYuoSf<$Su?m6Bg}FMk1xp&!jQ1ONaJLs;(s diff --git a/tests/repository_data/repository/metadata.staged/root.json b/tests/repository_data/repository/metadata.staged/root.json index f4c8ad4215bd5b6c6ed65c19f1b71eb5214574e8..1f79ec7140a51b4b5f9026a51f1ac7f695c17d22 100644 GIT binary patch delta 528 zcmWlW$&DC52m~d|Pp7bQ3czrNGhAtG1S>g^{W(F8|Ikzy-yh!}f4)AoSi>VaEkqnl zyL0Qp76t982y+n`NjkBvM%a=!73<(7)nKB66l}XLP^UNPtX?r!Ro^;`2FW7q*rtW0{bkz5i*|49I L)qlT!{QmV12H%m; delta 528 zcmWkr$&DC52$P~b)50n(a1Kxo$I!-Buu=-iFAZk)hrk%y{Qda*@#pI^4WZM*F{%JX z=|Um|9;8|%rz27`)(Dx#gQPhTV`jYN>M+7Ie5ryVR1RA!n%#s9CTia!Gne1;20;%G z^E(Qc5+ln9_1ZV4N7EXks$ zYi0s=;LdsE){o1Ja7>u1$=g5gq!K-4DdD=&h2FexFmi|uS=DDxm(|vhCBG<&u#5{|+)%AGn( diff --git a/tests/repository_data/repository/metadata.staged/root.json.gz b/tests/repository_data/repository/metadata.staged/root.json.gz index 655ba429bd4da8a4219271745e1e650d76e64024..2b282a01ed791f9a004cca540da47ccce78b23f3 100644 GIT binary patch literal 2053 zcmV+g2>SOQiwFRyxD!?a|IJs~j^j2KeebU*)T@aXpaCAx7Hh+#q?#EJ5f6BX7>YHb6d*`4$Mx#hsErHcYAnR!%7$<}nqL3#nrwqY} zD5)sa9+Ylhff3Tc;~GYu#VWFfGHZmj1ab^9rjSZ(1SCQcY)!!cbFdV8n>f8X1O=0hUD?BEcBKC<2jH`-4z1x!}D5w z3F0(feBI;am1qlr6AvexD9@7+x>Oizc_F`u{5g7+`(IbRyk-}*kM+l@?>|1ZrXTcQhSOR%sK0cY%je)a?FB(C*j4+r zO>){Mu`h0*Rvk54)#{*L9r5bNe0|e#&&{e>oZ83j;)dyY(3!@AUOBw4(|zNdPxjfv zn|I$)*_vcoADC`%sy!O`kdRb8=AA)V`sO@ZZmyzLKAK}(RRv3jpGjzrvx z&>1rARxb4o6W(UEmpQqu`pCRbwhR&7AI%?ORf_J-uk07H)4L?2$~xtc(Ryc$+N8L8 z4O8a+?Bd<|44$@+_T#}sYu{guXUlMQ>dR$$ZI}0AmbQl3hFM-?^>u1(e44j&u}HGh zZM?5%_v3MKz}sxUVi%K;&IQ4GIl_bahHceFS5~{aUuFw?;Qh<7n66V6##!3&v*sC8 zH*vNl8yeQoTcMApC39Hs^alN4&%r_7%jzhp+>`kxZUazRp;N0~xW89(V&md%nFQSl z9rs#~RmqML&TqZ*bhkX@#rRTIcKeoA{mXro1&ItMj&6yTk+0jZx?88-=&#F40u~V> z6>_D-`I2A?MaWuLN&tWqawQ_R;QMC??gS(dQ7_A z3=HF2;OImHQE$abt9HaLO?U;~=Tz)3XhG(q)}6okU6CA)aI{e45-VJ~h#U(Ggt|1C|@Bq^_=leC=~w(z*CI{7s19NC+w(_nh&ojq`? zCD_l}>vX)T@LS7{x6`Jpf8x#3#U3xzxz;{f75KN=+n^Q&<$#pIrsiF0$yupWZMJJG zKG@MgyiS+PB54Lv4=dp`&m@d5b(#+PJ1VyU>8H)z8P)Jx)|FG;93HTjBj$uG zhs&R*%l!#g|<%?h9G%Dn1QDuz%FdeL}x*q`c;;ohIkDeYcMc1uPbow3}4 zdb5+%{Qa_b-h`((mYetX15B>;RF&E7ql}hKx{7)jG+`U&q41~iIRYKr-DZ>B9PVwL zO;mr>=*Fwi7h}}irj6d76c-uD))uVO>0ffIPhX|=njiP(w`MCpKKr%bv=T?Zyng&o zism!Uk6%T1-nve3d)$BYlkj(9|6Sww?|^)BQ~0NVoMpZ-=i(1=e6!vEIUMo96lZzJ j{s_spI>diK@~dF3CNJV+`eowtr_cWcs^#wQtPTJGJiHwJ literal 2054 zcmV+h2>JIPiwFQaf!kC9|IJs~uH!ZmeebWhxUWudxrs~Oyvnk?TVBUOKvJSD%a&!y zn-uiF5AW^Hf&nIzOb;+IWI?E+ONUj5r^-KndU-A4T`I4)JStv)c=^YtmzSUI_vJN- z$~d%^S1LGFlrqgQl~`d$7>oc0+7KNvDXC@=2ptSCaw1AJq7)!T6=N#G%ptGeeZ)IO z*Zuh;;B;1>&FbHeXS1&^i{RZZ83bA~!J$wJK;obXNC*U^5Gex@N0bm1hD<{sCBq>^ zjtqqe8DrQe?r;zij6xhCB`^>Up_pk0G8Rc71x847LrEA~SU5BS0UhEHLu11C6%bPd zpd&d)7PW%}hC~U?8ITS}QgUXrV%Ra**jUO$N@}c#LK1PxZB7J1hEpk|6Gn<)2DC7U z7|11eq_i*40-BK8;5L^=bA@2Y2*yeyu6Yzn2O*0Kae@^S5_7FNA;NB(Au0@aLPYEe zM!16@BG7hbS>JT6- z31m^o?Dn)$0uU2vO9ZVYvcFN#5eN%gAX_>?Q3zy60LIdJwPg7G>E*}ozLGL}XUjVo zUw^umS@c1j^ZEKgnpb^Ji@3N(>Gh`~m07X3jvro(JQmT1KYC{I7fQb(OV;lJv{wMY z490GL}IIm+UJP!SZ_y8?h?2WTXXc5Gs7@@!qXwA&*>YsM`h zBgK9R^bz(Kz^CRee`>!W*#(w){dk|5VPD?7CPE;Qx5YFiezXs3-X)rC)3^TK zi~_nYyYoR;%nloWz*mdz97g+R5Z9@Q&IPHTi-8Ya!wxwhs8gA^5tO^j0`8q{+Fw0K zz?-jEu}7ljI(Sq*Y3|LWadL6X?YMj7@-p9DCL51Sa~;tkUlo~kontm^zd5s zNxiX6KZ?^Hw*IwVuOGGj{C@xZMclqwKL7u;Vm;_=WYn6B^Zlfl9*}t06pZ(=>egCk zM?8zbo@m;y-I_Zl@NRRIx%1czQPiF78FKV+vMhtT6#a)=JFcWZxFwU?Hsz4frawhJ zr?}%*RORmChM#-^FZ;4rmP~Yx!_9QD4i=Z8TvzvA^(+=?XPoVr;SJW@rpCmVWiJ=2 zB)dGO$7c3CpI0Zc&yE{*ixS7bA=s=YWVGC|y}Iez=FoJjY-LV-csm#KZOVc;OMN$M zUqO8rXM1Nyg9dsl^f~It95(&IXy_d|ILT*KpCq+svfRZz0BRd_=`;&_yxKDp7jNst z>(A(P&?z?+J4?8H46gIT`ji*bTU9$8J6a8I&rRkf(wo_IOSBGM-GlYRHpP>_PAdsm z=r~d#+vBV**>-?JWQ^e)0NXIR5}_4hO(IA`tM}R%33=!UX-P#iaQ+6gx^(c<4+o-C z&ieffjN^xA(}_l+*@=@*<4kOs$Od$mR2*+;`gSOH$M%o z7(5yYakJhwoo;IU(J|BgyiK_y-V9wG$x2-ty|YoC`&hh<8lhK>oXXoZ@U4+tmAW(* zho0i210BWNbiFQ;wkP$t7R>WZ!uZyt>1cSMa_>3Aw0*du26@Z+a<1Fs6TwMx?FCX* zX<~+()kxYrq=Q4cTk|__t6{&9xpd72m(AnR-;hTtgHhx(GK}zgD5Aw^xH5CJy9Q{` zI4|4JduOptJO71RN#Z!ficm;2rgjptY{sy!vAAC}8g7L5{4X7{hrJ7~iw6zNuDO|t##!%NA!FG|1$)cTvW>498I_fr0 zVBHJWeZ3b|w|47##i=ZB)i9^KGaBBvsXLxcAM5rQlYZ}by5v-dDmlzn4<>@QDc`_?%lkRg6cf#v5n`} zJ5|+m>nCuai@SSIo+Un&r*@4m4pm;aB^6^RM}u&>I2|v|a(r|bOG^9qianBvuQQe# z&}{oj!#%DC*IjUlW4Y`0N-(?AOI>D<_cmI!=_VWy(1tyj2g04l*AV!mzt3ieB|Mrq zo2lWX)sHuUE2gNuPg{edQ{1E{JA1HA=YL79K7FOuYkodPzYSaY`D0%DjVp2Z%ioXx zNz?p@^W#_2UH5hp+@Fu{^CbKo*?%`U{yQMwJQV&ZAXl01qHFO7IKEl#{~V6^6ctx_ k%Kiw+w<^ScK=LbL?onRE=k$x?^QX`M1kFh{a;y#j03PZ1RsaA1 diff --git a/tests/repository_data/repository/metadata.staged/snapshot.json b/tests/repository_data/repository/metadata.staged/snapshot.json index d528363e3c87d3e72f9ebd5ce2ffa7bb487f4c5e..99f5fa0e8c89e5d73de3bb3104b68732484e3d17 100644 GIT binary patch literal 906 zcmb7?L5>_Z3`O^Iibkztn-oQf^3FFf(pfnO0+OQaN#f~2cPFr6%-zH4u@_z$22=%< z#QOaFdSC7i+vWcKgI@nV$7Oqc`2Fs1_>#|I`#V10&)nJA7Xpw73mcIlS}l=p0}x{H zqRSa%4PC2LQJh|fdzbDQ*0*17dLQHZ;dB%H_4wv^eDmV%@%Z&99qf&I5i2gmCM&f7 zD`?$EEE^HCsdt~9icCgzg8(jlNSY%k5*2DCRSyZ6Jo_{-reP}5b}QVG!#ldyuF@ge zoD3CG3uc6(^sZb7Q(yzAWX^`D7&^UaDME9mjJRX~Jc&491ew)2G(cmh4ufH>WQ}@N za*d`T5`D32ljT_42G`(7Yi4alp#eFex-PER+v2(Aid4>$iIt&@!bP}oGjJ=EBc-L% zMkZZ4bJ1{Y)TvcnxW`k4cC%DV85o(N6T9UniboDT$xyrNTu=u^nKq?s(=jQA3>=X+ zZr&_o)(VsvQTCLU2bebwIUlab&OpXFP}tILz|u2??o`^e^dy=Qav1})Vj;{>NrrW9 zqafC`#$MxINJ?8)mrva;IN)4%_*3y1xADa(ei-bJ+fi^X2}wfjg&9&$M3k{64OipQ(Ly UKK&E;gFp5W_YU8;eY$)4ANNr8Hvj+t literal 1531 zcmb7^(Qafl42JLf6r*#^E_Q6k$=>Z7Aj%a8Av(GU{6$5G>77A zXkSx+#{hGw91v_WCoxcW_D<7kF9W<1_>$J6iISMlDcoa(6%dI&97pNq1DL6J>u8x{ zX@xbrMru^|QR;{Vvyd$YlU(f$Ct6qHSr{|kUZCnE1T4HdgkYZSR5Gu1lwRxNz21T; zQ);XE2M3}6sK_|fN!Z)$y-})wAkjsA4Qka)Wx2#$Z3a)6A|>m`1vL8*)?6V$OD!un zr~*TUf|=9;Tbc|CuC3X!hRZQbywClvc%?VOk=AiqlI8A~DMnV83{K0|Tvo!U8kd?! zC7o^Nh^3Qi@5ZpCWFB#&7Hid_#nsg`g+wZBKv{tcPmU#h_FX+{C3Q|0E4E(e=<2{k z&|plKwN}&tomB;qMGJV0c0_I;?D2d&U9RA4@yFx+ zGqzg+f;WKqd2!Jf^ZE4f_sU^A+WB}z|B8JkJ0Gn`-LqkV zrKMLBGD?F>?^=5j(9)%O1t2uvW4c*S%lCEid}mICXfG&By7GV6KE%V_<(P3vrS3b= z8~rvHfr&UT$i=rVzN^?=}D|9E!nH|ZrNPcR4Q5L==>$QeW z+D8Vfkh{wo1F;d}j$i8|$JJ^Q2{uQ9xb|5=ZDp^|p)0bBUr>g51^+ed>!;I)z}MRb zIjlxw$^Hna+3zi+O;1ahJm%H%C37}J#n9#k?4zEn=HrTHn;{yN;7LK3Ju5!)pc>j-WJa_S0r<`OsouL z6fVMzn}J)Q94RH0Hqz}ZVBtz}4b3q*x zW!jXkO~<4dGH^t`xOua*Su0R_M0uyQEMUGkk=G7Qg$<1cSsLm7Ko_AqY0-o8G?9+A_dK3zYZ zZ};GA@z>MCOKi82A$SXz>qmfh_x00DQ|3-zd5+uVe7=46`Fei*2RUwk#^w5O2XF_T nUm|(a%g4A~|4;Atm-Fv||8e5*dg|YW=fm^gi|g!#iUR-uwUrN1 literal 800 zcmV+*1K<1~iwFQaf!kC9|CN;AZe%qK#_#(SqjTL|?AVTzz1ufHlq(QIcH*>!rL8&x zVp;Xw@tKxC5~@^=&KW6A#{Tl<7k{}pY{%Q{J3IaQ7{~3y;rolj;Y)sp?dSM>J2SG$ zSO`F^NfReBMCTIu>i~@eMzJ*n+UmAe;bu4o_c65Tz;N9z&-s3d)6M-k;Wt+wudY76 z`}*qYWhxcy@v@>h6kkL8ngToqm`mk=V3RqCfx5GInpS%m;FZ9av>r{A#C%TS9wV%P zNc7=2N;eh$M=Y3yY%!Q*v^Sh+U5RI5%zXO>RVN`};ng7o zb8V-Rb*-cHS{Lv8EtoQ;wwix%APRtrj6+|P|# zZ_YWM$65Nuiqt(R7Fb$(H6f!kxb&{ICjl*8nr8q)a~#vndRo56$#cw{2+?-V_;mSP z=u_NXpKdZQsnmRTjP`>#c@O+FJ&zkh{wo1F;d}j$i8|+tq3k2{uQ9xb|K_ZRK5`Ls#T2enlDP z5&T!#SC99f0^e>MWV0HLCGST-&HLU$+Vr%9$+K^ah=4LHlA`S0b&MD~wQ#l=oBWEn euFtMryv%LC#pChztO+kN{PZV9HXS(o1pojj8=U6= diff --git a/tests/repository_data/repository/metadata.staged/targets.json b/tests/repository_data/repository/metadata.staged/targets.json index b1e493ae253ef61fafda4b55a3074ec794d15abd..4cf4c3534b5837a5b21e7142933678f12e7f7bf7 100644 GIT binary patch delta 527 zcmWlW*^LlD3_~Rl-`h%#$<+oOOpa+|8vu!3=x1vod`606Kj-V?>*LSwPYpXl>L)~a zs6qT7g-ZfDl7hchM(Ya0EE+Aix4Y~j{ zkKSf;d=g(eUw%hSuB#MFm&R08^E3_z z)y@jNjd)J3fZLwRWV^~ffKF;f3{t^LGW!Cx5M}WU3S$czcHIemarFLoe#5aor)BOiA!X!&a{I(xd4$6!hV=VH5G0OUT-JP zxSIn>tatpg9A6ebUsIEoH#H@-be}pPh1M)=*wZO1yNCA*xeG4ZQ8Q2-E+1lkM9yuK zPl28>2*;s5TCuU2jD=28j|T_o)h7z~c$A#N0?q?5gT<3N5P^Em&Jfh0?R*(A-gZ$~ z@UaBJ4)4onIld0T+@^}DPTiIn6N?4Jkk&i0Nd}Y}IjS3e2CZ>g&*f&gvbCIPs&9?wz*$I{u+jz^CH&r*0uezLveP5G||&OL*^l}v67&Rs8n8z2DvMib6Au=m;N~_mA>OsDnG$y6mg~O zh>lV^&lJ}#1*<)aETv=&AyiT0ZDia90aQgIY)Hvb6Qy;)wFpF(KkoERpcqbf3On7b z+s!^*cDrv|h+w;}tQL|fBdnAhSR~R2%B+!|P(zs_CPl?jw^funt!zwzMeVqP5F+&? z_^3_bR8mCcLS$a4=pz@B07ODV;v~q#Z8Tca#1u<}tF-mcQRYcZCTK~F1fn^YjyuPr za6(cCOafT2Jfd>0rFY&@Cj^aFTJ#g0Pu_4Tg7?x`7YPPKGDs{TG(!_5+MPEVat{+8OvXb%?!2_xpwmb6pI~i$`QseaK&hvFWv-q ztUdf7bp;VoC{F})41r_5LlB-BBpn6mHV{4Dz&qEyL8#BH0vj&M(u95_>i~i^-BFQYgYzO&%>wD znO&1gc2zsgvWt`K%4FO9lR*Q&YuR%nUU&LApER#ddx@~lY&m$y*Zo>gY|aY^!+Nvn zRo>Dcmh-f3f=ar!XrEM1QStSqUNx3in{{m7&4=sz#@&6ZI&uBH{*XV5cwAJPD}VfW zc6VIAJngS;e0FhDtZ&Y=n~k#5d6@Bfd{(u_nd|qPxh^&5yLAA8f&#>pUT)Nru2>c4*<}zGQ7-2`;k#*gp*H%;(blX=iPn3uh2Ibhw9wfqdR+t8V$=uF`c}tw5`n78O$fh zvbWkUD<$>__i->gDuZ-gl0$-yQi6NI2F47><*LkZ3q0f$iEv0}6W| zmzGg#G55Ho!C9P}1f-&nJdT*)to=7g_UJ7!`<$#`zKz&EWv@rCKLAm^H-X*<004$p BWpw}m literal 1237 zcmV;`1SRNxib9B}H3lXm=m?;Li_CfBi3cYI zMKGM}=m`c(Wt4_ea1^AlLho zAf8a^nGQk<%>o7|lbK0ux!~4HmXHypHB}0Qx5$NvoLdnzx~M3RT2Lu~@IaJ@6xBJw zJhNQ-D3BrJ(K9ImTJVy1=BaRm0wPOkbqXl}Q-q`>01u#o6-F=yF)>jz;YRJKWC3h2 zf+QHFBZ?+)59otaiK7FAgo<)Ooen9*S~{R!X>OUangLJeFeqX~5Y)&-%BXY@AZVUK zN|ub2U{VShj7Am-)mUay1}}({hH)k|n$l2|X%SX9!BRIS9;3+WAdR023*iYbVO$awIybNmCuVhMZU z?w8jv{b^UJ&R1?a@ul6T@>6S^HM^C|L9N|9sk9n*yTymR(`?q7U$bs)^!Pk}oSfMW zsbyD<(=59<$*xRR?w^cW>31W0ZiVaKFy~M0tJ6UwtT$hc9`enwF%aAH0{ytzZabBC z^oP|VZrZ4#ZY;Ye_0#0|`cki3tE=rMwC@(<&3)_czEgjKVcvYmpG7z>YVEZ<{(5$I z+`K#;u5Vm+aZ_w=&NR#?+3CX1c{4n#M(YfQ!*;HV?OKZu`DD3V-km?>e%P9Y>EpNg z=$1C!B0Ote8%(p8apS66&juaozHQG3H>5TY7yYk;*=g%!>C(C%>z-ZJ)mg(ruW@Jlt=U~7vmRV5py{8|bU44~-KUp%m-mPH?L%JR zd^S0su}M~3^omB4Zm&<)$Km1<=EoCsYs8bUxl;Pi#}0lwpM`$U-12#6)M<5w+m~@- zG1@fi*;Q71$alqQbnCy@tGX}l>(Sx;x}RK&+4L|rD+jGO@Q=kJz0yvxSm8UdOu|p| z|3jdAM_+zK$l+-x(PG~x$0a^`5bGR@@h{+PyiVvlg2_eymP%P}tBom!58Nwu?;&ZM~(Wtxm>t{7{WBvB9msew}+OnDm+m!^?O z(@QA9DMw*+S{Ee1uYsnRkBV{f%jvti?})HpiTnp7A(=q8TiiegMRm+)Zizd^D`Z-v>{WQ~h5WBZc59=-kp;3Eze-Uk2x$y;=C diff --git a/tests/repository_data/repository/metadata.staged/targets/role1.json b/tests/repository_data/repository/metadata.staged/targets/role1.json index 263d9895c5408c48daeb5831acbcb7ea4d84983b..7dfe6f87faada4a8fb0fbbf655732985da4da3f0 100644 GIT binary patch delta 528 zcmWlV$%z;+3`I!@Ogpl6A5)6fHl~7s^pZZNra0(g8QEX&eSds^{Q3F}SuyI#9h2fT zY}ze5dNs~Lr*jl@k6^ne?g}_)ly^g~x=*DsFf!$z7@#oakhcj&KtdN9hJ(tIpq64v1jK^U4_ou{mU}x K@7K@YKmGw*=9K6F delta 528 zcmWlW*=-mw3`5bPK-SS=9scYnI;=G^L4fYkJu*{O_{%a#kAD) z@pf8I4~2drt&3%Y`F+7Jll7_Hm?%IoMolyUP}O3yl&3Dw=-OkBq->dTZWAnNZpNF6 z(anQOl|`xRTgInng`3s;M2KNAd^WvdDR(%OvKzYx+%hi_$T%aGW1Q?IAfsaDXNO>m zqvuQk5YcSu%)Q!hf?l&DDm1b_hy;@x9iC%0Zq#Gbenj}w)jNG9+ZH=w*|A+$T*Pvg zyoWB-eWbZE~Pr1n{Qs5*10(=~WNSxmE6 zv8)<(#o{2m8cj3bUS0HV<8*U>5q!UYyWhXPez)KMZDoS9Q6X+p5zWdNP*cvPTXOPzwfmgBT%e3IEoT_FFQ>TdfLKaPh>0nV^yirw=W+y5KSSI>U{)al!~ literal 1078 zcma)*%Z}Ve3`O_z6%DOpgRBRO^v-_}xV=gc1SE?!-A?8KcRPq<%)f`NaTZww2v8CP zmUMaR^61;$VY{5~ANBg>b6mD}hoA2bhi`cuw%_9Gd1htP)_ZGSX7ftSg(gsl*cecz zNeo9=qfIKTS=B~T!H~!5ZK~S#!>;!b*VEH3_~H2eczpln$K&z)RyufXG^B^s=ma%| zW*~Mc?u%yCPGd@K2HDc3_S)Kj&Q^65gHm+GQs|YOtf-Gc#C?z}MO(#(PhB_^XCrFT z#w2% z17Xe#D;0Gp&DBj&1jI`b8L>*U!3;FYf{8*9EQN~8EKA#z&QhrjAAu>be4(q)I!QsD zwTMO?b#jju+AI1jeX$a!dWAy30U73ykjR-3bTrOfN^>I%2l8ozvZy!AsPbWkX`pEh zQI6_?v-(Y-Fn%+z|PAeu&FeAD(w|w)pe;{L1ZC zpu!u#Tpt16?dzx4ml8bvE|j{D`g}P(UEhAXJU#wXJ!~hvoOb?q_Os*Vq@~lX<+L34 zs7yRuJctY4fUxBm&`?h{hU9>i7f35Xk-=EWGXs^5^>(T}*Zvb6w%_CN{(4GX%#q!8 zUN`>j>+Stt|I1p2n%#WH3PA{Ao(XKQkpzatYy$zoyG}{UCo`@5KPHliTKg|+46r-P T+wN}rBR*fwdt7PxpA=*op1l`Ui;Z351ZHICMd6SKS3s zRr~K`x8;E396U~D#>%{Imi4gv{%FTv&v969mM@!Sd7Wojf5ywMPh>SJg{b9D4Xa45 zJ=IWk=9If@aDz0Mdd*@JHL8lqL3kAEX1-os_#WbT|8ybzw7uPKZ?8XYx9_9r;EX5Y zA{o&vwFY^_)E$fpwbBS?7tvx;iaF|ZlY>h%&w?C98}x!jqIBt1lIJKf2^*nQ(ZpI^ zH9{J3wWM6Nj+C@fVZdb3qvbYNL`}VFqTD-Y;@&!T2(ap5G@7g7$g`Q6QU)Mq%9fh} zU@@@30>!09RA^bHiH{6vic3{X0DAUC5EU8-$n&x?SVG?tl5OgI}E96~G zMcFDr$AQyX9(jymy(P_5@-PZ;9w-s_m2JIJc0S!d>gBgDao#@cf86c&-}2sXzr@$m%*v*% z_tw13=9QQWO`s65F`!J77>=+;n^ah{s*R+AA&=GDRJHBy#(Rj%@#zNq{_yc|`1tO- z!{Kcy6}&bY(!*+Wf|{Tih+T^NqFJ@mm{OZTwsfhzwl<)%Rb9oP6kV|tdL<_->SGXb zALL5WR`KCe7Y@bQh?=x9$z7zc#pcN90E58RTwzuLkphH=`5GZr3r}w1aBb%v6($tn zY+M`ITvkkIIbuY#%9iIqm@~~vq7J3Gx+#i)cqt+yR%tevfks&{Q3!&iP;r@MX`9kn zDz)JwFagUKy85h>6x3OZXw*?B_h_NLqR-M7D{;~*6ao%NGlzsk&V-<&ar#o48(BDz zPa~8?yL>yO>2m9R1cihuZW7IMP=31h^?U_;&d8)te|4d!j(Ozy};AeQX_Tw z63=o)ZgWw=(ivLQq^6B&t z=Zii(-;%S%AJ3=P+-?ObyaUYZ6TpYt^~39?44%G$lKZI7=i}4m{pa)3wuHb2YHJXWsiwAMRYancS8Z^{18$)uy$_u2Gph#n^iUluY?#rj`H4 mL^4rp|0Rt9ZpwCVYuoSf<$Su?m6Bg}FMk1xp&!jQ1ONaJLs;(s diff --git a/tests/repository_data/repository/metadata/root.json b/tests/repository_data/repository/metadata/root.json index f4c8ad4215bd5b6c6ed65c19f1b71eb5214574e8..1f79ec7140a51b4b5f9026a51f1ac7f695c17d22 100644 GIT binary patch delta 528 zcmWlW$&DC52m~d|Pp7bQ3czrNGhAtG1S>g^{W(F8|Ikzy-yh!}f4)AoSi>VaEkqnl zyL0Qp76t982y+n`NjkBvM%a=!73<(7)nKB66l}XLP^UNPtX?r!Ro^;`2FW7q*rtW0{bkz5i*|49I L)qlT!{QmV12H%m; delta 528 zcmWkr$&DC52$P~b)50n(a1Kxo$I!-Buu=-iFAZk)hrk%y{Qda*@#pI^4WZM*F{%JX z=|Um|9;8|%rz27`)(Dx#gQPhTV`jYN>M+7Ie5ryVR1RA!n%#s9CTia!Gne1;20;%G z^E(Qc5+ln9_1ZV4N7EXks$ zYi0s=;LdsE){o1Ja7>u1$=g5gq!K-4DdD=&h2FexFmi|uS=DDxm(|vhCBG<&u#5{|+)%AGn( diff --git a/tests/repository_data/repository/metadata/root.json.gz b/tests/repository_data/repository/metadata/root.json.gz index 655ba429bd4da8a4219271745e1e650d76e64024..2b282a01ed791f9a004cca540da47ccce78b23f3 100644 GIT binary patch literal 2053 zcmV+g2>SOQiwFRyxD!?a|IJs~j^j2KeebU*)T@aXpaCAx7Hh+#q?#EJ5f6BX7>YHb6d*`4$Mx#hsErHcYAnR!%7$<}nqL3#nrwqY} zD5)sa9+Ylhff3Tc;~GYu#VWFfGHZmj1ab^9rjSZ(1SCQcY)!!cbFdV8n>f8X1O=0hUD?BEcBKC<2jH`-4z1x!}D5w z3F0(feBI;am1qlr6AvexD9@7+x>Oizc_F`u{5g7+`(IbRyk-}*kM+l@?>|1ZrXTcQhSOR%sK0cY%je)a?FB(C*j4+r zO>){Mu`h0*Rvk54)#{*L9r5bNe0|e#&&{e>oZ83j;)dyY(3!@AUOBw4(|zNdPxjfv zn|I$)*_vcoADC`%sy!O`kdRb8=AA)V`sO@ZZmyzLKAK}(RRv3jpGjzrvx z&>1rARxb4o6W(UEmpQqu`pCRbwhR&7AI%?ORf_J-uk07H)4L?2$~xtc(Ryc$+N8L8 z4O8a+?Bd<|44$@+_T#}sYu{guXUlMQ>dR$$ZI}0AmbQl3hFM-?^>u1(e44j&u}HGh zZM?5%_v3MKz}sxUVi%K;&IQ4GIl_bahHceFS5~{aUuFw?;Qh<7n66V6##!3&v*sC8 zH*vNl8yeQoTcMApC39Hs^alN4&%r_7%jzhp+>`kxZUazRp;N0~xW89(V&md%nFQSl z9rs#~RmqML&TqZ*bhkX@#rRTIcKeoA{mXro1&ItMj&6yTk+0jZx?88-=&#F40u~V> z6>_D-`I2A?MaWuLN&tWqawQ_R;QMC??gS(dQ7_A z3=HF2;OImHQE$abt9HaLO?U;~=Tz)3XhG(q)}6okU6CA)aI{e45-VJ~h#U(Ggt|1C|@Bq^_=leC=~w(z*CI{7s19NC+w(_nh&ojq`? zCD_l}>vX)T@LS7{x6`Jpf8x#3#U3xzxz;{f75KN=+n^Q&<$#pIrsiF0$yupWZMJJG zKG@MgyiS+PB54Lv4=dp`&m@d5b(#+PJ1VyU>8H)z8P)Jx)|FG;93HTjBj$uG zhs&R*%l!#g|<%?h9G%Dn1QDuz%FdeL}x*q`c;;ohIkDeYcMc1uPbow3}4 zdb5+%{Qa_b-h`((mYetX15B>;RF&E7ql}hKx{7)jG+`U&q41~iIRYKr-DZ>B9PVwL zO;mr>=*Fwi7h}}irj6d76c-uD))uVO>0ffIPhX|=njiP(w`MCpKKr%bv=T?Zyng&o zism!Uk6%T1-nve3d)$BYlkj(9|6Sww?|^)BQ~0NVoMpZ-=i(1=e6!vEIUMo96lZzJ j{s_spI>diK@~dF3CNJV+`eowtr_cWcs^#wQtPTJGJiHwJ literal 2054 zcmV+h2>JIPiwFQaf!kC9|IJs~uH!ZmeebWhxUWudxrs~Oyvnk?TVBUOKvJSD%a&!y zn-uiF5AW^Hf&nIzOb;+IWI?E+ONUj5r^-KndU-A4T`I4)JStv)c=^YtmzSUI_vJN- z$~d%^S1LGFlrqgQl~`d$7>oc0+7KNvDXC@=2ptSCaw1AJq7)!T6=N#G%ptGeeZ)IO z*Zuh;;B;1>&FbHeXS1&^i{RZZ83bA~!J$wJK;obXNC*U^5Gex@N0bm1hD<{sCBq>^ zjtqqe8DrQe?r;zij6xhCB`^>Up_pk0G8Rc71x847LrEA~SU5BS0UhEHLu11C6%bPd zpd&d)7PW%}hC~U?8ITS}QgUXrV%Ra**jUO$N@}c#LK1PxZB7J1hEpk|6Gn<)2DC7U z7|11eq_i*40-BK8;5L^=bA@2Y2*yeyu6Yzn2O*0Kae@^S5_7FNA;NB(Au0@aLPYEe zM!16@BG7hbS>JT6- z31m^o?Dn)$0uU2vO9ZVYvcFN#5eN%gAX_>?Q3zy60LIdJwPg7G>E*}ozLGL}XUjVo zUw^umS@c1j^ZEKgnpb^Ji@3N(>Gh`~m07X3jvro(JQmT1KYC{I7fQb(OV;lJv{wMY z490GL}IIm+UJP!SZ_y8?h?2WTXXc5Gs7@@!qXwA&*>YsM`h zBgK9R^bz(Kz^CRee`>!W*#(w){dk|5VPD?7CPE;Qx5YFiezXs3-X)rC)3^TK zi~_nYyYoR;%nloWz*mdz97g+R5Z9@Q&IPHTi-8Ya!wxwhs8gA^5tO^j0`8q{+Fw0K zz?-jEu}7ljI(Sq*Y3|LWadL6X?YMj7@-p9DCL51Sa~;tkUlo~kontm^zd5s zNxiX6KZ?^Hw*IwVuOGGj{C@xZMclqwKL7u;Vm;_=WYn6B^Zlfl9*}t06pZ(=>egCk zM?8zbo@m;y-I_Zl@NRRIx%1czQPiF78FKV+vMhtT6#a)=JFcWZxFwU?Hsz4frawhJ zr?}%*RORmChM#-^FZ;4rmP~Yx!_9QD4i=Z8TvzvA^(+=?XPoVr;SJW@rpCmVWiJ=2 zB)dGO$7c3CpI0Zc&yE{*ixS7bA=s=YWVGC|y}Iez=FoJjY-LV-csm#KZOVc;OMN$M zUqO8rXM1Nyg9dsl^f~It95(&IXy_d|ILT*KpCq+svfRZz0BRd_=`;&_yxKDp7jNst z>(A(P&?z?+J4?8H46gIT`ji*bTU9$8J6a8I&rRkf(wo_IOSBGM-GlYRHpP>_PAdsm z=r~d#+vBV**>-?JWQ^e)0NXIR5}_4hO(IA`tM}R%33=!UX-P#iaQ+6gx^(c<4+o-C z&ieffjN^xA(}_l+*@=@*<4kOs$Od$mR2*+;`gSOH$M%o z7(5yYakJhwoo;IU(J|BgyiK_y-V9wG$x2-ty|YoC`&hh<8lhK>oXXoZ@U4+tmAW(* zho0i210BWNbiFQ;wkP$t7R>WZ!uZyt>1cSMa_>3Aw0*du26@Z+a<1Fs6TwMx?FCX* zX<~+()kxYrq=Q4cTk|__t6{&9xpd72m(AnR-;hTtgHhx(GK}zgD5Aw^xH5CJy9Q{` zI4|4JduOptJO71RN#Z!ficm;2rgjptY{sy!vAAC}8g7L5{4X7{hrJ7~iw6zNuDO|t##!%NA!FG|1$)cTvW>498I_fr0 zVBHJWeZ3b|w|47##i=ZB)i9^KGaBBvsXLxcAM5rQlYZ}by5v-dDmlzn4<>@QDc`_?%lkRg6cf#v5n`} zJ5|+m>nCuai@SSIo+Un&r*@4m4pm;aB^6^RM}u&>I2|v|a(r|bOG^9qianBvuQQe# z&}{oj!#%DC*IjUlW4Y`0N-(?AOI>D<_cmI!=_VWy(1tyj2g04l*AV!mzt3ieB|Mrq zo2lWX)sHuUE2gNuPg{edQ{1E{JA1HA=YL79K7FOuYkodPzYSaY`D0%DjVp2Z%ioXx zNz?p@^W#_2UH5hp+@Fu{^CbKo*?%`U{yQMwJQV&ZAXl01qHFO7IKEl#{~V6^6ctx_ k%Kiw+w<^ScK=LbL?onRE=k$x?^QX`M1kFh{a;y#j03PZ1RsaA1 diff --git a/tests/repository_data/repository/metadata/snapshot.json b/tests/repository_data/repository/metadata/snapshot.json index d528363e3c87d3e72f9ebd5ce2ffa7bb487f4c5e..edde816d8aa77135a4fe5937480051676057a466 100644 GIT binary patch literal 907 zcmb7?!Hyd@42JK13S-VqONydI<(4-H;+|Rr0ZCEGw#|BBJ4J(F-@P1fdg!Srpc!B! z&d;B}zHf))c76ExsJAbdxNfhHzaEaqZ}}Xz-{b4U%$<#WApnW6un{Su)e`wO03il1 zx|~7Q(6vex#p!jpcj=B{ef#0A_c?B#&Ue9gr#Gk5n^$j7r|(DUU~klmSaB&fS*ZnB zLF+zZ*@&1;z5DD`WHPE71aRp?(i}mNs8B1ZdPum0vkXjb2dc9(CJM}5t=h)#3cjZNyGso$gIwx0UASf7z}GAYt*Zf zYcvg!=!;#OEXU$DxCTdBGixgf4af=Ab#cYs7SAdR-nv?vZu5>z`SwD`EW&c1~Sfp!j^UemYyker_!dSC((?M%NVE?3t@&zGOTkO z1+lI*_8Rv>Qrg;*ljp!Z7Aj%a8Av(GU{6$5G>77A zXkSx+#{hGw91v_WCoxcW_D<7kF9W<1_>$J6iISMlDcoa(6%dI&97pNq1DL6J>u8x{ zX@xbrMru^|QR;{Vvyd$YlU(f$Ct6qHSr{|kUZCnE1T4HdgkYZSR5Gu1lwRxNz21T; zQ);XE2M3}6sK_|fN!Z)$y-})wAkjsA4Qka)Wx2#$Z3a)6A|>m`1vL8*)?6V$OD!un zr~*TUf|=9;Tbc|CuC3X!hRZQbywClvc%?VOk=AiqlI8A~DMnV83{K0|Tvo!U8kd?! zC7o^Nh^3Qi@5ZpCWFB#&7Hid_#nsg`g+wZBKv{tcPmU#h_FX+{C3Q|0E4E(e=<2{k z&|plKwN}&tomB;qMGJV0c0_I;?D2d&U9RA4@yFx+ zGqzg+f;WKqd2!Jf^ZE4f_sU^A+WB}z|B8JkJ0Gn`-LqkV zrKMLBGD?F>?^=5j(9)%O1t2uvW4c*S%lCEid}mICXfG&By7GV6KE%V_<(P3vrS3b= z8~rvHfr&UT$i=rVzN^?=}D|9E!nH|ZrNPcR4Q5L==>$QeW z+D8Vfkh{wo1F;d}j$i8|$JJ^Q2{uQ9xb|5=ZDp^|p)0bBUr>g51^+ed>!;I)z}MRb zIjlxw$^Hna+3zi+O;1ahJm%H%C37}J#n9#k?4zEn=HrTHn;{yN;7LK3Ju5!)pc>j-WJa_S0r<`OsouL z6fVMzn}J)Q94RH0Hqz}ZVBtz}4b3q*x zW!jXkO~<4dGH^t`xOua*Su0R_M0uyQEMUGkk=G7Qg$<1cSsLm7Ko_AqY0-o8G?9+A_dK3zYZ zZ};GA@z>MCOKi82A$SXz>qmfh_x00DQ|3-zd5+uVe7=46`Fei*2RUwk#^w5O2XF_T nUm|(a%g4A~|4;Atm-Fv||8e5*dg|YW=fm^gi|g!#iUR-uwUrN1 literal 800 zcmV+*1K<1~iwFQaf!kC9|CN;AZe%qK#_#(SqjTL|?AVTzz1ufHlq(QIcH*>!rL8&x zVp;Xw@tKxC5~@^=&KW6A#{Tl<7k{}pY{%Q{J3IaQ7{~3y;rolj;Y)sp?dSM>J2SG$ zSO`F^NfReBMCTIu>i~@eMzJ*n+UmAe;bu4o_c65Tz;N9z&-s3d)6M-k;Wt+wudY76 z`}*qYWhxcy@v@>h6kkL8ngToqm`mk=V3RqCfx5GInpS%m;FZ9av>r{A#C%TS9wV%P zNc7=2N;eh$M=Y3yY%!Q*v^Sh+U5RI5%zXO>RVN`};ng7o zb8V-Rb*-cHS{Lv8EtoQ;wwix%APRtrj6+|P|# zZ_YWM$65Nuiqt(R7Fb$(H6f!kxb&{ICjl*8nr8q)a~#vndRo56$#cw{2+?-V_;mSP z=u_NXpKdZQsnmRTjP`>#c@O+FJ&zkh{wo1F;d}j$i8|+tq3k2{uQ9xb|K_ZRK5`Ls#T2enlDP z5&T!#SC99f0^e>MWV0HLCGST-&HLU$+Vr%9$+K^ah=4LHlA`S0b&MD~wQ#l=oBWEn euFtMryv%LC#pChztO+kN{PZV9HXS(o1pojj8=U6= diff --git a/tests/repository_data/repository/metadata/targets.json b/tests/repository_data/repository/metadata/targets.json index b1e493ae253ef61fafda4b55a3074ec794d15abd..4cf4c3534b5837a5b21e7142933678f12e7f7bf7 100644 GIT binary patch delta 527 zcmWlW*^LlD3_~Rl-`h%#$<+oOOpa+|8vu!3=x1vod`606Kj-V?>*LSwPYpXl>L)~a zs6qT7g-ZfDl7hchM(Ya0EE+Aix4Y~j{ zkKSf;d=g(eUw%hSuB#MFm&R08^E3_z z)y@jNjd)J3fZLwRWV^~ffKF;f3{t^LGW!Cx5M}WU3S$czcHIemarFLoe#5aor)BOiA!X!&a{I(xd4$6!hV=VH5G0OUT-JP zxSIn>tatpg9A6ebUsIEoH#H@-be}pPh1M)=*wZO1yNCA*xeG4ZQ8Q2-E+1lkM9yuK zPl28>2*;s5TCuU2jD=28j|T_o)h7z~c$A#N0?q?5gT<3N5P^Em&Jfh0?R*(A-gZ$~ z@UaBJ4)4onIld0T+@^}DPTiIn6N?4Jkk&i0Nd}Y}IjS3e2CZ>g&*f&gvbCIPs&9?wz*$I{u+jz^CH&r*0uezLveP5G||&OL*^l}v67&Rs8n8z2DvMib6Au=m;N~_mA>OsDnG$y6mg~O zh>lV^&lJ}#1*<)aETv=&AyiT0ZDia90aQgIY)Hvb6Qy;)wFpF(KkoERpcqbf3On7b z+s!^*cDrv|h+w;}tQL|fBdnAhSR~R2%B+!|P(zs_CPl?jw^funt!zwzMeVqP5F+&? z_^3_bR8mCcLS$a4=pz@B07ODV;v~q#Z8Tca#1u<}tF-mcQRYcZCTK~F1fn^YjyuPr za6(cCOafT2Jfd>0rFY&@Cj^aFTJ#g0Pu_4Tg7?x`7YPPKGDs{TG(!_5+MPEVat{+8OvXb%?!2_xpwmb6pI~i$`QseaK&hvFWv-q ztUdf7bp;VoC{F})41r_5LlB-BBpn6mHV{4Dz&qEyL8#BH0vj&M(u95_>i~i^-BFQYgYzO&%>wD znO&1gc2zsgvWt`K%4FO9lR*Q&YuR%nUU&LApER#ddx@~lY&m$y*Zo>gY|aY^!+Nvn zRo>Dcmh-f3f=ar!XrEM1QStSqUNx3in{{m7&4=sz#@&6ZI&uBH{*XV5cwAJPD}VfW zc6VIAJngS;e0FhDtZ&Y=n~k#5d6@Bfd{(u_nd|qPxh^&5yLAA8f&#>pUT)Nru2>c4*<}zGQ7-2`;k#*gp*H%;(blX=iPn3uh2Ibhw9wfqdR+t8V$=uF`c}tw5`n78O$fh zvbWkUD<$>__i->gDuZ-gl0$-yQi6NI2F47><*LkZ3q0f$iEv0}6W| zmzGg#G55Ho!C9P}1f-&nJdT*)to=7g_UJ7!`<$#`zKz&EWv@rCKLAm^H-X*<004$p BWpw}m literal 1237 zcmV;`1SRNxib9B}H3lXm=m?;Li_CfBi3cYI zMKGM}=m`c(Wt4_ea1^AlLho zAf8a^nGQk<%>o7|lbK0ux!~4HmXHypHB}0Qx5$NvoLdnzx~M3RT2Lu~@IaJ@6xBJw zJhNQ-D3BrJ(K9ImTJVy1=BaRm0wPOkbqXl}Q-q`>01u#o6-F=yF)>jz;YRJKWC3h2 zf+QHFBZ?+)59otaiK7FAgo<)Ooen9*S~{R!X>OUangLJeFeqX~5Y)&-%BXY@AZVUK zN|ub2U{VShj7Am-)mUay1}}({hH)k|n$l2|X%SX9!BRIS9;3+WAdR023*iYbVO$awIybNmCuVhMZU z?w8jv{b^UJ&R1?a@ul6T@>6S^HM^C|L9N|9sk9n*yTymR(`?q7U$bs)^!Pk}oSfMW zsbyD<(=59<$*xRR?w^cW>31W0ZiVaKFy~M0tJ6UwtT$hc9`enwF%aAH0{ytzZabBC z^oP|VZrZ4#ZY;Ye_0#0|`cki3tE=rMwC@(<&3)_czEgjKVcvYmpG7z>YVEZ<{(5$I z+`K#;u5Vm+aZ_w=&NR#?+3CX1c{4n#M(YfQ!*;HV?OKZu`DD3V-km?>e%P9Y>EpNg z=$1C!B0Ote8%(p8apS66&juaozHQG3H>5TY7yYk;*=g%!>C(C%>z-ZJ)mg(ruW@Jlt=U~7vmRV5py{8|bU44~-KUp%m-mPH?L%JR zd^S0su}M~3^omB4Zm&<)$Km1<=EoCsYs8bUxl;Pi#}0lwpM`$U-12#6)M<5w+m~@- zG1@fi*;Q71$alqQbnCy@tGX}l>(Sx;x}RK&+4L|rD+jGO@Q=kJz0yvxSm8UdOu|p| z|3jdAM_+zK$l+-x(PG~x$0a^`5bGR@@h{+PyiVvlg2_eymP%P}tBom!58Nwu?;&ZM~(Wtxm>t{7{WBvB9msew}+OnDm+m!^?O z(@QA9DMw*+S{Ee1uYsnRkBV{f%jvti?})HpiTnp7A(=q8TiiegMRm+)Zizd^D`Z-v>{WQ~h5WBZc59=-kp;3Eze-Uk2x$y;=C diff --git a/tests/repository_data/repository/metadata/targets/role1.json b/tests/repository_data/repository/metadata/targets/role1.json index 263d9895c5408c48daeb5831acbcb7ea4d84983b..7dfe6f87faada4a8fb0fbbf655732985da4da3f0 100644 GIT binary patch delta 528 zcmWlV$%z;+3`I!@Ogpl6A5)6fHl~7s^pZZNra0(g8QEX&eSds^{Q3F}SuyI#9h2fT zY}ze5dNs~Lr*jl@k6^ne?g}_)ly^g~x=*DsFf!$z7@#oakhcj&KtdN9hJ(tIpq64v1jK^U4_ou{mU}x K@7K@YKmGw*=9K6F delta 528 zcmWlW*=-mw3`5bPK-SS=9scYnI;=G^L4fYkJu*{O_{%a#kAD) z@pf8I4~2drt&3%Y`F+7Jll7_Hm?%IoMolyUP}O3yl&3Dw=-OkBq->dTZWAnNZpNF6 z(anQOl|`xRTgInng`3s;M2KNAd^WvdDR(%OvKzYx+%hi_$T%aGW1Q?IAfsaDXNO>m zqvuQk5YcSu%)Q!hf?l&DDm1b_hy;@x9iC%0Zq#Gbenj}w)jNG9+ZH=w*|A+$T*Pvg zyoWB-eWbZE~Pr1n{Qs5*10(=~WNSxmE6 zv8)<(#o{2m8cj3bUS0HV<8*U>5q!UYyWhXPez)KMZDoS9Q6X+p5zWdNP*cvPTXOPzwfmgBT%e3IEoT_FFQ>TdfLKaPh>0nV^yirw=W+y5KSSI>U{)al!~ literal 1078 zcma)*%Z}Ve3`O_z6%DOpgRBRO^v-_}xV=gc1SE?!-A?8KcRPq<%)f`NaTZww2v8CP zmUMaR^61;$VY{5~ANBg>b6mD}hoA2bhi`cuw%_9Gd1htP)_ZGSX7ftSg(gsl*cecz zNeo9=qfIKTS=B~T!H~!5ZK~S#!>;!b*VEH3_~H2eczpln$K&z)RyufXG^B^s=ma%| zW*~Mc?u%yCPGd@K2HDc3_S)Kj&Q^65gHm+GQs|YOtf-Gc#C?z}MO(#(PhB_^XCrFT z#w2% z17Xe#D;0Gp&DBj&1jI`b8L>*U!3;FYf{8*9EQN~8EKA#z&QhrjAAu>be4(q)I!QsD zwTMO?b#jju+AI1jeX$a!dWAy30U73ykjR-3bTrOfN^>I%2l8ozvZy!AsPbWkX`pEh zQI6_?v-(Y-Fn%+z|PAeu&FeAD(w|w)pe;{L1ZC zpu!u#Tpt16?dzx4ml8bvE|j{D`g}P(UEhAXJU#wXJ!~hvoOb?q_Os*Vq@~lX<+L34 zs7yRuJctY4fUxBm&`?h{hU9>i7f35Xk-=EWGXs^5^>(T}*Zvb6w%_CN{(4GX%#q!8 zUN`>j>+Stt|I1p2n%#WH3PA{Ao(XKQkpzatYy$zoyG}{UCo`@5KPHliTKg|+46r-P T+wN}rBR*fwdt7PxpA=*op1l`Ui;Z351ZHICMd6SKS3s zRr~K`x8;E396U~D#>%{Imi4gv{%FTv&v969mM@!Sd7Wojf5ywMPh>SJg{b9D4Xa45 zJ=IWk=9If@aDz0Mdd*@JHL8lqL3kAEX1-os_#WbT|8ybzw7uPKZ?8XYx9_9r;EX5Y zA{o&vwFY^_)E$fpwbBS?7tvx;iaF|ZlY>h%&w?C98}x!jqIBt1lIJKf2^*nQ(ZpI^ zH9{J3wWM6Nj+C@fVZdb3qvbYNL`}VFqTD-Y;@&!T2(ap5G@7g7$g`Q6QU)Mq%9fh} zU@@@30>!09RA^bHiH{6vic3{X0DAUC5EU8-$n&x?SVG?tl5OgI}E96~G zMcFDr$AQyX9(jymy(P_5@-PZ;9w-s_m2JIJc0S!d>gBgDao#@cf86c&-}2sXzr@$m%*v*% z_tw13=9QQWO`s65F`!J77>=+;n^ah{s*R+AA&=GDRJHBy#(Rj%@#zNq{_yc|`1tO- z!{Kcy6}&bY(!*+Wf|{Tih+T^NqFJ@mm{OZTwsfhzwl<)%Rb9oP6kV|tdL<_->SGXb zALL5WR`KCe7Y@bQh?=x9$z7zc#pcN90E58RTwzuLkphH=`5GZr3r}w1aBb%v6($tn zY+M`ITvkkIIbuY#%9iIqm@~~vq7J3Gx+#i)cqt+yR%tevfks&{Q3!&iP;r@MX`9kn zDz)JwFagUKy85h>6x3OZXw*?B_h_NLqR-M7D{;~*6ao%NGlzsk&V-<&ar#o48(BDz zPa~8?yL>yO>2m9R1cihuZW7IMP=31h^?U_;&d8)te|4d!j(Ozy};AeQX_Tw z63=o)ZgWw=(ivLQq^6B&t z=Zii(-;%S%AJ3=P+-?ObyaUYZ6TpYt^~39?44%G$lKZI7=i}4m{pa)3wuHb2YHJXWsiwAMRYancS8Z^{18$)uy$_u2Gph#n^iUluY?#rj`H4 mL^4rp|0Rt9ZpwCVYuoSf<$Su?m6Bg}FMk1xp&!jQ1ONaJLs;(s From f9f44d7f043b049e8aefe522b61922fd2c99d735 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Thu, 15 Oct 2015 10:58:10 -0400 Subject: [PATCH 07/30] Request the expected file lengths for the different metadata roles --- tuf/client/updater.py | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 435efd54..8bfe6f0f 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -1124,6 +1124,7 @@ def _get_metadata_file(self, metadata_role, remote_filename, # If the version number is unspecified, ensure that the version number # downloaded is greater than the currently trusted version number. + print('metadata_signable: ' + repr(metadata_signable)) version_downloaded = metadata_signable['signed']['version'] if expected_version is None: if version_downloaded <= expected_version: @@ -1153,7 +1154,7 @@ def _get_metadata_file(self, metadata_role, remote_filename, else: logger.exception('Failed to update {0} from all mirrors: {1}'.format( - filepath, file_mirror_errors)) + remote_filename, file_mirror_errors)) raise tuf.NoWorkingMirrorError(file_mirror_errors) @@ -1432,12 +1433,12 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None, # "safe" targets. remote_filename = metadata_filename - + filename_version = '' + if self.consistent_snapshot: filename_version = str(version) - - dirname, basename = os.path.split(remote_filename) - remote_filename = os.path.join(dirname, filename_digest + '.' + basename) + dirname, basename = os.path.split(remote_filename) + remote_filename = os.path.join(dirname, filename_version + '.' + basename) metadata_file_object = \ self._get_metadata_file(metadata_role, remote_filename, @@ -1590,12 +1591,11 @@ def _update_metadata_if_changed(self, metadata_role, # untrusted data is not decompressed prior to verifying hashes, or # decompressing a file that may be invalid or partially intact. compression = None - compressed_fileinfo = None - # Extract the fileinfo of the uncompressed version of 'metadata_role'. - uncompressed_fileinfo = self.metadata['current'][referenced_metadata] \ - ['meta'] \ - [uncompressed_metadata_filename] + # Extract the versioninfo of the uncompressed version of 'metadata_role'. + expected_versioninfo = self.metadata['current'][referenced_metadata] \ + ['meta'] \ + [uncompressed_metadata_filename] # Check for the availability of compressed versions of 'snapshot.json', # 'targets.json', and delegated Targets (that also start with 'targets'). @@ -1607,8 +1607,6 @@ def _update_metadata_if_changed(self, metadata_role, if gzip_metadata_filename in self.metadata['current'] \ [referenced_metadata]['meta']: compression = 'gzip' - compressed_fileinfo = self.metadata['current'][referenced_metadata] \ - ['meta'][gzip_metadata_filename] logger.debug('Compressed version of '+\ repr(uncompressed_metadata_filename)+' is available at '+\ @@ -1620,7 +1618,7 @@ def _update_metadata_if_changed(self, metadata_role, # Simply return if the file has not changed, according to the metadata # about the uncompressed file provided by the referenced metadata. if not self._versioninfo_has_changed(uncompressed_metadata_filename, - uncompressed_fileinfo): + expected_versioninfo): logger.info(repr(uncompressed_metadata_filename)+' up-to-date.') return @@ -1628,9 +1626,24 @@ def _update_metadata_if_changed(self, metadata_role, logger.debug('Metadata '+repr(uncompressed_metadata_filename)+\ ' has changed.') + # The file lengths of metadata are unknown, only their version numbers + # known. Set an upper limit to the length of the download for each + # expected role. Note: The Timestamp role is not updated via this + # function. + if metadata_role == 'snapshot': + upperbound_filelength = tuf.conf.DEFAULT_SNAPSHOT_REQUIRED_LENGTH + + elif metadata_role == 'root': + upperbound_filelength = tuf.conf.DEFAULT_ROOT_REQUIRED_LENGTH + + # The metadata is considered Targets (or delegated Targets metadata). + else: + upperbound_filelength = tuf.conf.DEFAULT_TARGETS_REQUIRED_LENGTH + try: - self._update_metadata(metadata_role, uncompressed_fileinfo, compression, - compressed_fileinfo) + self._update_metadata(metadata_role, upperbound_filelength, + expected_versioninfo['version'], compression) + except: # The current metadata we have is not current but we couldn't # get new metadata. We shouldn't use the old metadata anymore. From 14ca0d156593c4333c853ae829ae7f97a3182139 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 20 Oct 2015 09:14:21 -0400 Subject: [PATCH 08/30] Update the repository test data following the addition of the 'compression algorithms' key in root.json --- .../client/metadata/current/root.json | Bin 3756 -> 3799 bytes .../client/metadata/current/root.json.gz | Bin 2053 -> 2079 bytes .../client/metadata/current/snapshot.json | Bin 906 -> 906 bytes .../client/metadata/current/snapshot.json.gz | Bin 549 -> 548 bytes .../client/metadata/current/targets.json | Bin 2014 -> 2014 bytes .../client/metadata/current/targets.json.gz | Bin 1239 -> 1235 bytes .../metadata/current/targets/role1.json | Bin 974 -> 974 bytes .../client/metadata/current/timestamp.json | Bin 817 -> 817 bytes .../client/metadata/current/timestamp.json.gz | Bin 530 -> 533 bytes .../client/metadata/previous/root.json | Bin 3756 -> 3799 bytes .../client/metadata/previous/root.json.gz | Bin 2053 -> 2079 bytes .../client/metadata/previous/snapshot.json | Bin 906 -> 906 bytes .../client/metadata/previous/snapshot.json.gz | Bin 549 -> 548 bytes .../client/metadata/previous/targets.json | Bin 2014 -> 2014 bytes .../client/metadata/previous/targets.json.gz | Bin 1239 -> 1235 bytes .../metadata/previous/targets/role1.json | Bin 974 -> 974 bytes .../client/metadata/previous/timestamp.json | Bin 817 -> 817 bytes .../metadata/previous/timestamp.json.gz | Bin 530 -> 533 bytes .../repository/metadata.staged/root.json | Bin 3756 -> 3799 bytes .../repository/metadata.staged/root.json.gz | Bin 2053 -> 2079 bytes .../repository/metadata.staged/snapshot.json | Bin 906 -> 906 bytes .../metadata.staged/snapshot.json.gz | Bin 549 -> 548 bytes .../repository/metadata.staged/targets.json | Bin 2014 -> 2014 bytes .../metadata.staged/targets.json.gz | Bin 1239 -> 1235 bytes .../metadata.staged/targets/role1.json | Bin 974 -> 974 bytes .../repository/metadata.staged/timestamp.json | Bin 817 -> 817 bytes .../metadata.staged/timestamp.json.gz | Bin 530 -> 533 bytes .../repository/metadata/root.json | Bin 3756 -> 3799 bytes .../repository/metadata/root.json.gz | Bin 2053 -> 2079 bytes .../repository/metadata/snapshot.json | Bin 907 -> 906 bytes .../repository/metadata/snapshot.json.gz | Bin 549 -> 548 bytes .../repository/metadata/targets.json | Bin 2014 -> 2014 bytes .../repository/metadata/targets.json.gz | Bin 1239 -> 1235 bytes .../repository/metadata/targets/role1.json | Bin 974 -> 974 bytes .../repository/metadata/timestamp.json | Bin 817 -> 817 bytes .../repository/metadata/timestamp.json.gz | Bin 530 -> 533 bytes 36 files changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/repository_data/client/metadata/current/root.json b/tests/repository_data/client/metadata/current/root.json index 1f79ec7140a51b4b5f9026a51f1ac7f695c17d22..60a349288b86eb2051d12405edaf1c03211ffc83 100644 GIT binary patch delta 569 zcmWlWO==J@5QU+*(lZoFac|}?$#@kJncrHlR$5nXdV+%BHQb43@hEPMs|;lF-uGU9 zUjDiK{``~}v8EX`5!U9F$wN1XDfu97OP1x+eH3u0$3{?^l|FlqELbBG)=H)jfC-D* zX(*{Qoi_o3L)oxw`YIe-Jwr&^OytRUr^O~C%7JkV-F>eNR|=FKiq$h%kEse)*eK3D z8VB=m&$YK7jEpIjHQFculUDVaN9TYu*Maq3Ig*Zgu7gCP2JtkF3nxq-+d-St!)qsF z%`T#zrp#&cWYkHHy9s!pum#spNlh#mM^Y*d!<^h@V+t-Ppp6|ERc&%$0C#vS!YSNJ zApJD|2b-)2V!2kYFlOr5y$!f@rJjoYD5&sU0D5SJLuwfJF>yIjFis!jIrlza5luqu zSwenRp#>0U=qVWkO?WG6oqHWG_LBJVeDUV>-Is@Ye7wE?{J}om+&|oY{do6y{r2kJ V%d4xa>zi-a=Y0R`_r>2={{TX>p=SU9 delta 532 zcmWlW$*mAb3d?c* zs1sI~4<|!_Fd)Jt-09Y?1F(ERhw@SiJgLbxLFXXZELT4_K;3*l+{A0>5hkC^pa;{* ztuAz*GzWmLw_OtoE)nZjf zftjgMZc|Aq^BJN7NyIR-6hOe&*jG#l>N5obf&hXykn6nh&7wGb)6o5rWFbEC?{*HueQa5+JNafC-aSN}w$4bLrdA z;)tq1Qf-4kSqub$#aMumPb`aC3~7k~N1SM6e&bsN0*(b#!oX7y*jxraK|~8dh`@Fr zd>aXb0swt;o|MvNfiF4oA<=+CrKyj(^)d*RFMz^<4KxH811SS$+tQ#2d`eWHfELKN zV1Ss?W(;%yEHhRHtJEw20W#JDW)LC5h2>L$6{Hr$z?K%*IFP(L+wLfDJ_V>VnN%NCZUiL ztU0oXrZ2ZB=FlP{U`#V#V2reHYg<@YaTahBUug_*f+@2sZom@k69oj;<_t-UBhWX| zKr*1VZSvr2L=cvNB1oF8mL?X2)aNF|(1(Ixn?tV5ItMl*e$9~K^QX5Tzxzr`^}yT{WSv~d=D)1cmVwH@$9~wXRbr>70z3Y_}T&h**39>L`Ybc zGW-fG!R zo+AIX>-*QI*>T+Q+jQ9+x~;cCXZta^Pe!iWbdQa3^AKOcICA&{G#j%{x6zoi8Z*1` zT5lZ&X54A;&844)n+K-X$zT~xM%DDWPfqP?zBpwiS`T}$>MpWu4AjuQG|M(V$0U*E zdN8RfM_p&z!<~1lvNOjGd3G>}+gy741Rl?*dLC0RX{DpOHM_Dn+?~wwwfn+pu(W;UescrybnV?@#wbfEk`}RIPL}`d$Z~-Efqe!s6n|( z3*1PH(ZF&vJLUkxZl$B9R~{}aD z`~RmE>sw?eRC_kf!&xyuLjEuzn1+$;)!QlI&%%R<(_#JAIxvn7yOYStV>gDXw+I;| z{&==7y@ue!hf_ao#9(xbXZ3wz8NGPpsjRn72Z zePE%y`SsnggNO*PrqF5cL75@8G4|D$MojEOb=7Z)113pz~3;+*K?U*7`BJE|1%DUd(S*{dns7 za(sL4GB+0P!h~DEZQ%HQt8v^XX!h4>B>)Rdd6SSLW^{=&2?MCLwrv1Rk+h|$9OzX= zEE)hKQ(6nl4op!pq{2QRe*;=wy68CwM!Z`thQka@qlat42`9YOjpA-I#U@R72YPGD zPdB(B>sj|{zdJ)7pHpjg+~t$U)=Rf-1#Ol@ft17IJvVA~x*&NQlgD%vwJk@k4$k;J ziIX_4?tBpUjTm zMqZo!R7=-+tllThz^x{vat}>(YsOb8FU{4lFYU>ZPNIFX-4=1j75=pDE%QuR(XB<3 z$@oY`=#p{LIbLBCzh^_S^gGitMsa-YdqP%8tjD{}M3^`vqhoT|+IPDn$HQjk&@CHX zc8|w!haZXXCW(2AuUG0CalRr#=IjTV@W%Tj~J3SFv2wolpz zkKw`@wB8x)xf82X#8bTL#DNYeo6aY_)){R3-gfARyz13&J-0ZQ<*gd$^pL{weV;hf z#r(1DOc5UTPv=WcIj`d5V)J0!d%r@AW7Um@_M*G?PV4im^rr0~sgL2^G;@vfeZ5bf zi>`n4PPV^tozl52Vex+N<;?_C>7>tguG8pNRm*9d!I7@+&OLsX=vC!5vCug;$ba=1WBc2WXjO7}%I)k|BoVKIu!Mj9}IQ04@SlsEQA+pD- zj8+}G3q}}ptiF|d+*wA~01WUj%ofMBb<$C`kmFf<80|cV&tWG_+M^RGZo(Da5bTrX zUs9`2U#V5g)06tG*~-(8e(g7|M8U6p-v6R#e%Sf(E9tJG=>)^{^lCk=-;w=yjpM%q z^36@*p8|3f`9WQaKfv+La{uRWL}yi8#X0*UB;TqK{{hLbgt@Ey^+({#!skz){|VW_ JMcLO5003`WA0hw% literal 2053 zcmV+g2>SOQiwFRyxD!?a|IJs~j^j2KeebU*)T@aXpaCAx7Hh+#q?#EJ5f6BX7>YHb6d*`4$Mx#hsErHcYAnR!%7$<}nqL3#nrwqY} zD5)sa9+Ylhff3Tc;~GYu#VWFfGHZmj1ab^9rjSZ(1SCQcY)!!cbFdV8n>f8X1O=0hUD?BEcBKC<2jH`-4z1x!}D5w z3F0(feBI;am1qlr6AvexD9@7+x>Oizc_F`u{5g7+`(IbRyk-}*kM+l@?>|1ZrXTcQhSOR%sK0cY%je)a?FB(C*j4+r zO>){Mu`h0*Rvk54)#{*L9r5bNe0|e#&&{e>oZ83j;)dyY(3!@AUOBw4(|zNdPxjfv zn|I$)*_vcoADC`%sy!O`kdRb8=AA)V`sO@ZZmyzLKAK}(RRv3jpGjzrvx z&>1rARxb4o6W(UEmpQqu`pCRbwhR&7AI%?ORf_J-uk07H)4L?2$~xtc(Ryc$+N8L8 z4O8a+?Bd<|44$@+_T#}sYu{guXUlMQ>dR$$ZI}0AmbQl3hFM-?^>u1(e44j&u}HGh zZM?5%_v3MKz}sxUVi%K;&IQ4GIl_bahHceFS5~{aUuFw?;Qh<7n66V6##!3&v*sC8 zH*vNl8yeQoTcMApC39Hs^alN4&%r_7%jzhp+>`kxZUazRp;N0~xW89(V&md%nFQSl z9rs#~RmqML&TqZ*bhkX@#rRTIcKeoA{mXro1&ItMj&6yTk+0jZx?88-=&#F40u~V> z6>_D-`I2A?MaWuLN&tWqawQ_R;QMC??gS(dQ7_A z3=HF2;OImHQE$abt9HaLO?U;~=Tz)3XhG(q)}6okU6CA)aI{e45-VJ~h#U(Ggt|1C|@Bq^_=leC=~w(z*CI{7s19NC+w(_nh&ojq`? zCD_l}>vX)T@LS7{x6`Jpf8x#3#U3xzxz;{f75KN=+n^Q&<$#pIrsiF0$yupWZMJJG zKG@MgyiS+PB54Lv4=dp`&m@d5b(#+PJ1VyU>8H)z8P)Jx)|FG;93HTjBj$uG zhs&R*%l!#g|<%?h9G%Dn1QDuz%FdeL}x*q`c;;ohIkDeYcMc1uPbow3}4 zdb5+%{Qa_b-h`((mYetX15B>;RF&E7ql}hKx{7)jG+`U&q41~iIRYKr-DZ>B9PVwL zO;mr>=*Fwi7h}}irj6d76c-uD))uVO>0ffIPhX|=njiP(w`MCpKKr%bv=T?Zyng&o zism!Uk6%T1-nve3d)$BYlkj(9|6Sww?|^)BQ~0NVoMpZ-=i(1=e6!vEIUMo96lZzJ j{s_spI>diK@~dF3CNJV+`eowtr_cWcs^#wQtPTJGJiHwJ diff --git a/tests/repository_data/client/metadata/current/snapshot.json b/tests/repository_data/client/metadata/current/snapshot.json index 99f5fa0e8c89e5d73de3bb3104b68732484e3d17..6e7cfad7f26abab3704b83816c9cb9d3ba309fd6 100644 GIT binary patch literal 906 zcmb7?-EI^y42AFaDMoWm!TE9G^saA!Sgt?_*^bjLKiz6~KvdPfJDzE|;0hsSMx#t} z&awSH?WXyBxV`7)$H~s~_4IK!O;7$z^H+O3bl;g#M*v7fNGK_B2$G}uTL35_Xyl;? zG#ZZ)GjZsen`))1!KBWwHofn*+#NT;cl(?D{^siKe*f~z6Rbu(X%+%-I~oKAn5=Um zwkDk23t9y$MraUn44NT;19Naz_-caO|DcF5L=Q-NwN zf|QOWYQ~xhH}Mu`i)*T!l1eP4c2MhpF`#hul8AFPQ4r&*R7OdJy;7*?$2?Q^5`s`s zZ&x6XB*krW_a12-U20A~nbMULjFwgEa1&}6hlrq!IuesY4=qMBZlQ$C98rje&#jX; zfktd8j4>)Iw&>|n>t3~KsbUsDDX%kP=qk{l*5KdlLZORjP%P$?K=cL;S?4LCSV$#% zJXnY70CmJtil7!VCunv~F1zWI^Rhu{D=jOG^XKL9!B#}}_xy0aJ1!eIv;BHFJYzdY zNN@$g*L#5LZGCt?_Z3`O^Iibkztn-oQf^3FFf(pfnO0+OQaN#f~2cPFr6%-zH4u@_z$22=%< z#QOaFdSC7i+vWcKgI@nV$7Oqc`2Fs1_>#|I`#V10&)nJA7Xpw73mcIlS}l=p0}x{H zqRSa%4PC2LQJh|fdzbDQ*0*17dLQHZ;dB%H_4wv^eDmV%@%Z&99qf&I5i2gmCM&f7 zD`?$EEE^HCsdt~9icCgzg8(jlNSY%k5*2DCRSyZ6Jo_{-reP}5b}QVG!#ldyuF@ge zoD3CG3uc6(^sZb7Q(yzAWX^`D7&^UaDME9mjJRX~Jc&491ew)2G(cmh4ufH>WQ}@N za*d`T5`D32ljT_42G`(7Yi4alp#eFex-PER+v2(Aid4>$iIt&@!bP}oGjJ=EBc-L% zMkZZ4bJ1{Y)TvcnxW`k4cC%DV85o(N6T9UniboDT$xyrNTu=u^nKq?s(=jQA3>=X+ zZr&_o)(VsvQTCLU2bebwIUlab&OpXFP}tILz|u2??o`^e^dy=Qav1})Vj;{>NrrW9 zqafC`#$MxINJ?8)mrva;IN)4%_*3y1xADa(ei-bJ+fi^X2}wfjg&9&$M3k{64OipQ(Ly UKK&E;gFp5W_YU8;eY$)4ANNr8Hvj+t diff --git a/tests/repository_data/client/metadata/current/snapshot.json.gz b/tests/repository_data/client/metadata/current/snapshot.json.gz index fdb53131c194fec42e8bc1ae4595986573e97d58..f34fa3507d6eac26d6c4800e27d44fd614bed7f9 100644 GIT binary patch literal 548 zcmV+<0^9u`iwFSR0wq=g|CLfrZxk^Mz3;CW&AA0TiSyB0{{dn-fe^ACr`-bGYIi_X z)&6%p+vUIsA$dlT6TkOtzn9H4A9r_;y!<%WalV;8ZKmnPziIwz&%5rKnRFmPtd%NB zjEpELnEwT!Mgk2yl%S5rV}w*0yQZdEYEd*P^VJ#eyDj(oGvNE}?RI;6{cgLxT)BcZ z-=ij}08Up&1p|uKIaanNoSX|<0n1j&08>CsfFLPa?Gdv}sWIsY44f$UtfGMiTGGzB zv{FYzA@Qon)^m;{c~xShF&cF1Mk-Wt3G6rlL$6Yt?hIQBr^v;nG)E5}HKd-SC4xw< zA$I3ToI;0Wy>iS@tObzVDF;ndV&*13!en7pm10y0IhPJ<9Vj3P7jLO@DkchIT(stq zW2Igy7V>AVDR~PhHLH&ct4osXw5j`yw2Uq#Mc+*B$`M9ODrGoHZ4`!xkc~1TSA~u( z1T$`!F);^{%ER~8s}G@$&?1g83MsYV>SF8OwP`M5hM+aAJ7VZ6(4Z9bGdmD=5k;kJ zz6l~9(2#YXg4ri<(!2yo4JE#H^XT&eZIPw(pI;uW}Lq)&rh~mWc$cZ z$NT+qHqLCno_4RcodZO;2K4vD7`8AMR{9{?FUDhy6Fhe^hKPRexWcHmBbO6kTJA0{{Ro0R%7r literal 549 zcmV+=0^0o_iwFRyxD!?a|CLg~j$1bjy!$JRcTQST6eTLR{6SFn)S@Uzib^(Z)(hJ$ z8U*|A<@0V2JrxD?9-tvL8jj}M;kaENK0fO0k4s#)cgLR($K$vBj@xhX^C37}J#n9#k?4zEn=HrTHn;{yN;7LK3Ju5!)pc>j-WJa_S0r<`OsouL z6fVMzn}J)Q94RH0Hqz}ZVBtz}4b3q*x zW!jXkO~<4dGH^t`xOua*Su0R_M0uyQEMUGkk=G7Qg$<1cSsLm7Ko_AqY0-o8G?9+A_dK3zYZ zZ};GA@z>MCOKi82A$SXz>qmfh_x00DQ|3-zd5+uVe7=46`Fei*2RUwk#^w5O2XF_T nUm|(a%g4A~|4;Atm-Fv||8e5*dg|YW=fm^gi|g!#iUR-uwUrN1 diff --git a/tests/repository_data/client/metadata/current/targets.json b/tests/repository_data/client/metadata/current/targets.json index 4cf4c3534b5837a5b21e7142933678f12e7f7bf7..144e44717c6e8a087f042b985989cb225e82dc0b 100644 GIT binary patch delta 528 zcmW-d$&C;|3+i?kkKeyO9oHG7&9X=U zg!mR;YTdO>(&0`bAK)Af6H~SuEXoctkP8WNmI*WNbl1D@oVV64n1>kTB&V71Nib_x) zLy`Bg^6hyG1$lmOFP_~A)5sNr%g-S};5EXrEyH$rN_k&@W}S~3l{n+ovFjs6FZjMN L^yk;NpFjQq66=)O delta 528 zcmWlW*=-O&3`8Xa-)Y6Sy=72>uWch0M4}h^Ni8g&mOP$$mze?4dH78Wx4!0qo1rdihMa;w7UC zY!`eqkvpXoQ)2zn(uTD^eT<%)5<;~^n+%3N8SI`UHgo*> zI}3^9|9%cvGR0j7ta!@phYH~aJHr5U;3k15#0#4Whc_O7AQpETokv&B)ZKgS2p{S< z8_a7YYU>Gj{F-h}n7#*)l=V4_)bJ!#lR%?H)w~OJ!{oj*7>_#~#Sr?T*jwHJqxtzd LpTA!}e*gLhhY^s- diff --git a/tests/repository_data/client/metadata/current/targets.json.gz b/tests/repository_data/client/metadata/current/targets.json.gz index 40165c5d64b77dcb44c6a8420a4ef43525f4d1e0..3a722b6d6e1557023fb163a466f04755bb7cb361 100644 GIT binary patch literal 1235 zcmV;^1T6a>iwFSR0wq=g|E*QYavDhxz3VFouC|BT7vCCzG>A}dR?3|k}+Q`{k6y^h& zaVmo11f0ULzsf|}7}pU|;#i8+oDg6n`U5n=!Bg%%ARMuUAec%bjkd%X>!ZF(C_$Mu zF&$GP5+%yvfPt2y8D7syUp~H>5(>8zW-Ncr_ZbuxxizaXbJc-kSNZxKcUW*!k2Iai5A;wSL*?l&%NWR^zb6&fMz_-_O+uYpjcfK`RO?D5>(L=koFvGO*m_9RK$*Zlctvp?h zDvj%l;r7lZ-Mf5ucPULaO)ge0qYeKo>di|t9JW%K@3)eDNT=)ddUW-ex?ywa=d*9w z_#QXx%0Ek59n6!LN&Ti%O9pLjzwNIEcc?mG-Tu>HdC@!{J5}w^I#Ov-&6NAYmeK__Hh3)iB*hu zjaqV(R3FnrbLySipZzKyoBMipy1yQGuGMmW+BQokuUO+|`6}MxO1|2_JF`svukioF zphuuDKQp9eaj!5JTl4zQS=Gnc-67&Rs8n8z2DvMib6Au=m;N~_mA>OsDnG$y6mg~O zh>lV^&lJ}#1*<)aETv=&AyiT0ZDia90aQgIY)Hvb6Qy;)wFpF(KkoERpcqbf3On7b z+s!^*cDrv|h+w;}tQL|fBdnAhSR~R2%B+!|P(zs_CPl?jw^funt!zwzMeVqP5F+&? z_^3_bR8mCcLS$a4=pz@B07ODV;v~q#Z8Tca#1u<}tF-mcQRYcZCTK~F1fn^YjyuPr za6(cCOafT2Jfd>0rFY&@Cj^aFTJ#g0Pu_4Tg7?x`7YPPKGDs{TG(!_5+MPEVat{+8OvXb%?!2_xpwmb6pI~i$`QseaK&hvFWv-q ztUdf7bp;VoC{F})41r_5LlB-BBpn6mHV{4Dz&qEyL8#BH0vj&M(u95_>i~i^-BFQYgYzO&%>wD znO&1gc2zsgvWt`K%4FO9lR*Q&YuR%nUU&LApER#ddx@~lY&m$y*Zo>gY|aY^!+Nvn zRo>Dcmh-f3f=ar!XrEM1QStSqUNx3in{{m7&4=sz#@&6ZI&uBH{*XV5cwAJPD}VfW zc6VIAJngS;e0FhDtZ&Y=n~k#5d6@Bfd{(u_nd|qPxh^&5yLAA8f&#>pUT)Nru2>c4*<}zGQ7-2`;k#*gp*H%;(blX=iPn3uh2Ibhw9wfqdR+t8V$=uF`c}tw5`n78O$fh zvbWkUD<$>__i->gDuZ-gl0$-yQi6NI2F47><*LkZ3q0f$iEv0}6W| zmzGg#G55Ho!C9P}1f-&nJdT*)to=7g_UJ7!`<$#`zKz&EWv@rCKLAm^H-X*<004$p BWpw}m diff --git a/tests/repository_data/client/metadata/current/targets/role1.json b/tests/repository_data/client/metadata/current/targets/role1.json index 7dfe6f87faada4a8fb0fbbf655732985da4da3f0..0c99c8599fab18c19a7923879fc9fe49e586622f 100644 GIT binary patch delta 527 zcmWlW%Z(5~3_}GRww?SYlU(T}-!`@Zkm!XzsEOHQBqz^y{(b!W`1AXdd$CImj^LJG z08^0L$I8oGhdjKxMq)tB+=N8h<65CdMxLs-wKX0hiwdJ9bFt=B>U1vQs`ts3R_aZT z18p(`_Gv3Jr00P0uA>uXVk#u=%yQJWTnU2OdJ_mZVDL3O@I}m9N~#GdJT)wjoaHl4 znnt=#3425G;B!AI!cVjn07{>Le7fU$&*6JTFiw`d@_yg%u@=zd&%c$M!aZjuZUAj6`I7d_5yzW#~#!mOh*#&&LB(I*|tmhxJQ2| L)!$#=e}4N9x{#D; delta 527 zcmWlV$&C;&3`GSTrX9z7u5{wHjcEWRdZCZ0i6CF%$jSG6Umsr|e|~?4tQhs=j!AJE zHtm)jy&C7B(>aQ{N3h)!cLf|Y%DbUg-KSC_T>v&RfY&l znLNc?^mA$}0g49$QA-XWO_Qb>A(&rkiw3~#Cd?8_Yd+ly%s!y>K5Drcf#v&9x-|B1 z$ICW0z`LLdnQS}>*T>-jzq90LE>H+s$#`5|DdatVnjtnCsi_V}0l)kG-`r;@s-1&e z_X(k~$I0-BXEA+Vv!IIbII6*x8KrfVZ3f|}9 zl@Te96(vR)IPQbQmZPuH42AbOMXT2Yxm-VXWtSTuCMys^w#!8Zlw_0{5K%OD$31}sQgy3kyI8H}os5N9vz4Ayf#<|A5^E;b4fkezL_wa@tLoM=_aWFaLBTl*sATLc z;vO@LN>3q!bCuiAs%*;+1*IMnNwOR>OJ=9ukewKppZ((kn!~RCm+D zMIcETK4ayUV$HS&C+Dl>OYm}m(l=T*7}u|-mxFJJ-0dIyc#?<1g`CxYANFtDt_T^f zA;q}^xVfB9Z$$x~{1pn_M>!ntpHA<;ADZE~Pr1n{Qs5*10(=~WNSxmE6 zv8)<(#o{2m8cj3bUS0HV<8*U>5q!UYyWhXPez)KMZDoS9Q6X+p5zWdNP*cvPTXOPzwfmgBT%e3IEoT_FFQ>TdfLKaPh>0nV^yirw=W+y5KSSI>U{)al!~ diff --git a/tests/repository_data/client/metadata/current/timestamp.json.gz b/tests/repository_data/client/metadata/current/timestamp.json.gz index de5e0569767478c8a8387279ecac0cb8e0df35b7..04940c9c2c354a0aeaec5f085e166a1e546f40f2 100644 GIT binary patch literal 533 zcmV+w0_y!AiwFSR0wq=g|5Z}UZW}QSy!$JRJtqZ4JxJx2KPc*)S`-CIQH@(9F=Dr9 z5cuEAb<#rt?P4*cW{1Oh-E5cR{`;exem(nfx!Hc%Y`3rRY?q(@vLAsgQfe*5a1_gp z8QXy7y(Gwk|_Okx(aNVb*M=Csp7%ag4;;iBzcx&q2*c z$kN(iRAtayJlS*+XwwSkjNVjQlSWVnK}+6}mT7LC*=Ejzs^?Z>dX9g~q@;sVm#lMY zFdGFUA`T)fq~x_TqF7Cqb0zBGp38Kctr0@2Cc|sSl0XON3O&YakW5oCsoiQOv1abI z1d~UD)mpprY!HpZrR8ayFnqS)3H8ZiHQK0Rtfhrn!rWV~F=o_fLaX84jE`u@b9z-> zC37EwEfW-+lYmOb&LZwHv#9ih;+0jXgHThf158Shu$-vWP!zAsQ`Qvp!eBM5x5z`H zF&d~t9!YwIz)N*EEnEbWl;Ja0ZYkDmON8OP*?zrx52deZS;M$|JG~ryjmX{p!H*|- zI9!sm_@Bf6Ew>9ohHFT1?f`Bs=kr@r1W(?Hg7;Al$NQ(#habnM$A9Q{`R&ig{RK!X Xa9)*MF8^0JZ_a-KYZyb!F#`Yq!uSak literal 530 zcmV+t0`2`DiwFRyxD!?a|5Z}KZd5T0yzf^$y>pA=*op1l`Ui;Z351ZHICMd6SKS3s zRr~K`x8;E396U~D#>%{Imi4gv{%FTv&v969mM@!Sd7Wojf5ywMPh>SJg{b9D4Xa45 zJ=IWk=9If@aDz0Mdd*@JHL8lqL3kAEX1-os_#WbT|8ybzw7uPKZ?8XYx9_9r;EX5Y zA{o&vwFY^_)E$fpwbBS?7tvx;iaF|ZlY>h%&w?C98}x!jqIBt1lIJKf2^*nQ(ZpI^ zH9{J3wWM6Nj+C@fVZdb3qvbYNL`}VFqTD-Y;@&!T2(ap5G@7g7$g`Q6QU)Mq%9fh} zU@@@30>!09RA^bHiH{6vic3{X0DAUC5EU8-$n&x?SVG?tl5OgI}E96~G zMcFDr$AQyX9(jymy(P_5@-PZ;9w-s_meNR|=FKiq$h%kEse)*eK3D z8VB=m&$YK7jEpIjHQFculUDVaN9TYu*Maq3Ig*Zgu7gCP2JtkF3nxq-+d-St!)qsF z%`T#zrp#&cWYkHHy9s!pum#spNlh#mM^Y*d!<^h@V+t-Ppp6|ERc&%$0C#vS!YSNJ zApJD|2b-)2V!2kYFlOr5y$!f@rJjoYD5&sU0D5SJLuwfJF>yIjFis!jIrlza5luqu zSwenRp#>0U=qVWkO?WG6oqHWG_LBJVeDUV>-Is@Ye7wE?{J}om+&|oY{do6y{r2kJ V%d4xa>zi-a=Y0R`_r>2={{TX>p=SU9 delta 532 zcmWlW$*mAb3d?c* zs1sI~4<|!_Fd)Jt-09Y?1F(ERhw@SiJgLbxLFXXZELT4_K;3*l+{A0>5hkC^pa;{* ztuAz*GzWmLw_OtoE)nZjf zftjgMZc|Aq^BJN7NyIR-6hOe&*jG#l>N5obf&hXykn6nh&7wGb)6o5rWFbEC?{*HueQa5+JNafC-aSN}w$4bLrdA z;)tq1Qf-4kSqub$#aMumPb`aC3~7k~N1SM6e&bsN0*(b#!oX7y*jxraK|~8dh`@Fr zd>aXb0swt;o|MvNfiF4oA<=+CrKyj(^)d*RFMz^<4KxH811SS$+tQ#2d`eWHfELKN zV1Ss?W(;%yEHhRHtJEw20W#JDW)LC5h2>L$6{Hr$z?K%*IFP(L+wLfDJ_V>VnN%NCZUiL ztU0oXrZ2ZB=FlP{U`#V#V2reHYg<@YaTahBUug_*f+@2sZom@k69oj;<_t-UBhWX| zKr*1VZSvr2L=cvNB1oF8mL?X2)aNF|(1(Ixn?tV5ItMl*e$9~K^QX5Tzxzr`^}yT{WSv~d=D)1cmVwH@$9~wXRbr>70z3Y_}T&h**39>L`Ybc zGW-fG!R zo+AIX>-*QI*>T+Q+jQ9+x~;cCXZta^Pe!iWbdQa3^AKOcICA&{G#j%{x6zoi8Z*1` zT5lZ&X54A;&844)n+K-X$zT~xM%DDWPfqP?zBpwiS`T}$>MpWu4AjuQG|M(V$0U*E zdN8RfM_p&z!<~1lvNOjGd3G>}+gy741Rl?*dLC0RX{DpOHM_Dn+?~wwwfn+pu(W;UescrybnV?@#wbfEk`}RIPL}`d$Z~-Efqe!s6n|( z3*1PH(ZF&vJLUkxZl$B9R~{}aD z`~RmE>sw?eRC_kf!&xyuLjEuzn1+$;)!QlI&%%R<(_#JAIxvn7yOYStV>gDXw+I;| z{&==7y@ue!hf_ao#9(xbXZ3wz8NGPpsjRn72Z zePE%y`SsnggNO*PrqF5cL75@8G4|D$MojEOb=7Z)113pz~3;+*K?U*7`BJE|1%DUd(S*{dns7 za(sL4GB+0P!h~DEZQ%HQt8v^XX!h4>B>)Rdd6SSLW^{=&2?MCLwrv1Rk+h|$9OzX= zEE)hKQ(6nl4op!pq{2QRe*;=wy68CwM!Z`thQka@qlat42`9YOjpA-I#U@R72YPGD zPdB(B>sj|{zdJ)7pHpjg+~t$U)=Rf-1#Ol@ft17IJvVA~x*&NQlgD%vwJk@k4$k;J ziIX_4?tBpUjTm zMqZo!R7=-+tllThz^x{vat}>(YsOb8FU{4lFYU>ZPNIFX-4=1j75=pDE%QuR(XB<3 z$@oY`=#p{LIbLBCzh^_S^gGitMsa-YdqP%8tjD{}M3^`vqhoT|+IPDn$HQjk&@CHX zc8|w!haZXXCW(2AuUG0CalRr#=IjTV@W%Tj~J3SFv2wolpz zkKw`@wB8x)xf82X#8bTL#DNYeo6aY_)){R3-gfARyz13&J-0ZQ<*gd$^pL{weV;hf z#r(1DOc5UTPv=WcIj`d5V)J0!d%r@AW7Um@_M*G?PV4im^rr0~sgL2^G;@vfeZ5bf zi>`n4PPV^tozl52Vex+N<;?_C>7>tguG8pNRm*9d!I7@+&OLsX=vC!5vCug;$ba=1WBc2WXjO7}%I)k|BoVKIu!Mj9}IQ04@SlsEQA+pD- zj8+}G3q}}ptiF|d+*wA~01WUj%ofMBb<$C`kmFf<80|cV&tWG_+M^RGZo(Da5bTrX zUs9`2U#V5g)06tG*~-(8e(g7|M8U6p-v6R#e%Sf(E9tJG=>)^{^lCk=-;w=yjpM%q z^36@*p8|3f`9WQaKfv+La{uRWL}yi8#X0*UB;TqK{{hLbgt@Ey^+({#!skz){|VW_ JMcLO5003`WA0hw% literal 2053 zcmV+g2>SOQiwFRyxD!?a|IJs~j^j2KeebU*)T@aXpaCAx7Hh+#q?#EJ5f6BX7>YHb6d*`4$Mx#hsErHcYAnR!%7$<}nqL3#nrwqY} zD5)sa9+Ylhff3Tc;~GYu#VWFfGHZmj1ab^9rjSZ(1SCQcY)!!cbFdV8n>f8X1O=0hUD?BEcBKC<2jH`-4z1x!}D5w z3F0(feBI;am1qlr6AvexD9@7+x>Oizc_F`u{5g7+`(IbRyk-}*kM+l@?>|1ZrXTcQhSOR%sK0cY%je)a?FB(C*j4+r zO>){Mu`h0*Rvk54)#{*L9r5bNe0|e#&&{e>oZ83j;)dyY(3!@AUOBw4(|zNdPxjfv zn|I$)*_vcoADC`%sy!O`kdRb8=AA)V`sO@ZZmyzLKAK}(RRv3jpGjzrvx z&>1rARxb4o6W(UEmpQqu`pCRbwhR&7AI%?ORf_J-uk07H)4L?2$~xtc(Ryc$+N8L8 z4O8a+?Bd<|44$@+_T#}sYu{guXUlMQ>dR$$ZI}0AmbQl3hFM-?^>u1(e44j&u}HGh zZM?5%_v3MKz}sxUVi%K;&IQ4GIl_bahHceFS5~{aUuFw?;Qh<7n66V6##!3&v*sC8 zH*vNl8yeQoTcMApC39Hs^alN4&%r_7%jzhp+>`kxZUazRp;N0~xW89(V&md%nFQSl z9rs#~RmqML&TqZ*bhkX@#rRTIcKeoA{mXro1&ItMj&6yTk+0jZx?88-=&#F40u~V> z6>_D-`I2A?MaWuLN&tWqawQ_R;QMC??gS(dQ7_A z3=HF2;OImHQE$abt9HaLO?U;~=Tz)3XhG(q)}6okU6CA)aI{e45-VJ~h#U(Ggt|1C|@Bq^_=leC=~w(z*CI{7s19NC+w(_nh&ojq`? zCD_l}>vX)T@LS7{x6`Jpf8x#3#U3xzxz;{f75KN=+n^Q&<$#pIrsiF0$yupWZMJJG zKG@MgyiS+PB54Lv4=dp`&m@d5b(#+PJ1VyU>8H)z8P)Jx)|FG;93HTjBj$uG zhs&R*%l!#g|<%?h9G%Dn1QDuz%FdeL}x*q`c;;ohIkDeYcMc1uPbow3}4 zdb5+%{Qa_b-h`((mYetX15B>;RF&E7ql}hKx{7)jG+`U&q41~iIRYKr-DZ>B9PVwL zO;mr>=*Fwi7h}}irj6d76c-uD))uVO>0ffIPhX|=njiP(w`MCpKKr%bv=T?Zyng&o zism!Uk6%T1-nve3d)$BYlkj(9|6Sww?|^)BQ~0NVoMpZ-=i(1=e6!vEIUMo96lZzJ j{s_spI>diK@~dF3CNJV+`eowtr_cWcs^#wQtPTJGJiHwJ diff --git a/tests/repository_data/client/metadata/previous/snapshot.json b/tests/repository_data/client/metadata/previous/snapshot.json index 99f5fa0e8c89e5d73de3bb3104b68732484e3d17..6e7cfad7f26abab3704b83816c9cb9d3ba309fd6 100644 GIT binary patch literal 906 zcmb7?-EI^y42AFaDMoWm!TE9G^saA!Sgt?_*^bjLKiz6~KvdPfJDzE|;0hsSMx#t} z&awSH?WXyBxV`7)$H~s~_4IK!O;7$z^H+O3bl;g#M*v7fNGK_B2$G}uTL35_Xyl;? zG#ZZ)GjZsen`))1!KBWwHofn*+#NT;cl(?D{^siKe*f~z6Rbu(X%+%-I~oKAn5=Um zwkDk23t9y$MraUn44NT;19Naz_-caO|DcF5L=Q-NwN zf|QOWYQ~xhH}Mu`i)*T!l1eP4c2MhpF`#hul8AFPQ4r&*R7OdJy;7*?$2?Q^5`s`s zZ&x6XB*krW_a12-U20A~nbMULjFwgEa1&}6hlrq!IuesY4=qMBZlQ$C98rje&#jX; zfktd8j4>)Iw&>|n>t3~KsbUsDDX%kP=qk{l*5KdlLZORjP%P$?K=cL;S?4LCSV$#% zJXnY70CmJtil7!VCunv~F1zWI^Rhu{D=jOG^XKL9!B#}}_xy0aJ1!eIv;BHFJYzdY zNN@$g*L#5LZGCt?_Z3`O^Iibkztn-oQf^3FFf(pfnO0+OQaN#f~2cPFr6%-zH4u@_z$22=%< z#QOaFdSC7i+vWcKgI@nV$7Oqc`2Fs1_>#|I`#V10&)nJA7Xpw73mcIlS}l=p0}x{H zqRSa%4PC2LQJh|fdzbDQ*0*17dLQHZ;dB%H_4wv^eDmV%@%Z&99qf&I5i2gmCM&f7 zD`?$EEE^HCsdt~9icCgzg8(jlNSY%k5*2DCRSyZ6Jo_{-reP}5b}QVG!#ldyuF@ge zoD3CG3uc6(^sZb7Q(yzAWX^`D7&^UaDME9mjJRX~Jc&491ew)2G(cmh4ufH>WQ}@N za*d`T5`D32ljT_42G`(7Yi4alp#eFex-PER+v2(Aid4>$iIt&@!bP}oGjJ=EBc-L% zMkZZ4bJ1{Y)TvcnxW`k4cC%DV85o(N6T9UniboDT$xyrNTu=u^nKq?s(=jQA3>=X+ zZr&_o)(VsvQTCLU2bebwIUlab&OpXFP}tILz|u2??o`^e^dy=Qav1})Vj;{>NrrW9 zqafC`#$MxINJ?8)mrva;IN)4%_*3y1xADa(ei-bJ+fi^X2}wfjg&9&$M3k{64OipQ(Ly UKK&E;gFp5W_YU8;eY$)4ANNr8Hvj+t diff --git a/tests/repository_data/client/metadata/previous/snapshot.json.gz b/tests/repository_data/client/metadata/previous/snapshot.json.gz index fdb53131c194fec42e8bc1ae4595986573e97d58..f34fa3507d6eac26d6c4800e27d44fd614bed7f9 100644 GIT binary patch literal 548 zcmV+<0^9u`iwFSR0wq=g|CLfrZxk^Mz3;CW&AA0TiSyB0{{dn-fe^ACr`-bGYIi_X z)&6%p+vUIsA$dlT6TkOtzn9H4A9r_;y!<%WalV;8ZKmnPziIwz&%5rKnRFmPtd%NB zjEpELnEwT!Mgk2yl%S5rV}w*0yQZdEYEd*P^VJ#eyDj(oGvNE}?RI;6{cgLxT)BcZ z-=ij}08Up&1p|uKIaanNoSX|<0n1j&08>CsfFLPa?Gdv}sWIsY44f$UtfGMiTGGzB zv{FYzA@Qon)^m;{c~xShF&cF1Mk-Wt3G6rlL$6Yt?hIQBr^v;nG)E5}HKd-SC4xw< zA$I3ToI;0Wy>iS@tObzVDF;ndV&*13!en7pm10y0IhPJ<9Vj3P7jLO@DkchIT(stq zW2Igy7V>AVDR~PhHLH&ct4osXw5j`yw2Uq#Mc+*B$`M9ODrGoHZ4`!xkc~1TSA~u( z1T$`!F);^{%ER~8s}G@$&?1g83MsYV>SF8OwP`M5hM+aAJ7VZ6(4Z9bGdmD=5k;kJ zz6l~9(2#YXg4ri<(!2yo4JE#H^XT&eZIPw(pI;uW}Lq)&rh~mWc$cZ z$NT+qHqLCno_4RcodZO;2K4vD7`8AMR{9{?FUDhy6Fhe^hKPRexWcHmBbO6kTJA0{{Ro0R%7r literal 549 zcmV+=0^0o_iwFRyxD!?a|CLg~j$1bjy!$JRcTQST6eTLR{6SFn)S@Uzib^(Z)(hJ$ z8U*|A<@0V2JrxD?9-tvL8jj}M;kaENK0fO0k4s#)cgLR($K$vBj@xhX^C37}J#n9#k?4zEn=HrTHn;{yN;7LK3Ju5!)pc>j-WJa_S0r<`OsouL z6fVMzn}J)Q94RH0Hqz}ZVBtz}4b3q*x zW!jXkO~<4dGH^t`xOua*Su0R_M0uyQEMUGkk=G7Qg$<1cSsLm7Ko_AqY0-o8G?9+A_dK3zYZ zZ};GA@z>MCOKi82A$SXz>qmfh_x00DQ|3-zd5+uVe7=46`Fei*2RUwk#^w5O2XF_T nUm|(a%g4A~|4;Atm-Fv||8e5*dg|YW=fm^gi|g!#iUR-uwUrN1 diff --git a/tests/repository_data/client/metadata/previous/targets.json b/tests/repository_data/client/metadata/previous/targets.json index 4cf4c3534b5837a5b21e7142933678f12e7f7bf7..144e44717c6e8a087f042b985989cb225e82dc0b 100644 GIT binary patch delta 528 zcmW-d$&C;|3+i?kkKeyO9oHG7&9X=U zg!mR;YTdO>(&0`bAK)Af6H~SuEXoctkP8WNmI*WNbl1D@oVV64n1>kTB&V71Nib_x) zLy`Bg^6hyG1$lmOFP_~A)5sNr%g-S};5EXrEyH$rN_k&@W}S~3l{n+ovFjs6FZjMN L^yk;NpFjQq66=)O delta 528 zcmWlW*=-O&3`8Xa-)Y6Sy=72>uWch0M4}h^Ni8g&mOP$$mze?4dH78Wx4!0qo1rdihMa;w7UC zY!`eqkvpXoQ)2zn(uTD^eT<%)5<;~^n+%3N8SI`UHgo*> zI}3^9|9%cvGR0j7ta!@phYH~aJHr5U;3k15#0#4Whc_O7AQpETokv&B)ZKgS2p{S< z8_a7YYU>Gj{F-h}n7#*)l=V4_)bJ!#lR%?H)w~OJ!{oj*7>_#~#Sr?T*jwHJqxtzd LpTA!}e*gLhhY^s- diff --git a/tests/repository_data/client/metadata/previous/targets.json.gz b/tests/repository_data/client/metadata/previous/targets.json.gz index 40165c5d64b77dcb44c6a8420a4ef43525f4d1e0..3a722b6d6e1557023fb163a466f04755bb7cb361 100644 GIT binary patch literal 1235 zcmV;^1T6a>iwFSR0wq=g|E*QYavDhxz3VFouC|BT7vCCzG>A}dR?3|k}+Q`{k6y^h& zaVmo11f0ULzsf|}7}pU|;#i8+oDg6n`U5n=!Bg%%ARMuUAec%bjkd%X>!ZF(C_$Mu zF&$GP5+%yvfPt2y8D7syUp~H>5(>8zW-Ncr_ZbuxxizaXbJc-kSNZxKcUW*!k2Iai5A;wSL*?l&%NWR^zb6&fMz_-_O+uYpjcfK`RO?D5>(L=koFvGO*m_9RK$*Zlctvp?h zDvj%l;r7lZ-Mf5ucPULaO)ge0qYeKo>di|t9JW%K@3)eDNT=)ddUW-ex?ywa=d*9w z_#QXx%0Ek59n6!LN&Ti%O9pLjzwNIEcc?mG-Tu>HdC@!{J5}w^I#Ov-&6NAYmeK__Hh3)iB*hu zjaqV(R3FnrbLySipZzKyoBMipy1yQGuGMmW+BQokuUO+|`6}MxO1|2_JF`svukioF zphuuDKQp9eaj!5JTl4zQS=Gnc-67&Rs8n8z2DvMib6Au=m;N~_mA>OsDnG$y6mg~O zh>lV^&lJ}#1*<)aETv=&AyiT0ZDia90aQgIY)Hvb6Qy;)wFpF(KkoERpcqbf3On7b z+s!^*cDrv|h+w;}tQL|fBdnAhSR~R2%B+!|P(zs_CPl?jw^funt!zwzMeVqP5F+&? z_^3_bR8mCcLS$a4=pz@B07ODV;v~q#Z8Tca#1u<}tF-mcQRYcZCTK~F1fn^YjyuPr za6(cCOafT2Jfd>0rFY&@Cj^aFTJ#g0Pu_4Tg7?x`7YPPKGDs{TG(!_5+MPEVat{+8OvXb%?!2_xpwmb6pI~i$`QseaK&hvFWv-q ztUdf7bp;VoC{F})41r_5LlB-BBpn6mHV{4Dz&qEyL8#BH0vj&M(u95_>i~i^-BFQYgYzO&%>wD znO&1gc2zsgvWt`K%4FO9lR*Q&YuR%nUU&LApER#ddx@~lY&m$y*Zo>gY|aY^!+Nvn zRo>Dcmh-f3f=ar!XrEM1QStSqUNx3in{{m7&4=sz#@&6ZI&uBH{*XV5cwAJPD}VfW zc6VIAJngS;e0FhDtZ&Y=n~k#5d6@Bfd{(u_nd|qPxh^&5yLAA8f&#>pUT)Nru2>c4*<}zGQ7-2`;k#*gp*H%;(blX=iPn3uh2Ibhw9wfqdR+t8V$=uF`c}tw5`n78O$fh zvbWkUD<$>__i->gDuZ-gl0$-yQi6NI2F47><*LkZ3q0f$iEv0}6W| zmzGg#G55Ho!C9P}1f-&nJdT*)to=7g_UJ7!`<$#`zKz&EWv@rCKLAm^H-X*<004$p BWpw}m diff --git a/tests/repository_data/client/metadata/previous/targets/role1.json b/tests/repository_data/client/metadata/previous/targets/role1.json index 7dfe6f87faada4a8fb0fbbf655732985da4da3f0..0c99c8599fab18c19a7923879fc9fe49e586622f 100644 GIT binary patch delta 527 zcmWlW%Z(5~3_}GRww?SYlU(T}-!`@Zkm!XzsEOHQBqz^y{(b!W`1AXdd$CImj^LJG z08^0L$I8oGhdjKxMq)tB+=N8h<65CdMxLs-wKX0hiwdJ9bFt=B>U1vQs`ts3R_aZT z18p(`_Gv3Jr00P0uA>uXVk#u=%yQJWTnU2OdJ_mZVDL3O@I}m9N~#GdJT)wjoaHl4 znnt=#3425G;B!AI!cVjn07{>Le7fU$&*6JTFiw`d@_yg%u@=zd&%c$M!aZjuZUAj6`I7d_5yzW#~#!mOh*#&&LB(I*|tmhxJQ2| L)!$#=e}4N9x{#D; delta 527 zcmWlV$&C;&3`GSTrX9z7u5{wHjcEWRdZCZ0i6CF%$jSG6Umsr|e|~?4tQhs=j!AJE zHtm)jy&C7B(>aQ{N3h)!cLf|Y%DbUg-KSC_T>v&RfY&l znLNc?^mA$}0g49$QA-XWO_Qb>A(&rkiw3~#Cd?8_Yd+ly%s!y>K5Drcf#v&9x-|B1 z$ICW0z`LLdnQS}>*T>-jzq90LE>H+s$#`5|DdatVnjtnCsi_V}0l)kG-`r;@s-1&e z_X(k~$I0-BXEA+Vv!IIbII6*x8KrfVZ3f|}9 zl@Te96(vR)IPQbQmZPuH42AbOMXT2Yxm-VXWtSTuCMys^w#!8Zlw_0{5K%OD$31}sQgy3kyI8H}os5N9vz4Ayf#<|A5^E;b4fkezL_wa@tLoM=_aWFaLBTl*sATLc z;vO@LN>3q!bCuiAs%*;+1*IMnNwOR>OJ=9ukewKppZ((kn!~RCm+D zMIcETK4ayUV$HS&C+Dl>OYm}m(l=T*7}u|-mxFJJ-0dIyc#?<1g`CxYANFtDt_T^f zA;q}^xVfB9Z$$x~{1pn_M>!ntpHA<;ADZE~Pr1n{Qs5*10(=~WNSxmE6 zv8)<(#o{2m8cj3bUS0HV<8*U>5q!UYyWhXPez)KMZDoS9Q6X+p5zWdNP*cvPTXOPzwfmgBT%e3IEoT_FFQ>TdfLKaPh>0nV^yirw=W+y5KSSI>U{)al!~ diff --git a/tests/repository_data/client/metadata/previous/timestamp.json.gz b/tests/repository_data/client/metadata/previous/timestamp.json.gz index de5e0569767478c8a8387279ecac0cb8e0df35b7..04940c9c2c354a0aeaec5f085e166a1e546f40f2 100644 GIT binary patch literal 533 zcmV+w0_y!AiwFSR0wq=g|5Z}UZW}QSy!$JRJtqZ4JxJx2KPc*)S`-CIQH@(9F=Dr9 z5cuEAb<#rt?P4*cW{1Oh-E5cR{`;exem(nfx!Hc%Y`3rRY?q(@vLAsgQfe*5a1_gp z8QXy7y(Gwk|_Okx(aNVb*M=Csp7%ag4;;iBzcx&q2*c z$kN(iRAtayJlS*+XwwSkjNVjQlSWVnK}+6}mT7LC*=Ejzs^?Z>dX9g~q@;sVm#lMY zFdGFUA`T)fq~x_TqF7Cqb0zBGp38Kctr0@2Cc|sSl0XON3O&YakW5oCsoiQOv1abI z1d~UD)mpprY!HpZrR8ayFnqS)3H8ZiHQK0Rtfhrn!rWV~F=o_fLaX84jE`u@b9z-> zC37EwEfW-+lYmOb&LZwHv#9ih;+0jXgHThf158Shu$-vWP!zAsQ`Qvp!eBM5x5z`H zF&d~t9!YwIz)N*EEnEbWl;Ja0ZYkDmON8OP*?zrx52deZS;M$|JG~ryjmX{p!H*|- zI9!sm_@Bf6Ew>9ohHFT1?f`Bs=kr@r1W(?Hg7;Al$NQ(#habnM$A9Q{`R&ig{RK!X Xa9)*MF8^0JZ_a-KYZyb!F#`Yq!uSak literal 530 zcmV+t0`2`DiwFRyxD!?a|5Z}KZd5T0yzf^$y>pA=*op1l`Ui;Z351ZHICMd6SKS3s zRr~K`x8;E396U~D#>%{Imi4gv{%FTv&v969mM@!Sd7Wojf5ywMPh>SJg{b9D4Xa45 zJ=IWk=9If@aDz0Mdd*@JHL8lqL3kAEX1-os_#WbT|8ybzw7uPKZ?8XYx9_9r;EX5Y zA{o&vwFY^_)E$fpwbBS?7tvx;iaF|ZlY>h%&w?C98}x!jqIBt1lIJKf2^*nQ(ZpI^ zH9{J3wWM6Nj+C@fVZdb3qvbYNL`}VFqTD-Y;@&!T2(ap5G@7g7$g`Q6QU)Mq%9fh} zU@@@30>!09RA^bHiH{6vic3{X0DAUC5EU8-$n&x?SVG?tl5OgI}E96~G zMcFDr$AQyX9(jymy(P_5@-PZ;9w-s_meNR|=FKiq$h%kEse)*eK3D z8VB=m&$YK7jEpIjHQFculUDVaN9TYu*Maq3Ig*Zgu7gCP2JtkF3nxq-+d-St!)qsF z%`T#zrp#&cWYkHHy9s!pum#spNlh#mM^Y*d!<^h@V+t-Ppp6|ERc&%$0C#vS!YSNJ zApJD|2b-)2V!2kYFlOr5y$!f@rJjoYD5&sU0D5SJLuwfJF>yIjFis!jIrlza5luqu zSwenRp#>0U=qVWkO?WG6oqHWG_LBJVeDUV>-Is@Ye7wE?{J}om+&|oY{do6y{r2kJ V%d4xa>zi-a=Y0R`_r>2={{TX>p=SU9 delta 532 zcmWlW$*mAb3d?c* zs1sI~4<|!_Fd)Jt-09Y?1F(ERhw@SiJgLbxLFXXZELT4_K;3*l+{A0>5hkC^pa;{* ztuAz*GzWmLw_OtoE)nZjf zftjgMZc|Aq^BJN7NyIR-6hOe&*jG#l>N5obf&hXykn6nh&7wGb)6o5rWFbEC?{*HueQa5+JNafC-aSN}w$4bLrdA z;)tq1Qf-4kSqub$#aMumPb`aC3~7k~N1SM6e&bsN0*(b#!oX7y*jxraK|~8dh`@Fr zd>aXb0swt;o|MvNfiF4oA<=+CrKyj(^)d*RFMz^<4KxH811SS$+tQ#2d`eWHfELKN zV1Ss?W(;%yEHhRHtJEw20W#JDW)LC5h2>L$6{Hr$z?K%*IFP(L+wLfDJ_V>VnN%NCZUiL ztU0oXrZ2ZB=FlP{U`#V#V2reHYg<@YaTahBUug_*f+@2sZom@k69oj;<_t-UBhWX| zKr*1VZSvr2L=cvNB1oF8mL?X2)aNF|(1(Ixn?tV5ItMl*e$9~K^QX5Tzxzr`^}yT{WSv~d=D)1cmVwH@$9~wXRbr>70z3Y_}T&h**39>L`Ybc zGW-fG!R zo+AIX>-*QI*>T+Q+jQ9+x~;cCXZta^Pe!iWbdQa3^AKOcICA&{G#j%{x6zoi8Z*1` zT5lZ&X54A;&844)n+K-X$zT~xM%DDWPfqP?zBpwiS`T}$>MpWu4AjuQG|M(V$0U*E zdN8RfM_p&z!<~1lvNOjGd3G>}+gy741Rl?*dLC0RX{DpOHM_Dn+?~wwwfn+pu(W;UescrybnV?@#wbfEk`}RIPL}`d$Z~-Efqe!s6n|( z3*1PH(ZF&vJLUkxZl$B9R~{}aD z`~RmE>sw?eRC_kf!&xyuLjEuzn1+$;)!QlI&%%R<(_#JAIxvn7yOYStV>gDXw+I;| z{&==7y@ue!hf_ao#9(xbXZ3wz8NGPpsjRn72Z zePE%y`SsnggNO*PrqF5cL75@8G4|D$MojEOb=7Z)113pz~3;+*K?U*7`BJE|1%DUd(S*{dns7 za(sL4GB+0P!h~DEZQ%HQt8v^XX!h4>B>)Rdd6SSLW^{=&2?MCLwrv1Rk+h|$9OzX= zEE)hKQ(6nl4op!pq{2QRe*;=wy68CwM!Z`thQka@qlat42`9YOjpA-I#U@R72YPGD zPdB(B>sj|{zdJ)7pHpjg+~t$U)=Rf-1#Ol@ft17IJvVA~x*&NQlgD%vwJk@k4$k;J ziIX_4?tBpUjTm zMqZo!R7=-+tllThz^x{vat}>(YsOb8FU{4lFYU>ZPNIFX-4=1j75=pDE%QuR(XB<3 z$@oY`=#p{LIbLBCzh^_S^gGitMsa-YdqP%8tjD{}M3^`vqhoT|+IPDn$HQjk&@CHX zc8|w!haZXXCW(2AuUG0CalRr#=IjTV@W%Tj~J3SFv2wolpz zkKw`@wB8x)xf82X#8bTL#DNYeo6aY_)){R3-gfARyz13&J-0ZQ<*gd$^pL{weV;hf z#r(1DOc5UTPv=WcIj`d5V)J0!d%r@AW7Um@_M*G?PV4im^rr0~sgL2^G;@vfeZ5bf zi>`n4PPV^tozl52Vex+N<;?_C>7>tguG8pNRm*9d!I7@+&OLsX=vC!5vCug;$ba=1WBc2WXjO7}%I)k|BoVKIu!Mj9}IQ04@SlsEQA+pD- zj8+}G3q}}ptiF|d+*wA~01WUj%ofMBb<$C`kmFf<80|cV&tWG_+M^RGZo(Da5bTrX zUs9`2U#V5g)06tG*~-(8e(g7|M8U6p-v6R#e%Sf(E9tJG=>)^{^lCk=-;w=yjpM%q z^36@*p8|3f`9WQaKfv+La{uRWL}yi8#X0*UB;TqK{{hLbgt@Ey^+({#!skz){|VW_ JMcLO5003`WA0hw% literal 2053 zcmV+g2>SOQiwFRyxD!?a|IJs~j^j2KeebU*)T@aXpaCAx7Hh+#q?#EJ5f6BX7>YHb6d*`4$Mx#hsErHcYAnR!%7$<}nqL3#nrwqY} zD5)sa9+Ylhff3Tc;~GYu#VWFfGHZmj1ab^9rjSZ(1SCQcY)!!cbFdV8n>f8X1O=0hUD?BEcBKC<2jH`-4z1x!}D5w z3F0(feBI;am1qlr6AvexD9@7+x>Oizc_F`u{5g7+`(IbRyk-}*kM+l@?>|1ZrXTcQhSOR%sK0cY%je)a?FB(C*j4+r zO>){Mu`h0*Rvk54)#{*L9r5bNe0|e#&&{e>oZ83j;)dyY(3!@AUOBw4(|zNdPxjfv zn|I$)*_vcoADC`%sy!O`kdRb8=AA)V`sO@ZZmyzLKAK}(RRv3jpGjzrvx z&>1rARxb4o6W(UEmpQqu`pCRbwhR&7AI%?ORf_J-uk07H)4L?2$~xtc(Ryc$+N8L8 z4O8a+?Bd<|44$@+_T#}sYu{guXUlMQ>dR$$ZI}0AmbQl3hFM-?^>u1(e44j&u}HGh zZM?5%_v3MKz}sxUVi%K;&IQ4GIl_bahHceFS5~{aUuFw?;Qh<7n66V6##!3&v*sC8 zH*vNl8yeQoTcMApC39Hs^alN4&%r_7%jzhp+>`kxZUazRp;N0~xW89(V&md%nFQSl z9rs#~RmqML&TqZ*bhkX@#rRTIcKeoA{mXro1&ItMj&6yTk+0jZx?88-=&#F40u~V> z6>_D-`I2A?MaWuLN&tWqawQ_R;QMC??gS(dQ7_A z3=HF2;OImHQE$abt9HaLO?U;~=Tz)3XhG(q)}6okU6CA)aI{e45-VJ~h#U(Ggt|1C|@Bq^_=leC=~w(z*CI{7s19NC+w(_nh&ojq`? zCD_l}>vX)T@LS7{x6`Jpf8x#3#U3xzxz;{f75KN=+n^Q&<$#pIrsiF0$yupWZMJJG zKG@MgyiS+PB54Lv4=dp`&m@d5b(#+PJ1VyU>8H)z8P)Jx)|FG;93HTjBj$uG zhs&R*%l!#g|<%?h9G%Dn1QDuz%FdeL}x*q`c;;ohIkDeYcMc1uPbow3}4 zdb5+%{Qa_b-h`((mYetX15B>;RF&E7ql}hKx{7)jG+`U&q41~iIRYKr-DZ>B9PVwL zO;mr>=*Fwi7h}}irj6d76c-uD))uVO>0ffIPhX|=njiP(w`MCpKKr%bv=T?Zyng&o zism!Uk6%T1-nve3d)$BYlkj(9|6Sww?|^)BQ~0NVoMpZ-=i(1=e6!vEIUMo96lZzJ j{s_spI>diK@~dF3CNJV+`eowtr_cWcs^#wQtPTJGJiHwJ diff --git a/tests/repository_data/repository/metadata.staged/snapshot.json b/tests/repository_data/repository/metadata.staged/snapshot.json index 99f5fa0e8c89e5d73de3bb3104b68732484e3d17..6e7cfad7f26abab3704b83816c9cb9d3ba309fd6 100644 GIT binary patch literal 906 zcmb7?-EI^y42AFaDMoWm!TE9G^saA!Sgt?_*^bjLKiz6~KvdPfJDzE|;0hsSMx#t} z&awSH?WXyBxV`7)$H~s~_4IK!O;7$z^H+O3bl;g#M*v7fNGK_B2$G}uTL35_Xyl;? zG#ZZ)GjZsen`))1!KBWwHofn*+#NT;cl(?D{^siKe*f~z6Rbu(X%+%-I~oKAn5=Um zwkDk23t9y$MraUn44NT;19Naz_-caO|DcF5L=Q-NwN zf|QOWYQ~xhH}Mu`i)*T!l1eP4c2MhpF`#hul8AFPQ4r&*R7OdJy;7*?$2?Q^5`s`s zZ&x6XB*krW_a12-U20A~nbMULjFwgEa1&}6hlrq!IuesY4=qMBZlQ$C98rje&#jX; zfktd8j4>)Iw&>|n>t3~KsbUsDDX%kP=qk{l*5KdlLZORjP%P$?K=cL;S?4LCSV$#% zJXnY70CmJtil7!VCunv~F1zWI^Rhu{D=jOG^XKL9!B#}}_xy0aJ1!eIv;BHFJYzdY zNN@$g*L#5LZGCt?_Z3`O^Iibkztn-oQf^3FFf(pfnO0+OQaN#f~2cPFr6%-zH4u@_z$22=%< z#QOaFdSC7i+vWcKgI@nV$7Oqc`2Fs1_>#|I`#V10&)nJA7Xpw73mcIlS}l=p0}x{H zqRSa%4PC2LQJh|fdzbDQ*0*17dLQHZ;dB%H_4wv^eDmV%@%Z&99qf&I5i2gmCM&f7 zD`?$EEE^HCsdt~9icCgzg8(jlNSY%k5*2DCRSyZ6Jo_{-reP}5b}QVG!#ldyuF@ge zoD3CG3uc6(^sZb7Q(yzAWX^`D7&^UaDME9mjJRX~Jc&491ew)2G(cmh4ufH>WQ}@N za*d`T5`D32ljT_42G`(7Yi4alp#eFex-PER+v2(Aid4>$iIt&@!bP}oGjJ=EBc-L% zMkZZ4bJ1{Y)TvcnxW`k4cC%DV85o(N6T9UniboDT$xyrNTu=u^nKq?s(=jQA3>=X+ zZr&_o)(VsvQTCLU2bebwIUlab&OpXFP}tILz|u2??o`^e^dy=Qav1})Vj;{>NrrW9 zqafC`#$MxINJ?8)mrva;IN)4%_*3y1xADa(ei-bJ+fi^X2}wfjg&9&$M3k{64OipQ(Ly UKK&E;gFp5W_YU8;eY$)4ANNr8Hvj+t diff --git a/tests/repository_data/repository/metadata.staged/snapshot.json.gz b/tests/repository_data/repository/metadata.staged/snapshot.json.gz index fdb53131c194fec42e8bc1ae4595986573e97d58..f34fa3507d6eac26d6c4800e27d44fd614bed7f9 100644 GIT binary patch literal 548 zcmV+<0^9u`iwFSR0wq=g|CLfrZxk^Mz3;CW&AA0TiSyB0{{dn-fe^ACr`-bGYIi_X z)&6%p+vUIsA$dlT6TkOtzn9H4A9r_;y!<%WalV;8ZKmnPziIwz&%5rKnRFmPtd%NB zjEpELnEwT!Mgk2yl%S5rV}w*0yQZdEYEd*P^VJ#eyDj(oGvNE}?RI;6{cgLxT)BcZ z-=ij}08Up&1p|uKIaanNoSX|<0n1j&08>CsfFLPa?Gdv}sWIsY44f$UtfGMiTGGzB zv{FYzA@Qon)^m;{c~xShF&cF1Mk-Wt3G6rlL$6Yt?hIQBr^v;nG)E5}HKd-SC4xw< zA$I3ToI;0Wy>iS@tObzVDF;ndV&*13!en7pm10y0IhPJ<9Vj3P7jLO@DkchIT(stq zW2Igy7V>AVDR~PhHLH&ct4osXw5j`yw2Uq#Mc+*B$`M9ODrGoHZ4`!xkc~1TSA~u( z1T$`!F);^{%ER~8s}G@$&?1g83MsYV>SF8OwP`M5hM+aAJ7VZ6(4Z9bGdmD=5k;kJ zz6l~9(2#YXg4ri<(!2yo4JE#H^XT&eZIPw(pI;uW}Lq)&rh~mWc$cZ z$NT+qHqLCno_4RcodZO;2K4vD7`8AMR{9{?FUDhy6Fhe^hKPRexWcHmBbO6kTJA0{{Ro0R%7r literal 549 zcmV+=0^0o_iwFRyxD!?a|CLg~j$1bjy!$JRcTQST6eTLR{6SFn)S@Uzib^(Z)(hJ$ z8U*|A<@0V2JrxD?9-tvL8jj}M;kaENK0fO0k4s#)cgLR($K$vBj@xhX^C37}J#n9#k?4zEn=HrTHn;{yN;7LK3Ju5!)pc>j-WJa_S0r<`OsouL z6fVMzn}J)Q94RH0Hqz}ZVBtz}4b3q*x zW!jXkO~<4dGH^t`xOua*Su0R_M0uyQEMUGkk=G7Qg$<1cSsLm7Ko_AqY0-o8G?9+A_dK3zYZ zZ};GA@z>MCOKi82A$SXz>qmfh_x00DQ|3-zd5+uVe7=46`Fei*2RUwk#^w5O2XF_T nUm|(a%g4A~|4;Atm-Fv||8e5*dg|YW=fm^gi|g!#iUR-uwUrN1 diff --git a/tests/repository_data/repository/metadata.staged/targets.json b/tests/repository_data/repository/metadata.staged/targets.json index 4cf4c3534b5837a5b21e7142933678f12e7f7bf7..144e44717c6e8a087f042b985989cb225e82dc0b 100644 GIT binary patch delta 528 zcmW-d$&C;|3+i?kkKeyO9oHG7&9X=U zg!mR;YTdO>(&0`bAK)Af6H~SuEXoctkP8WNmI*WNbl1D@oVV64n1>kTB&V71Nib_x) zLy`Bg^6hyG1$lmOFP_~A)5sNr%g-S};5EXrEyH$rN_k&@W}S~3l{n+ovFjs6FZjMN L^yk;NpFjQq66=)O delta 528 zcmWlW*=-O&3`8Xa-)Y6Sy=72>uWch0M4}h^Ni8g&mOP$$mze?4dH78Wx4!0qo1rdihMa;w7UC zY!`eqkvpXoQ)2zn(uTD^eT<%)5<;~^n+%3N8SI`UHgo*> zI}3^9|9%cvGR0j7ta!@phYH~aJHr5U;3k15#0#4Whc_O7AQpETokv&B)ZKgS2p{S< z8_a7YYU>Gj{F-h}n7#*)l=V4_)bJ!#lR%?H)w~OJ!{oj*7>_#~#Sr?T*jwHJqxtzd LpTA!}e*gLhhY^s- diff --git a/tests/repository_data/repository/metadata.staged/targets.json.gz b/tests/repository_data/repository/metadata.staged/targets.json.gz index 40165c5d64b77dcb44c6a8420a4ef43525f4d1e0..3a722b6d6e1557023fb163a466f04755bb7cb361 100644 GIT binary patch literal 1235 zcmV;^1T6a>iwFSR0wq=g|E*QYavDhxz3VFouC|BT7vCCzG>A}dR?3|k}+Q`{k6y^h& zaVmo11f0ULzsf|}7}pU|;#i8+oDg6n`U5n=!Bg%%ARMuUAec%bjkd%X>!ZF(C_$Mu zF&$GP5+%yvfPt2y8D7syUp~H>5(>8zW-Ncr_ZbuxxizaXbJc-kSNZxKcUW*!k2Iai5A;wSL*?l&%NWR^zb6&fMz_-_O+uYpjcfK`RO?D5>(L=koFvGO*m_9RK$*Zlctvp?h zDvj%l;r7lZ-Mf5ucPULaO)ge0qYeKo>di|t9JW%K@3)eDNT=)ddUW-ex?ywa=d*9w z_#QXx%0Ek59n6!LN&Ti%O9pLjzwNIEcc?mG-Tu>HdC@!{J5}w^I#Ov-&6NAYmeK__Hh3)iB*hu zjaqV(R3FnrbLySipZzKyoBMipy1yQGuGMmW+BQokuUO+|`6}MxO1|2_JF`svukioF zphuuDKQp9eaj!5JTl4zQS=Gnc-67&Rs8n8z2DvMib6Au=m;N~_mA>OsDnG$y6mg~O zh>lV^&lJ}#1*<)aETv=&AyiT0ZDia90aQgIY)Hvb6Qy;)wFpF(KkoERpcqbf3On7b z+s!^*cDrv|h+w;}tQL|fBdnAhSR~R2%B+!|P(zs_CPl?jw^funt!zwzMeVqP5F+&? z_^3_bR8mCcLS$a4=pz@B07ODV;v~q#Z8Tca#1u<}tF-mcQRYcZCTK~F1fn^YjyuPr za6(cCOafT2Jfd>0rFY&@Cj^aFTJ#g0Pu_4Tg7?x`7YPPKGDs{TG(!_5+MPEVat{+8OvXb%?!2_xpwmb6pI~i$`QseaK&hvFWv-q ztUdf7bp;VoC{F})41r_5LlB-BBpn6mHV{4Dz&qEyL8#BH0vj&M(u95_>i~i^-BFQYgYzO&%>wD znO&1gc2zsgvWt`K%4FO9lR*Q&YuR%nUU&LApER#ddx@~lY&m$y*Zo>gY|aY^!+Nvn zRo>Dcmh-f3f=ar!XrEM1QStSqUNx3in{{m7&4=sz#@&6ZI&uBH{*XV5cwAJPD}VfW zc6VIAJngS;e0FhDtZ&Y=n~k#5d6@Bfd{(u_nd|qPxh^&5yLAA8f&#>pUT)Nru2>c4*<}zGQ7-2`;k#*gp*H%;(blX=iPn3uh2Ibhw9wfqdR+t8V$=uF`c}tw5`n78O$fh zvbWkUD<$>__i->gDuZ-gl0$-yQi6NI2F47><*LkZ3q0f$iEv0}6W| zmzGg#G55Ho!C9P}1f-&nJdT*)to=7g_UJ7!`<$#`zKz&EWv@rCKLAm^H-X*<004$p BWpw}m diff --git a/tests/repository_data/repository/metadata.staged/targets/role1.json b/tests/repository_data/repository/metadata.staged/targets/role1.json index 7dfe6f87faada4a8fb0fbbf655732985da4da3f0..0c99c8599fab18c19a7923879fc9fe49e586622f 100644 GIT binary patch delta 527 zcmWlW%Z(5~3_}GRww?SYlU(T}-!`@Zkm!XzsEOHQBqz^y{(b!W`1AXdd$CImj^LJG z08^0L$I8oGhdjKxMq)tB+=N8h<65CdMxLs-wKX0hiwdJ9bFt=B>U1vQs`ts3R_aZT z18p(`_Gv3Jr00P0uA>uXVk#u=%yQJWTnU2OdJ_mZVDL3O@I}m9N~#GdJT)wjoaHl4 znnt=#3425G;B!AI!cVjn07{>Le7fU$&*6JTFiw`d@_yg%u@=zd&%c$M!aZjuZUAj6`I7d_5yzW#~#!mOh*#&&LB(I*|tmhxJQ2| L)!$#=e}4N9x{#D; delta 527 zcmWlV$&C;&3`GSTrX9z7u5{wHjcEWRdZCZ0i6CF%$jSG6Umsr|e|~?4tQhs=j!AJE zHtm)jy&C7B(>aQ{N3h)!cLf|Y%DbUg-KSC_T>v&RfY&l znLNc?^mA$}0g49$QA-XWO_Qb>A(&rkiw3~#Cd?8_Yd+ly%s!y>K5Drcf#v&9x-|B1 z$ICW0z`LLdnQS}>*T>-jzq90LE>H+s$#`5|DdatVnjtnCsi_V}0l)kG-`r;@s-1&e z_X(k~$I0-BXEA+Vv!IIbII6*x8KrfVZ3f|}9 zl@Te96(vR)IPQbQmZPuH42AbOMXT2Yxm-VXWtSTuCMys^w#!8Zlw_0{5K%OD$31}sQgy3kyI8H}os5N9vz4Ayf#<|A5^E;b4fkezL_wa@tLoM=_aWFaLBTl*sATLc z;vO@LN>3q!bCuiAs%*;+1*IMnNwOR>OJ=9ukewKppZ((kn!~RCm+D zMIcETK4ayUV$HS&C+Dl>OYm}m(l=T*7}u|-mxFJJ-0dIyc#?<1g`CxYANFtDt_T^f zA;q}^xVfB9Z$$x~{1pn_M>!ntpHA<;ADZE~Pr1n{Qs5*10(=~WNSxmE6 zv8)<(#o{2m8cj3bUS0HV<8*U>5q!UYyWhXPez)KMZDoS9Q6X+p5zWdNP*cvPTXOPzwfmgBT%e3IEoT_FFQ>TdfLKaPh>0nV^yirw=W+y5KSSI>U{)al!~ diff --git a/tests/repository_data/repository/metadata.staged/timestamp.json.gz b/tests/repository_data/repository/metadata.staged/timestamp.json.gz index de5e0569767478c8a8387279ecac0cb8e0df35b7..04940c9c2c354a0aeaec5f085e166a1e546f40f2 100644 GIT binary patch literal 533 zcmV+w0_y!AiwFSR0wq=g|5Z}UZW}QSy!$JRJtqZ4JxJx2KPc*)S`-CIQH@(9F=Dr9 z5cuEAb<#rt?P4*cW{1Oh-E5cR{`;exem(nfx!Hc%Y`3rRY?q(@vLAsgQfe*5a1_gp z8QXy7y(Gwk|_Okx(aNVb*M=Csp7%ag4;;iBzcx&q2*c z$kN(iRAtayJlS*+XwwSkjNVjQlSWVnK}+6}mT7LC*=Ejzs^?Z>dX9g~q@;sVm#lMY zFdGFUA`T)fq~x_TqF7Cqb0zBGp38Kctr0@2Cc|sSl0XON3O&YakW5oCsoiQOv1abI z1d~UD)mpprY!HpZrR8ayFnqS)3H8ZiHQK0Rtfhrn!rWV~F=o_fLaX84jE`u@b9z-> zC37EwEfW-+lYmOb&LZwHv#9ih;+0jXgHThf158Shu$-vWP!zAsQ`Qvp!eBM5x5z`H zF&d~t9!YwIz)N*EEnEbWl;Ja0ZYkDmON8OP*?zrx52deZS;M$|JG~ryjmX{p!H*|- zI9!sm_@Bf6Ew>9ohHFT1?f`Bs=kr@r1W(?Hg7;Al$NQ(#habnM$A9Q{`R&ig{RK!X Xa9)*MF8^0JZ_a-KYZyb!F#`Yq!uSak literal 530 zcmV+t0`2`DiwFRyxD!?a|5Z}KZd5T0yzf^$y>pA=*op1l`Ui;Z351ZHICMd6SKS3s zRr~K`x8;E396U~D#>%{Imi4gv{%FTv&v969mM@!Sd7Wojf5ywMPh>SJg{b9D4Xa45 zJ=IWk=9If@aDz0Mdd*@JHL8lqL3kAEX1-os_#WbT|8ybzw7uPKZ?8XYx9_9r;EX5Y zA{o&vwFY^_)E$fpwbBS?7tvx;iaF|ZlY>h%&w?C98}x!jqIBt1lIJKf2^*nQ(ZpI^ zH9{J3wWM6Nj+C@fVZdb3qvbYNL`}VFqTD-Y;@&!T2(ap5G@7g7$g`Q6QU)Mq%9fh} zU@@@30>!09RA^bHiH{6vic3{X0DAUC5EU8-$n&x?SVG?tl5OgI}E96~G zMcFDr$AQyX9(jymy(P_5@-PZ;9w-s_meNR|=FKiq$h%kEse)*eK3D z8VB=m&$YK7jEpIjHQFculUDVaN9TYu*Maq3Ig*Zgu7gCP2JtkF3nxq-+d-St!)qsF z%`T#zrp#&cWYkHHy9s!pum#spNlh#mM^Y*d!<^h@V+t-Ppp6|ERc&%$0C#vS!YSNJ zApJD|2b-)2V!2kYFlOr5y$!f@rJjoYD5&sU0D5SJLuwfJF>yIjFis!jIrlza5luqu zSwenRp#>0U=qVWkO?WG6oqHWG_LBJVeDUV>-Is@Ye7wE?{J}om+&|oY{do6y{r2kJ V%d4xa>zi-a=Y0R`_r>2={{TX>p=SU9 delta 532 zcmWlW$*mAb3d?c* zs1sI~4<|!_Fd)Jt-09Y?1F(ERhw@SiJgLbxLFXXZELT4_K;3*l+{A0>5hkC^pa;{* ztuAz*GzWmLw_OtoE)nZjf zftjgMZc|Aq^BJN7NyIR-6hOe&*jG#l>N5obf&hXykn6nh&7wGb)6o5rWFbEC?{*HueQa5+JNafC-aSN}w$4bLrdA z;)tq1Qf-4kSqub$#aMumPb`aC3~7k~N1SM6e&bsN0*(b#!oX7y*jxraK|~8dh`@Fr zd>aXb0swt;o|MvNfiF4oA<=+CrKyj(^)d*RFMz^<4KxH811SS$+tQ#2d`eWHfELKN zV1Ss?W(;%yEHhRHtJEw20W#JDW)LC5h2>L$6{Hr$z?K%*IFP(L+wLfDJ_V>VnN%NCZUiL ztU0oXrZ2ZB=FlP{U`#V#V2reHYg<@YaTahBUug_*f+@2sZom@k69oj;<_t-UBhWX| zKr*1VZSvr2L=cvNB1oF8mL?X2)aNF|(1(Ixn?tV5ItMl*e$9~K^QX5Tzxzr`^}yT{WSv~d=D)1cmVwH@$9~wXRbr>70z3Y_}T&h**39>L`Ybc zGW-fG!R zo+AIX>-*QI*>T+Q+jQ9+x~;cCXZta^Pe!iWbdQa3^AKOcICA&{G#j%{x6zoi8Z*1` zT5lZ&X54A;&844)n+K-X$zT~xM%DDWPfqP?zBpwiS`T}$>MpWu4AjuQG|M(V$0U*E zdN8RfM_p&z!<~1lvNOjGd3G>}+gy741Rl?*dLC0RX{DpOHM_Dn+?~wwwfn+pu(W;UescrybnV?@#wbfEk`}RIPL}`d$Z~-Efqe!s6n|( z3*1PH(ZF&vJLUkxZl$B9R~{}aD z`~RmE>sw?eRC_kf!&xyuLjEuzn1+$;)!QlI&%%R<(_#JAIxvn7yOYStV>gDXw+I;| z{&==7y@ue!hf_ao#9(xbXZ3wz8NGPpsjRn72Z zePE%y`SsnggNO*PrqF5cL75@8G4|D$MojEOb=7Z)113pz~3;+*K?U*7`BJE|1%DUd(S*{dns7 za(sL4GB+0P!h~DEZQ%HQt8v^XX!h4>B>)Rdd6SSLW^{=&2?MCLwrv1Rk+h|$9OzX= zEE)hKQ(6nl4op!pq{2QRe*;=wy68CwM!Z`thQka@qlat42`9YOjpA-I#U@R72YPGD zPdB(B>sj|{zdJ)7pHpjg+~t$U)=Rf-1#Ol@ft17IJvVA~x*&NQlgD%vwJk@k4$k;J ziIX_4?tBpUjTm zMqZo!R7=-+tllThz^x{vat}>(YsOb8FU{4lFYU>ZPNIFX-4=1j75=pDE%QuR(XB<3 z$@oY`=#p{LIbLBCzh^_S^gGitMsa-YdqP%8tjD{}M3^`vqhoT|+IPDn$HQjk&@CHX zc8|w!haZXXCW(2AuUG0CalRr#=IjTV@W%Tj~J3SFv2wolpz zkKw`@wB8x)xf82X#8bTL#DNYeo6aY_)){R3-gfARyz13&J-0ZQ<*gd$^pL{weV;hf z#r(1DOc5UTPv=WcIj`d5V)J0!d%r@AW7Um@_M*G?PV4im^rr0~sgL2^G;@vfeZ5bf zi>`n4PPV^tozl52Vex+N<;?_C>7>tguG8pNRm*9d!I7@+&OLsX=vC!5vCug;$ba=1WBc2WXjO7}%I)k|BoVKIu!Mj9}IQ04@SlsEQA+pD- zj8+}G3q}}ptiF|d+*wA~01WUj%ofMBb<$C`kmFf<80|cV&tWG_+M^RGZo(Da5bTrX zUs9`2U#V5g)06tG*~-(8e(g7|M8U6p-v6R#e%Sf(E9tJG=>)^{^lCk=-;w=yjpM%q z^36@*p8|3f`9WQaKfv+La{uRWL}yi8#X0*UB;TqK{{hLbgt@Ey^+({#!skz){|VW_ JMcLO5003`WA0hw% literal 2053 zcmV+g2>SOQiwFRyxD!?a|IJs~j^j2KeebU*)T@aXpaCAx7Hh+#q?#EJ5f6BX7>YHb6d*`4$Mx#hsErHcYAnR!%7$<}nqL3#nrwqY} zD5)sa9+Ylhff3Tc;~GYu#VWFfGHZmj1ab^9rjSZ(1SCQcY)!!cbFdV8n>f8X1O=0hUD?BEcBKC<2jH`-4z1x!}D5w z3F0(feBI;am1qlr6AvexD9@7+x>Oizc_F`u{5g7+`(IbRyk-}*kM+l@?>|1ZrXTcQhSOR%sK0cY%je)a?FB(C*j4+r zO>){Mu`h0*Rvk54)#{*L9r5bNe0|e#&&{e>oZ83j;)dyY(3!@AUOBw4(|zNdPxjfv zn|I$)*_vcoADC`%sy!O`kdRb8=AA)V`sO@ZZmyzLKAK}(RRv3jpGjzrvx z&>1rARxb4o6W(UEmpQqu`pCRbwhR&7AI%?ORf_J-uk07H)4L?2$~xtc(Ryc$+N8L8 z4O8a+?Bd<|44$@+_T#}sYu{guXUlMQ>dR$$ZI}0AmbQl3hFM-?^>u1(e44j&u}HGh zZM?5%_v3MKz}sxUVi%K;&IQ4GIl_bahHceFS5~{aUuFw?;Qh<7n66V6##!3&v*sC8 zH*vNl8yeQoTcMApC39Hs^alN4&%r_7%jzhp+>`kxZUazRp;N0~xW89(V&md%nFQSl z9rs#~RmqML&TqZ*bhkX@#rRTIcKeoA{mXro1&ItMj&6yTk+0jZx?88-=&#F40u~V> z6>_D-`I2A?MaWuLN&tWqawQ_R;QMC??gS(dQ7_A z3=HF2;OImHQE$abt9HaLO?U;~=Tz)3XhG(q)}6okU6CA)aI{e45-VJ~h#U(Ggt|1C|@Bq^_=leC=~w(z*CI{7s19NC+w(_nh&ojq`? zCD_l}>vX)T@LS7{x6`Jpf8x#3#U3xzxz;{f75KN=+n^Q&<$#pIrsiF0$yupWZMJJG zKG@MgyiS+PB54Lv4=dp`&m@d5b(#+PJ1VyU>8H)z8P)Jx)|FG;93HTjBj$uG zhs&R*%l!#g|<%?h9G%Dn1QDuz%FdeL}x*q`c;;ohIkDeYcMc1uPbow3}4 zdb5+%{Qa_b-h`((mYetX15B>;RF&E7ql}hKx{7)jG+`U&q41~iIRYKr-DZ>B9PVwL zO;mr>=*Fwi7h}}irj6d76c-uD))uVO>0ffIPhX|=njiP(w`MCpKKr%bv=T?Zyng&o zism!Uk6%T1-nve3d)$BYlkj(9|6Sww?|^)BQ~0NVoMpZ-=i(1=e6!vEIUMo96lZzJ j{s_spI>diK@~dF3CNJV+`eowtr_cWcs^#wQtPTJGJiHwJ diff --git a/tests/repository_data/repository/metadata/snapshot.json b/tests/repository_data/repository/metadata/snapshot.json index edde816d8aa77135a4fe5937480051676057a466..6e7cfad7f26abab3704b83816c9cb9d3ba309fd6 100644 GIT binary patch literal 906 zcmb7?-EI^y42AFaDMoWm!TE9G^saA!Sgt?_*^bjLKiz6~KvdPfJDzE|;0hsSMx#t} z&awSH?WXyBxV`7)$H~s~_4IK!O;7$z^H+O3bl;g#M*v7fNGK_B2$G}uTL35_Xyl;? zG#ZZ)GjZsen`))1!KBWwHofn*+#NT;cl(?D{^siKe*f~z6Rbu(X%+%-I~oKAn5=Um zwkDk23t9y$MraUn44NT;19Naz_-caO|DcF5L=Q-NwN zf|QOWYQ~xhH}Mu`i)*T!l1eP4c2MhpF`#hul8AFPQ4r&*R7OdJy;7*?$2?Q^5`s`s zZ&x6XB*krW_a12-U20A~nbMULjFwgEa1&}6hlrq!IuesY4=qMBZlQ$C98rje&#jX; zfktd8j4>)Iw&>|n>t3~KsbUsDDX%kP=qk{l*5KdlLZORjP%P$?K=cL;S?4LCSV$#% zJXnY70CmJtil7!VCunv~F1zWI^Rhu{D=jOG^XKL9!B#}}_xy0aJ1!eIv;BHFJYzdY zNN@$g*L#5LZGCt?Nf7bA_%TmF!!@`n9MmpzyJFF=l6$_C6!UO1mA%q zOf8?6KGQc!tCh)GFe+g{NZlR+1KAunha+lipry4FQrI)hu9}u8%9LW&(J+O%-2w-; z3wkE-!&F0#3lCm^!&#DuffHRfh>W|p$I38|G_wr&aOP=`ff$eQ1kX#Gi8{d*b7Ra# zpHy6Xg;eQsMrOBV-Z#V!ix?jjPY&vR(y!la^=2B6067VnT)7FA#0#~X=z3t2icn19 zW#xO~;isTu{JEA7k>jkzVyWCGTFqiNCx%w^gX28Z*~=d{ym{OleGnk)=9-QJyqb8A z+T9NnC|E*#v!{Vm7HRT@CWz^J&|5YK!5nW-ZFW^~`* K-(UZI`SBm`!jz2w diff --git a/tests/repository_data/repository/metadata/snapshot.json.gz b/tests/repository_data/repository/metadata/snapshot.json.gz index fdb53131c194fec42e8bc1ae4595986573e97d58..f34fa3507d6eac26d6c4800e27d44fd614bed7f9 100644 GIT binary patch literal 548 zcmV+<0^9u`iwFSR0wq=g|CLfrZxk^Mz3;CW&AA0TiSyB0{{dn-fe^ACr`-bGYIi_X z)&6%p+vUIsA$dlT6TkOtzn9H4A9r_;y!<%WalV;8ZKmnPziIwz&%5rKnRFmPtd%NB zjEpELnEwT!Mgk2yl%S5rV}w*0yQZdEYEd*P^VJ#eyDj(oGvNE}?RI;6{cgLxT)BcZ z-=ij}08Up&1p|uKIaanNoSX|<0n1j&08>CsfFLPa?Gdv}sWIsY44f$UtfGMiTGGzB zv{FYzA@Qon)^m;{c~xShF&cF1Mk-Wt3G6rlL$6Yt?hIQBr^v;nG)E5}HKd-SC4xw< zA$I3ToI;0Wy>iS@tObzVDF;ndV&*13!en7pm10y0IhPJ<9Vj3P7jLO@DkchIT(stq zW2Igy7V>AVDR~PhHLH&ct4osXw5j`yw2Uq#Mc+*B$`M9ODrGoHZ4`!xkc~1TSA~u( z1T$`!F);^{%ER~8s}G@$&?1g83MsYV>SF8OwP`M5hM+aAJ7VZ6(4Z9bGdmD=5k;kJ zz6l~9(2#YXg4ri<(!2yo4JE#H^XT&eZIPw(pI;uW}Lq)&rh~mWc$cZ z$NT+qHqLCno_4RcodZO;2K4vD7`8AMR{9{?FUDhy6Fhe^hKPRexWcHmBbO6kTJA0{{Ro0R%7r literal 549 zcmV+=0^0o_iwFRyxD!?a|CLg~j$1bjy!$JRcTQST6eTLR{6SFn)S@Uzib^(Z)(hJ$ z8U*|A<@0V2JrxD?9-tvL8jj}M;kaENK0fO0k4s#)cgLR($K$vBj@xhX^C37}J#n9#k?4zEn=HrTHn;{yN;7LK3Ju5!)pc>j-WJa_S0r<`OsouL z6fVMzn}J)Q94RH0Hqz}ZVBtz}4b3q*x zW!jXkO~<4dGH^t`xOua*Su0R_M0uyQEMUGkk=G7Qg$<1cSsLm7Ko_AqY0-o8G?9+A_dK3zYZ zZ};GA@z>MCOKi82A$SXz>qmfh_x00DQ|3-zd5+uVe7=46`Fei*2RUwk#^w5O2XF_T nUm|(a%g4A~|4;Atm-Fv||8e5*dg|YW=fm^gi|g!#iUR-uwUrN1 diff --git a/tests/repository_data/repository/metadata/targets.json b/tests/repository_data/repository/metadata/targets.json index 4cf4c3534b5837a5b21e7142933678f12e7f7bf7..144e44717c6e8a087f042b985989cb225e82dc0b 100644 GIT binary patch delta 528 zcmW-d$&C;|3+i?kkKeyO9oHG7&9X=U zg!mR;YTdO>(&0`bAK)Af6H~SuEXoctkP8WNmI*WNbl1D@oVV64n1>kTB&V71Nib_x) zLy`Bg^6hyG1$lmOFP_~A)5sNr%g-S};5EXrEyH$rN_k&@W}S~3l{n+ovFjs6FZjMN L^yk;NpFjQq66=)O delta 528 zcmWlW*=-O&3`8Xa-)Y6Sy=72>uWch0M4}h^Ni8g&mOP$$mze?4dH78Wx4!0qo1rdihMa;w7UC zY!`eqkvpXoQ)2zn(uTD^eT<%)5<;~^n+%3N8SI`UHgo*> zI}3^9|9%cvGR0j7ta!@phYH~aJHr5U;3k15#0#4Whc_O7AQpETokv&B)ZKgS2p{S< z8_a7YYU>Gj{F-h}n7#*)l=V4_)bJ!#lR%?H)w~OJ!{oj*7>_#~#Sr?T*jwHJqxtzd LpTA!}e*gLhhY^s- diff --git a/tests/repository_data/repository/metadata/targets.json.gz b/tests/repository_data/repository/metadata/targets.json.gz index 40165c5d64b77dcb44c6a8420a4ef43525f4d1e0..3a722b6d6e1557023fb163a466f04755bb7cb361 100644 GIT binary patch literal 1235 zcmV;^1T6a>iwFSR0wq=g|E*QYavDhxz3VFouC|BT7vCCzG>A}dR?3|k}+Q`{k6y^h& zaVmo11f0ULzsf|}7}pU|;#i8+oDg6n`U5n=!Bg%%ARMuUAec%bjkd%X>!ZF(C_$Mu zF&$GP5+%yvfPt2y8D7syUp~H>5(>8zW-Ncr_ZbuxxizaXbJc-kSNZxKcUW*!k2Iai5A;wSL*?l&%NWR^zb6&fMz_-_O+uYpjcfK`RO?D5>(L=koFvGO*m_9RK$*Zlctvp?h zDvj%l;r7lZ-Mf5ucPULaO)ge0qYeKo>di|t9JW%K@3)eDNT=)ddUW-ex?ywa=d*9w z_#QXx%0Ek59n6!LN&Ti%O9pLjzwNIEcc?mG-Tu>HdC@!{J5}w^I#Ov-&6NAYmeK__Hh3)iB*hu zjaqV(R3FnrbLySipZzKyoBMipy1yQGuGMmW+BQokuUO+|`6}MxO1|2_JF`svukioF zphuuDKQp9eaj!5JTl4zQS=Gnc-67&Rs8n8z2DvMib6Au=m;N~_mA>OsDnG$y6mg~O zh>lV^&lJ}#1*<)aETv=&AyiT0ZDia90aQgIY)Hvb6Qy;)wFpF(KkoERpcqbf3On7b z+s!^*cDrv|h+w;}tQL|fBdnAhSR~R2%B+!|P(zs_CPl?jw^funt!zwzMeVqP5F+&? z_^3_bR8mCcLS$a4=pz@B07ODV;v~q#Z8Tca#1u<}tF-mcQRYcZCTK~F1fn^YjyuPr za6(cCOafT2Jfd>0rFY&@Cj^aFTJ#g0Pu_4Tg7?x`7YPPKGDs{TG(!_5+MPEVat{+8OvXb%?!2_xpwmb6pI~i$`QseaK&hvFWv-q ztUdf7bp;VoC{F})41r_5LlB-BBpn6mHV{4Dz&qEyL8#BH0vj&M(u95_>i~i^-BFQYgYzO&%>wD znO&1gc2zsgvWt`K%4FO9lR*Q&YuR%nUU&LApER#ddx@~lY&m$y*Zo>gY|aY^!+Nvn zRo>Dcmh-f3f=ar!XrEM1QStSqUNx3in{{m7&4=sz#@&6ZI&uBH{*XV5cwAJPD}VfW zc6VIAJngS;e0FhDtZ&Y=n~k#5d6@Bfd{(u_nd|qPxh^&5yLAA8f&#>pUT)Nru2>c4*<}zGQ7-2`;k#*gp*H%;(blX=iPn3uh2Ibhw9wfqdR+t8V$=uF`c}tw5`n78O$fh zvbWkUD<$>__i->gDuZ-gl0$-yQi6NI2F47><*LkZ3q0f$iEv0}6W| zmzGg#G55Ho!C9P}1f-&nJdT*)to=7g_UJ7!`<$#`zKz&EWv@rCKLAm^H-X*<004$p BWpw}m diff --git a/tests/repository_data/repository/metadata/targets/role1.json b/tests/repository_data/repository/metadata/targets/role1.json index 7dfe6f87faada4a8fb0fbbf655732985da4da3f0..0c99c8599fab18c19a7923879fc9fe49e586622f 100644 GIT binary patch delta 527 zcmWlW%Z(5~3_}GRww?SYlU(T}-!`@Zkm!XzsEOHQBqz^y{(b!W`1AXdd$CImj^LJG z08^0L$I8oGhdjKxMq)tB+=N8h<65CdMxLs-wKX0hiwdJ9bFt=B>U1vQs`ts3R_aZT z18p(`_Gv3Jr00P0uA>uXVk#u=%yQJWTnU2OdJ_mZVDL3O@I}m9N~#GdJT)wjoaHl4 znnt=#3425G;B!AI!cVjn07{>Le7fU$&*6JTFiw`d@_yg%u@=zd&%c$M!aZjuZUAj6`I7d_5yzW#~#!mOh*#&&LB(I*|tmhxJQ2| L)!$#=e}4N9x{#D; delta 527 zcmWlV$&C;&3`GSTrX9z7u5{wHjcEWRdZCZ0i6CF%$jSG6Umsr|e|~?4tQhs=j!AJE zHtm)jy&C7B(>aQ{N3h)!cLf|Y%DbUg-KSC_T>v&RfY&l znLNc?^mA$}0g49$QA-XWO_Qb>A(&rkiw3~#Cd?8_Yd+ly%s!y>K5Drcf#v&9x-|B1 z$ICW0z`LLdnQS}>*T>-jzq90LE>H+s$#`5|DdatVnjtnCsi_V}0l)kG-`r;@s-1&e z_X(k~$I0-BXEA+Vv!IIbII6*x8KrfVZ3f|}9 zl@Te96(vR)IPQbQmZPuH42AbOMXT2Yxm-VXWtSTuCMys^w#!8Zlw_0{5K%OD$31}sQgy3kyI8H}os5N9vz4Ayf#<|A5^E;b4fkezL_wa@tLoM=_aWFaLBTl*sATLc z;vO@LN>3q!bCuiAs%*;+1*IMnNwOR>OJ=9ukewKppZ((kn!~RCm+D zMIcETK4ayUV$HS&C+Dl>OYm}m(l=T*7}u|-mxFJJ-0dIyc#?<1g`CxYANFtDt_T^f zA;q}^xVfB9Z$$x~{1pn_M>!ntpHA<;ADZE~Pr1n{Qs5*10(=~WNSxmE6 zv8)<(#o{2m8cj3bUS0HV<8*U>5q!UYyWhXPez)KMZDoS9Q6X+p5zWdNP*cvPTXOPzwfmgBT%e3IEoT_FFQ>TdfLKaPh>0nV^yirw=W+y5KSSI>U{)al!~ diff --git a/tests/repository_data/repository/metadata/timestamp.json.gz b/tests/repository_data/repository/metadata/timestamp.json.gz index de5e0569767478c8a8387279ecac0cb8e0df35b7..04940c9c2c354a0aeaec5f085e166a1e546f40f2 100644 GIT binary patch literal 533 zcmV+w0_y!AiwFSR0wq=g|5Z}UZW}QSy!$JRJtqZ4JxJx2KPc*)S`-CIQH@(9F=Dr9 z5cuEAb<#rt?P4*cW{1Oh-E5cR{`;exem(nfx!Hc%Y`3rRY?q(@vLAsgQfe*5a1_gp z8QXy7y(Gwk|_Okx(aNVb*M=Csp7%ag4;;iBzcx&q2*c z$kN(iRAtayJlS*+XwwSkjNVjQlSWVnK}+6}mT7LC*=Ejzs^?Z>dX9g~q@;sVm#lMY zFdGFUA`T)fq~x_TqF7Cqb0zBGp38Kctr0@2Cc|sSl0XON3O&YakW5oCsoiQOv1abI z1d~UD)mpprY!HpZrR8ayFnqS)3H8ZiHQK0Rtfhrn!rWV~F=o_fLaX84jE`u@b9z-> zC37EwEfW-+lYmOb&LZwHv#9ih;+0jXgHThf158Shu$-vWP!zAsQ`Qvp!eBM5x5z`H zF&d~t9!YwIz)N*EEnEbWl;Ja0ZYkDmON8OP*?zrx52deZS;M$|JG~ryjmX{p!H*|- zI9!sm_@Bf6Ew>9ohHFT1?f`Bs=kr@r1W(?Hg7;Al$NQ(#habnM$A9Q{`R&ig{RK!X Xa9)*MF8^0JZ_a-KYZyb!F#`Yq!uSak literal 530 zcmV+t0`2`DiwFRyxD!?a|5Z}KZd5T0yzf^$y>pA=*op1l`Ui;Z351ZHICMd6SKS3s zRr~K`x8;E396U~D#>%{Imi4gv{%FTv&v969mM@!Sd7Wojf5ywMPh>SJg{b9D4Xa45 zJ=IWk=9If@aDz0Mdd*@JHL8lqL3kAEX1-os_#WbT|8ybzw7uPKZ?8XYx9_9r;EX5Y zA{o&vwFY^_)E$fpwbBS?7tvx;iaF|ZlY>h%&w?C98}x!jqIBt1lIJKf2^*nQ(ZpI^ zH9{J3wWM6Nj+C@fVZdb3qvbYNL`}VFqTD-Y;@&!T2(ap5G@7g7$g`Q6QU)Mq%9fh} zU@@@30>!09RA^bHiH{6vic3{X0DAUC5EU8-$n&x?SVG?tl5OgI}E96~G zMcFDr$AQyX9(jymy(P_5@-PZ;9w-s_m Date: Tue, 20 Oct 2015 09:16:39 -0400 Subject: [PATCH 09/30] Resolve remaining issues with writing / loading the new changes --- tests/test_repository_lib.py | 56 +++++++---- tests/test_repository_tool.py | 2 +- tests/test_updater.py | 33 +++---- tuf/client/updater.py | 57 ++++++----- tuf/formats.py | 12 ++- tuf/repository_lib.py | 180 +++++++++++++++++++--------------- tuf/repository_tool.py | 22 +++-- 7 files changed, 205 insertions(+), 157 deletions(-) diff --git a/tests/test_repository_lib.py b/tests/test_repository_lib.py index 1c6e64a4..d4a35ed9 100755 --- a/tests/test_repository_lib.py +++ b/tests/test_repository_lib.py @@ -50,6 +50,8 @@ import tuf.hash import tuf.repository_lib as repo_lib +import tuf.repository_tool as repo_tool + import six logger = logging.getLogger('tuf.test_repository_lib') @@ -429,7 +431,7 @@ def test_generate_root_metadata(self): expires = '1985-10-21T01:22:00Z' root_metadata = repo_lib.generate_root_metadata(1, expires, - consistent_snapshot=False) + consistent_snapshot=False) self.assertTrue(tuf.formats.ROOT_SCHEMA.matches(root_metadata)) @@ -547,7 +549,6 @@ def test_generate_targets_metadata(self): - """ def test_generate_snapshot_metadata(self): # Test normal case. temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) @@ -557,21 +558,27 @@ def test_generate_snapshot_metadata(self): shutil.copytree(original_repository_path, repository_directory) metadata_directory = os.path.join(repository_directory, repo_lib.METADATA_STAGED_DIRECTORY_NAME) + targets_directory = os.path.join(repository_directory, repo_lib.TARGETS_DIRECTORY_NAME) root_filename = os.path.join(metadata_directory, repo_lib.ROOT_FILENAME) targets_filename = os.path.join(metadata_directory, repo_lib.TARGETS_FILENAME) version = 1 expiration_date = '1985-10-21T13:20:00Z' - - # TODO: + + + repository = repo_tool.Repository(repository_directory, metadata_directory, + targets_directory) + + repository_junk = repo_tool.load_repository(repository_directory) + root_filename = 'root' targets_filename = 'targets' snapshot_metadata = \ repo_lib.generate_snapshot_metadata(metadata_directory, version, - expiration_date, root_filename, - targets_filename, - consistent_snapshot=False) + expiration_date, root_filename, + targets_filename, + consistent_snapshot=False) self.assertTrue(tuf.formats.SNAPSHOT_SCHEMA.matches(snapshot_metadata)) @@ -594,7 +601,7 @@ def test_generate_snapshot_metadata(self): self.assertRaises(tuf.FormatError, repo_lib.generate_snapshot_metadata, metadata_directory, version, expiration_date, root_filename, targets_filename, 3) - """ + def test_generate_timestamp_metadata(self): @@ -613,21 +620,23 @@ def test_generate_timestamp_metadata(self): version = 1 expiration_date = '1985-10-21T13:20:00Z' - compressions = ['gz'] + compression_algorithms = ['gz'] snapshot_metadata = \ repo_lib.generate_timestamp_metadata(snapshot_filename, version, - expiration_date, compressions) + expiration_date, + compression_algorithms) self.assertTrue(tuf.formats.TIMESTAMP_SCHEMA.matches(snapshot_metadata)) # Test improperly formatted arguments. self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, - 3, version, expiration_date, compressions) + 3, version, expiration_date, compression_algorithms) self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, - snapshot_filename, '3', expiration_date, compressions) + snapshot_filename, '3', expiration_date, + compression_algorithms) self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, - snapshot_filename, version, '3', compressions) + snapshot_filename, version, '3', compression_algorithms) self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, snapshot_filename, version, expiration_date, 3) self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, @@ -697,24 +706,31 @@ def test_write_metadata_file(self): root_signable = tuf.util.load_json_file(root_filename) output_filename = os.path.join(temporary_directory, 'root.json') - compressions = ['gz'] + compression_algorithms = ['gz'] + version_number = root_signable['signed']['version'] + 1 self.assertFalse(os.path.exists(output_filename)) - repo_lib.write_metadata_file(root_signable, output_filename, compressions, - consistent_snapshot=False) + repo_lib.write_metadata_file(root_signable, output_filename, + version_number, + compression_algorithms, + consistent_snapshot=False) self.assertTrue(os.path.exists(output_filename)) self.assertTrue(os.path.exists(output_filename + '.gz')) # Test improperly formatted arguments. self.assertRaises(tuf.FormatError, repo_lib.write_metadata_file, - 3, output_filename, compressions, False) + 3, output_filename, version_number, + compression_algorithms, False) self.assertRaises(tuf.FormatError, repo_lib.write_metadata_file, - root_signable, 3, compressions, False) + root_signable, 3, version_number, compression_algorithms, + False) self.assertRaises(tuf.FormatError, repo_lib.write_metadata_file, - root_signable, output_filename, 3, False) + root_signable, output_filename, '3', + compression_algorithms, False) self.assertRaises(tuf.FormatError, repo_lib.write_metadata_file, - root_signable, output_filename, compressions, 3) + root_signable, output_filename, version_number, + compression_algorithms, 3) diff --git a/tests/test_repository_tool.py b/tests/test_repository_tool.py index e323b887..4fd57be1 100755 --- a/tests/test_repository_tool.py +++ b/tests/test_repository_tool.py @@ -326,7 +326,7 @@ def test_write_and_write_partial(self): repository.snapshot.load_signing_key(snapshot_privkey) # Verify that consistent snapshot can be written and loaded. - repository.write(consistent_snapshot=True) + repository.write(consistent_snapshot=True) repo_tool.load_repository(repository_directory) # Test improperly formatted arguments. diff --git a/tests/test_updater.py b/tests/test_updater.py index 1b82d5ec..692e41e4 100755 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -546,21 +546,15 @@ def test_3__update_metadata(self): # This is the default metadata that we would create for the timestamp role, # because it has no signed metadata for itself. - DEFAULT_TIMESTAMP_FILEINFO = { - 'hashes': {}, - 'length': tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH - } + DEFAULT_TIMESTAMP_FILELENGTH = tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH - # Save the fileinfo of 'targets.json' and 'targets.json.gz', needed later - # when re-installing with _update_metadata(). - targets_fileinfo = \ + # Save the versioninfo of 'targets.json,' needed later when re-installing + # with _update_metadata(). + targets_versioninfo = \ self.repository_updater.metadata['current']['snapshot']['meta']\ ['targets.json'] - targets_compressed_fileinfo = \ - self.repository_updater.metadata['current']['snapshot']['meta']\ - ['targets.json.gz'] - # Remove the currently installed metadata from the store, and disk. Verify + # Remove the currently installed metadata from the store and disk. Verify # that the metadata dictionary is re-populated after calling # _update_metadata(). self.repository_updater.metadata['current'].clear() @@ -575,16 +569,18 @@ def test_3__update_metadata(self): # Verify 'timestamp.json' is properly installed. self.assertFalse('timestamp' in self.repository_updater.metadata) self.repository_updater._update_metadata('timestamp', - DEFAULT_TIMESTAMP_FILEINFO) + DEFAULT_TIMESTAMP_FILELENGTH) self.assertTrue('timestamp' in self.repository_updater.metadata['current']) os.path.exists(timestamp_filepath) # Verify 'targets.json' is properly installed. self.assertFalse('targets' in self.repository_updater.metadata['current']) - self.repository_updater._update_metadata('targets', targets_fileinfo) + self.repository_updater._update_metadata('targets', targets_versioninfo) self.assertTrue('targets' in self.repository_updater.metadata['current']) - length, hashes = tuf.util.get_file_details(targets_filepath) - self.assertEqual(targets_fileinfo, tuf.formats.make_fileinfo(length, hashes)) + + targets_signable = tuf.util.load_json_file(targets_filepath) + loaded_targets_version = targets_signable['signed']['version'] + self.assertEqual(targets_versioninfo['version'], loaded_targets_version) # Remove the 'targets.json' metadata so that the compressed version may be # tested next. @@ -676,12 +672,9 @@ def test_3__update_metadata_if_changed(self): # Update 'targets.json' and verify that the client's current 'targets.json' # has been updated. 'timestamp' and 'snapshot' must be manually updated # so that new 'targets' may be recognized. - DEFAULT_TIMESTAMP_FILEINFO = { - 'hashes': {}, - 'length': tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH - } + DEFAULT_TIMESTAMP_FILELENGTH = tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH - self.repository_updater._update_metadata('timestamp', DEFAULT_TIMESTAMP_FILEINFO) + self.repository_updater._update_metadata('timestamp', DEFAULT_TIMESTAMP_FILELENGTH) self.repository_updater._update_metadata_if_changed('snapshot', 'timestamp') self.repository_updater._update_metadata_if_changed('targets') targets_path = os.path.join(self.client_metadata_current, 'targets.json') diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 8bfe6f0f..31323710 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -1115,16 +1115,11 @@ def _get_metadata_file(self, metadata_role, remote_filename, # 'file_object' is also verified if decompressed above (i.e., the # uncompressed version). - # Verify that the version number is the version that was requested. - current_version = \ - self.metadata['current'][metadata_role]['signed']['version'] - metadata_signable = \ tuf.util.load_json_string(file_object.read().decode('utf-8')) # If the version number is unspecified, ensure that the version number # downloaded is greater than the currently trusted version number. - print('metadata_signable: ' + repr(metadata_signable)) version_downloaded = metadata_signable['signed']['version'] if expected_version is None: if version_downloaded <= expected_version: @@ -1132,7 +1127,9 @@ def _get_metadata_file(self, metadata_role, remote_filename, 'Downloaded version number: ' + repr(version_downloaded) + '.' \ ' Version number MUST be greater than: ' + repr(expected_version) raise tuf.BadVersionNumberError(message) - + + # Otherwise, verify that the downloaded version matches the version + # requested. else: if version_downloaded != expected_version: message = \ @@ -1436,9 +1433,9 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None, filename_version = '' if self.consistent_snapshot: - filename_version = str(version) + filename_version = version dirname, basename = os.path.split(remote_filename) - remote_filename = os.path.join(dirname, filename_version + '.' + basename) + remote_filename = os.path.join(dirname, str(filename_version) + '.' + basename) metadata_file_object = \ self._get_metadata_file(metadata_role, remote_filename, @@ -1490,7 +1487,7 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None, logger.debug('Updated ' + repr(current_filepath) + '.') self.metadata['previous'][metadata_role] = current_metadata_object self.metadata['current'][metadata_role] = updated_metadata_object - self._update_fileinfo(uncompressed_metadata_filename) + self._update_versioninfo(uncompressed_metadata_filename) # Ensure the role and key information of the top-level roles is also updated # according to the newly-installed Root metadata. @@ -1566,15 +1563,15 @@ def _update_metadata_if_changed(self, metadata_role, # Ensure the referenced metadata has been loaded. The 'root' role may be # updated without having 'snapshot' available. if referenced_metadata not in self.metadata['current']: - message = 'Cannot update '+repr(metadata_role)+' because ' \ - +referenced_metadata+' is missing.' + message = 'Cannot update ' + repr(metadata_role) + ' because ' \ + + referenced_metadata + ' is missing.' raise tuf.RepositoryError(message) # The referenced metadata has been loaded. Extract the new # versioninfo for 'metadata_role' from it. else: - message = repr(metadata_role)+' referenced in '+\ - repr(referenced_metadata)+'. '+repr(metadata_role)+' may be updated.' + message = repr(metadata_role) + ' referenced in ' +\ + repr(referenced_metadata)+ '. ' + repr(metadata_role)+' may be updated.' logger.debug(message) # There might be a compressed version of 'snapshot.json' or Targets @@ -1601,29 +1598,28 @@ def _update_metadata_if_changed(self, metadata_role, # 'targets.json', and delegated Targets (that also start with 'targets'). # For 'targets.json' and delegated metadata, 'referenced_metata' # should always be 'snapshot'. 'snapshot.json' specifies all roles - # provided by a repository, including their file lengths and hashes. + # provided by a repository, including their version numbers. if metadata_role == 'snapshot' or metadata_role.startswith('targets'): gzip_metadata_filename = uncompressed_metadata_filename + '.gz' - if gzip_metadata_filename in self.metadata['current'] \ - [referenced_metadata]['meta']: + if 'gzip' in self.metadata['current']['root']['compression_algorithms']: compression = 'gzip' - logger.debug('Compressed version of '+\ - repr(uncompressed_metadata_filename)+' is available at '+\ - repr(gzip_metadata_filename)+'.') + logger.debug('Compressed version of ' + \ + repr(uncompressed_metadata_filename) + ' is available at ' + \ + repr(gzip_metadata_filename) + '.') else: - logger.debug('Compressed version of '+\ - repr(uncompressed_metadata_filename)+' not available.') + logger.debug('Compressed version of ' + \ + repr(uncompressed_metadata_filename) + ' not available.') # Simply return if the file has not changed, according to the metadata # about the uncompressed file provided by the referenced metadata. if not self._versioninfo_has_changed(uncompressed_metadata_filename, expected_versioninfo): - logger.info(repr(uncompressed_metadata_filename)+' up-to-date.') + logger.info(repr(uncompressed_metadata_filename) + ' up-to-date.') return - logger.debug('Metadata '+repr(uncompressed_metadata_filename)+\ + logger.debug('Metadata ' + repr(uncompressed_metadata_filename) + \ ' has changed.') # The file lengths of metadata are unknown, only their version numbers @@ -1785,9 +1781,18 @@ def _update_versioninfo(self, metadata_filename): return # Extract the version information from the trusted snapshot role and save - # it to the fileinfo store. - trusted_versioninfo = \ - self.metadata['current']['snapshot']['meta'][metadata_filename] + # it to the versioninfo store. + if metadata_filename == 'timestamp.json': + trusted_versioninfo = \ + self.metadata['current']['timestamp']['version'] + + elif metadata_filename == 'snapshot.json': + trusted_versioninfo = \ + self.metadata['current']['timestamp']['meta']['snapshot.json'] + + else: + trusted_versioninfo = \ + self.metadata['current']['snapshot']['meta'][metadata_filename] self.versioninfo[metadata_filename] = trusted_versioninfo diff --git a/tuf/formats.py b/tuf/formats.py index ed498ad2..a638e401 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -447,6 +447,7 @@ _type = SCHEMA.String('Root'), version = METADATAVERSION_SCHEMA, consistent_snapshot = BOOLEAN_SCHEMA, + compression_algorithms = COMPRESSIONS_SCHEMA, expires = ISO8601_DATETIME_SCHEMA, keys = KEYDICT_SCHEMA, roles = ROLEDICT_SCHEMA) @@ -600,13 +601,15 @@ def make_metadata(version, expiration_date, versiondict): class RootFile(MetaFile): - def __init__(self, version, expires, keys, roles, consistent_snapshot): + def __init__(self, version, expires, keys, roles, consistent_snapshot, + compression_algorithms): self.info = {} self.info['version'] = version self.info['expires'] = expires self.info['keys'] = keys self.info['roles'] = roles self.info['consistent_snapshot'] = consistent_snapshot + self.info['compression_algorithms'] = compression_algorithms @staticmethod @@ -620,19 +623,22 @@ def from_metadata(object): keys = object['keys'] roles = object['roles'] consistent_snapshot = object['consistent_snapshot'] + compression_algorithms = object['compression_algorithms'] - return RootFile(version, expires, keys, roles, consistent_snapshot) + return RootFile(version, expires, keys, roles, consistent_snapshot, + compression_algorithms) @staticmethod def make_metadata(version, expiration_date, keydict, roledict, - consistent_snapshot): + consistent_snapshot, compression_algorithms): result = {'_type' : 'Root'} result['version'] = version result['expires'] = expiration_date result['keys'] = keydict result['roles'] = roledict result['consistent_snapshot'] = consistent_snapshot + result['compression_algorithms'] = compression_algorithms # Is 'result' a Root metadata file? # Raise 'tuf.FormatError' if not. diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index 0229d50a..07eb80ea 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -99,12 +99,13 @@ SUPPORTED_COMPRESSION_EXTENSIONS = ['.gz'] # The full list of supported TUF metadata extensions. -METADATA_EXTENSIONS = ['.json', '.json.gz'] +METADATA_EXTENSIONS = ['.json'] def _generate_and_write_metadata(rolename, metadata_filename, write_partial, targets_directory, metadata_directory, - consistent_snapshot=False, filenames=None): + consistent_snapshot=False, filenames=None, + compressions=['gz']): """ Non-public function that can generate and write the metadata of the specified top-level 'rolename'. It also increments version numbers if: @@ -125,7 +126,8 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, # Generate the appropriate role metadata for 'rolename'. if rolename == 'root': metadata = generate_root_metadata(roleinfo['version'], - roleinfo['expires'], consistent_snapshot) + roleinfo['expires'], consistent_snapshot, + compressions) _log_warning_if_expires_soon(ROOT_FILENAME, roleinfo['expires'], ROOT_EXPIRES_WARN_SECONDS) @@ -199,16 +201,16 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, if tuf.sig.verify(signable, rolename) or write_partial: _remove_invalid_and_duplicate_signatures(signable) - compressions = roleinfo['compressions'] - filename = write_metadata_file(signable, metadata_filename, compressions, + filename = write_metadata_file(signable, metadata_filename, + metadata['version'], compressions, consistent_snapshot) # The root and timestamp files should also be written without a digest if # 'consistent_snaptshots' is True. Client may request a timestamp and root # file without knowing its digest and file size. if rolename == 'root' or rolename == 'timestamp': - write_metadata_file(signable, metadata_filename, compressions, - consistent_snapshot=False) + write_metadata_file(signable, metadata_filename, metadata['version'], + compressions, consistent_snapshot=False) # 'signable' contains an invalid threshold of signatures. @@ -440,15 +442,16 @@ def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, metadata_name = \ metadata_path[len(metadata_directory):].lstrip(os.path.sep) - # Strip the digest if 'consistent_snapshot' is True. - # Example: 'targets/unclaimed/13df98ab0.django.json' --> + # Strip the version number if 'consistent_snapshot' is True. Example: + # 'targets/unclaimed/10.django.json' --> # 'targets/unclaimed/django.json'. Consistent and non-consistent - # metadata might co-exist if write() and write(consistent_snapshot=True) - # are mixed, so ensure only 'digest.filename' metadata is stripped. - embeded_digest = None + # metadata might co-exist if write() and + # write(consistent_snapshot=True) are mixed, so ensure only + # 'version_number.filename' metadata is stripped. + embeded_version_number = None if metadata_name not in snapshot_metadata['meta']: - metadata_name, embeded_digest = \ - _strip_consistent_snapshot_digest(metadata_name, consistent_snapshot) + metadata_name, embeded_version_number = \ + _strip_consistent_snapshot_version_number(metadata_name, consistent_snapshot) # Strip filename extensions. The role database does not include the # metadata extension. @@ -464,23 +467,24 @@ def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, logger.info('Removing outdated metadata: ' + repr(metadata_path)) os.remove(metadata_path) - # Delete outdated consistent snapshots. snapshot metadata includes - # the file extension of roles. - if consistent_snapshot and embeded_digest is not None: + # Delete outdated consistent snapshots. snapshot metadata includes the + # file extension of roles. TODO: Perhaps we should leave it up to the + # integrators to remove outdated consistent snapshots? + """ + if consistent_snapshot and embeded_version_number is not None: file_hashes = list(snapshot_metadata['meta'][metadata_name_extension] \ ['hashes'].values()) if embeded_digest not in file_hashes: logger.info('Removing outdated metadata: ' + repr(metadata_path)) os.remove(metadata_path) + """ - -def _get_written_metadata_and_digests(metadata_signable): +def _get_written_metadata(metadata_signable): """ - Non-public function that returns the actual content of written metadata and - its digest. + Non-public function that returns the actual content of written metadata. """ # Explicitly specify the JSON separators for Python 2 + 3 consistent. @@ -488,41 +492,36 @@ def _get_written_metadata_and_digests(metadata_signable): json.dumps(metadata_signable, indent=1, separators=(',', ': '), sort_keys=True).encode('utf-8') - written_metadata_digests = {} - - for hash_algorithm in tuf.conf.REPOSITORY_HASH_ALGORITHMS: - digest_object = tuf.hash.digest(hash_algorithm) - digest_object.update(written_metadata_content) - written_metadata_digests.update({hash_algorithm: digest_object.hexdigest()}) - - return written_metadata_content, written_metadata_digests + return written_metadata_content -def _strip_consistent_snapshot_digest(metadata_filename, consistent_snapshot): +def _strip_consistent_snapshot_version_number(metadata_filename, + consistent_snapshot): """ - Strip from 'metadata_filename' any digest data (in the expected - '{dirname}/digest.filename' format) that it may contain, and return it. + Strip from 'metadata_filename' any version data (in the expected + '{dirname}/version_number.filename' format) that it may contain, and return + it. """ - embeded_digest = '' + embeded_version_number = '' - # Strip the digest if 'consistent_snapshot' is True. - # Example: 'targets/unclaimed/13df98ab0.django.json' --> + # Strip the version number if 'consistent_snapshot' is True. + # Example: 'targets/unclaimed/10.django.json' --> # 'targets/unclaimed/django.json' if consistent_snapshot: dirname, basename = os.path.split(metadata_filename) - embeded_digest = basename[:basename.find('.')] + embeded_version_number = basename[:basename.find('.')] - # Ensure the digest, including the period, is stripped. + # Ensure the version number, including the period, is stripped. basename = basename[basename.find('.') + 1:] metadata_filename = os.path.join(dirname, basename) - return metadata_filename, embeded_digest + return metadata_filename, embeded_version_number @@ -544,7 +543,8 @@ def _load_top_level_metadata(repository, top_level_filenames): snapshot_metadata = None timestamp_metadata = None - # Load 'root.json'. A Root role file without a digest is always written. + # Load 'root.json'. A Root role file without a version number is always + # written. if os.path.exists(root_filename): # Initialize the key and role metadata of the top-level roles. signable = tuf.util.load_json_file(root_filename) @@ -560,11 +560,11 @@ def _load_top_level_metadata(repository, top_level_filenames): if signature not in roleinfo['signatures']: roleinfo['signatures'].append(signature) - if os.path.exists(root_filename+'.gz'): + if os.path.exists(root_filename + '.gz'): roleinfo['compressions'].append('gz') - # By default, roleinfo['partial_loaded'] of top-level roles should be set to - # False in 'create_roledb_from_root_metadata()'. Update this field, if + # By default, roleinfo['partial_loaded'] of top-level roles should be set + # to False in 'create_roledb_from_root_metadata()'. Update this field, if # necessary, now that we have its signable object. if _metadata_is_partially_loaded('root', signable, roleinfo): roleinfo['partial_loaded'] = True @@ -581,8 +581,8 @@ def _load_top_level_metadata(repository, top_level_filenames): message = 'Cannot load the required root file: '+repr(root_filename) raise tuf.RepositoryError(message) - # Load 'timestamp.json'. A Timestamp role file without a digest is always - # written. + # Load 'timestamp.json'. A Timestamp role file without a version number is + # always written. if os.path.exists(timestamp_filename): signable = tuf.util.load_json_file(timestamp_filename) timestamp_metadata = signable['signed'] @@ -610,10 +610,9 @@ def _load_top_level_metadata(repository, top_level_filenames): # Load 'snapshot.json'. A consistent snapshot of Snapshot must be calculated # if 'consistent_snapshot' is True. if consistent_snapshot: - snapshot_hashes = timestamp_metadata['meta'][SNAPSHOT_FILENAME]['hashes'] - snapshot_digest = random.choice(list(snapshot_hashes.values())) + snapshot_version = timestamp_metadata['meta'][SNAPSHOT_FILENAME]['version'] dirname, basename = os.path.split(snapshot_filename) - snapshot_filename = os.path.join(dirname, snapshot_digest + '.' + basename) + snapshot_filename = os.path.join(dirname, str(snapshot_version) + '.' + basename) if os.path.exists(snapshot_filename): signable = tuf.util.load_json_file(snapshot_filename) @@ -626,7 +625,7 @@ def _load_top_level_metadata(repository, top_level_filenames): roleinfo = tuf.roledb.get_roleinfo('snapshot') roleinfo['expires'] = snapshot_metadata['expires'] roleinfo['version'] = snapshot_metadata['version'] - if os.path.exists(snapshot_filename+'.gz'): + if os.path.exists(snapshot_filename + '.gz'): roleinfo['compressions'].append('gz') if _metadata_is_partially_loaded('snapshot', signable, roleinfo): @@ -640,13 +639,12 @@ def _load_top_level_metadata(repository, top_level_filenames): else: pass - # Load 'targets.json'. A consistent snapshot of Targets must be calculated if - # 'consistent_snapshot' is True. + # Load 'targets.json'. A consistent snapshot of the Targets role must be + # calculated if 'consistent_snapshot' is True. if consistent_snapshot: - targets_hashes = snapshot_metadata['meta'][TARGETS_FILENAME]['hashes'] - targets_digest = random.choice(list(targets_hashes.values())) + targets_version = snapshot_metadata['meta'][TARGETS_FILENAME]['version'] dirname, basename = os.path.split(targets_filename) - targets_filename = os.path.join(dirname, targets_digest + '.' + basename) + targets_filename = os.path.join(dirname, str(targets_version) + '.' + basename) if os.path.exists(targets_filename): signable = tuf.util.load_json_file(targets_filename) @@ -663,7 +661,7 @@ def _load_top_level_metadata(repository, top_level_filenames): roleinfo['version'] = targets_metadata['version'] roleinfo['expires'] = targets_metadata['expires'] roleinfo['delegations'] = targets_metadata['delegations'] - if os.path.exists(targets_filename+'.gz'): + if os.path.exists(targets_filename + '.gz'): roleinfo['compressions'].append('gz') if _metadata_is_partially_loaded('targets', signable, roleinfo): @@ -1344,7 +1342,8 @@ def get_target_hash(target_filepath): -def generate_root_metadata(version, expiration_date, consistent_snapshot): +def generate_root_metadata(version, expiration_date, consistent_snapshot, + compression_algorithms=['gz']): """ Create the root metadata. 'tuf.roledb.py' and 'tuf.keydb.py' are read and @@ -1365,6 +1364,11 @@ def generate_root_metadata(version, expiration_date, consistent_snapshot): Boolean. If True, a file digest is expected to be prepended to the filename of any target file located in the targets directory. Each digest is stripped from the target filename and listed in the snapshot metadata. + + compression_algorithms: + A list of compression algorithms to use when generating the compressed + metadata files for the repository. The root file specifies the + algorithms used by the repository. tuf.FormatError, if the generated root metadata object could not @@ -1387,6 +1391,7 @@ def generate_root_metadata(version, expiration_date, consistent_snapshot): tuf.formats.METADATAVERSION_SCHEMA.check_match(version) tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(expiration_date) tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) + tuf.formats.COMPRESSIONS_SCHEMA.check_match(compression_algorithms) # The role and key dictionaries to be saved in the root metadata object. # Conformant to 'ROLEDICT_SCHEMA' and 'KEYDICT_SCHEMA', respectively. @@ -1444,7 +1449,8 @@ def generate_root_metadata(version, expiration_date, consistent_snapshot): # Generate the root metadata object. root_metadata = tuf.formats.RootFile.make_metadata(version, expiration_date, keydict, roledict, - consistent_snapshot) + consistent_snapshot, + compression_algorithms) return root_metadata @@ -1646,6 +1652,7 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date, versiondict[ROOT_FILENAME] = get_metadata_versioninfo(root_filename) versiondict[TARGETS_FILENAME] = get_metadata_versioninfo(targets_filename) + """ # Add compressed versions of the 'targets.json' and 'root.json' metadata, # if they exist. for extension in SUPPORTED_COMPRESSION_EXTENSIONS: @@ -1661,9 +1668,11 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date, versiondict[TARGETS_FILENAME+extension] = \ get_metadata_versioninfo(compressed_targets_filename) - # Walk the 'targets/' directory and generate the fileinfo of all the role - # files found. This information is stored in the 'meta' field of the snapshot - # metadata object. + """ + + # Walk the 'targets/' directory and generate the versioninfo of all the role + # files found. This information is stored in the 'meta' field of the + # snapshot metadata object. targets_metadata = os.path.join(metadata_directory, 'targets') if os.path.exists(targets_metadata) and os.path.isdir(targets_metadata): for directory_path, junk_directories, files in os.walk(targets_metadata): @@ -1674,14 +1683,13 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date, metadata_name = \ metadata_path[len(metadata_directory):].lstrip(os.path.sep) - # Strip the digest if 'consistent_snapshot' is True. - # Example: 'targets/unclaimed/13df98ab0.django.json' --> + # Strip the version number if 'consistent_snapshot' is True. + # Example: 'targets/unclaimed/10.django.json' --> # 'targets/unclaimed/django.json' - metadata_name, digest_junk = \ - _strip_consistent_snapshot_digest(metadata_name, consistent_snapshot) + metadata_name, version_number_junk = \ + _strip_consistent_snapshot_version_number(metadata_name, consistent_snapshot) - # All delegated roles are added to the snapshot file, including - # compressed versions. + # All delegated roles are added to the snapshot file. for metadata_extension in METADATA_EXTENSIONS: if metadata_name.endswith(metadata_extension): rolename = metadata_name[:-len(metadata_extension)] @@ -1754,7 +1762,7 @@ def generate_timestamp_metadata(snapshot_filename, version, versioninfo[SNAPSHOT_FILENAME] = get_metadata_versioninfo('snapshot') # Save the versioninfo of the compressed versions of 'timestamp.json' - # in 'fileinfo'. Log the files included in 'fileinfo'. + # in 'versioninfo'. Log the files included in 'fileinfo'. # TODO: Since version numbers are now stored, the version numbers of # compressed roles do not change and can thus be excluded. Remove this # after testing. @@ -1764,6 +1772,7 @@ def generate_timestamp_metadata(snapshot_filename, version, continue compressed_filename = snapshot_filename + '.' + file_extension + try: compressed_fileinfo = get_metadata_fileinfo(compressed_filename) @@ -1874,7 +1883,8 @@ def sign_metadata(metadata_object, keyids, filename): -def write_metadata_file(metadata, filename, compressions, consistent_snapshot): +def write_metadata_file(metadata, filename, version_number, compressions, + consistent_snapshot): """ If necessary, write the 'metadata' signable object to 'filename', and the @@ -1893,6 +1903,11 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshot): If a compression algorithm is specified in 'compressions', the compression extention is appended to 'filename'. + version_number: + The version number of the metadata file to be written. The version + number is needed for consistent snapshots, which prepend the version + number to 'filename'. + compressions: Specify the algorithms, as a list of strings, used to compress the file; The only currently available compression option is 'gz' (gzip). @@ -1922,6 +1937,7 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshot): # Raise 'tuf.FormatError' if the check fails. tuf.formats.SIGNABLE_SCHEMA.check_match(metadata) tuf.formats.PATH_SCHEMA.check_match(filename) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version_number) tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) @@ -1929,21 +1945,19 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshot): # path so that temporary files are moved to their expected destinations. filename = os.path.abspath(filename) written_filename = filename + written_consistent_filename = None _check_directory(os.path.dirname(filename)) - consistent_filenames = [] # Generate the actual metadata file content of 'metadata'. Metadata is - # saved as json and includes formatting, such as indentation and sorted + # saved as JSON and includes formatting, such as indentation and sorted # objects. The new digest of 'metadata' is also calculated to help determine # if re-saving is required. - file_content, new_digests = _get_written_metadata_and_digests(metadata) + file_content = _get_written_metadata(metadata) if consistent_snapshot: - for new_digest in six.itervalues(new_digests): - dirname, basename = os.path.split(filename) - digest_and_filename = new_digest + '.' + basename - consistent_filenames.append(os.path.join(dirname, digest_and_filename)) - written_filename = consistent_filenames.pop() + dirname, basename = os.path.split(filename) + version_and_filename = str(version_number) + '.' + basename + written_consistent_filename = os.path.join(dirname, version_and_filename) # Verify whether new metadata needs to be written (i.e., has not been # previously written or has changed. @@ -1953,6 +1967,13 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshot): # 'write_compressed_version' to True so that it is written. # compressed metadata should only be written if it does not exist or the # uncompressed version has changed). + new_digests = {} + hash_algorithms = tuf.conf.REPOSITORY_HASH_ALGORITHMS + for hash_algorithm in hash_algorithms: + digest_object = tuf.hash.digest(hash_algorithm) + digest_object.update(file_content) + new_digests.update({hash_algorithm: digest_object.hexdigest()}) + try: file_length_junk, old_digests = tuf.util.get_file_details(written_filename) if old_digests != new_digests: @@ -1976,11 +1997,10 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshot): file_object.write(file_content) logger.debug('Saving ' + repr(written_filename)) file_object.move(written_filename) - - for consistent_filename in consistent_filenames: - logger.info('Linking ' + repr(consistent_filename)) - os.link(written_filename, consistent_filename) + if consistent_snapshot: + logger.info('Linking ' + repr(written_consistent_filename)) + os.link(written_filename, written_consistent_filename) # Generate the compressed versions of 'metadata', if necessary. A compressed # file may be written (without needing to write the uncompressed version) if @@ -2009,7 +2029,7 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshot): gzip_object.close() else: - raise tuf.FormatError('Unknown compression algorithm: '+repr(compression)) + raise tuf.FormatError('Unknown compression algorithm: ' + repr(compression)) # Save the compressed version, ensuring an unchanged file is not re-saved. # Re-saving the same compressed version may cause its digest to unexpectedly diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index bf9e4542..082c68ae 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -178,7 +178,8 @@ def __init__(self, repository_directory, metadata_directory, targets_directory): - def write(self, write_partial=False, consistent_snapshot=False): + def write(self, write_partial=False, consistent_snapshot=False, + compressions=['gz']): """ Write all the JSON Metadata objects to their corresponding files. @@ -200,6 +201,11 @@ def write(self, write_partial=False, consistent_snapshot=False): .targets.json.gz, .README.json, where is the file's SHA256 digest. Example: 1f4e35a60c8f96d439e27e858ce2869c770c1cdd54e1ef76657ceaaf01da18a3.root.json' + + compressions: + A list of compression algorithms. Each of these algorithms will be + used to compress all of the metadata available on the repository. + By default, all metadata is compressed with gzip. tuf.UnsignedMetadataError, if any of the top-level and delegated roles do @@ -217,7 +223,9 @@ def write(self, write_partial=False, consistent_snapshot=False): # types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if any are improperly formatted. tuf.formats.BOOLEAN_SCHEMA.check_match(write_partial) - tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) + tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) + tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) + # At this point the tuf.keydb and tuf.roledb stores must be fully # populated, otherwise write() throwns a 'tuf.UnsignedMetadataError' @@ -263,7 +271,7 @@ def write(self, write_partial=False, consistent_snapshot=False): repo_lib._generate_and_write_metadata('root', root_filename, write_partial, self._targets_directory, self._metadata_directory, - consistent_snapshot) + consistent_snapshot, compressions) # Generate the 'targets.json' metadata file. targets_filename = repo_lib.TARGETS_FILENAME @@ -2726,11 +2734,11 @@ def load_repository(repository_directory): metadata_name = \ metadata_path[len(metadata_directory):].lstrip(os.path.sep) - # Strip the digest if 'consistent_snapshot' is True. - # Example: 'targets/unclaimed/13df98ab0.django.json' --> + # Strip the version number if 'consistent_snapshot' is True. + # Example: 'targets/unclaimed/10.django.json' --> # 'targets/unclaimed/django.json' - metadata_name, digest_junk = \ - repo_lib._strip_consistent_snapshot_digest(metadata_name, + metadata_name, version_number_junk = \ + repo_lib._strip_consistent_snapshot_version_number(metadata_name, consistent_snapshot) if metadata_name.endswith(METADATA_EXTENSION): From 4cb851ca0a13683ee12bf712db34c0f80a9832bb Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 27 Oct 2015 16:11:11 -0400 Subject: [PATCH 10/30] Fix remaining issues with unit tests after implementing version numbers in snapshot.json --- tests/test_endless_data_attack.py | 2 +- tests/test_extraneous_dependencies_attack.py | 2 +- tests/test_formats.py | 66 +++---- tests/test_keydb.py | 8 +- tests/test_mix_and_match_attack.py | 2 +- tests/test_replay_attack.py | 3 +- tests/test_repository_lib.py | 17 +- tests/test_roledb.py | 12 +- tests/test_updater.py | 170 ++++++++++--------- tuf/client/updater.py | 109 ++++++++---- tuf/developer_tool.py | 3 +- tuf/repository_lib.py | 25 ++- tuf/repository_tool.py | 22 +-- 13 files changed, 253 insertions(+), 188 deletions(-) diff --git a/tests/test_endless_data_attack.py b/tests/test_endless_data_attack.py index 0c842016..afc12e27 100755 --- a/tests/test_endless_data_attack.py +++ b/tests/test_endless_data_attack.py @@ -282,7 +282,7 @@ def test_with_tuf(self): except tuf.NoWorkingMirrorError as exception: for mirror_url, mirror_error in six.iteritems(exception.mirror_errors): - self.assertTrue(isinstance(mirror_error, tuf.InvalidMetadataJSONError)) + self.assertTrue(isinstance(mirror_error, tuf.Error)) else: self.fail('TUF did not prevent an endless data attack.') diff --git a/tests/test_extraneous_dependencies_attack.py b/tests/test_extraneous_dependencies_attack.py index 2265ea70..2ff98971 100755 --- a/tests/test_extraneous_dependencies_attack.py +++ b/tests/test_extraneous_dependencies_attack.py @@ -223,7 +223,7 @@ def test_with_tuf(self): # Verify that 'role1.json' is the culprit. self.assertEqual(url_file, mirror_url) - self.assertTrue(isinstance(mirror_error, tuf.BadHashError)) + self.assertTrue(isinstance(mirror_error, tuf.ForbiddenTargetError)) else: self.fail('TUF did not prevent an extraneous dependencies attack.') diff --git a/tests/test_formats.py b/tests/test_formats.py index 1cc25c1c..162ed9c3 100755 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -197,6 +197,7 @@ def test_schemas(self): {'_type': 'Root', 'version': 8, 'consistent_snapshot': False, + 'compression_algorithms': ['gz'], 'expires': '1985-10-21T13:20:00Z', 'keys': {'123abc': {'keytype': 'rsa', 'keyval': {'public': 'pubkey', @@ -223,17 +224,13 @@ def test_schemas(self): {'_type': 'Snapshot', 'version': 8, 'expires': '1985-10-21T13:20:00Z', - 'meta': {'metadata/snapshot.json': {'length': 1024, - 'hashes': {'sha256': 'ABCD123'}, - 'custom': {'type': 'metadata'}}}}), + 'meta': {'metadata/snapshot.json': {'version': 1024}}}), 'TIMESTAMP_SCHEMA': (tuf.formats.TIMESTAMP_SCHEMA, {'_type': 'Timestamp', 'version': 8, 'expires': '1985-10-21T13:20:00Z', - 'meta': {'metadata/timestamp.json': {'length': 1024, - 'hashes': {'sha256': 'ABCD123'}, - 'custom': {'type': 'metadata'}}}}), + 'meta': {'metadata/timestamp.json': {'version': 1024}}}), 'MIRROR_SCHEMA': (tuf.formats.MIRROR_SCHEMA, {'url_prefix': 'http://localhost:8001', @@ -303,29 +300,27 @@ def test_TimestampFile(self): # Test conditions for valid instances of 'tuf.formats.TimestampFile'. version = 8 expires = '1985-10-21T13:20:00Z' - filedict = {'metadata/timestamp.json': {'length': 1024, - 'hashes': {'sha256': 'ABCD123'}, - 'custom': {'type': 'metadata'}}} + versiondict = {'targets.json': {'version': version}} make_metadata = tuf.formats.TimestampFile.make_metadata from_metadata = tuf.formats.TimestampFile.from_metadata TIMESTAMP_SCHEMA = tuf.formats.TIMESTAMP_SCHEMA self.assertTrue(TIMESTAMP_SCHEMA.matches(make_metadata(version, expires, - filedict))) - metadata = make_metadata(version, expires, filedict) + versiondict))) + metadata = make_metadata(version, expires, versiondict) self.assertTrue(isinstance(from_metadata(metadata), tuf.formats.TimestampFile)) # Test conditions for invalid arguments. bad_version = 'eight' bad_expires = '2000' - bad_filedict = 123 + bad_versiondict = 123 self.assertRaises(tuf.FormatError, make_metadata, bad_version, - expires, filedict) + expires, versiondict) self.assertRaises(tuf.FormatError, make_metadata, version, - bad_expires, filedict) + bad_expires, versiondict) self.assertRaises(tuf.FormatError, make_metadata, version, - expires, bad_filedict) + expires, bad_versiondict) self.assertRaises(tuf.FormatError, from_metadata, 123) @@ -345,6 +340,8 @@ def test_RootFile(self): roledict = {'root': {'keyids': ['123abc'], 'threshold': 1, 'paths': ['path1/', 'path2']}} + + compression_algorithms = ['gz'] make_metadata = tuf.formats.RootFile.make_metadata from_metadata = tuf.formats.RootFile.from_metadata @@ -352,9 +349,10 @@ def test_RootFile(self): self.assertTrue(ROOT_SCHEMA.matches(make_metadata(version, expires, keydict, roledict, - consistent_snapshot))) + consistent_snapshot, + compression_algorithms))) metadata = make_metadata(version, expires, keydict, roledict, - consistent_snapshot) + consistent_snapshot, compression_algorithms) self.assertTrue(isinstance(from_metadata(metadata), tuf.formats.RootFile)) # Test conditions for invalid arguments. @@ -362,23 +360,28 @@ def test_RootFile(self): bad_expires = 'eight' bad_keydict = 123 bad_roledict = 123 + bad_compression_algorithms = 'nozip' self.assertRaises(tuf.FormatError, make_metadata, bad_version, expires, keydict, roledict, - consistent_snapshot) + consistent_snapshot, + compression_algorithms) self.assertRaises(tuf.FormatError, make_metadata, version, bad_expires, keydict, roledict, - consistent_snapshot) + consistent_snapshot, + compression_algorithms) self.assertRaises(tuf.FormatError, make_metadata, version, expires, bad_keydict, roledict, - consistent_snapshot) + consistent_snapshot, + compression_algorithms) self.assertRaises(tuf.FormatError, make_metadata, version, expires, keydict, bad_roledict, - consistent_snapshot) + consistent_snapshot, + compression_algorithms) self.assertRaises(tuf.FormatError, from_metadata, 'bad') @@ -388,30 +391,27 @@ def test_SnapshotFile(self): # Test conditions for valid instances of 'tuf.formats.SnapshotFile'. version = 8 expires = '1985-10-21T13:20:00Z' - - filedict = {'metadata/snapshot.json': {'length': 1024, - 'hashes': {'sha256': 'ABCD123'}, - 'custom': {'type': 'metadata'}}} - + versiondict = {'targets.json' : {'version': version}} + make_metadata = tuf.formats.SnapshotFile.make_metadata from_metadata = tuf.formats.SnapshotFile.from_metadata SNAPSHOT_SCHEMA = tuf.formats.SNAPSHOT_SCHEMA self.assertTrue(SNAPSHOT_SCHEMA.matches(make_metadata(version, expires, - filedict))) - metadata = make_metadata(version, expires, filedict) + versiondict))) + metadata = make_metadata(version, expires, versiondict) self.assertTrue(isinstance(from_metadata(metadata), tuf.formats.SnapshotFile)) # Test conditions for invalid arguments. bad_version = '8' bad_expires = '2000' - bad_filedict = 123 + bad_versiondict = 123 self.assertRaises(tuf.FormatError, make_metadata, version, - expires, bad_filedict) + expires, bad_versiondict) self.assertRaises(tuf.FormatError, make_metadata, bad_version, expires, - filedict) + versiondict) self.assertRaises(tuf.FormatError, make_metadata, version, bad_expires, - bad_filedict) + bad_versiondict) self.assertRaises(tuf.FormatError, from_metadata, 123) @@ -548,6 +548,7 @@ def test_make_signable(self): root = {'_type': 'Root', 'version': 8, 'consistent_snapshot': False, + 'compression_algorithms': ['gz'], 'expires': '1985-10-21T13:20:00Z', 'keys': {'123abc': {'keytype': 'rsa', 'keyval': {'public': 'pubkey', @@ -679,6 +680,7 @@ def test_check_signable_object_format(self): root = {'_type': 'Root', 'version': 8, 'consistent_snapshot': False, + 'compression_algorithms': ['gz'], 'expires': '1985-10-21T13:20:00Z', 'keys': {'123abc': {'keytype': 'rsa', 'keyval': {'public': 'pubkey', diff --git a/tests/test_keydb.py b/tests/test_keydb.py index 394e66a6..d80b0257 100755 --- a/tests/test_keydb.py +++ b/tests/test_keydb.py @@ -190,11 +190,13 @@ def test_create_keydb_from_root_metadata(self): version = 8 consistent_snapshot = False expires = '1985-10-21T01:21:00Z' + compression_algorithms = ['gz'] root_metadata = tuf.formats.RootFile.make_metadata(version, expires, keydict, roledict, - consistent_snapshot) + consistent_snapshot, + compression_algorithms) self.assertEqual(None, tuf.keydb.create_keydb_from_root_metadata(root_metadata)) tuf.keydb.create_keydb_from_root_metadata(root_metadata) @@ -230,11 +232,13 @@ def test_create_keydb_from_root_metadata(self): keydict[keyid3] = rsakey3 version = 8 expires = '1985-10-21T01:21:00Z' + compression_algorithms = ['gz'] root_metadata = tuf.formats.RootFile.make_metadata(version, expires, keydict, roledict, - consistent_snapshot) + consistent_snapshot, + compression_algorithms) self.assertEqual(None, tuf.keydb.create_keydb_from_root_metadata(root_metadata)) # Ensure only 'keyid2' was added to the keydb database. 'keyid' and diff --git a/tests/test_mix_and_match_attack.py b/tests/test_mix_and_match_attack.py index c2de9838..01d82e3c 100755 --- a/tests/test_mix_and_match_attack.py +++ b/tests/test_mix_and_match_attack.py @@ -248,7 +248,7 @@ def test_with_tuf(self): # Verify that 'timestamp.json' is the culprit. self.assertEqual(url_file, mirror_url) - self.assertTrue(isinstance(mirror_error, tuf.BadHashError)) + self.assertTrue(isinstance(mirror_error, tuf.BadVersionNumberError)) else: self.fail('TUF did not prevent a mix-and-match attack.') diff --git a/tests/test_replay_attack.py b/tests/test_replay_attack.py index 6251c8c5..f94d04d2 100755 --- a/tests/test_replay_attack.py +++ b/tests/test_replay_attack.py @@ -289,7 +289,7 @@ def test_with_tuf(self): # version. repository.timestamp.expiration = datetime.datetime(2030, 1, 1, 12, 12) repository.write() - + # Move the staged metadata to the "live" metadata. shutil.rmtree(os.path.join(self.repository_directory, 'metadata')) shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'), @@ -315,6 +315,7 @@ def test_with_tuf(self): # Restore the previous version of 'timestamp.json' on the remote repository # and verify that the non-TUF client downloads it (expected, but not ideal). shutil.move(backup_timestamp, timestamp_path) + logger.info('Moving the backup timestamp to the current version.') # Verify that the TUF client detects replayed metadata and refuses to # continue the update process. diff --git a/tests/test_repository_lib.py b/tests/test_repository_lib.py index d4a35ed9..fd9ef9fe 100755 --- a/tests/test_repository_lib.py +++ b/tests/test_repository_lib.py @@ -620,27 +620,20 @@ def test_generate_timestamp_metadata(self): version = 1 expiration_date = '1985-10-21T13:20:00Z' - compression_algorithms = ['gz'] - snapshot_metadata = \ repo_lib.generate_timestamp_metadata(snapshot_filename, version, - expiration_date, - compression_algorithms) + expiration_date) self.assertTrue(tuf.formats.TIMESTAMP_SCHEMA.matches(snapshot_metadata)) # Test improperly formatted arguments. self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, - 3, version, expiration_date, compression_algorithms) + 3, version, expiration_date) self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, - snapshot_filename, '3', expiration_date, - compression_algorithms) + snapshot_filename, '3', expiration_date) self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, - snapshot_filename, version, '3', compression_algorithms) - self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, - snapshot_filename, version, expiration_date, 3) - self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, - snapshot_filename, version, expiration_date, ['compress']) + snapshot_filename, version, '3') + diff --git a/tests/test_roledb.py b/tests/test_roledb.py index 74dd7e09..5c3caeb4 100755 --- a/tests/test_roledb.py +++ b/tests/test_roledb.py @@ -329,12 +329,14 @@ def test_create_roledb_from_root_metadata(self): 'targets': {'keyids': [keyid2], 'threshold': 1}} version = 8 consistent_snapshot = False - expires = '1985-10-21T01:21:00Z' + expires = '1985-10-21T01:21:00Z' + compression_algorithms = ['gz'] root_metadata = tuf.formats.RootFile.make_metadata(version, expires, keydict, roledict, - consistent_snapshot) + consistent_snapshot, + compression_algorithms) self.assertEqual(None, tuf.roledb.create_roledb_from_root_metadata(root_metadata)) # Ensure 'Root' and 'Targets' were added to the role database. @@ -372,7 +374,8 @@ def test_create_roledb_from_root_metadata(self): root_metadata = tuf.formats.RootFile.make_metadata(version, expires, keydict, roledict, - consistent_snapshot) + consistent_snapshot, + compression_algorithms) self.assertRaises(tuf.Error, tuf.roledb.create_roledb_from_root_metadata, root_metadata) # Remove the invalid role and re-generate 'root_metadata' to test for the @@ -381,7 +384,8 @@ def test_create_roledb_from_root_metadata(self): root_metadata = tuf.formats.RootFile.make_metadata(version, expires, keydict, roledict, - consistent_snapshot) + consistent_snapshot, + compression_algorithms) self.assertEqual(None, tuf.roledb.create_roledb_from_root_metadata(root_metadata)) diff --git a/tests/test_updater.py b/tests/test_updater.py index 692e41e4..191a6da7 100755 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -547,7 +547,10 @@ def test_3__update_metadata(self): # This is the default metadata that we would create for the timestamp role, # because it has no signed metadata for itself. DEFAULT_TIMESTAMP_FILELENGTH = tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH - + + # This is the the upper bound length for Targets metadata. + DEFAULT_TARGETS_FILELENGTH = tuf.conf.DEFAULT_TARGETS_REQUIRED_LENGTH + # Save the versioninfo of 'targets.json,' needed later when re-installing # with _update_metadata(). targets_versioninfo = \ @@ -557,7 +560,9 @@ def test_3__update_metadata(self): # Remove the currently installed metadata from the store and disk. Verify # that the metadata dictionary is re-populated after calling # _update_metadata(). - self.repository_updater.metadata['current'].clear() + del self.repository_updater.metadata['current']['timestamp'] + del self.repository_updater.metadata['current']['targets'] + timestamp_filepath = \ os.path.join(self.client_metadata_current, 'timestamp.json') targets_filepath = os.path.join(self.client_metadata_current, 'targets.json') @@ -575,7 +580,9 @@ def test_3__update_metadata(self): # Verify 'targets.json' is properly installed. self.assertFalse('targets' in self.repository_updater.metadata['current']) - self.repository_updater._update_metadata('targets', targets_versioninfo) + self.repository_updater._update_metadata('targets', + DEFAULT_TARGETS_FILELENGTH, + targets_versioninfo['version']) self.assertTrue('targets' in self.repository_updater.metadata['current']) targets_signable = tuf.util.load_json_file(targets_filepath) @@ -590,45 +597,47 @@ def test_3__update_metadata(self): # Verify 'targets.json.gz' is properly intalled. Note: The uncompressed # version is installed if the compressed one is downloaded. self.assertFalse('targets' in self.repository_updater.metadata['current']) - self.repository_updater._update_metadata('targets', targets_fileinfo, 'gzip', - targets_compressed_fileinfo) + self.repository_updater._update_metadata('targets', + DEFAULT_TARGETS_FILELENGTH, + targets_versioninfo['version'], + 'gzip') self.assertTrue('targets' in self.repository_updater.metadata['current']) - length, hashes = tuf.util.get_file_details(targets_filepath) - self.assertEqual(targets_fileinfo, tuf.formats.make_fileinfo(length, hashes)) + self.assertEqual(targets_versioninfo['version'], + self.repository_updater.metadata['current']['targets']['version']) - # Test: Invalid fileinfo. - # Invalid fileinfo for the uncompressed version of 'targets.json'. + # Test: Invalid version numbers. + # Invalid version number for the uncompressed version of 'targets.json'. self.assertRaises(tuf.NoWorkingMirrorError, self.repository_updater._update_metadata, - 'targets', targets_compressed_fileinfo) + 'targets', DEFAULT_TARGETS_FILELENGTH, 88) # Verify that the specific exception raised is correct for the previous # case. try: self.repository_updater._update_metadata('targets', - targets_compressed_fileinfo) + DEFAULT_TARGETS_FILELENGTH, 88) except tuf.NoWorkingMirrorError as e: for mirror_error in six.itervalues(e.mirror_errors): - assert isinstance(mirror_error, tuf.BadHashError) + assert isinstance(mirror_error, tuf.BadVersionNumberError) - # Invalid fileinfo for the compressed version of 'targets.json' + # Invalid version number for the compressed version of 'targets.json' self.assertRaises(tuf.NoWorkingMirrorError, self.repository_updater._update_metadata, - 'targets', targets_compressed_fileinfo, 'gzip', - targets_fileinfo) + 'targets', DEFAULT_TARGETS_FILELENGTH, 88, + 'gzip') # Verify that the specific exception raised is correct for the previous - # case. The length is checked before the hashes, so the specific error in + # case. The version number is checked before the hashes, so the specific error in # this case should be 'tuf.DownloadLengthMismatchError'. try: self.repository_updater._update_metadata('targets', - targets_compressed_fileinfo, - 'gzip', targets_fileinfo) + DEFAULT_TARGETS_FILELENGTH, + 88, 'gzip') except tuf.NoWorkingMirrorError as e: for mirror_error in six.itervalues(e.mirror_errors): - assert isinstance(mirror_error, tuf.DownloadLengthMismatchError) + assert isinstance(mirror_error, tuf.BadVersionNumberError) @@ -652,7 +661,6 @@ def test_3__update_metadata_if_changed(self): # Verify the current version of 'targets.json' has not changed. self.assertEqual(self.repository_updater.metadata['current']['targets']['version'], 1) - # Modify one target file on the remote repository. repository = repo_tool.load_repository(self.repository_directory) target3 = os.path.join(self.repository_directory, 'targets', 'file3.txt') @@ -668,12 +676,12 @@ def test_3__update_metadata_if_changed(self): shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'), os.path.join(self.repository_directory, 'metadata')) - # Update 'targets.json' and verify that the client's current 'targets.json' # has been updated. 'timestamp' and 'snapshot' must be manually updated # so that new 'targets' may be recognized. DEFAULT_TIMESTAMP_FILELENGTH = tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH + logger.info('Attempting to increment targets to version 2...') self.repository_updater._update_metadata('timestamp', DEFAULT_TIMESTAMP_FILELENGTH) self.repository_updater._update_metadata_if_changed('snapshot', 'timestamp') self.repository_updater._update_metadata_if_changed('targets') @@ -1013,68 +1021,65 @@ def test_6_download_target(self): target_filepaths = \ list(self.repository_updater.metadata['current']['targets']['targets'].keys()) - # Test: normal case. # Get the target info, which is an argument to 'download_target()'. - for target_filepath in target_filepaths: - target_fileinfo = self.repository_updater.target(target_filepath) - self.repository_updater.download_target(target_fileinfo, - destination_directory) + + # 'target_filepaths' is expected to have at least two targets. The first + # target will be used to test against download_target(). The second + # will be used to test against download_target() and a repository with + # consistent snapshots. + target_filepath1 = target_filepaths.pop() + target_fileinfo = self.repository_updater.target(target_filepath1) + self.repository_updater.download_target(target_fileinfo, + destination_directory) - download_filepath = \ - os.path.join(destination_directory, target_filepath.lstrip('/')) - self.assertTrue(os.path.exists(download_filepath)) - length, hashes = tuf.util.get_file_details(download_filepath) - download_targetfileinfo = tuf.formats.make_fileinfo(length, hashes) + download_filepath = \ + os.path.join(destination_directory, target_filepath1.lstrip('/')) + self.assertTrue(os.path.exists(download_filepath)) + length, hashes = tuf.util.get_file_details(download_filepath) + download_targetfileinfo = tuf.formats.make_fileinfo(length, hashes) + + # Add any 'custom' data from the repository's target fileinfo to the + # 'download_targetfileinfo' object being tested. + if 'custom' in target_fileinfo['fileinfo']: + download_targetfileinfo['custom'] = target_fileinfo['fileinfo']['custom'] + self.assertEqual(target_fileinfo['fileinfo'], download_targetfileinfo) + + # Test when consistent snapshots is set. First, create a valid + # repository with consistent snapshots set (root.json contains a + # "consistent_snapshot" entry that the updater uses to correctly fetch + # snapshots. The updater expects the existence of + # .filename files if root.json sets 'consistent_snapshot + # = True'. + + # The repository must be rewritten with consistent snapshots set. + repository = repo_tool.load_repository(self.repository_directory) + + repository.root.load_signing_key(self.role_keys['root']['private']) + repository.snapshot.load_signing_key(self.role_keys['snapshot']['private']) + repository.timestamp.load_signing_key(self.role_keys['timestamp']['private']) - # Add any 'custom' data from the repository's target fileinfo to the - # 'download_targetfileinfo' object being tested. - if 'custom' in target_fileinfo['fileinfo']: - download_targetfileinfo['custom'] = target_fileinfo['fileinfo']['custom'] - self.assertEqual(target_fileinfo['fileinfo'], download_targetfileinfo) - - # Test when consistent snapshots is set. First, create a valid - # repository with consistent snapshots set (root.json contains a - # "consistent_snapshots" entry that the updater uses to correctly fetch - # snapshots. The updater expects the existence of .filename files - # if root.json sets 'consistent_snapshot = True'. - - # The repository must be rewritten with consistent snapshots set. - repository = repo_tool.load_repository(self.repository_directory) - - repository.root.load_signing_key(self.role_keys['root']['private']) - repository.snapshot.load_signing_key(self.role_keys['snapshot']['private']) - repository.timestamp.load_signing_key(self.role_keys['timestamp']['private']) - repository.write(consistent_snapshot=True) - - # Move the staged metadata to the "live" metadata. - shutil.rmtree(os.path.join(self.repository_directory, 'metadata')) - shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'), - os.path.join(self.repository_directory, 'metadata')) - - self.repository_updater.refresh() - - # self.repository_updater.consistent_snapshot = True - - self.repository_updater.download_target(target_fileinfo, - destination_directory) + repository.write(consistent_snapshot=True) + + # Move the staged metadata to the "live" metadata. + shutil.rmtree(os.path.join(self.repository_directory, 'metadata')) + shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'), + os.path.join(self.repository_directory, 'metadata')) + + self.repository_updater.refresh() + + target_filepath2 = target_filepaths.pop() + target_fileinfo2 = self.repository_updater.target(target_filepath2) + self.repository_updater.download_target(target_fileinfo2, + destination_directory) # Test: Invalid arguments. self.assertRaises(tuf.FormatError, self.repository_updater.download_target, 8, destination_directory) - random_target_filepath = target_filepaths.pop() - target_fileinfo = self.repository_updater.target(random_target_filepath) self.assertRaises(tuf.FormatError, self.repository_updater.download_target, target_fileinfo, 8) - # Non-existent destination. - # TODO: test for non-existent directories. - """ - self.assertRaises(tuf.Error, self.repository_updater.download_target, - target_fileinfo, 'non-existent/bad_path') - """ - # Test: # Attempt a file download of a valid target, however, a download exception # occurs because the target is not within the mirror's confined target @@ -1100,15 +1105,16 @@ def test_6_download_target(self): def test_7_updated_targets(self): - # Verify that list contains all files that need to be updated, these - # files include modified and new target files. Also, confirm that files - # than need not to be updated are absent from the list. + # Verify that the list of targets returned by updated_targets() contains + # all the files that need to be updated, these files include modified and + # new target files. Also, confirm that files than need not to be updated + # are absent from the list. # Setup # Create temporary directory which will hold client's target files. destination_directory = self.make_temp_directory() - # Get the list of target files. It will be used as an argument to - # 'updated_targets' function. + # Get the list of target files. It will be used as an argument to the + # 'updated_targets()' function. all_targets = self.repository_updater.all_targets() # Test for duplicates and targets in the root directory of the repository. @@ -1161,9 +1167,18 @@ def test_7_updated_targets(self): target1 = os.path.join(self.repository_directory, 'targets', 'file1.txt') repository.targets.remove_target(target1) + + length, hashes = tuf.util.get_file_details(target1) + + repository.targets.add_target(target1) + repository.targets.load_signing_key(self.role_keys['targets']['private']) + repository.snapshot.load_signing_key(self.role_keys['snapshot']['private']) + with open(target1, 'a') as file_object: file_object.write('append extra text') + length, hashes = tuf.util.get_file_details(target1) + repository.targets.add_target(target1) repository.targets.load_signing_key(self.role_keys['targets']['private']) repository.snapshot.load_signing_key(self.role_keys['snapshot']['private']) @@ -1175,7 +1190,8 @@ def test_7_updated_targets(self): shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'), os.path.join(self.repository_directory, 'metadata')) - # Ensure the client has the up-to-date metadata. + # Ensure the client has up-to-date metadata. + logger.info('refreshing top-level metadata after updating targets.json..') self.repository_updater.refresh() # Verify that the new target file is considered updated. diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 31323710..3b08c0ed 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -921,10 +921,13 @@ def _verify_uncompressed_metadata_file(self, metadata_file_object, # Is 'metadata_signable' expired? self._ensure_not_expired(metadata_signable['signed'], metadata_role) + """ + # Is 'metadata_signable' newer than the currently installed # version? current_metadata_role = self.metadata['current'].get(metadata_role) + # Compare metadata version numbers. Ensure there is a current # version of the metadata role to be updated. if current_metadata_role is not None: @@ -933,6 +936,12 @@ def _verify_uncompressed_metadata_file(self, metadata_file_object, if downloaded_version < current_version: raise tuf.ReplayedMetadataError(metadata_role, downloaded_version, current_version) + else: + logger.info('current_version >= downloaded_version') + + else: + logger.info('current_metadata_role is None') + """ # Reject the metadata if any specified targets are not allowed. # 'tuf.ForbiddenTargetError' raised if any of the targets of 'metadata_role' @@ -1114,29 +1123,44 @@ def _get_metadata_file(self, metadata_role, remote_filename, # Verify 'file_object' according to the callable function. # 'file_object' is also verified if decompressed above (i.e., the # uncompressed version). - metadata_signable = \ tuf.util.load_json_string(file_object.read().decode('utf-8')) # If the version number is unspecified, ensure that the version number - # downloaded is greater than the currently trusted version number. + # downloaded is greater than the currently trusted version number for + # 'metadata_role'. version_downloaded = metadata_signable['signed']['version'] - if expected_version is None: - if version_downloaded <= expected_version: - message = \ - 'Downloaded version number: ' + repr(version_downloaded) + '.' \ - ' Version number MUST be greater than: ' + repr(expected_version) - raise tuf.BadVersionNumberError(message) - - # Otherwise, verify that the downloaded version matches the version - # requested. - else: + + if expected_version is not None: + # Verify that the downloaded version matches the version expected by + # the caller. if version_downloaded != expected_version: message = \ 'Downloaded version number: ' + repr(version_downloaded) + '.' \ ' Version number MUST be: ' + repr(expected_version) raise tuf.BadVersionNumberError(message) - + + # The caller does not know which version to download. Verify that the + # downloaded version is at least greater than the one locally available. + else: + # Verify that the version number of the locally stored + # 'timestamp.json', if available, is less than what was downloaded. + # Otherwise, accept the new timestamp with version number + # 'version_downloaded'. + logger.info('metadata_role: ' + repr(metadata_role)) + try: + current_version = \ + self.metadata['current'][metadata_role]['version'] + + if version_downloaded < current_version: + raise tuf.ReplayedMetadataError(metadata_role, version_downloaded, + current_version) + + except KeyError: + logger.info(metadata_role + ' not available locally.') + + self._verify_uncompressed_metadata_file(file_object, metadata_role) + except Exception as exception: # Remember the error from this mirror, and "reset" the target file. logger.exception('Update failed from ' + file_mirror + '.') @@ -1436,7 +1460,8 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None, filename_version = version dirname, basename = os.path.split(remote_filename) remote_filename = os.path.join(dirname, str(filename_version) + '.' + basename) - + + logger.info('Verifying ' + repr(metadata_role) + ' requesting version: ' + repr(version)) metadata_file_object = \ self._get_metadata_file(metadata_role, remote_filename, upperbound_filelength, version, compression) @@ -1703,7 +1728,7 @@ def _versioninfo_has_changed(self, metadata_filename, new_versioninfo): Boolean. True if the versioninfo has increased, false otherwise. """ - + # If there is no versioninfo currently stored for 'metadata_filename', # try to load the file, calculate the versioninfo, and store it. if metadata_filename not in self.versioninfo: @@ -1786,13 +1811,36 @@ def _update_versioninfo(self, metadata_filename): trusted_versioninfo = \ self.metadata['current']['timestamp']['version'] + # When updating snapshot.json, the client either (1) has a copy of + # snapshot.json, or (2) in the process of obtaining it by first downloading + # timestamp.json. Note: Clients may have only root.json and perform a + # refresh of top-level metadata to obtain the remaining roles. elif metadata_filename == 'snapshot.json': - trusted_versioninfo = \ - self.metadata['current']['timestamp']['meta']['snapshot.json'] + + # Verify the version number of the currently trusted snapshot.json in + # snapshot.json itself. Checking the version number specified in + # timestamp.json may be greater than the version specified in the + # client's copy of snapshot.json. + try: + timestamp_version_number = self.metadata['current']['snapshot']['version'] + trusted_versioninfo = tuf.formats.make_versioninfo(timestamp_version_number) + + except KeyError: + trusted_versioninfo = \ + self.metadata['current']['timestamp']['meta']['snapshot.json'] else: - trusted_versioninfo = \ - self.metadata['current']['snapshot']['meta'][metadata_filename] + + try: + # The metadata file names in 'self.metadata' exclude the role + # extension. Strip the '.json' extension when checking if + # 'metadata_filename' currently exists. + targets_version_number = self.metadata['current'][metadata_filename[:-len('.json')]]['version'] + trusted_versioninfo = tuf.formats.make_versioninfo(targets_version_number) + + except KeyError: + trusted_versioninfo = \ + self.metadata['current']['snapshot']['meta'][metadata_filenamed] self.versioninfo[metadata_filename] = trusted_versioninfo @@ -2841,20 +2889,15 @@ def download_target(self, target, destination_directory): target_filepath.lstrip(os.sep)) destination = os.path.abspath(destination) target_dirpath = os.path.dirname(destination) - if target_dirpath: - try: - os.makedirs(target_dirpath) - - except OSError as e: - if e.errno == errno.EEXIST: - pass - - else: - raise - else: - message = repr(target_dirpath) + ' does not exist.' - logger.warning(message) - raise tuf.Error(message) + try: + os.makedirs(target_dirpath) + + except OSError as e: + if e.errno == errno.EEXIST: + pass + + else: + raise target_file_object.move(destination) diff --git a/tuf/developer_tool.py b/tuf/developer_tool.py index 4557c412..13ea3c6f 100755 --- a/tuf/developer_tool.py +++ b/tuf/developer_tool.py @@ -526,7 +526,8 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, if tuf.sig.verify(signable, rolename) or write_partial: _remove_invalid_and_duplicate_signatures(signable) compressions = roleinfo['compressions'] - filename = write_metadata_file(signable, metadata_filename, compressions, + filename = write_metadata_file(signable, metadata_filename, + metadata['version'], compressions, False) # 'signable' contains an invalid threshold of signatures. diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index 07eb80ea..2bf65e86 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -121,7 +121,6 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, # Retrieve the roleinfo of 'rolename' to extract the needed metadata # attributes, such as version number, expiration, etc. roleinfo = tuf.roledb.get_roleinfo(rolename) - snapshot_compressions = tuf.roledb.get_roleinfo('snapshot')['compressions'] # Generate the appropriate role metadata for 'rolename'. if rolename == 'root': @@ -140,6 +139,7 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, roleinfo['expires'], roleinfo['delegations'], consistent_snapshot) + if rolename == 'targets': _log_warning_if_expires_soon(TARGETS_FILENAME, roleinfo['expires'], TARGETS_EXPIRES_WARN_SECONDS) @@ -152,6 +152,7 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, roleinfo['expires'], root_filename, targets_filename, consistent_snapshot) + _log_warning_if_expires_soon(SNAPSHOT_FILENAME, roleinfo['expires'], SNAPSHOT_EXPIRES_WARN_SECONDS) @@ -160,8 +161,7 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, snapshot_filename = filenames['snapshot'] metadata = generate_timestamp_metadata(snapshot_filename, roleinfo['version'], - roleinfo['expires'], - snapshot_compressions) + roleinfo['expires']) _log_warning_if_expires_soon(TIMESTAMP_FILENAME, roleinfo['expires'], TIMESTAMP_EXPIRES_WARN_SECONDS) @@ -205,9 +205,9 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, metadata['version'], compressions, consistent_snapshot) - # The root and timestamp files should also be written without a digest if - # 'consistent_snaptshots' is True. Client may request a timestamp and root - # file without knowing its digest and file size. + # The root and timestamp files should also be written without a version + # number prepended if 'consistent_snaptshots' is True. Client may request + # a timestamp and root file without knowing its version number. if rolename == 'root' or rolename == 'timestamp': write_metadata_file(signable, metadata_filename, metadata['version'], compressions, consistent_snapshot=False) @@ -218,7 +218,7 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, message = 'Not enough signatures for ' + repr(metadata_filename) raise tuf.UnsignedMetadataError(message, signable) - return signable, filename + return signable, filename @@ -578,7 +578,7 @@ def _load_top_level_metadata(repository, top_level_filenames): consistent_snapshot = root_metadata['consistent_snapshot'] else: - message = 'Cannot load the required root file: '+repr(root_filename) + message = 'Cannot load the required root file: ' + repr(root_filename) raise tuf.RepositoryError(message) # Load 'timestamp.json'. A Timestamp role file without a version number is @@ -1542,7 +1542,7 @@ def generate_targets_metadata(targets_directory, target_files, version, # Note: join() discards 'targets_directory' if 'target' contains a leading # path separator (i.e., is treated as an absolute path). target_path = os.path.join(targets_directory, target.lstrip(os.sep)) - + # Ensure all target files listed in 'target_files' exist. If just one of # these files does not exist, raise an exception. if not os.path.exists(target_path): @@ -1711,7 +1711,7 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date, def generate_timestamp_metadata(snapshot_filename, version, - expiration_date, compressions=()): + expiration_date): """ Generate the timestamp metadata object. The 'snapshot.json' file must @@ -1755,7 +1755,6 @@ def generate_timestamp_metadata(snapshot_filename, version, tuf.formats.PATH_SCHEMA.check_match(snapshot_filename) tuf.formats.METADATAVERSION_SCHEMA.check_match(version) tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(expiration_date) - tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) # Retrieve the versioninfo of the Snapshot metadata file. versioninfo = {} @@ -1764,8 +1763,8 @@ def generate_timestamp_metadata(snapshot_filename, version, # Save the versioninfo of the compressed versions of 'timestamp.json' # in 'versioninfo'. Log the files included in 'fileinfo'. # TODO: Since version numbers are now stored, the version numbers of - # compressed roles do not change and can thus be excluded. Remove this - # after testing. + # compressed roles do not change and can thus be excluded. Remove the + # following commented lines after testing. """ for file_extension in compressions: if not len(file_extension): diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index 082c68ae..11903a78 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -197,10 +197,10 @@ def write(self, write_partial=False, consistent_snapshot=False, consistent_snapshot: A boolean indicating whether written metadata and target files should - include a digest in the filename (i.e., .root.json, - .targets.json.gz, .README.json, where is the - file's SHA256 digest. Example: - 1f4e35a60c8f96d439e27e858ce2869c770c1cdd54e1ef76657ceaaf01da18a3.root.json' + include a version number in the filename (i.e., + .root.json, .targets.json.gz, + .README.json, where is the file's + SHA256 digest. Example: 13.root.json' compressions: A list of compression algorithms. Each of these algorithms will be @@ -271,7 +271,7 @@ def write(self, write_partial=False, consistent_snapshot=False, repo_lib._generate_and_write_metadata('root', root_filename, write_partial, self._targets_directory, self._metadata_directory, - consistent_snapshot, compressions) + consistent_snapshot) # Generate the 'targets.json' metadata file. targets_filename = repo_lib.TARGETS_FILENAME @@ -2704,9 +2704,9 @@ def load_repository(repository_directory): filenames = repo_lib.get_metadata_filenames(metadata_directory) - # The Root file is always available without a consistent snapshots digest - # attached to the filename. Store the 'consistent_snapshot' value read the - # loaded Root file so that other metadata files may be located. + # The Root file is always available without a consistent snapshots version + # number attached to the filename. Store the 'consistent_snapshot' value + # read the loaded Root file so that other metadata files may be located. # 'consistent_snapshot' value. consistent_snapshot = False @@ -2747,10 +2747,10 @@ def load_repository(repository_directory): else: continue - + # Keep a store metadata previously loaded metadata to prevent # re-loading duplicate versions. Duplicate versions may occur with - # consistent_snapshot, where the same metadata may be available in + # 'consistent_snapshot', where the same metadata may be available in # multiples files (the different hash is included in each filename. if metadata_name in loaded_metadata: continue @@ -2766,6 +2766,8 @@ def load_repository(repository_directory): # Extract the metadata attributes 'metadata_name' and update its # corresponding roleinfo. + # TODO: Test for detection of the Targets role. + roleinfo = tuf.roledb.get_roleinfo(metadata_name) roleinfo['signatures'].extend(signable['signatures']) roleinfo['version'] = metadata_object['version'] From d027d8831276cb60771427e81815c9456ca82eac Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 27 Oct 2015 16:40:02 -0400 Subject: [PATCH 11/30] Review of 'formats.py' changes --- tuf/formats.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tuf/formats.py b/tuf/formats.py index a638e401..1dd77f01 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -255,16 +255,16 @@ keyid = KEYID_SCHEMA, keyval = KEYVAL_SCHEMA) -# Information that describes both metadata and target files. -# This schema allows the storage of multiple hashes for the same file -# (e.g., sha256 and sha512 may be computed for the same file and stored). +# Information about target files, like file length and file hash(es). This +# schema allows the storage of multiple hashes for the same file (e.g., sha256 +# and sha512 may be computed for the same file and stored). FILEINFO_SCHEMA = SCHEMA.Object( object_name = 'FILEINFO_SCHEMA', length = LENGTH_SCHEMA, hashes = HASHDICT_SCHEMA, custom = SCHEMA.Optional(SCHEMA.Object())) -# Version information included in "snapshot.json" for each role available on +# Version information specified in "snapshot.json" for each role available on # the TUF repository. The 'FILEINFO_SCHEMA' object was previously listed in # the snapshot role, but was switched to this object format to reduce the # amount of metadata that needs to be downloaded. Listing version numbers in @@ -274,13 +274,16 @@ object_name = 'VERSIONINFO_SCHEMA', version = METADATAVERSION_SCHEMA) -# +# A dict holding the version information for a particular metadata role. The +# dict keys hold the relative file paths, and the dict values the corresponding +# version numbers. VERSIONDICT_SCHEMA = SCHEMA.DictOf( key_schema = RELPATH_SCHEMA, value_schema = VERSIONINFO_SCHEMA) -# A dict holding the information for a particular file. The keys hold the -# relative file path and the values the relevant file information. +# A dict holding the information for a particular target / file. The dict keys +# hold the relative file paths, and the dict values the corresponding file +# information. FILEDICT_SCHEMA = SCHEMA.DictOf( key_schema = RELPATH_SCHEMA, value_schema = FILEINFO_SCHEMA) @@ -1014,20 +1017,17 @@ def make_fileinfo(length, hashes, custom=None): - def make_versioninfo(version_number): """ - Create a dictionary conformant to 'VERSIONINFO_SCHEMA'. - This dict describes both metadata and target files. + Create a dictionary conformant to 'VERSIONINFO_SCHEMA'. This dict + describes both metadata and target files. - length: - An integer representing the size of the file. - - hashes: - A dict of hashes in 'HASHDICT_SCHEMA' format, which has the form: - {'sha256': 123df8a9b12, 'sha512': 324324dfc121, ...} + version_number: + An integer representing the version of a particular metadata role. + The dictionary returned by this function is expected to be included + in Snapshot metadata. tuf.FormatError, if the 'VERSIONINFO_SCHEMA' to be returned @@ -1039,8 +1039,8 @@ def make_versioninfo(version_number): will raise a 'tuf.FormatError' exception. - A dictionary conformant to 'VERSIONINFO_SCHEMA', ile - information of a metadata or target file. + A dictionary conformant to 'VERSIONINFO_SCHEMA', containing the version + information of a metadata role. """ versioninfo = {'version' : version_number} From 0912495b2bc96b9aae74a980b11992bcb07e0bd6 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 27 Oct 2015 16:55:51 -0400 Subject: [PATCH 12/30] Review 'test_formats.py' --- tests/test_formats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_formats.py b/tests/test_formats.py index 162ed9c3..4bcff487 100755 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -360,7 +360,7 @@ def test_RootFile(self): bad_expires = 'eight' bad_keydict = 123 bad_roledict = 123 - bad_compression_algorithms = 'nozip' + bad_compression_algorithms = ['nozip'] self.assertRaises(tuf.FormatError, make_metadata, bad_version, expires, From af6f5c9d83b38de141a66d3839b0353c4938c718 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 27 Oct 2015 17:00:56 -0400 Subject: [PATCH 13/30] Review __init__.py --- tuf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tuf/__init__.py b/tuf/__init__.py index dd159764..fc3c9c93 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -107,7 +107,7 @@ def __str__(self): class BadVersionNumberError(Error): - """Indicate an error after fetching metadata that contains an invalid version""" + """Indicate an error for metadata that contains an invalid version number.""" From 7f91f56802dd6b851ca6bc5645744b9617011d55 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 27 Oct 2015 17:04:29 -0400 Subject: [PATCH 14/30] Review conf.py --- tuf/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tuf/conf.py b/tuf/conf.py index 0421abe8..884fb7dc 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -57,11 +57,11 @@ # metadata. DEFAULT_ROOT_REQUIRED_LENGTH = 512000 #bytes -# Set a default but sane upper bound for the number of bytes required to +# Set a default, but sane, upper bound for the number of bytes required to # download Snapshot metadata. DEFAULT_SNAPSHOT_REQUIRED_LENGTH = 2000000 #bytes -# Set a default but sane upper bound for the number of bytes required to +# Set a default, but sane, upper bound for the number of bytes required to # download Targets metadata. DEFAULT_TARGETS_REQUIRED_LENGTH = 5000000 #bytes From 25d98780fbfed9f2cdd895fe3a876ea29dcf081d Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 27 Oct 2015 17:15:46 -0400 Subject: [PATCH 15/30] Review 'test_extraneous_dependencies_attack.py' --- tests/test_extraneous_dependencies_attack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_extraneous_dependencies_attack.py b/tests/test_extraneous_dependencies_attack.py index 2ff98971..cece8f89 100755 --- a/tests/test_extraneous_dependencies_attack.py +++ b/tests/test_extraneous_dependencies_attack.py @@ -214,8 +214,8 @@ def test_with_tuf(self): try: self.repository_updater.targets_of_role('targets/role1') - # Verify that the specific 'tuf.BadHashError' exception is raised by each - # mirror. + # Verify that the specific 'tuf.ForbiddenTargetError' exception is raised + # by each mirror. except tuf.NoWorkingMirrorError as exception: for mirror_url, mirror_error in six.iteritems(exception.mirror_errors): url_prefix = self.repository_mirrors['mirror1']['url_prefix'] From 25412763206d42cea7509a09ae6cb40994ae8531 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 27 Oct 2015 17:22:14 -0400 Subject: [PATCH 16/30] Review 'test_mix_and_match_attack.py --- tests/test_mix_and_match_attack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_mix_and_match_attack.py b/tests/test_mix_and_match_attack.py index 01d82e3c..de384c0b 100755 --- a/tests/test_mix_and_match_attack.py +++ b/tests/test_mix_and_match_attack.py @@ -239,14 +239,14 @@ def test_with_tuf(self): try: self.repository_updater.targets_of_role('targets/role1') - # Verify that the specific 'tuf.BadHashError' exception is raised by each - # mirror. + # Verify that the specific 'tuf.BadVersionNumberError' exception is raised + # by each mirror. except tuf.NoWorkingMirrorError as exception: for mirror_url, mirror_error in six.iteritems(exception.mirror_errors): url_prefix = self.repository_mirrors['mirror1']['url_prefix'] url_file = os.path.join(url_prefix, 'metadata', 'targets', 'role1.json') - # Verify that 'timestamp.json' is the culprit. + # Verify that 'role1.json' is the culprit. self.assertEqual(url_file, mirror_url) self.assertTrue(isinstance(mirror_error, tuf.BadVersionNumberError)) From 7bf14b3bc42e888f6b074d93c3147ee8a6eebb8d Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 27 Oct 2015 17:24:57 -0400 Subject: [PATCH 17/30] Review 'test_replay_attack' --- tests/test_replay_attack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_replay_attack.py b/tests/test_replay_attack.py index f94d04d2..4d071956 100755 --- a/tests/test_replay_attack.py +++ b/tests/test_replay_attack.py @@ -315,7 +315,7 @@ def test_with_tuf(self): # Restore the previous version of 'timestamp.json' on the remote repository # and verify that the non-TUF client downloads it (expected, but not ideal). shutil.move(backup_timestamp, timestamp_path) - logger.info('Moving the backup timestamp to the current version.') + logger.info('Moving the timestamp.json backup to the current version.') # Verify that the TUF client detects replayed metadata and refuses to # continue the update process. From 1f16868775ee0fb51adb6502a56ee2236afb835f Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 27 Oct 2015 17:49:25 -0400 Subject: [PATCH 18/30] Review repository_tool.py --- tuf/repository_tool.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index 11903a78..df8bc34f 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -179,7 +179,7 @@ def __init__(self, repository_directory, metadata_directory, targets_directory): def write(self, write_partial=False, consistent_snapshot=False, - compressions=['gz']): + compression_algorithms=['gz']): """ Write all the JSON Metadata objects to their corresponding files. @@ -202,7 +202,7 @@ def write(self, write_partial=False, consistent_snapshot=False, .README.json, where is the file's SHA256 digest. Example: 13.root.json' - compressions: + compression_algorithms: A list of compression algorithms. Each of these algorithms will be used to compress all of the metadata available on the repository. By default, all metadata is compressed with gzip. @@ -224,7 +224,7 @@ def write(self, write_partial=False, consistent_snapshot=False, # Raise 'tuf.FormatError' if any are improperly formatted. tuf.formats.BOOLEAN_SCHEMA.check_match(write_partial) tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) - tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) + tuf.formats.COMPRESSIONS_SCHEMA.check_match(compression_algorithms) # At this point the tuf.keydb and tuf.roledb stores must be fully @@ -2704,10 +2704,9 @@ def load_repository(repository_directory): filenames = repo_lib.get_metadata_filenames(metadata_directory) - # The Root file is always available without a consistent snapshots version - # number attached to the filename. Store the 'consistent_snapshot' value - # read the loaded Root file so that other metadata files may be located. - # 'consistent_snapshot' value. + # The Root file is always available without a version number (a consistent + # snapshot) attached to the filename. Store the 'consistent_snapshot' value + # and read the loaded Root file so that other metadata files may be located. consistent_snapshot = False # Load the metadata of the top-level roles (i.e., Root, Timestamp, Targets, @@ -2766,8 +2765,6 @@ def load_repository(repository_directory): # Extract the metadata attributes 'metadata_name' and update its # corresponding roleinfo. - # TODO: Test for detection of the Targets role. - roleinfo = tuf.roledb.get_roleinfo(metadata_name) roleinfo['signatures'].extend(signable['signatures']) roleinfo['version'] = metadata_object['version'] From a41532e5c91fb80f11ab80947d75bd29346681cd Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 28 Oct 2015 11:36:18 -0400 Subject: [PATCH 19/30] Review repository_lib.py --- tuf/repository_lib.py | 134 ++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 83 deletions(-) diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index 2bf65e86..08398019 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -105,7 +105,7 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, targets_directory, metadata_directory, consistent_snapshot=False, filenames=None, - compressions=['gz']): + compression_algorithms=['gz']): """ Non-public function that can generate and write the metadata of the specified top-level 'rolename'. It also increments version numbers if: @@ -126,7 +126,7 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, if rolename == 'root': metadata = generate_root_metadata(roleinfo['version'], roleinfo['expires'], consistent_snapshot, - compressions) + compression_algorithms) _log_warning_if_expires_soon(ROOT_FILENAME, roleinfo['expires'], ROOT_EXPIRES_WARN_SECONDS) @@ -145,8 +145,8 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, TARGETS_EXPIRES_WARN_SECONDS) elif rolename == 'snapshot': - root_filename = 'root' - targets_filename = 'targets' + root_filename = ROOT_FILENAME[:-len(METADATA_EXTENSION)] + targets_filename = TARGETS_FILENAME[:-len(METADATA_EXTENSION)] metadata = generate_snapshot_metadata(metadata_directory, roleinfo['version'], roleinfo['expires'], root_filename, @@ -188,6 +188,9 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, metadata_filename) # non-partial write() else: + # If writing a new version of 'rolename,' increment its version number in + # both the metadata file and roledb (required so that snapshot references + # the latest version). if tuf.sig.verify(signable, rolename) and not roleinfo['partial_loaded']: metadata['version'] = metadata['version'] + 1 roleinfo = tuf.roledb.get_roleinfo(rolename) @@ -202,15 +205,15 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, if tuf.sig.verify(signable, rolename) or write_partial: _remove_invalid_and_duplicate_signatures(signable) filename = write_metadata_file(signable, metadata_filename, - metadata['version'], compressions, + metadata['version'], compression_algorithms, consistent_snapshot) # The root and timestamp files should also be written without a version - # number prepended if 'consistent_snaptshots' is True. Client may request - # a timestamp and root file without knowing its version number. + # number prepended if 'consistent_snaptshot' is True. Clients may request + # a timestamp and root file without knowing their version numbers. if rolename == 'root' or rolename == 'timestamp': write_metadata_file(signable, metadata_filename, metadata['version'], - compressions, consistent_snapshot=False) + compression_algorithms, consistent_snapshot=False) # 'signable' contains an invalid threshold of signatures. @@ -447,7 +450,7 @@ def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, # 'targets/unclaimed/django.json'. Consistent and non-consistent # metadata might co-exist if write() and # write(consistent_snapshot=True) are mixed, so ensure only - # 'version_number.filename' metadata is stripped. + # '.filename' metadata is stripped. embeded_version_number = None if metadata_name not in snapshot_metadata['meta']: metadata_name, embeded_version_number = \ @@ -461,15 +464,15 @@ def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, metadata_name = metadata_name[:-len(metadata_extension)] # Delete the metadata file if it does not exist in 'tuf.roledb'. - # 'repository_tool.py' might have marked 'metadata_name' as removed, but - # its metadata file is not actually deleted yet. Do it now. + # 'repository_tool.py' might have removed 'metadata_name,' + # but its metadata file is not actually deleted yet. Do it now. if not tuf.roledb.role_exists(metadata_name): logger.info('Removing outdated metadata: ' + repr(metadata_path)) os.remove(metadata_path) - # Delete outdated consistent snapshots. snapshot metadata includes the - # file extension of roles. TODO: Perhaps we should leave it up to the - # integrators to remove outdated consistent snapshots? + # Delete outdated consistent snapshots. Snapshot metadata includes the + # file extension of roles. TODO: Should we leave it up to integrators + # to remove outdated consistent snapshots? """ if consistent_snapshot and embeded_version_number is not None: file_hashes = list(snapshot_metadata['meta'][metadata_name_extension] \ @@ -482,12 +485,13 @@ def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, + def _get_written_metadata(metadata_signable): """ Non-public function that returns the actual content of written metadata. """ - # Explicitly specify the JSON separators for Python 2 + 3 consistent. + # Explicitly specify the JSON separators for Python 2 + 3 consistency. written_metadata_content = \ json.dumps(metadata_signable, indent=1, separators=(',', ': '), sort_keys=True).encode('utf-8') @@ -503,7 +507,7 @@ def _strip_consistent_snapshot_version_number(metadata_filename, """ Strip from 'metadata_filename' any version data (in the expected '{dirname}/version_number.filename' format) that it may contain, and return - it. + the stripped filename and its version number as a tuple. """ embeded_version_number = '' @@ -529,8 +533,8 @@ def _strip_consistent_snapshot_version_number(metadata_filename, def _load_top_level_metadata(repository, top_level_filenames): """ - Load the metadata of the Root, Timestamp, Targets, and Snapshot roles. - At a minimum, the Root role must exist and successfully load. + Load the metadata of the Root, Timestamp, Targets, and Snapshot roles. At a + minimum, the Root role must exist and successfully load. """ root_filename = top_level_filenames[ROOT_FILENAME] @@ -1281,7 +1285,8 @@ def get_metadata_versioninfo(rolename): rolename: - The metadata role whose version number is needed. It must exist. + The metadata role whose versioninfo is needed. It must exist, otherwise + a 'tuf.UnknownRoleError' exception is raised. tuf.FormatError, if 'rolename' is improperly formatted. @@ -1646,29 +1651,16 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date, metadata_directory = _check_directory(metadata_directory) - # Retrieve the fileinfo of 'root.json' and 'targets.json'. This file - # information includes data such as file length, hashes of the file, etc. + # Retrieve the versioninfo of 'root.json' and 'targets.json'. The + # versioninfo contains the version number of these roles. versiondict = {} versiondict[ROOT_FILENAME] = get_metadata_versioninfo(root_filename) versiondict[TARGETS_FILENAME] = get_metadata_versioninfo(targets_filename) - """ - # Add compressed versions of the 'targets.json' and 'root.json' metadata, - # if they exist. - for extension in SUPPORTED_COMPRESSION_EXTENSIONS: - compressed_root_filename = root_filename+extension - compressed_targets_filename = targets_filename+extension - - # If the compressed versions of the root and targets metadata is found, - # add their file attributes to 'versiondict'. - if os.path.exists(compressed_root_filename): - versiondict[ROOT_FILENAME+extension] = \ - get_metadata_versioninfo(compressed_root_filename) - if os.path.exists(compressed_targets_filename): - versiondict[TARGETS_FILENAME+extension] = \ - get_metadata_versioninfo(compressed_targets_filename) - - """ + # We previously also stored the compressed versions of roles in + # snapshot.json, however, this is no longer needed as their hashes and + # lengths are no longer used and their version numbers match the uncompressed + # role files. # Walk the 'targets/' directory and generate the versioninfo of all the role # files found. This information is stored in the 'meta' field of the @@ -1695,11 +1687,11 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date, rolename = metadata_name[:-len(metadata_extension)] # Obsolete role files may still be found. Ensure only roles loaded - # in the roledb are included in the snapshot metadata. + # in the roledb are included in the Snapshot metadata. if tuf.roledb.role_exists(rolename): versiondict[metadata_name] = get_metadata_versioninfo(rolename) - # Generate the snapshot metadata object. + # Generate the Snapshot metadata object. snapshot_metadata = tuf.formats.SnapshotFile.make_metadata(version, expiration_date, versiondict) @@ -1710,8 +1702,7 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date, -def generate_timestamp_metadata(snapshot_filename, version, - expiration_date): +def generate_timestamp_metadata(snapshot_filename, version, expiration_date): """ Generate the timestamp metadata object. The 'snapshot.json' file must @@ -1731,12 +1722,6 @@ def generate_timestamp_metadata(snapshot_filename, version, The expiration date of the metadata file, conformant to 'tuf.formats.ISO8601_DATETIME_SCHEMA'. - compressions: - Compression extensions (e.g., 'gz'). If 'snapshot.json' is also saved in - compressed form, these compression extensions should be stored in - 'compressions' so the compressed timestamp files can be added to the - timestamp metadata object. - tuf.FormatError, if the generated timestamp metadata object cannot be formatted correctly, or one of the arguments is improperly formatted. @@ -1760,28 +1745,10 @@ def generate_timestamp_metadata(snapshot_filename, version, versioninfo = {} versioninfo[SNAPSHOT_FILENAME] = get_metadata_versioninfo('snapshot') - # Save the versioninfo of the compressed versions of 'timestamp.json' - # in 'versioninfo'. Log the files included in 'fileinfo'. - # TODO: Since version numbers are now stored, the version numbers of - # compressed roles do not change and can thus be excluded. Remove the - # following commented lines after testing. - """ - for file_extension in compressions: - if not len(file_extension): - continue - - compressed_filename = snapshot_filename + '.' + file_extension - - try: - compressed_fileinfo = get_metadata_fileinfo(compressed_filename) - - except: - logger.warning('Cannot get fileinfo about ' + repr(compressed_filename)) - - else: - logger.info('Including fileinfo about ' + repr(compressed_filename)) - fileinfo[SNAPSHOT_FILENAME + '.' + file_extension] = compressed_fileinfo - """ + # We previously saved the versioninfo of the compressed versions of + # 'snapshot.json' in 'versioninfo'. Since version numbers are now stored, + # the version numbers of compressed roles do not change and can thus be + # excluded. # Generate the timestamp metadata object. timestamp_metadata = tuf.formats.TimestampFile.make_metadata(version, @@ -1882,8 +1849,8 @@ def sign_metadata(metadata_object, keyids, filename): -def write_metadata_file(metadata, filename, version_number, compressions, - consistent_snapshot): +def write_metadata_file(metadata, filename, version_number, + compression_algorithms, consistent_snapshot): """ If necessary, write the 'metadata' signable object to 'filename', and the @@ -1899,7 +1866,7 @@ def write_metadata_file(metadata, filename, version_number, compressions, filename: The filename of the metadata to be written (e.g., 'root.json'). - If a compression algorithm is specified in 'compressions', the + If a compression algorithm is specified in 'compression_algorithms', the compression extention is appended to 'filename'. version_number: @@ -1907,9 +1874,10 @@ def write_metadata_file(metadata, filename, version_number, compressions, number is needed for consistent snapshots, which prepend the version number to 'filename'. - compressions: - Specify the algorithms, as a list of strings, used to compress the file; - The only currently available compression option is 'gz' (gzip). + compression_algorithms: + Specify the algorithms, as a list of strings, used to compress the + 'metadata'; The only currently available compression option is 'gz' + (gzip). consistent_snapshot: Boolean that determines whether the metadata file's digest should be @@ -1937,7 +1905,7 @@ def write_metadata_file(metadata, filename, version_number, compressions, tuf.formats.SIGNABLE_SCHEMA.check_match(metadata) tuf.formats.PATH_SCHEMA.check_match(filename) tuf.formats.METADATAVERSION_SCHEMA.check_match(version_number) - tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) + tuf.formats.COMPRESSIONS_SCHEMA.check_match(compression_algorithms) tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) # Verify the directory of 'filename', and convert 'filename' to its absolute @@ -1963,8 +1931,8 @@ def write_metadata_file(metadata, filename, version_number, compressions, write_new_metadata = False # Has the uncompressed metadata changed? Does it exist? If so, set - # 'write_compressed_version' to True so that it is written. - # compressed metadata should only be written if it does not exist or the + # 'write_compressed_version' to 'True' so that it is written. + # Compressed metadata should only be written if it does not exist or the # uncompressed version has changed). new_digests = {} hash_algorithms = tuf.conf.REPOSITORY_HASH_ALGORITHMS @@ -2005,15 +1973,15 @@ def write_metadata_file(metadata, filename, version_number, compressions, # file may be written (without needing to write the uncompressed version) if # the repository maintainer adds compression after writing the uncompressed # version. - for compression in compressions: + for compression_algorithm in compression_algorithms: file_object = None # Ignore the empty string that signifies non-compression. The uncompressed # file was previously written above, if necessary. - if not len(compression): + if not len(compression_algorithm): continue - elif compression == 'gz': + elif compression_algorithm == 'gz': file_object = tuf.util.TempFile() compressed_filename = filename + '.gz' @@ -2028,7 +1996,7 @@ def write_metadata_file(metadata, filename, version_number, compressions, gzip_object.close() else: - raise tuf.FormatError('Unknown compression algorithm: ' + repr(compression)) + raise tuf.FormatError('Unknown compression algorithm: ' + repr(compressio_algorithm)) # Save the compressed version, ensuring an unchanged file is not re-saved. # Re-saving the same compressed version may cause its digest to unexpectedly From fab23480b341a9cc5b9f959fc1cf331cdf706dd9 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 28 Oct 2015 11:45:01 -0400 Subject: [PATCH 20/30] Review 'test_repository_lib.py' --- tests/test_repository_lib.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_repository_lib.py b/tests/test_repository_lib.py index fd9ef9fe..7683e041 100755 --- a/tests/test_repository_lib.py +++ b/tests/test_repository_lib.py @@ -49,7 +49,6 @@ import tuf.keydb import tuf.hash import tuf.repository_lib as repo_lib - import tuf.repository_tool as repo_tool import six @@ -565,15 +564,15 @@ def test_generate_snapshot_metadata(self): version = 1 expiration_date = '1985-10-21T13:20:00Z' - + # Load a valid repository so that top-level roles exist in roledb and + # generate_snapshot_metadata() has roles to specify in snapshot metadata. repository = repo_tool.Repository(repository_directory, metadata_directory, - targets_directory) + targets_directory) repository_junk = repo_tool.load_repository(repository_directory) root_filename = 'root' targets_filename = 'targets' - snapshot_metadata = \ repo_lib.generate_snapshot_metadata(metadata_directory, version, expiration_date, root_filename, From cba763239ff2f0b93cc21dd4e1beb18fd9a464d2 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 28 Oct 2015 13:58:14 -0400 Subject: [PATCH 21/30] Review updater.py --- tuf/client/updater.py | 209 ++++++++++++++++-------------------------- 1 file changed, 81 insertions(+), 128 deletions(-) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 3b08c0ed..3f127483 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -161,7 +161,7 @@ class Updater(object): The directory where trusted metadata is stored. self.versioninfo: - A cache of version numbers of stored metadata files. + A cache of version numbers for the roles available on the repository. Example: {'root.json': {'version': 128}, ...} @@ -297,10 +297,10 @@ def __init__(self, updater_name, repository_mirrors): # Store the previously trusted/verified metadata. self.metadata['previous'] = {} - # Store the version numbers of all metadata files. The dict keys are - # paths, the dict values versioninfo data. This information can help - # determine whether a metadata file has changed and so needs to be - # re-downloaded. + # Store the version numbers of all roles available on the repository. The + # dict keys are paths, and the dict values versioninfo data. This + # information can help determine whether a metadata file has changed and + # needs to be re-downloaded. self.versioninfo = {} # Store the location of the client's metadata directory. @@ -575,8 +575,9 @@ def refresh(self, unsafely_update_root_if_necessary=True): unsafely_update_root_if_necessary: - Boolean that indicates whether to unsafely update the Root metadata - if any of the top-level metadata cannot be downloaded successfully. + Boolean that indicates whether to unsafely update the Root metadata if + any of the top-level metadata cannot be downloaded successfully. The + Root role is unsafely updated if its current version number is unknown. tuf.NoWorkingMirrorError: @@ -600,16 +601,17 @@ def refresh(self, unsafely_update_root_if_necessary=True): # Raise 'tuf.FormatError' if the check fail. tuf.formats.BOOLEAN_SCHEMA.check_match(unsafely_update_root_if_necessary) - # The timestamp role does not have signed metadata about it; otherwise we + # The Timestamp role does not have signed metadata about it; otherwise we # would need an infinite regress of metadata. Therefore, we use some - # default, sane length for its metadata. - DEFAULT_TIMESTAMP_FILELENGTH = tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH + # default, but sane, upper file length for its metadata. + DEFAULT_TIMESTAMP_UPPERLENGTH = tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH - # The Root role may be updated without knowing its hash if top-level - # metadata cannot be safely downloaded (e.g., keys may have been revoked, - # thus requiring a new Root file that includes the updated keys) and - # 'unsafely_update_root_if_necessary' is True. - DEFAULT_ROOT_FILELENGTH = tuf.conf.DEFAULT_ROOT_REQUIRED_LENGTH + # The Root role may be updated without knowing its version number if + # top-level metadata cannot be safely downloaded (e.g., keys may have been + # revoked, thus requiring a new Root file that includes the updated keys) + # and 'unsafely_update_root_if_necessary' is True. + # We use some default, but sane, upper file length for its metadata. + DEFAULT_ROOT_UPPERLENGTH = tuf.conf.DEFAULT_ROOT_REQUIRED_LENGTH # Update the top-level metadata. The _update_metadata_if_changed() and # _update_metadata() calls below do NOT perform an update if there @@ -629,12 +631,12 @@ def refresh(self, unsafely_update_root_if_necessary=True): except tuf.ExpiredMetadataError as e: # Raise 'tuf.NoWorkingMirrorError' if a valid (not expired, properly - # signed, and valid metadata) 'root' cannot be installed. + # signed, and valid metadata) 'root.json' cannot be installed. if unsafely_update_root_if_necessary: message = \ 'Expired Root metadata was loaded from disk. Try to update it now.' logger.info(message) - self._update_metadata('root', DEFAULT_ROOT_FILELENGTH) + self._update_metadata('root', DEFAULT_ROOT_UPPERLENGTH) # The caller explicitly requested not to unsafely fetch an expired Root. else: @@ -644,7 +646,7 @@ def refresh(self, unsafely_update_root_if_necessary=True): # Use default but sane information for timestamp metadata, and do not # require strict checks on its required length. try: - self._update_metadata('timestamp', DEFAULT_TIMESTAMP_FILELENGTH) + self._update_metadata('timestamp', DEFAULT_TIMESTAMP_UPPERLENGTH) self._update_metadata_if_changed('snapshot', referenced_metadata='timestamp') self._update_metadata_if_changed('root') @@ -656,7 +658,7 @@ def refresh(self, unsafely_update_root_if_necessary=True): 'update the Root metadata.' logger.info(message) - self._update_metadata('root', DEFAULT_ROOT_FILELENGTH) + self._update_metadata('root', DEFAULT_ROOT_UPPERLENGTH) self.refresh(unsafely_update_root_if_necessary=False) else: @@ -867,7 +869,9 @@ def _verify_uncompressed_metadata_file(self, metadata_file_object, metadata_role): """ - Non-public method that verifies an uncompressed metadata file. + Non-public method that verifies an uncompressed metadata file. An + exception is raised if 'metadata_file_object is invalid, and there is no + return value. metadata_file_object: @@ -899,7 +903,7 @@ def _verify_uncompressed_metadata_file(self, metadata_file_object, In case the metadata file does not have a valid signature. - The contents of 'metadata_file_object' is read and loaded. + The content of 'metadata_file_object' is read and loaded. None. @@ -920,28 +924,10 @@ def _verify_uncompressed_metadata_file(self, metadata_file_object, # Is 'metadata_signable' expired? self._ensure_not_expired(metadata_signable['signed'], metadata_role) - - """ - - # Is 'metadata_signable' newer than the currently installed - # version? - current_metadata_role = self.metadata['current'].get(metadata_role) - - - # Compare metadata version numbers. Ensure there is a current - # version of the metadata role to be updated. - if current_metadata_role is not None: - current_version = current_metadata_role['version'] - downloaded_version = metadata_signable['signed']['version'] - if downloaded_version < current_version: - raise tuf.ReplayedMetadataError(metadata_role, downloaded_version, - current_version) - else: - logger.info('current_version >= downloaded_version') - - else: - logger.info('current_metadata_role is None') - """ + + # We previously verified version numbers in this function, but have since + # moved version number verification to the functions that retrieve + # metadata. # Reject the metadata if any specified targets are not allowed. # 'tuf.ForbiddenTargetError' raised if any of the targets of 'metadata_role' @@ -1049,44 +1035,34 @@ def unsafely_verify_compressed_metadata_file(metadata_file_object): def _get_metadata_file(self, metadata_role, remote_filename, - upperbound_filelength, expected_version, compression): + upperbound_filelength, expected_version, + compression_algorithm): """ Non-public method that tries downloading, up to a certain length, a - metadata or target file from a list of known mirrors. As soon as the first - valid copy of the file is found, the rest of the mirrors will be skipped. + metadata file from a list of known mirrors. As soon as the first valid + copy of the file is found, the downloaded file is returned and the + remaining mirrors are skipped. - filepath: - The relative metadata or target filepath. + metadata_role: + The role name of the metadata (e.g., 'root', 'targets', + 'targets/linux/x86'). - verify_file_function: - A callable function that expects a 'tuf.util.TempFile' file-like object - and raises an exception if the file is invalid. Target files and - uncompressed versions of metadata may be verified with - 'verify_file_function'. + remote_filename: + The relative file path (on the remove repository) of 'metadata_role'. - file_type: - Type of data needed for download, must correspond to one of the strings - in the list ['meta', 'target']. 'meta' for metadata file type or - 'target' for target file type. It should correspond to the - 'tuf.formats.NAME_SCHEMA' format. + upperbound_filelength: + The expected length, or upper bound, of the metadata file to be + downloaded. - file_length: - The expected length, or upper bound, of the target or metadata file to - be downloaded. + expected_version: + The expected and required version number of the 'metadata_role' file + downloaded. 'expected_version' is an integer. - compression: - The name of the compression algorithm (e.g., 'gzip'), if the metadata - file is compressed. - - verify_compressed_file_function: - If compression is specified, in the case of metadata files, this - callable function may be set to perform verification of the compressed - version of the metadata file. Decompressed metadata is also verified. - - download_safely: - A boolean switch to toggle safe or unsafe download of the file. + compression_algorithm: + The name of the compression algorithm (e.g., 'gzip'). The algorithm is + needed if the remote metadata file is compressed. tuf.NoWorkingMirrorError: @@ -1099,7 +1075,7 @@ def _get_metadata_file(self, metadata_role, remote_filename, file and returned. - A 'tuf.util.TempFile' file-like object containing the metadata or target. + A 'tuf.util.TempFile' file-like object containing the metadata. """ file_mirrors = tuf.mirrors.get_list_of_mirrors('meta', remote_filename, @@ -1113,9 +1089,9 @@ def _get_metadata_file(self, metadata_role, remote_filename, file_object = tuf.download.unsafe_download(file_mirror, upperbound_filelength) - if compression is not None: + if compression_algorithm is not None: logger.info('Decompressing ' + str(file_mirror)) - file_object.decompress_temp_file_object(compression) + file_object.decompress_temp_file_object(compression_algorithm) else: logger.info('Not decompressing ' + str(file_mirror)) @@ -1371,7 +1347,7 @@ def _get_file(self, filepath, verify_file_function, file_type, def _update_metadata(self, metadata_role, upperbound_filelength, version=None, - compression=None): + compression_algorithm=None): """ Non-public method that downloads, verifies, and 'installs' the metadata @@ -1384,31 +1360,21 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None, metadata_role: The name of the metadata. This is a role name and should not end in '.json'. Examples: 'root', 'targets', 'targets/linux/x86'. - - uncompressed_fileinfo: - A dictionary containing length and hashes of the uncompressed metadata - file. - - Example: + + upperbound_filelength: + The expected length, or upper bound, of the metadata file to be + downloaded. - {"hashes": {"sha256": "3a5a6ec1f353...dedce36e0"}, - "length": 1340} - - compression: + version: + The expected and required version number of the 'metadata_role' file + downloaded. 'expected_version' is an integer. + + compression_algorithm: A string designating the compression type of 'metadata_role'. The 'snapshot' metadata file may be optionally downloaded and stored in compressed form. Currently, only metadata files compressed with 'gzip' are considered. Any other string is ignored. - compressed_fileinfo: - A dictionary containing length and hashes of the compressed metadata - file. - - Example: - - {"hashes": {"sha256": "3a5a6ec1f353...dedce36e0"}, - "length": 1340} - tuf.NoWorkingMirrorError: The metadata cannot be updated. This is not specific to a single @@ -1430,7 +1396,7 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None, # The 'snapshot' or Targets metadata may be compressed. Add the appropriate # extension to 'metadata_filename'. - if compression == 'gzip': + if compression_algorithm == 'gzip': metadata_filename = metadata_filename + '.gz' # Attempt a file download from each mirror until the file is downloaded and @@ -1464,7 +1430,8 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None, logger.info('Verifying ' + repr(metadata_role) + ' requesting version: ' + repr(version)) metadata_file_object = \ self._get_metadata_file(metadata_role, remote_filename, - upperbound_filelength, version, compression) + upperbound_filelength, version, + compression_algorithm) # The metadata has been verified. Move the metadata file into place. # First, move the 'current' metadata file to the 'previous' directory @@ -1488,7 +1455,7 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None, # 'metadata_file_object' is an instance of tuf.util.TempFile. metadata_signable = \ tuf.util.load_json_string(metadata_file_object.read().decode('utf-8')) - if compression == 'gzip': + if compression_algorithm == 'gzip': current_uncompressed_filepath = \ os.path.join(self.metadata_directory['current'], uncompressed_metadata_filename) @@ -1647,8 +1614,8 @@ def _update_metadata_if_changed(self, metadata_role, logger.debug('Metadata ' + repr(uncompressed_metadata_filename) + \ ' has changed.') - # The file lengths of metadata are unknown, only their version numbers - # known. Set an upper limit to the length of the download for each + # The file lengths of metadata are unknown, only their version numbers are + # known. Set an upper limit for the length of the downloaded file for each # expected role. Note: The Timestamp role is not updated via this # function. if metadata_role == 'snapshot': @@ -1675,14 +1642,14 @@ def _update_metadata_if_changed(self, metadata_role, # We shouldn't need to, but we need to check the trust # implications of the current implementation. self._delete_metadata(metadata_role) - logger.error('Metadata for '+repr(metadata_role)+' cannot be updated.') + logger.error('Metadata for ' +repr(metadata_role) + ' cannot be updated.') raise else: - # We need to remove delegated roles because the delegated roles - # may not be trusted anymore. + # We need to remove delegated roles because the delegated roles may not + # be trusted anymore. if metadata_role == 'targets' or metadata_role.startswith('targets/'): - logger.debug('Removing delegated roles of '+repr(metadata_role)+'.') + logger.debug('Removing delegated roles of ' + repr(metadata_role) + '.') # TODO: Should we also remove the keys of the delegated roles? tuf.roledb.remove_delegated_roles(metadata_role) @@ -1726,7 +1693,7 @@ def _versioninfo_has_changed(self, metadata_filename, new_versioninfo): try to load it. - Boolean. True if the versioninfo has increased, false otherwise. + Boolean. True if the versioninfo has changed, false otherwise. """ # If there is no versioninfo currently stored for 'metadata_filename', @@ -1748,22 +1715,6 @@ def _versioninfo_has_changed(self, metadata_filename, new_versioninfo): else: return False - # Now compare hashes. Note that the reason we can't just do a simple - # equality check on the versioninfo dicts is that we want to support the - # case where the hash algorithms listed in the metadata have changed - # without having that result in considering all files as needing to be - # updated, or not all hash algorithms listed can be calculated on the - # specific client. - """ - for algorithm, hash_value in six.iteritems(new_fileinfo['hashes']): - # We're only looking for a single match. This isn't a security - # check, we just want to prevent unnecessary downloads. - if algorithm in current_fileinfo['hashes']: - if hash_value == current_fileinfo['hashes'][algorithm]: - return False - - return True - """ @@ -1786,8 +1737,8 @@ def _update_versioninfo(self, metadata_filename): None. - The version number of 'metadata_filename' is calculated and - stored in its corresponding entry in 'self.versioninfo'. + The version number of 'metadata_filename' is calculated and stored in its + corresponding entry in 'self.versioninfo'. None. @@ -1806,7 +1757,7 @@ def _update_versioninfo(self, metadata_filename): return # Extract the version information from the trusted snapshot role and save - # it to the versioninfo store. + # it to the 'self.versioninfo' store. if metadata_filename == 'timestamp.json': trusted_versioninfo = \ self.metadata['current']['timestamp']['version'] @@ -1835,16 +1786,18 @@ def _update_versioninfo(self, metadata_filename): # The metadata file names in 'self.metadata' exclude the role # extension. Strip the '.json' extension when checking if # 'metadata_filename' currently exists. - targets_version_number = self.metadata['current'][metadata_filename[:-len('.json')]]['version'] - trusted_versioninfo = tuf.formats.make_versioninfo(targets_version_number) + targets_version_number = \ + self.metadata['current'][metadata_filename[:-len('.json')]]['version'] + trusted_versioninfo = \ + tuf.formats.make_versioninfo(targets_version_number) except KeyError: trusted_versioninfo = \ self.metadata['current']['snapshot']['meta'][metadata_filenamed] self.versioninfo[metadata_filename] = trusted_versioninfo - - + + From b5f7a1aa8ddda197b1d0df93b66c0f76ca7f32bc Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 28 Oct 2015 14:26:49 -0400 Subject: [PATCH 22/30] Review test_updater.py --- tests/test_updater.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/tests/test_updater.py b/tests/test_updater.py index 191a6da7..ce07bdff 100755 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -351,7 +351,7 @@ def test_1__rebuild_key_and_role_db(self): def test_1__update_versioninfo(self): # Tests - # Verify that the 'self.versioninfo' dictionary is empty (its starts off + # Verify that the 'self.versioninfo' dictionary is empty (it starts off # empty and is only populated if _update_versioninfo() is called. versioninfo_dict = self.repository_updater.versioninfo self.assertEqual(len(versioninfo_dict), 0) @@ -363,7 +363,7 @@ def test_1__update_versioninfo(self): self.assertTrue(tuf.formats.VERSIONDICT_SCHEMA.matches(versioninfo_dict)) # The Snapshot role stores the version numbers of all the roles available - # on the repository. Load Snapshot to extract root's version number + # on the repository. Load Snapshot to extract Root's version number # and compare it against the one loaded by 'self.repository_updater'. snapshot_filepath = os.path.join(self.client_metadata_current, 'snapshot.json') snapshot_signable = tuf.util.load_json_file(snapshot_filepath) @@ -378,7 +378,7 @@ def test_1__update_versioninfo(self): self.repository_updater._update_versioninfo('targets.json') self.assertEqual(len(versioninfo_dict), 2) - # Verify that 'self.versioninfo' is inremented if a non-existent role is + # Verify that 'self.versioninfo' is incremented if a non-existent role is # requested, and has its versioninfo entry set to 'None'. self.repository_updater._update_versioninfo('bad_role.json') self.assertEqual(len(versioninfo_dict), 3) @@ -605,7 +605,7 @@ def test_3__update_metadata(self): self.assertEqual(targets_versioninfo['version'], self.repository_updater.metadata['current']['targets']['version']) - # Test: Invalid version numbers. + # Test: Invalid / untrusted version numbers. # Invalid version number for the uncompressed version of 'targets.json'. self.assertRaises(tuf.NoWorkingMirrorError, self.repository_updater._update_metadata, @@ -628,8 +628,8 @@ def test_3__update_metadata(self): 'gzip') # Verify that the specific exception raised is correct for the previous - # case. The version number is checked before the hashes, so the specific error in - # this case should be 'tuf.DownloadLengthMismatchError'. + # case. The version number is checked, so the specific error in + # this case should be 'tuf.BadVersionNumberError'. try: self.repository_updater._update_metadata('targets', DEFAULT_TARGETS_FILELENGTH, @@ -678,10 +678,9 @@ def test_3__update_metadata_if_changed(self): # Update 'targets.json' and verify that the client's current 'targets.json' # has been updated. 'timestamp' and 'snapshot' must be manually updated - # so that new 'targets' may be recognized. + # so that new 'targets' can be recognized. DEFAULT_TIMESTAMP_FILELENGTH = tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH - logger.info('Attempting to increment targets to version 2...') self.repository_updater._update_metadata('timestamp', DEFAULT_TIMESTAMP_FILELENGTH) self.repository_updater._update_metadata_if_changed('snapshot', 'timestamp') self.repository_updater._update_metadata_if_changed('targets') @@ -1027,7 +1026,7 @@ def test_6_download_target(self): # 'target_filepaths' is expected to have at least two targets. The first # target will be used to test against download_target(). The second # will be used to test against download_target() and a repository with - # consistent snapshots. + # 'consistent_snapshot' set to True. target_filepath1 = target_filepaths.pop() target_fileinfo = self.repository_updater.target(target_filepath1) self.repository_updater.download_target(target_fileinfo, @@ -1049,10 +1048,10 @@ def test_6_download_target(self): # repository with consistent snapshots set (root.json contains a # "consistent_snapshot" entry that the updater uses to correctly fetch # snapshots. The updater expects the existence of - # .filename files if root.json sets 'consistent_snapshot + # '.filename' files if root.json sets 'consistent_snapshot # = True'. - # The repository must be rewritten with consistent snapshots set. + # The repository must be rewritten with 'consistent_snapshot' set. repository = repo_tool.load_repository(self.repository_directory) repository.root.load_signing_key(self.role_keys['root']['private']) @@ -1065,7 +1064,8 @@ def test_6_download_target(self): shutil.rmtree(os.path.join(self.repository_directory, 'metadata')) shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'), os.path.join(self.repository_directory, 'metadata')) - + + # And ensure the client has the latest top-level metadata. self.repository_updater.refresh() target_filepath2 = target_filepaths.pop() @@ -1099,7 +1099,7 @@ def test_6_download_target(self): # directories. get_list_of_mirrors() returns an empty list in this case, # which does not generate specific exception errors. self.assertEqual(len(exception.mirror_errors), 0) - + @@ -1107,7 +1107,7 @@ def test_6_download_target(self): def test_7_updated_targets(self): # Verify that the list of targets returned by updated_targets() contains # all the files that need to be updated, these files include modified and - # new target files. Also, confirm that files than need not to be updated + # new target files. Also, confirm that files that need not to be updated # are absent from the list. # Setup # Create temporary directory which will hold client's target files. @@ -1191,7 +1191,6 @@ def test_7_updated_targets(self): os.path.join(self.repository_directory, 'metadata')) # Ensure the client has up-to-date metadata. - logger.info('refreshing top-level metadata after updating targets.json..') self.repository_updater.refresh() # Verify that the new target file is considered updated. From b2177aaaad972dc332e0e1fc426be4e43648263e Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Fri, 22 Jan 2016 10:35:59 -0500 Subject: [PATCH 23/30] Update tox.ini Test and build against Python 3.5 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0e254846..2c3cd128 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py26, py27, py33, py34 +envlist = py26, py27, py33, py34, py35 [testenv] changedir = tests From c83cc3257802a76a963bb0a4c36316ea004afd72 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Fri, 22 Jan 2016 10:59:21 -0500 Subject: [PATCH 24/30] Update setup.py * Bump version number to 0.10.0 (pre-release) * Update URL * Remove classifier for Python 3.2 --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 1b73af6f..c53879df 100755 --- a/setup.py +++ b/setup.py @@ -80,12 +80,12 @@ setup( name = 'tuf', - version = '0.9.9', + version = '0.10.0', description = 'A secure updater framework for Python', long_description = long_description, - author = 'http://www.theupdateframework.com', + author = 'https://www.updateframework.com', author_email = 'theupdateframework@googlegroups.com', - url = 'http://www.theupdateframework.com', + url = 'https://www.updateframework.com', keywords = 'update updater secure authentication key compromise revocation', classifiers = [ 'Development Status :: 4 - Beta', @@ -101,9 +101,9 @@ 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Security', 'Topic :: Software Development' From 2517bd1341d1f169b40cce13f1dcaa0016419162 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Fri, 22 Jan 2016 11:09:55 -0500 Subject: [PATCH 25/30] Update MANIFEST.in Remove iso8601 entry --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 82813772..f015228a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,7 +11,6 @@ include tests/repository_data/keystore/targets_key include tests/repository_data/keystore/timestamp_key include tuf/_vendor/ed25519/test_data/ed25519 include tuf/_vendor/ed25519/LICENSE -include tuf/_vendor/iso8601/LICENSE recursive-include docs *.txt recursive-include docs/papers *.pdf From 1f06c656ea75e15f845006240e27d41bb56b7ed6 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 27 Jan 2016 16:30:52 -0500 Subject: [PATCH 26/30] Remove certificates used to test https connections --- tests/https_client.pem | 15 --------------- tests/https_server.pem | 31 ------------------------------- 2 files changed, 46 deletions(-) delete mode 100644 tests/https_client.pem delete mode 100644 tests/https_server.pem diff --git a/tests/https_client.pem b/tests/https_client.pem deleted file mode 100644 index 94e2ee62..00000000 --- a/tests/https_client.pem +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICbDCCAdWgAwIBAgIJANAI4zEreOenMA0GCSqGSIb3DQEBBQUAME8xCzAJBgNV -BAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwIQnJvb2tseW4xDDAKBgNVBAoM -A05ZVTESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTE0MDYxNzEyMzUxOFoXDTE1MDYx -NzEyMzUxOFowTzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhC -cm9va2x5bjEMMAoGA1UECgwDTllVMRIwEAYDVQQDDAlsb2NhbGhvc3QwgZ8wDQYJ -KoZIhvcNAQEBBQADgY0AMIGJAoGBAOMQNpdLIX2z0xcnQmroU5qnwaOPdP4Wy+Kz -QSleHj8Ny/iC/24uZ/wu8Dt0Zru/yUOPSnzA2BWfie9jYK4bmRdChYm7fI+WZekj -JZtmrdQpCexYxNqxHuDNL+OoNmGVspRwsBWyyInoxhPfd8y37nVRE5O/+CeFpk9k -TDTeKbs9AgMBAAGjUDBOMB0GA1UdDgQWBBQeAkYt0Yip/L9+SXYpOFpL2ZwOuzAf -BgNVHSMEGDAWgBQeAkYt0Yip/L9+SXYpOFpL2ZwOuzAMBgNVHRMEBTADAQH/MA0G -CSqGSIb3DQEBBQUAA4GBAOFgy2N+ZFlR/tGMd7HHXS2vmNNa52ItWK+96YrbroKO -Izx91LG/QKEyeBXlAGTGGILK4s3v7sJd0Mmg10XwhMqLLUwpOZ4kLzo3GNxhYr5J -UDA+M/0OdPjqWZ7R6B7pM2kubAva17CvGomho34Is64kq5cBTzs90FdmJovOK40K ------END CERTIFICATE----- diff --git a/tests/https_server.pem b/tests/https_server.pem deleted file mode 100644 index d80d8e2d..00000000 --- a/tests/https_server.pem +++ /dev/null @@ -1,31 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOMQNpdLIX2z0xcn -QmroU5qnwaOPdP4Wy+KzQSleHj8Ny/iC/24uZ/wu8Dt0Zru/yUOPSnzA2BWfie9j -YK4bmRdChYm7fI+WZekjJZtmrdQpCexYxNqxHuDNL+OoNmGVspRwsBWyyInoxhPf -d8y37nVRE5O/+CeFpk9kTDTeKbs9AgMBAAECgYA7G2lakPAy7LCygkHD2p6Iz8qU -bS+jRQPmC4uW3S06dLj4BAHCCMqA1ycqEu77SL13nMFjIEAfwNwDOPdd20lKSygK -vNFKw6wWYzvB9GHFPUKUOAelKrwsaXmYfROOn51A03Uf+dLjEtY5tsxXn47neq+G -iB2WLSk0Vl6YYI9U8QJBAPnyNRWydbz2CTW+nf/jxthcGXT2EBqpZ3HkutCa6YMb -YIrQ5CbSzpdGYY1iNI1i2r7BLwN7qUeMTxvJ4raP9jcCQQDokB75SBixx1ulV4vG -aSZcBrJCC+/yVaym5JwqzJKdi/jDoRPpMYBo1BTNwwacXzNtbCATKIvEQ2URqeUq -L6ArAkAeFlLfjsDvgypupsh8Mh4Qk12ZH7mmi/fg1OjMDanIV3ZSn3ynU778pMM/ -cq/iySCNz9Fp+OvSqggnzzCUS1YXAkEApDFMjPcn6Cw2OhALMTP/zy0zIYpICDIQ -yWvSDi2MvgqawZOx+Qvn+xrw7SzqN/DG4FRceOpBc3mZm9T1ZMlnLQJBANoVg0kj -EkaLBX8+73Cl+ERoD6Z/ylCB8twWiedvCbORPf9RTZjQbB2fQ6ay9xS3DMFfnU6R -LJYfLnt2X1E6++c= ------END PRIVATE KEY----- ------BEGIN CERTIFICATE----- -MIICbDCCAdWgAwIBAgIJANAI4zEreOenMA0GCSqGSIb3DQEBBQUAME8xCzAJBgNV -BAYTAlVTMQswCQYDVQQIDAJOWTERMA8GA1UEBwwIQnJvb2tseW4xDDAKBgNVBAoM -A05ZVTESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTE0MDYxNzEyMzUxOFoXDTE1MDYx -NzEyMzUxOFowTzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMREwDwYDVQQHDAhC -cm9va2x5bjEMMAoGA1UECgwDTllVMRIwEAYDVQQDDAlsb2NhbGhvc3QwgZ8wDQYJ -KoZIhvcNAQEBBQADgY0AMIGJAoGBAOMQNpdLIX2z0xcnQmroU5qnwaOPdP4Wy+Kz -QSleHj8Ny/iC/24uZ/wu8Dt0Zru/yUOPSnzA2BWfie9jYK4bmRdChYm7fI+WZekj -JZtmrdQpCexYxNqxHuDNL+OoNmGVspRwsBWyyInoxhPfd8y37nVRE5O/+CeFpk9k -TDTeKbs9AgMBAAGjUDBOMB0GA1UdDgQWBBQeAkYt0Yip/L9+SXYpOFpL2ZwOuzAf -BgNVHSMEGDAWgBQeAkYt0Yip/L9+SXYpOFpL2ZwOuzAMBgNVHRMEBTADAQH/MA0G -CSqGSIb3DQEBBQUAA4GBAOFgy2N+ZFlR/tGMd7HHXS2vmNNa52ItWK+96YrbroKO -Izx91LG/QKEyeBXlAGTGGILK4s3v7sJd0Mmg10XwhMqLLUwpOZ4kLzo3GNxhYr5J -UDA+M/0OdPjqWZ7R6B7pM2kubAva17CvGomho34Is64kq5cBTzs90FdmJovOK40K ------END CERTIFICATE----- From dfbdc5aaea3f5afabc43167f79980aaf77afbe7c Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 27 Jan 2016 16:31:33 -0500 Subject: [PATCH 27/30] Add SSL certificate and key used to test https connections --- tests/ssl_cert.crt | 28 ++++++++++++++++++++++++++++ tests/ssl_cert.key | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/ssl_cert.crt create mode 100644 tests/ssl_cert.key diff --git a/tests/ssl_cert.crt b/tests/ssl_cert.crt new file mode 100644 index 00000000..4812078b --- /dev/null +++ b/tests/ssl_cert.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIE1TCCAz2gAwIBAgIJAKqz8ew7Z44mMA0GCSqGSIb3DQEBCwUAMIGAMQswCQYD +VQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCEJyb29rbHluMQww +CgYDVQQKDANOWVUxKTAnBgNVBAsMIENvbXB1dGVyIFNjaWVuY2UgYW5kIEVuZ2lu +ZWVyaW5nMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTYwMTI3MjEyMTMxWhcNMjYw +MTI0MjEyMTMxWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREw +DwYDVQQHDAhCcm9va2x5bjEMMAoGA1UECgwDTllVMSkwJwYDVQQLDCBDb21wdXRl +ciBTY2llbmNlIGFuZCBFbmdpbmVlcmluZzESMBAGA1UEAwwJbG9jYWxob3N0MIIB +ojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxyFVeRsWnb1UlCKBks2azM9W +9K+J/ZkzdSb6eCxOIxv79M/Ug54CfWqkySSaQejsu0U/gJxkFYRvwQAy5lATrspY +2kyiWYiggWXFDWz+i8ETPkL9zn59v13sNIpT/IXQj0S3Mr9ZnsUn1qCyEOOIxJxZ +lyuV/M/XP1DP4tArhEvrex12V6MQIK+8fYzEjHG/W7vIIet+wTStIR8ArvVQi0Kv +PbbGCfrZ+e+gq+UpBLBuAfMzM95TW+YJ5duMchie2n6LDmOeegA4jMEv2ppeOr8Q +JJtZuKpXWVbJvLg81yrDjr1rAwJR/WQrnk8GQWPCyPLneAA4mJbi75LqjLxn0AoJ +b3kzLfGEMJJEWXspxNg06bLQU948hB4L7nKARq6s7KoESjEV+/L4koMPWJoNq6fx +OUVw2+S3ITNrDctecRQ1j3RGVPaj5l6bn03C7KV9uRrfqFY3OUjn7A0kDczvRnmr +e1BZIpe+mfGFB+Uu7JiQoBv6I6fqyrdH9rX1LUKlAgMBAAGjUDBOMB0GA1UdDgQW +BBT8LvRkvodP9bR/bBs/aI+AydRIvTAfBgNVHSMEGDAWgBT8LvRkvodP9bR/bBs/ +aI+AydRIvTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBgQC6kwuSEF0Y +5yLMf1TKfVkeBaZ4tOqR2kzpggzPPog+JcfIQgVmI2QTUDritHWFIM4YUwQ/00WU +uol2BCUpgaLci5gNgyTw8p538Q5cZHXE3kK/CWJA4zKag+oHdmXzGjMalqzvPuVJ +9VdtPrwHhB0Xntf72iWWhE2dIn1QZqVmJ/8hhIU8cQ91pIqTjYjhrYE/GhGH7HMW +bRiRolt37VxbzfXjEBMqVH6fOQq0piTRxwTNPBFp6JO5mRakRmWRvN3dnR8J9qXi +6tQhNNn2uQIpPlKlqVQnh5j5YxFrb50b0FCjDw+eNilXP93yjV4+lWK2QZychcGl +6/7Wu8snZkJCImPbwmcT80XSKesf918zIkauekWiaJE02+ljNtbM7MUAE+XLsKJy +NFGzpyZJ9LihGC/eeVl7K+xqC41jGVOXOOHtbDMbIQfaEZd1nPvy3+V/tublv+am +jPSlj/FW3bLTkjF0OspFjHvJeCeAJdM9kJdYfZoahd6kcejGJc+vjXE= +-----END CERTIFICATE----- diff --git a/tests/ssl_cert.key b/tests/ssl_cert.key new file mode 100644 index 00000000..b483851d --- /dev/null +++ b/tests/ssl_cert.key @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIG4wIBAAKCAYEAxyFVeRsWnb1UlCKBks2azM9W9K+J/ZkzdSb6eCxOIxv79M/U +g54CfWqkySSaQejsu0U/gJxkFYRvwQAy5lATrspY2kyiWYiggWXFDWz+i8ETPkL9 +zn59v13sNIpT/IXQj0S3Mr9ZnsUn1qCyEOOIxJxZlyuV/M/XP1DP4tArhEvrex12 +V6MQIK+8fYzEjHG/W7vIIet+wTStIR8ArvVQi0KvPbbGCfrZ+e+gq+UpBLBuAfMz +M95TW+YJ5duMchie2n6LDmOeegA4jMEv2ppeOr8QJJtZuKpXWVbJvLg81yrDjr1r +AwJR/WQrnk8GQWPCyPLneAA4mJbi75LqjLxn0AoJb3kzLfGEMJJEWXspxNg06bLQ +U948hB4L7nKARq6s7KoESjEV+/L4koMPWJoNq6fxOUVw2+S3ITNrDctecRQ1j3RG +VPaj5l6bn03C7KV9uRrfqFY3OUjn7A0kDczvRnmre1BZIpe+mfGFB+Uu7JiQoBv6 +I6fqyrdH9rX1LUKlAgMBAAECggGAEogMn0ehFC7xdxO7AUF3HYZSLlVDv0EJo+Zr +utFMuEG7ce4Bdfo3exp4mWt5m5akqUzpevuS6Nm5WLm/AuYC3upf2Hj3RuPLJB+n +dfdlvPXL56huXFAzPaLs/3q8FC0T2rFnZyadnYP1kCjGSYITUVDHmaTpwWxKOM85 +eX8r/ZTfJkb4o3E+Z/xSy1BVXkibqVrRZi63Th2r2wA6nQ2hYERlcJXY2kbpEDR3 +vGeIKLKOmknawwH2uf+vfh+vc1LNE7p9C5w16ex0OcmCo6G1ln7/dcwmXmcS3M0S +Bax5Jzu5ozaJFL9G59o0AUGJoZj9Gj9leeKPZvShsGcA0JmBMQiLIdhgRwj0B83x +HrYXTZ6P5BjJmwrIv4mGdv2bHV20pbWKAATUwo8EVBzylipexhhAtQJ5B6OsPDPS +HTluaEC2niD6lE613uRnzzbjw4SlwkoMLE0aqOhQyWIPS9/8oRjTzQi4otL7Dt69 +oMrVhmSfxUqZhh2R3KMHDcMKt5nBAoHBAOXkDovYOhTMD3ei0WbKpbSB1sJp5t2d +/9gVil4nWLa4ahw7/TsZi3Co+c9gD2UJku1L9JbOy6TVZ2LoXOybLvIJfeAjNdYH +vi/ElG7498fgsSyw6bua/1VEd7VtbtpWJIQt1LdJG1+O3ZbJNTY6tbLbYVuy4FIO +e/484F8kdZ9PtRsn+I0I7kfoYJ2IFoM0UWgwQETOBguBCua43ZnHoxrvyHKABAO+ +Iuvw4RBZKphGVxMCEjvTCB9S/CpGCRAkkQKBwQDdvu3reA/lVdFDN56VNUn0u3vr +zPSoiOjojlHDyWVAWiLB9I0qaE61UMvVgChM8VkmjhHYQEW6Cj0XMZMkCnsfKDQn +TYF16jt/sTteWSTcx0PTeiCGs3yM5wK4B8q9coOlzSqDd39mjDIFiUz4e+44OIcU ++ISc8pGbwxw0W8qRwIUJPTSVoaUZDnupuR/IE48q8CTPT1Gf00sMLWuv3SYuFHKX +djpcMLWVf4HclIY6y3BqNIZ0JaUAOd+OZT2kdtUCgcBLWPwLics/lcJcC9lmP3Ug +PI4PGna4nFiGkkjPo0XIXZkpt9+/xxeUzU1TUsC49PJbJFH+O7kzRV6lZFNQmWxB +mCrRk7jJdbA4J84esStFL7fiVfnFq3+UiuRRapSyqxk82WimyidWopSuHzR5mbSD +8rNuQqqTOnwZUAqaJHEIzi8lv2wPjaXLm7ZO65O1XShxZZ8q7fu9OYZBKMY46N3k +rkKchKjMMT1w53pcyVzUm/leGYewY/J9kc1kbZ/60oECgcEAj/qdzwt4/sa3BncB +wA4GxCJL9zJwFVI4MG/gRUjqNluQP/GDC2sI2A/rGeiJwlPfN/p9ObWZ0I8/VWT6 +DifEA9n96xsXGTIKigHQ85TcK4Iy1whwQCYgk/iXOljM2i+VrT1HAm+/yBz1icS5 +ton5hoWlqAcpTCLwSnvoP1Lud67ScspL73Aym89cmjo6mZWhmxasP/NXo3f1PaXs +SxdD6B2cvh2lDSEPdk+BSXEiquBXUI5kUtvyg/AP6Qxxdu01AoHAO05qTh9zokkT +yg0sZf4Z5i01em2ys4ZhQjhhbw+I5lIO76e/ZyUWpEZusBVd9TV5BHgiATOHw4yr +nbjEZKwLEb3SXoHl3/CD/l9vWk4gKAYDJdW+oPZttDlkp6dfPJVDupQwLhrxXYmE +fgs4WFmY3Q5b1wut2pnSs1UEPDqJBvykt59gFgn7yVwyTy8VLihNVtH4mwVPYXha +jz2T6BzRAPlYqx/FpkK2YHHNcyj+HFtnBUMMzacnSl/aXpJgHTKw +-----END RSA PRIVATE KEY----- From 5ce17a3945a72d1d7624f2c1427be0c45535b7c9 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 27 Jan 2016 16:32:59 -0500 Subject: [PATCH 28/30] Add the newly-generated SSL cert and key to simple_https_server.py --- tests/simple_https_server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/simple_https_server.py b/tests/simple_https_server.py index 402dbd3c..8c15608a 100755 --- a/tests/simple_https_server.py +++ b/tests/simple_https_server.py @@ -60,7 +60,8 @@ def _generate_random_port(): httpd = six.moves.BaseHTTPServer.HTTPServer(('localhost', PORT), six.moves.SimpleHTTPServer.SimpleHTTPRequestHandler) -httpd.socket = ssl.wrap_socket(httpd.socket, certfile='https_server.pem', +httpd.socket = ssl.wrap_socket(httpd.socket, keyfile='ssl_cert.key', + certfile='ssl_cert.crt', server_side=True) #print('Starting https server on port: ' + str(PORT)) From b4356188781d411cc9c8d3ee1f6f56e4a15a10da Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 27 Jan 2016 16:34:13 -0500 Subject: [PATCH 29/30] Remove extra whitespace around argument --- tuf/download.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tuf/download.py b/tuf/download.py index 087e4174..82716b8e 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -413,7 +413,7 @@ def _get_opener(scheme=None): https_handler = VerifiedHTTPSHandler() opener = six.moves.urllib.request.build_opener(https_handler) - # strip out HTTPHandler to prevent MITM spoof + # Strip out HTTPHandler to prevent MITM spoof. for handler in opener.handlers: if isinstance(handler, six.moves.urllib.request.HTTPHandler): opener.handlers.remove(handler) @@ -662,7 +662,7 @@ def connect(self): self._tunnel() # set location of certificate authorities - assert os.path.isfile( tuf.conf.ssl_certificates ) + assert os.path.isfile(tuf.conf.ssl_certificates) cert_path = tuf.conf.ssl_certificates # TODO: Disallow SSLv2. From f38d99ab27b9c88a5cf7ffa4538914319f06ec05 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 27 Jan 2016 16:35:23 -0500 Subject: [PATCH 30/30] Add the new path to the SSL certificate used for testing --- tests/test_download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_download.py b/tests/test_download.py index ade5ad7e..617f3c1d 100755 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -229,7 +229,7 @@ def test_https_connection(self): https_url = 'https://localhost:' + str(port) + '/' + relative_target_filepath # Download the target file using an https connection. - tuf.conf.ssl_certificates = 'https_client.pem' + tuf.conf.ssl_certificates = 'ssl_cert.crt' message = 'Downloading target file from https server: ' + https_url logger.info(message) try: