From 009ddd9f3766151f41ae771aeadc32ea1f9556a6 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 29 Jan 2014 11:26:56 -0500 Subject: [PATCH 1/3] Rename top-level role and functions of repository_tool.py. Update documentation and diagram. add_key() -> add_verification_key() remove_key() -> remove_verification_key() release.json -> snapshot.json Update repository_tool-diagram.png Update README following the renamed functions and release role changes. Minor edits, such as removing lint left over from a previous merge conflict. --- docs/images/repository_tool-diagram.png | Bin 85903 -> 87515 bytes .../test_arbitrary_package_attack.py | 2 +- tuf/README.md | 36 +-- tuf/client/README.md | 18 +- tuf/client/updater.py | 72 +++--- tuf/formats.py | 40 +-- tuf/interposition/updater.py | 4 +- tuf/mirrors.py | 4 +- tuf/repository_tool.py | 227 +++++++++--------- tuf/roledb.py | 24 +- tuf/sig.py | 6 +- 11 files changed, 216 insertions(+), 217 deletions(-) diff --git a/docs/images/repository_tool-diagram.png b/docs/images/repository_tool-diagram.png index 32d1da20d76a430f1fed534c2b5b6d464db22ff3..0149b0014fb0a88d3cce64f58740d7fa27c7e0b6 100644 GIT binary patch delta 24750 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 Ne;b}O$kDS2{0|7qvakRE delta 23273 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 IeXeo;0l`~zOaK4? diff --git a/tests/integration/test_arbitrary_package_attack.py b/tests/integration/test_arbitrary_package_attack.py index 697366f9..3412c761 100755 --- a/tests/integration/test_arbitrary_package_attack.py +++ b/tests/integration/test_arbitrary_package_attack.py @@ -181,8 +181,8 @@ def test_arbitrary_package_attack(using_tuf=False, modify_metadata=False): try: test_arbitrary_package_attack(using_tuf=True, modify_metadata=False) except ArbitraryPackageAlert, error: ->>>>>>> develop print(error) + else: print('Extraneous dependency attack failed.') print() diff --git a/tuf/README.md b/tuf/README.md index 7fedcf96..a5c1d073 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -81,14 +81,14 @@ Enter a password for the encrypted ED25519 key: # Continuing from the previous section . . . # Create a new Repository object that holds the file path to the repository and the four -# top-level role objects (Root, Targets, Release, Timestamp). Metadata files are created when +# top-level role objects (Root, Targets, Snapshot, Timestamp). Metadata files are created when # repository.write() is called. The repository directory is created if it does not exist. repository = create_new_repository("path/to/repository/") # The Repository instance, 'repository', initially contains top-level Metadata objects. # Add one of the public keys, created in the previous section, to the root role. Metadata is # considered valid if it is signed by the public key's corresponding private key. -repository.root.add_key(public_root_key) +repository.root.add_verification_key(public_root_key) # Role keys (i.e., the key's keyid) may be queried. Other attributes include: signing_keys, version, # signatures, expiration, threshold, delegations (Targets role), and compressions. @@ -98,7 +98,7 @@ repository.root.keys # Add a second public key to the root role. Although previously generated and saved to a file, # the second public key must be imported before it can added to a role. public_root_key2 = import_rsa_publickey_from_file("path/to/root_key2.pub") -repository.root.add_key(public_root_key2) +repository.root.add_verification_key(public_root_key2) # Threshold of each role defaults to 1. Users may change the threshold value, but repository_tool.py # validates thresholds and warns users. Set the threshold of the root role to 2, @@ -121,12 +121,12 @@ repository.status() try: repository.write() -# An exception is raised here by write() because the other top-level roles (targets, release, +# An exception is raised here by write() because the other top-level roles (targets, snapshot, # and timestamp) have not been configured with keys. Another option is to call # repository.write_partial() and generate metadata that may contain an invalid threshold of # signatures, required public keys, etc. write_partial() allows multiple repository maintainers to # independently sign metadata and generate them separately. load_repository() can load partially -# written metadata.q +# written metadata. except tuf.Error, e: print e Not enough signatures for 'path/to/repository/metadata.staged/targets.json' @@ -134,7 +134,7 @@ Not enough signatures for 'path/to/repository/metadata.staged/targets.json' # In the next section, update the other top-level roles and create a repository with valid metadata. ``` -#### Create Timestamp, Release, Targets +#### Create Timestamp, Snapshot, Targets ```python # Continuing from the previous section . . . @@ -142,19 +142,19 @@ Not enough signatures for 'path/to/repository/metadata.staged/targets.json' # Generate keys for the remaining top-level roles. The root keys have been set above. # The password argument may be omitted if a password prompt is needed. generate_and_write_rsa_keypair("path/to/targets_key", password="password") -generate_and_write_rsa_keypair("path/to/release_key", password="password") +generate_and_write_rsa_keypair("path/to/snapshot_key", password="password") generate_and_write_rsa_keypair("path/to/timestamp_key", password="password") # Add the public keys of the remaining top-level roles. -repository.targets.add_key(import_rsa_publickey_from_file("path/to/targets_key.pub")) -repository.release.add_key(import_rsa_publickey_from_file("path/to/release_key.pub")) -repository.timestamp.add_key(import_rsa_publickey_from_file("path/to/timestamp_key.pub")) +repository.targets.add_verification_key(import_rsa_publickey_from_file("path/to/targets_key.pub")) +repository.snapshot.add_verification_key(import_rsa_publickey_from_file("path/to/snapshot_key.pub")) +repository.timestamp.add_verification_key(import_rsa_publickey_from_file("path/to/timestamp_key.pub")) # Import the signing keys of the remaining top-level roles. Prompt for passwords. private_targets_key = import_rsa_privatekey_from_file("path/to/targets_key") Enter a password for the encrypted RSA key: -private_release_key = import_rsa_privatekey_from_file("path/to/release_key") +private_snapshot_key = import_rsa_privatekey_from_file("path/to/snapshot_key") Enter a password for the encrypted RSA key: private_timestamp_key = import_rsa_privatekey_from_file("path/to/timestamp_key") @@ -163,16 +163,16 @@ Enter a password for the encrypted RSA key: # Load the signing keys of the remaining roles so that valid signatures are generated when # repository.write() is called. repository.targets.load_signing_key(private_targets_key) -repository.release.load_signing_key(private_release_key) +repository.snapshot.load_signing_key(private_snapshot_key) repository.timestamp.load_signing_key(private_timestamp_key) # Optionally set the expiration date of the timestamp role. By default, roles are set to expire -# as follows: root(1 year), targets(3 months), release(1 week), timestamp(1 day). +# as follows: root(1 year), targets(3 months), snapshot(1 week), timestamp(1 day). repository.timestamp.expiration = "2014-10-28 12:08:00" # Metadata files may also be compressed. Only "gz" is currently supported. repository.targets.compressions = ["gz"] -repository.release.compressions = ["gz"] +repository.snapshot.compressions = ["gz"] # Write all metadata to "path/to/repository/metadata.staged/". The common case is to crawl the # filesystem for all delegated roles in "path/to/repository/metadata.staged/targets/". @@ -225,7 +225,7 @@ Enter a password for the encrypted RSA key: private_root_key2 = import_rsa_privatekey_from_file("path/to/root_key2") Enter a password for the encrypted RSA key: -private_release_key = import_rsa_privatekey_from_file("path/to/release_key") +private_snapshot_key = import_rsa_privatekey_from_file("path/to/snapshot_key") Enter a password for the encrypted RSA key: private_timestamp_key = import_rsa_privatekey_from_file("path/to/timestamp_key") @@ -233,7 +233,7 @@ Enter a password for the encrypted RSA key: repository.root.load_signing_key(private_root_key) repository.root.load_signing_key(private_root_key2) -repository.release.load_signing_key(private_release_key) +repository.snapshot.load_signing_key(private_snapshot_key) repository.timestamp.load_signing_key(private_timestamp_key) # Generate new versions of all the top-level metadata. @@ -249,7 +249,7 @@ repository.write() repository.targets.remove_target("path/to/repository/targets/file3.txt") # repository.write() creates any new metadata files, updates those that have changed, and any that -# need updating to make a new "release" (new release.json and timestamp.json). +# need updating to make a new "snapshot" (new snapshot.json and timestamp.json). repository.write() ``` @@ -286,7 +286,7 @@ repository.targets('unclaimed')('django').load_signing_key(private_unclaimed_key repository.targets('unclaimed')('django').add_target("path/to/repository/targets/django/file4.txt") repository.targets('unclaimed')('django').compressions = ["gz"] -# Write the metadata of "targets/unclaimed", "targets/unclaimed/django", root, targets, release, +# Write the metadata of "targets/unclaimed", "targets/unclaimed/django", root, targets, snapshot, # and timestamp. repository.write() ``` diff --git a/tuf/client/README.md b/tuf/client/README.md index b7f06376..633e1516 100644 --- a/tuf/client/README.md +++ b/tuf/client/README.md @@ -21,11 +21,11 @@ for more information on interposing Python urllib calls with TUF. 2. TUF downloads and verifies timestamp.json. -3. If timestamp.json indicates that release.json has changed, TUF downloads and -verifies release.json. +3. If timestamp.json indicates that snapshot.json has changed, TUF downloads and +verifies snapshot.json. -4. TUF determines which metadata files listed in release.json differ from those -described in the last release.json that TUF has seen. If root.json has changed, +4. TUF determines which metadata files listed in snapshot.json differ from those +described in the last snapshot.json that TUF has seen. If root.json has changed, the update process starts over using the new root.json. 5. TUF provides the software update system with a list of available files @@ -75,7 +75,7 @@ repository_mirrors = {'mirror1': {'url_prefix': 'http://localhost:8001', updater = tuf.client.updater.Updater('updater', repository_mirrors) # The client calls the refresh() method to ensure it has the latest -# copies of the top-level metadata files (i.e., Root, Targets, Release, +# copies of the top-level metadata files (i.e., Root, Targets, Snapshot, # Timestamp). updater.refresh() @@ -97,7 +97,7 @@ for target in updated_targets: updater.download_target(target, destination_directory) # Remove any files from the destination directory that are no longer being -# tracked. For example, a target file from a previous release that has since +# tracked. For example, a target file from a previous snapshot that has since # been removed on the remote repository. updater.remove_obsolete_targets(destination_directory) ``` @@ -107,7 +107,7 @@ updater.remove_obsolete_targets(destination_directory) # Example demonstrating an update that only downloads the targets of # a specific role (i.e., 'targets/django'). -# Refresh the metadata of the top-level roles (i.e., Root, Targets, Release, Timestamp). +# Refresh the metadata of the top-level roles (i.e., Root, Targets, Snapshot, Timestamp). updater.refresh() # Update the 'targets/django' role, and determine the target files that have changed. @@ -124,7 +124,7 @@ for target in updated_targets: ```Python # Example demonstrating an update that downloads a specific target. -# Refresh the metadata of the top-level roles (i.e., Root, Targets, Release, Timestamp). +# Refresh the metadata of the top-level roles (i.e., Root, Targets, Snapshot, Timestamp). updater.refresh() # target() updates role metadata when required. @@ -154,7 +154,7 @@ reported_length (545) < required_length (2048) Downloaded 545 bytes, but expected 2048 bytes. There is a difference of 1503 bytes! [2013-12-16 16:17:05,611 UTC] [tuf.download] [INFO][_download_file:726@download.py] -Downloading: http://localhost:8001/metadata/release.json +Downloading: http://localhost:8001/metadata/snapshot.json [2013-12-16 16:17:05,612 UTC] [tuf.client.updater] [INFO][_check_hashes:636@updater.py] The file\'s sha256 hash is correct: 782675fadd650eeb2926d33c401b5896caacf4fd6766498baf2bce2f3b739db4 diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 484d1121..fd342d0b 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -28,11 +28,11 @@ 2. TUF downloads and verifies timestamp.json. - 3. If timestamp.json indicates that release.json has changed, TUF downloads - and verifies release.json. + 3. If timestamp.json indicates that snapshot.json has changed, TUF downloads + and verifies snapshot.json. - 4. TUF determines which metadata files listed in release.json differ from - those described in the last release.json that TUF has seen. If root.json + 4. TUF determines which metadata files listed in snapshot.json differ from + those described in the last snapshot.json that TUF has seen. If root.json has changed, the update process starts over using the new root.json. 5. TUF provides the software update system with a list of available files @@ -160,7 +160,7 @@ class Updater(object): refresh(): This method downloads, verifies, and loads metadata for the top-level - roles in a specific order (i.e., timestamp -> release -> root -> targets) + roles in a specific order (i.e., timestamp -> snapshot -> root -> targets) The expiration time for downloaded metadata is also verified. The metadata for delegated roles are not refreshed by this method, but by @@ -324,7 +324,7 @@ def __init__(self, updater_name, repository_mirrors): # Load current and previous metadata. for metadata_set in ['current', 'previous']: - for metadata_role in ['root', 'targets', 'release', 'timestamp']: + for metadata_role in ['root', 'targets', 'snapshot', 'timestamp']: self._load_metadata_from_file(metadata_set, metadata_role) # Raise an exception if the repository is missing the required 'root' @@ -430,7 +430,7 @@ def _rebuild_key_and_role_db(self): 'root' metadata object extracted from 'root.json'. This private method is called when a new/updated 'root' metadata file is loaded. This method will only store the role information for the top-level - roles (i.e., 'root', 'targets', 'release', 'timestamp'). + roles (i.e., 'root', 'targets', 'snapshot', 'timestamp'). None. @@ -604,7 +604,7 @@ def refresh(self, unsafely_update_root_if_necessary=True): # require strict checks on its required length. try: self._update_metadata('timestamp', DEFAULT_TIMESTAMP_FILEINFO) - self._update_metadata_if_changed('release', + self._update_metadata_if_changed('snapshot', referenced_metadata='timestamp') self._update_metadata_if_changed('root') self._update_metadata_if_changed('targets') @@ -625,7 +625,7 @@ def refresh(self, unsafely_update_root_if_necessary=True): # Updated the top-level metadata (which all had valid signatures), # however, have they expired? Raise 'tuf.ExpiredMetadataError' if any of # the metadata has expired. - for metadata_role in ['timestamp', 'root', 'release', 'targets']: + for metadata_role in ['timestamp', 'root', 'snapshot', 'targets']: self._ensure_not_expired(metadata_role) @@ -1210,7 +1210,7 @@ def _update_metadata(self, metadata_role, uncompressed_fileinfo, compression: A string designating the compression type of 'metadata_role'. - The 'release' metadata file may be optionally downloaded and stored in + 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. @@ -1240,7 +1240,7 @@ def _update_metadata(self, metadata_role, uncompressed_fileinfo, metadata_filename = metadata_role + '.json' uncompressed_metadata_filename = metadata_filename - # The 'release' or Targets metadata may be compressed. Add the appropriate + # The 'snapshot' or Targets metadata may be compressed. Add the appropriate # extension to 'metadata_filename'. if compression == 'gzip': metadata_filename = metadata_filename + '.gz' @@ -1352,7 +1352,7 @@ def _update_metadata(self, metadata_role, uncompressed_fileinfo, - def _update_metadata_if_changed(self, metadata_role, referenced_metadata='release'): + def _update_metadata_if_changed(self, metadata_role, referenced_metadata='snapshot'): """ Update the metadata for 'metadata_role' if it has changed. With the @@ -1360,7 +1360,7 @@ def _update_metadata_if_changed(self, metadata_role, referenced_metadata='releas by this method. The 'timestamp' role is always downloaded from a mirror without first checking if it has been updated; it is updated in refresh() by calling _update_metadata('timestamp'). This method is also called for - delegated role metadata, which are referenced by 'release'. + delegated role metadata, which are referenced by 'snapshot'. If the metadata needs to be updated but an update cannot be obtained, this method will delete the file (with the exception of the root @@ -1368,7 +1368,7 @@ def _update_metadata_if_changed(self, metadata_role, referenced_metadata='releas Due to the way in which metadata files are updated, it is expected that 'referenced_metadata' is not out of date and trusted. The refresh() - method updates the top-level roles in 'timestamp -> release -> + method updates the top-level roles in 'timestamp -> snapshot -> root -> targets' order. For delegated metadata, the parent role is updated before the delegated role. Taking into account that 'referenced_metadata' is updated and verified before 'metadata_role', @@ -1382,11 +1382,11 @@ def _update_metadata_if_changed(self, metadata_role, referenced_metadata='releas referenced_metadata: This is the metadata that provides the role information for - 'metadata_role'. For the top-level roles, the 'release' role + 'metadata_role'. For the top-level roles, the 'snapshot' role is the referenced metadata for the 'root', and 'targets' roles. The 'timestamp' metadata is always downloaded regardless. In other words, it is updated by calling _update_metadata('timestamp') - and not by this method. The referenced metadata for 'release' + and not by this method. The referenced metadata for 'snapshot' is 'timestamp'. See refresh(). @@ -1412,7 +1412,7 @@ def _update_metadata_if_changed(self, metadata_role, referenced_metadata='releas uncompressed_metadata_filename = metadata_role + '.json' # Ensure the referenced metadata has been loaded. The 'root' role may be - # updated without having 'release' available. + # updated without having 'snapshot' available. if referenced_metadata not in self.metadata['current']: message = 'Cannot update '+repr(metadata_role)+' because ' \ +referenced_metadata+' is missing.' @@ -1425,11 +1425,11 @@ def _update_metadata_if_changed(self, metadata_role, referenced_metadata='releas repr(referenced_metadata)+'. '+repr(metadata_role)+' may be updated.' logger.debug(message) - # There might be a compressed version of 'release.json' or Targets + # There might be a compressed version of 'snapshot.json' or Targets # metadata available for download. Check the 'meta' field of # 'referenced_metadata' to see if it is listed when 'metadata_role' - # is 'release'. The full rolename for delegated Targets metadata - # must begin with 'targets/'. The Release role lists all the Targets + # is 'snapshot'. The full rolename for delegated Targets metadata + # must begin with 'targets/'. The snapshot role lists all the Targets # metadata available on the repository, including any that may be in # compressed form. # @@ -1446,12 +1446,12 @@ def _update_metadata_if_changed(self, metadata_role, referenced_metadata='releas ['meta'] \ [uncompressed_metadata_filename] - # Check for the availability of compressed versions of 'release.json', + # Check for the availability of compressed versions of 'snapshot.json', # 'targets.json', and delegated Targets (that also start with 'targets'). # For 'targets.json' and delegated metadata, 'referenced_metata' - # should always be 'release'. 'release.json' specifies all roles + # should always be 'snapshot'. 'snapshot.json' specifies all roles # provided by a repository, including their file lengths and hashes. - if metadata_role == 'release' or metadata_role.startswith('targets'): + 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']: @@ -1514,10 +1514,10 @@ def _fileinfo_has_changed(self, metadata_filename, new_fileinfo): differs from 'new_fileinfo'. The 'new_fileinfo' argument should be extracted from the latest copy of the metadata that references 'metadata_filename'. Example: 'root.json' - would be referenced by 'release.json'. + would be referenced by 'snapshot.json'. 'new_fileinfo' should only be 'None' if this is for updating - 'root.json' without having 'release.json' available. + 'root.json' without having 'snapshot.json' available. metadadata_filename: @@ -1527,7 +1527,7 @@ def _fileinfo_has_changed(self, metadata_filename, new_fileinfo): new_fileinfo: A dict object representing the new file information for 'metadata_filename'. 'new_fileinfo' may be 'None' when - updating 'root' without having 'release' available. This + updating 'root' without having 'snapshot' available. This dict conforms to 'tuf.formats.FILEINFO_SCHEMA' and has the form: {'length': 23423 @@ -1774,7 +1774,7 @@ def all_targets(self): tuf.RepositoryError: If the metadata for the 'targets' role is missing from - the 'release' metadata. + the 'snapshot' metadata. tuf.UnknownRoleError: If one of the roles could not be found in the role database. @@ -1830,7 +1830,7 @@ def _refresh_targets_metadata(self, rolename='targets', include_delegations=Fals tuf.RepositoryError: If the metadata file for the 'targets' role is missing - from the 'release' metadata. + from the 'snapshot' metadata. The metadata for the delegated roles are loaded and updated if they @@ -1846,7 +1846,7 @@ def _refresh_targets_metadata(self, rolename='targets', include_delegations=Fals # See if this role provides metadata and, if we're including # delegations, look for metadata from delegated roles. role_prefix = rolename + '/' - for metadata_path in self.metadata['current']['release']['meta'].keys(): + for metadata_path in self.metadata['current']['snapshot']['meta'].keys(): if metadata_path == rolename + '.json': roles_to_update.append(metadata_path[:-len('.json')]) elif include_delegations and metadata_path.startswith(role_prefix): @@ -1861,7 +1861,7 @@ def _refresh_targets_metadata(self, rolename='targets', include_delegations=Fals try: roles_to_update.remove('targets') except ValueError: - message = 'The Release metadata file is missing the targets.json entry.' + message = 'The snapshot metadata file is missing the targets.json entry.' raise tuf.RepositoryError(message) # If there is nothing to refresh, we are done. @@ -1922,7 +1922,7 @@ def refresh_targets_metadata_chain(self, rolename): tuf.RepositoryError: If the metadata of any of the parent roles of 'rolename' is missing - from the 'release.json' metadata file. + from the 'snapshot.json' metadata file. The metadata of the parent roles of 'rolename' are loaded from disk and @@ -1966,15 +1966,15 @@ def refresh_targets_metadata_chain(self, rolename): repr(parent_roles)+'.' logger.info(message) - # Check if 'release.json' provides metadata for each of the roles in + # Check if 'snapshot.json' provides metadata for each of the roles in # 'parent_roles'. All the available roles on the repository are specified - # in the 'release.json' metadata. - targets_metadata_allowed = self.metadata['current']['release']['meta'].keys() + # in the 'snapshot.json' metadata. + targets_metadata_allowed = self.metadata['current']['snapshot']['meta'].keys() for parent_role in parent_roles: parent_role = parent_role + '.json' if parent_role not in targets_metadata_allowed: - message = '"release.json" does not provide all the parent roles '+\ + message = '"snapshot.json" does not provide all the parent roles '+\ 'of '+repr(rolename)+'.' raise tuf.RepositoryError(message) @@ -1984,7 +1984,7 @@ def refresh_targets_metadata_chain(self, rolename): try: parent_roles.remove('targets') except ValueError: - message = 'The Release metadata file is missing the "targets.json" entry.' + message = 'The snapshot metadata file is missing the "targets.json" entry.' raise tuf.RepositoryError(message) # If there is nothing to refresh, we are done. diff --git a/tuf/formats.py b/tuf/formats.py index 0a6f29b3..76939ccc 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -407,15 +407,15 @@ targets = FILEDICT_SCHEMA, delegations = SCHEMA.Optional(DELEGATIONS_SCHEMA)) -# Release role: indicates the latest versions of all metadata (except timestamp). -RELEASE_SCHEMA = SCHEMA.Object( - object_name = 'RELEASE_SCHEMA', - _type = SCHEMA.String('Release'), +# Snapshot role: indicates the latest versions of all metadata (except timestamp). +SNAPSHOT_SCHEMA = SCHEMA.Object( + object_name = 'SNAPSHOT_SCHEMA', + _type = SCHEMA.String('Snapshot'), version = METADATAVERSION_SCHEMA, expires = TIME_SCHEMA, meta = FILEDICT_SCHEMA) -# Timestamp role: indicates the latest version of the release file. +# Timestamp role: indicates the latest version of the snapshot file. TIMESTAMP_SCHEMA = SCHEMA.Object( object_name = 'TIMESTAMP_SCHEMA', _type = SCHEMA.String('Timestamp'), @@ -450,8 +450,8 @@ expires = TIME_SCHEMA, mirrors = SCHEMA.ListOf(MIRROR_SCHEMA)) -# Any of the role schemas (e.g., TIMESTAMP_SCHEMA, RELEASE_SCHEMA, etc.) -ANYROLE_SCHEMA = SCHEMA.OneOf([ROOT_SCHEMA, TARGETS_SCHEMA, RELEASE_SCHEMA, +# Any of the role schemas (e.g., TIMESTAMP_SCHEMA, SNAPSHOT_SCHEMA, etc.) +ANYROLE_SCHEMA = SCHEMA.OneOf([ROOT_SCHEMA, TARGETS_SCHEMA, SNAPSHOT_SCHEMA, TIMESTAMP_SCHEMA, MIRROR_SCHEMA]) @@ -463,7 +463,7 @@ class MetaFile(object): Base class for all metadata file classes. Classes representing metadata files such as RootFile - and ReleaseFile all inherit from MetaFile. The + and SnapshotFile all inherit from MetaFile. The __eq__, __ne__, perform 'equal' and 'not equal' comparisons between Metadata File objects. """ @@ -575,7 +575,7 @@ def make_metadata(version, expiration_date, keydict, roledict, -class ReleaseFile(MetaFile): +class SnapshotFile(MetaFile): def __init__(self, version, expires, filedict): self.info = {} self.info['version'] = version @@ -585,27 +585,27 @@ def __init__(self, version, expires, filedict): @staticmethod def from_metadata(object): - # Is 'object' a Release metadata file? + # Is 'object' a Snapshot metadata file? # Raise 'tuf.FormatError' if not. - RELEASE_SCHEMA.check_match(object) + SNAPSHOT_SCHEMA.check_match(object) version = object['version'] expires = parse_time(object['expires']) filedict = object['meta'] - return ReleaseFile(version, expires, filedict) + return SnapshotFile(version, expires, filedict) @staticmethod def make_metadata(version, expiration_date, filedict): - result = {'_type' : 'Release'} + result = {'_type' : 'Snapshot'} result['version'] = version result['expires'] = expiration_date result['meta'] = filedict - # Is 'result' a Release metadata file? + # Is 'result' a Snapshot metadata file? # Raise 'tuf.FormatError' if not. - RELEASE_SCHEMA.check_match(result) + SNAPSHOT_SCHEMA.check_match(result) return result @@ -687,7 +687,7 @@ def make_metadata(): SCHEMAS_BY_TYPE = { 'Root' : ROOT_SCHEMA, 'Targets' : TARGETS_SCHEMA, - 'Release' : RELEASE_SCHEMA, + 'Snapshot' : SNAPSHOT_SCHEMA, 'Timestamp' : TIMESTAMP_SCHEMA, 'Mirrors' : MIRRORLIST_SCHEMA} @@ -696,7 +696,7 @@ def make_metadata(): ROLE_CLASSES_BY_TYPE = { 'Root' : RootFile, 'Targets' : TargetsFile, - 'Release' : ReleaseFile, + 'Snapshot' : SnapshotFile, 'Timestamp' : TimestampFile, 'Mirrors' : MirrorsFile} @@ -858,7 +858,7 @@ def make_signable(object): object: - A role schema dict (e.g., 'ROOT_SCHEMA', 'RELEASE_SCHEMA'). + A role schema dict (e.g., 'ROOT_SCHEMA', 'SNAPSHOT_SCHEMA'). None. @@ -1022,8 +1022,8 @@ def get_role_class(expected_rolename): The class corresponding to 'expected_rolename'. - E.g., 'Release' as an argument to this function causes - 'ReleaseFile' to be returned. + E.g., 'Snapshot' as an argument to this function causes + SnapshotFile' to be returned. """ # Does 'expected_rolename' have the correct type? diff --git a/tuf/interposition/updater.py b/tuf/interposition/updater.py index 1badb186..c2d266e2 100644 --- a/tuf/interposition/updater.py +++ b/tuf/interposition/updater.py @@ -87,8 +87,8 @@ def download_target(self, target_filepath): # Locate the fileinfo of 'target_filepath'. updater.target() searches # Targets metadata in order of trust, according to the currently trusted - # release. To prevent consecutive target file requests from referring to - # different releases, top-level metadata is not automatically refreshed. + # snapshot. To prevent consecutive target file requests from referring to + # different snapshots, top-level metadata is not automatically refreshed. targets = [self.updater.target(target_filepath)] # TODO: targets are always updated if destination directory is new, right? diff --git a/tuf/mirrors.py b/tuf/mirrors.py index 0a1b518e..49c956aa 100755 --- a/tuf/mirrors.py +++ b/tuf/mirrors.py @@ -15,7 +15,6 @@ To extract a list of mirror urls corresponding to the file type and the location of the file with respect to the base url. - """ import os @@ -55,7 +54,7 @@ def get_list_of_mirrors(file_type, file_path, mirrors_dict): {'url_prefix': 'http://localhost:8001', 'metadata_path': 'metadata/', 'targets_path': 'targets/', - 'confined_target_dirs': ['targets/release1/', ...], + 'confined_target_dirs': ['targets/snapshot1/', ...], 'custom': {...}} The 'custom' field is optional. @@ -68,7 +67,6 @@ def get_list_of_mirrors(file_type, file_path, mirrors_dict): List of mirror urls corresponding to the file_type and file_path. If no match is found, empty list is returned. - """ # Checking if all the arguments have appropriate format. diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index 0da944fe..325a814a 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -66,7 +66,7 @@ # The metadata filenames of the top-level roles. ROOT_FILENAME = 'root' + METADATA_EXTENSION TARGETS_FILENAME = 'targets' + METADATA_EXTENSION -RELEASE_FILENAME = 'release' + METADATA_EXTENSION +SNAPSHOT_FILENAME = 'snapshot' + METADATA_EXTENSION TIMESTAMP_FILENAME = 'timestamp' + METADATA_EXTENSION # The targets and metadata directory names. Metadata files are written @@ -94,8 +94,8 @@ # Initial 'targets.json' expiration time of 3 months. TARGETS_EXPIRATION = 7889230 -# Initial 'release.json' expiration time of 1 week. -RELEASE_EXPIRATION = 604800 +# Initial 'snapshot.json' expiration time of 1 week. +SNAPSHOT_EXPIRATION = 604800 # Initial 'timestamp.json' expiration time of 1 day. TIMESTAMP_EXPIRATION = 86400 @@ -113,13 +113,13 @@ class Repository(object): repository.root.version = 2 repository.timestamp.expiration = "2015-08-08 12:00:00" - repository.release.add_key(...) + repository.snapshot.add_verification_key(...) repository.targets.delegate('unclaimed', ...) Delegating a role from 'targets' updates the attributes of the parent delegation, which then provides: - repository.targets('unclaimed').add_key(...) + repository.targets('unclaimed').add_verification_key(...) @@ -165,7 +165,7 @@ def __init__(self, repository_directory, metadata_directory, targets_directory): # Set the top-level role objects. self.root = Root() - self.release = Release() + self.snapshot = Snapshot() self.timestamp = Timestamp() self.targets = Targets(self._targets_directory, 'targets') @@ -265,13 +265,13 @@ def write(self, write_partial=False, consistent_snapshots=False): self._metadata_directory, consistent_snapshots) - # Generate the 'release.json' metadata file. - release_filename = os.path.join(self._metadata_directory, 'release') - release_filename = 'release' + METADATA_EXTENSION - release_filename = os.path.join(self._metadata_directory, release_filename) + # Generate the 'snapshot.json' metadata file. + snapshot_filename = os.path.join(self._metadata_directory, 'snapshot') + snapshot_filename = 'snapshot' + METADATA_EXTENSION + snapshot_filename = os.path.join(self._metadata_directory, snapshot_filename) filenames = {'root': root_filename, 'targets': targets_filename} - release_signable, release_filename = \ - _generate_and_write_metadata('release', release_filename, write_partial, + snapshot_signable, snapshot_filename = \ + _generate_and_write_metadata('snapshot', snapshot_filename, write_partial, self._targets_directory, self._metadata_directory, consistent_snapshots, filenames) @@ -279,7 +279,7 @@ def write(self, write_partial=False, consistent_snapshots=False): # Generate the 'timestamp.json' metadata file. timestamp_filename = 'timestamp' + METADATA_EXTENSION timestamp_filename = os.path.join(self._metadata_directory, timestamp_filename) - filenames = {'release': release_filename} + filenames = {'snapshot': snapshot_filename} _generate_and_write_metadata('timestamp', timestamp_filename, write_partial, self._targets_directory, self._metadata_directory, consistent_snapshots, @@ -288,7 +288,7 @@ def write(self, write_partial=False, consistent_snapshots=False): # Delete the metadata of roles no longer in 'tuf.roledb'. Obsolete roles # may have been revoked. _delete_obsolete_metadata(self._metadata_directory, - release_signable['signed'], consistent_snapshots) + snapshot_signable['signed'], consistent_snapshots) @@ -319,12 +319,13 @@ def status(self): """ Determine the status of the top-level roles, including those delegated by - the targets role. status() checks if each role provides sufficient public - keys, signatures, and that a valid metadata file is generated if write() - were to be called. Metadata files are temporary written to check that - proper metadata files are written, where file hashes and lengths are - calculated and referenced by the top-level roles. status() does not do a - simple check for number of threshold keys and signatures. + the Targets role. status() checks if each role provides sufficient public + and private keys, signatures, and that a valid metadata file is generated + if write() were to be called. Metadata files are temporarily written so + that file hashes and lengths may be verified, determine if delegated role + trust is fully obeyed, and target paths valid according to parent roles. + status() does not do a simple check for number of threshold keys and + signatures. None. @@ -478,7 +479,7 @@ class Metadata(object): """ Provide a base class to represent a TUF Metadata role. There are four - top-level roles: Root, Targets, Release, and Timestamp. The Metadata class + top-level roles: Root, Targets, Snapshot, and Timestamp. The Metadata class provides methods that are needed by all top-level roles, such as adding and removing public keys, private keys, and signatures. Metadata attributes, such as rolename, version, threshold, expiration, key list, and @@ -502,7 +503,7 @@ def __init__(self): - def add_key(self, key): + def add_verification_key(self, key): """ Add 'key' to the role. Adding a key, which should contain only the public @@ -556,7 +557,7 @@ def add_key(self, key): - def remove_key(self, key): + def remove_verification_key(self, key): """ Remove 'key' from the role's currently recognized list of role keys. @@ -570,7 +571,7 @@ def remove_key(self, key): key: The role's key, conformant to 'tuf.formats.ANYKEY_SCHEMA'. 'key' should contain the only the public portion, as only the public key - is needed. The 'add_key()' method should have previously added 'key'. + is needed. The 'add_verification_key()' method should have previously added 'key'. tuf.FormatError, if the 'key' argument is improperly formatted. @@ -792,7 +793,7 @@ def signatures(self): A getter method that returns the role's signatures. A role is considered fully signed if it contains a threshold number of signatures, where each signature must be provided by the generated by the private key. Keys - are added to a role with the add_key() method. + are added to a role with the add_verification_key() method. None. @@ -1245,7 +1246,7 @@ def __init__(self): self._rolename = 'root' - # By default, 'release' metadata is set to expire 1 week from the current + # By default, 'snapshot' metadata is set to expire 1 week from the current # time. The expiration may be modified. expiration = tuf.formats.format_time(time.time()+ROOT_EXPIRATION) @@ -1266,7 +1267,7 @@ class Timestamp(Metadata): """ Represent a Timestamp role object. The timestamp role is responsible for - referencing the latest version of the Release role. Under normal + referencing the latest version of the Snapshot role. Under normal conditions, it is the only role to be downloaded from a remote repository without a known file length and hash. An upper length limit is set, though. Also, its signatures are also verified to be valid according to the Root @@ -1278,7 +1279,7 @@ class Timestamp(Metadata): This Timestamp object sub-classes Metadata, so the expected Metadata operations like adding/removing public keys, signatures, private keys, and updating metadata attributes (e.g., version and expiration) is supported. - Since Release is a top-level role and must exist, a default Timestamp object + Since Snapshot is a top-level role and must exist, a default Timestamp object is instantiated when a new Repository object is created. >>> @@ -1304,7 +1305,7 @@ def __init__(self): self._rolename = 'timestamp' - # By default, 'release' metadata is set to expire 1 week from the current + # By default, 'snapshot' metadata is set to expire 1 week from the current # time. The expiration may be modified. expiration = tuf.formats.format_time(time.time()+TIMESTAMP_EXPIRATION) @@ -1321,18 +1322,18 @@ def __init__(self): -class Release(Metadata): +class Snapshot(Metadata): """ - Represent a Release role object. The release role is responsible for + Represent a Snapshot role object. The snapshot role is responsible for referencing the other top-level roles (excluding Timestamp) and all delegated roles. - This Release object sub-classes Metadata, so the expected + This Snapshot object sub-classes Metadata, so the expected Metadata operations like adding/removing public keys, signatures, private keys, and updating metadata attributes (e.g., version and expiration) is - supported. Since Release is a top-level role and must exist, a default - Release object is instantiated when a new Repository object is created. + supported. Since Snapshot is a top-level role and must exist, a default + Snapshot object is instantiated when a new Repository object is created. >>> >>> @@ -1345,7 +1346,7 @@ class Release(Metadata): None. - A 'release' role is added to 'tuf.roledb.py'. + A 'snapshot' role is added to 'tuf.roledb.py'. None. @@ -1353,13 +1354,13 @@ class Release(Metadata): def __init__(self): - super(Release, self).__init__() + super(Snapshot, self).__init__() - self._rolename = 'release' + self._rolename = 'snapshot' - # By default, 'release' metadata is set to expire 1 week from the current + # By default, 'snapshot' metadata is set to expire 1 week from the current # time. The expiration may be modified. - expiration = tuf.formats.format_time(time.time()+RELEASE_EXPIRATION) + expiration = tuf.formats.format_time(time.time()+SNAPSHOT_EXPIRATION) roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1, 'signatures': [], 'version': 0, 'compressions': [''], @@ -2022,7 +2023,7 @@ def delegate(self, rolename, public_keys, list_of_targets, # Update the public keys of 'new_targets_object'. for key in public_keys: - new_targets_object.add_key(key) + new_targets_object.add_verification_key(key) # Add the new delegation to this Targets object. For example, 'django' is # added to 'repository.targets' (i.e., repository.targets('django')). @@ -2121,7 +2122,7 @@ def delegate_hashed_bins(self, list_of_targets, keys_of_hashed_bins, The initial public keys of the delegated roles. Public keys may be later added or removed by calling the usual methods of the delegated Targets object. For example: - repository.targets('unclaimed')('000-003').add_key() + repository.targets('unclaimed')('000-003').add_verification_key() number_of_bins: The number of delegated roles, or hashed bins, that should be generated @@ -2293,7 +2294,7 @@ 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) - release_compressions = tuf.roledb.get_roleinfo('release')['compressions'] + snapshot_compressions = tuf.roledb.get_roleinfo('snapshot')['compressions'] # Generate the appropriate role metadata for 'rolename'. if rolename == 'root': @@ -2309,21 +2310,21 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, roleinfo['delegations'], consistent_snapshots) - elif rolename == 'release': + elif rolename == 'snapshot': root_filename = filenames['root'] targets_filename = filenames['targets'] - metadata = generate_release_metadata(metadata_directory, + metadata = generate_snapshot_metadata(metadata_directory, roleinfo['version'], roleinfo['expires'], root_filename, targets_filename, consistent_snapshots ) elif rolename == 'timestamp': - release_filename = filenames['release'] - metadata = generate_timestamp_metadata(release_filename, + snapshot_filename = filenames['snapshot'] + metadata = generate_timestamp_metadata(snapshot_filename, roleinfo['version'], roleinfo['expires'], - release_compressions) + snapshot_compressions) signable = sign_metadata(metadata, roleinfo['signing_keyids'], metadata_filename) @@ -2380,14 +2381,14 @@ def _print_status_of_top_level_roles(targets_directory, metadata_directory): Non-public function that prints whether any of the top-level roles contain an invalid number of public and private keys, or an insufficient threshold of signatures. Considering that the top-level metadata have to be verified in - the expected root -> targets -> release -> timestamp order, this function + the expected root -> targets -> snapshot -> timestamp order, this function prints the error message and returns as soon as a required metadata file is found to be invalid. It is assumed here that the delegated roles have been written and verified. Example output: 'root' role contains 1 / 1 signatures. 'targets' role contains 1 / 1 signatures. - 'release' role contains 1 / 1 signatures. + 'snapshot' role contains 1 / 1 signatures. 'timestamp' role contains 1 / 1 signatures. """ @@ -2396,12 +2397,12 @@ def _print_status_of_top_level_roles(targets_directory, metadata_directory): filenames = get_metadata_filenames(metadata_directory) root_filename = filenames[ROOT_FILENAME] targets_filename = filenames[TARGETS_FILENAME] - release_filename = filenames[RELEASE_FILENAME] + snapshot_filename = filenames[SNAPSHOT_FILENAME] timestamp_filename = filenames[TIMESTAMP_FILENAME] # Verify that the top-level roles contain a valid number of public keys and # that their corresponding private keys have been loaded. - for rolename in ['root', 'targets', 'release', 'timestamp']: + for rolename in ['root', 'targets', 'snapshot', 'timestamp']: try: _check_role_keys(rolename) @@ -2410,7 +2411,7 @@ def _print_status_of_top_level_roles(targets_directory, metadata_directory): return # Do the top-level roles contain a valid threshold of signatures? Top-level - # metadata is verified in Root -> Targets -> Release -> Timestamp order. + # metadata is verified in Root -> Targets -> Snapshot -> Timestamp order. # Verify the metadata of the Root role. try: signable, root_filename = \ @@ -2437,25 +2438,25 @@ def _print_status_of_top_level_roles(targets_directory, metadata_directory): _print_status('targets', signable) return - # Verify the metadata of the Release role. + # Verify the metadata of the snapshot role. filenames = {'root': root_filename, 'targets': targets_filename} try: - signable, release_filename = \ - _generate_and_write_metadata('release', release_filename, False, + signable, snapshot_filename = \ + _generate_and_write_metadata('snapshot', snapshot_filename, False, targets_directory, metadata_directory, False, filenames) - _print_status('release', signable) + _print_status('snapshot', signable) except tuf.UnsignedMetadataError, e: signable = e[1] - _print_status('release', signable) + _print_status('snapshot', signable) return # Verify the metadata of the Timestamp role. - filenames = {'release': release_filename} + filenames = {'snapshot': snapshot_filename} try: - signable, release_filename = \ - _generate_and_write_metadata('timestamp', release_filename, False, + signable, snapshot_filename = \ + _generate_and_write_metadata('timestamp', snapshot_filename, False, targets_directory, metadata_directory, False, filenames) _print_status('timestamp', signable) @@ -2659,7 +2660,7 @@ def _remove_invalid_and_duplicate_signatures(signable): -def _delete_obsolete_metadata(metadata_directory, release_metadata, +def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, consistent_snapshots): """ Non-public function that deletes metadata files marked as removed by @@ -2706,11 +2707,11 @@ def _delete_obsolete_metadata(metadata_directory, release_metadata, logger.info('Removing outdated metadata: ' + repr(metadata_path)) os.remove(metadata_path) - # Delete outdated consistent snapshots. release metadata includes + # Delete outdated consistent snapshots. snapshot metadata includes # the file extension of roles. if consistent_snapshots: #metadata_name_extension = metadata_name + METADATA_EXTENSION - file_hashes = release_metadata['meta'][metadata_name] \ + file_hashes = snapshot_metadata['meta'][metadata_name] \ ['hashes'].values() if embeded_digest not in file_hashes: logger.info('Removing outdated metadata: ' + repr(metadata_path)) @@ -2828,7 +2829,7 @@ def create_new_repository(repository_directory): os.path.join(repository_directory, TARGETS_DIRECTORY_NAME) # Try to create the metadata directory that will hold all of the metadata - # files, such as 'root.json' and 'release.json'. + # files, such as 'root.json' and 'snapshot.json'. try: message = 'Creating '+repr(metadata_directory) logger.info(message) @@ -2909,13 +2910,13 @@ def load_repository(repository_directory): consistent_snapshots = False # Load the metadata of the top-level roles (i.e., Root, Timestamp, Targets, - # and Release). + # and Snapshot). repository, consistent_snapshots = _load_top_level_metadata(repository, filenames) # Load delegated targets metadata. # Walk the 'targets/' directory and generate the fileinfo of all the files - # listed. This information is stored in the 'meta' field of the release + # listed. This information is stored in the 'meta' field of the snapshot # metadata object. targets_objects = {} loaded_metadata = [] @@ -3014,18 +3015,18 @@ def load_repository(repository_directory): def _load_top_level_metadata(repository, top_level_filenames): """ - Load the metadata of the Root, Timestamp, Targets, and Release roles. + 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] targets_filename = top_level_filenames[TARGETS_FILENAME] - release_filename = top_level_filenames[RELEASE_FILENAME] + snapshot_filename = top_level_filenames[SNAPSHOT_FILENAME] timestamp_filename = top_level_filenames[TIMESTAMP_FILENAME] root_metadata = None targets_metadata = None - release_metadata = None + snapshot_metadata = None timestamp_metadata = None # Load ROOT.json. A Root role file without a digest is always written. @@ -3078,30 +3079,30 @@ def _load_top_level_metadata(repository, top_level_filenames): else: pass - # Load RELEASE.json. A consistent snapshot of Release must be calculated + # Load SNAPSHOT.json. A consistent snapshot of Snapshot must be calculated # if 'consistent_snapshots' is True. if consistent_snapshots: - release_hashes = timestamp_metadata['meta'][RELEASE_FILENAME]['hashes'] - release_digest = random.choice(release_hashes.values()) - dirname, basename = os.path.split(release_filename) - release_filename = os.path.join(dirname, release_digest + '.' + basename) + snapshot_hashes = timestamp_metadata['meta'][SNAPSHOT_FILENAME]['hashes'] + snapshot_digest = random.choice(snapshot_hashes.values()) + dirname, basename = os.path.split(snapshot_filename) + snapshot_filename = os.path.join(dirname, snapshot_digest + '.' + basename) - if os.path.exists(release_filename): - signable = tuf.util.load_json_file(release_filename) + if os.path.exists(snapshot_filename): + signable = tuf.util.load_json_file(snapshot_filename) tuf.formats.check_signable_object_format(signable) - release_metadata = signable['signed'] + snapshot_metadata = signable['signed'] for signature in signable['signatures']: - repository.release.add_signature(signature) + repository.snapshot.add_signature(signature) - # Load Release's roleinfo and update 'tuf.roledb'. - roleinfo = tuf.roledb.get_roleinfo('release') - roleinfo['expires'] = release_metadata['expires'] - roleinfo['version'] = release_metadata['version'] - if os.path.exists(release_filename+'.gz'): + # Load Snapshot's roleinfo and update 'tuf.roledb'. + roleinfo = tuf.roledb.get_roleinfo('snapshot') + roleinfo['expires'] = snapshot_metadata['expires'] + roleinfo['version'] = snapshot_metadata['version'] + if os.path.exists(snapshot_filename+'.gz'): roleinfo['compressions'].append('gz') - _check_if_partial_loaded('release', signable, roleinfo) - tuf.roledb.update_roleinfo('release', roleinfo) + _check_if_partial_loaded('snapshot', signable, roleinfo) + tuf.roledb.update_roleinfo('snapshot', roleinfo) else: pass @@ -3109,7 +3110,7 @@ def _load_top_level_metadata(repository, top_level_filenames): # Load TARGETS.json. A consistent snapshot of Targets must be calculated if # 'consistent_snapshots' is True. if consistent_snapshots: - targets_hashes = release_metadata['meta'][TARGETS_FILENAME]['hashes'] + targets_hashes = snapshot_metadata['meta'][TARGETS_FILENAME]['hashes'] targets_digest = random.choice(targets_hashes.values()) dirname, basename = os.path.split(targets_filename) targets_filename = os.path.join(dirname, targets_digest + '.' + basename) @@ -3565,7 +3566,7 @@ def get_metadata_filenames(metadata_directory=None): filenames = {'root': 'metadata/root.json', 'targets': 'metadata/targets.json', - 'release': 'metadata/release.json', + 'snapshot': 'metadata/snapshot.json', 'timestamp': 'metadata/timestamp.json'} If the metadata directory is not set by the caller, the current @@ -3583,7 +3584,7 @@ def get_metadata_filenames(metadata_directory=None): A dictionary containing the expected filenames of the top-level - metadata files, such as 'root.json' and 'release.json'. + metadata files, such as 'root.json' and 'snapshot.json'. """ # Does 'metadata_directory' have the correct format? @@ -3609,8 +3610,8 @@ def get_metadata_filenames(metadata_directory=None): filenames[TARGETS_FILENAME] = \ os.path.join(metadata_directory, TARGETS_FILENAME) - filenames[RELEASE_FILENAME] = \ - os.path.join(metadata_directory, RELEASE_FILENAME) + filenames[SNAPSHOT_FILENAME] = \ + os.path.join(metadata_directory, SNAPSHOT_FILENAME) filenames[TIMESTAMP_FILENAME] = \ os.path.join(metadata_directory, TIMESTAMP_FILENAME) @@ -3781,7 +3782,7 @@ def generate_root_metadata(version, expiration_date, consistent_snapshots): # Extract the role, threshold, and keyid information of the top-level roles, # which Root stores in its metadata. The necessary role metadata is generated # from this information. - for rolename in ['root', 'targets', 'release', 'timestamp']: + for rolename in ['root', 'targets', 'snapshot', 'timestamp']: # If a top-level role is missing from 'tuf.roledb.py', raise an exception. if not tuf.roledb.role_exists(rolename): @@ -3951,15 +3952,15 @@ def generate_targets_metadata(targets_directory, target_files, version, -def generate_release_metadata(metadata_directory, version, expiration_date, +def generate_snapshot_metadata(metadata_directory, version, expiration_date, root_filename, targets_filename, consistent_snapshots): """ - Create the release metadata. The minimum metadata must exist + Create the snapshot metadata. The minimum metadata must exist (i.e., 'root.json' and 'targets.json'). This will also look through the 'targets/' directory in 'metadata_directory' and the resulting - release file will list all the delegated roles. + snapshot file will list all the delegated roles. metadata_directory: @@ -3984,14 +3985,14 @@ def generate_release_metadata(metadata_directory, version, expiration_date, tuf.FormatError, if 'metadata_directory' is improperly formatted. - tuf.Error, if an error occurred trying to generate the release metadata + tuf.Error, if an error occurred trying to generate the snapshot metadata object. The 'root.json' and 'targets.json' files are read. - The release metadata object, conformant to 'tuf.formats.RELEASE_SCHEMA'. + The snapshot metadata object, conformant to 'tuf.formats.SNAPSHOT_SCHEMA'. """ # Do the arguments have the correct format? @@ -4028,7 +4029,7 @@ def generate_release_metadata(metadata_directory, version, expiration_date, get_metadata_file_info(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 release + # 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): @@ -4046,37 +4047,37 @@ def generate_release_metadata(metadata_directory, version, expiration_date, metadata_name, digest_junk = \ _strip_consistent_snapshots_digest(metadata_name, consistent_snapshots) - # All delegated roles are added to the release file, including + # All delegated roles are added to the snapshot file, including # compressed versions. for metadata_extension in METADATA_EXTENSIONS: if metadata_name.endswith(metadata_extension): rolename = metadata_name[:-len(metadata_extension)] # Obsolete role files may still be found. Ensure only roles loaded - # in the roledb are included in the release metadata. + # in the roledb are included in the snapshot metadata. if tuf.roledb.role_exists(rolename): filedict[metadata_name] = get_metadata_file_info(metadata_path) - # Generate the release metadata object. - release_metadata = tuf.formats.ReleaseFile.make_metadata(version, + # Generate the snapshot metadata object. + snapshot_metadata = tuf.formats.SnapshotFile.make_metadata(version, expiration_date, filedict) - return release_metadata + return snapshot_metadata -def generate_timestamp_metadata(release_filename, version, +def generate_timestamp_metadata(snapshot_filename, version, expiration_date, compressions=()): """ - Generate the timestamp metadata object. The 'release.json' file must exist. + Generate the timestamp metadata object. The 'snapshot.json' file must exist. - release_filename: - The required filename of the release metadata file. + snapshot_filename: + The required filename of the snapshot metadata file. version: The timestamp's version number. Clients use the version number to @@ -4087,10 +4088,10 @@ def generate_timestamp_metadata(release_filename, version, The expiration date, in UTC, of the metadata file, conformant to 'tuf.formats.TIME_SCHEMA'. - release_filename: + snapshot_filename: compressions: - Compression extensions (e.g., 'gz'). If 'release.json' is also saved in + 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. @@ -4110,15 +4111,15 @@ def generate_timestamp_metadata(release_filename, version, # This check ensures arguments have the appropriate number of objects and # object types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if the check fails. - tuf.formats.PATH_SCHEMA.check_match(release_filename) + tuf.formats.PATH_SCHEMA.check_match(snapshot_filename) tuf.formats.METADATAVERSION_SCHEMA.check_match(version) tuf.formats.TIME_SCHEMA.check_match(expiration_date) tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) - # Retrieve the fileinfo of the release metadata file. + # Retrieve the fileinfo of the snapshot metadata file. # This file information contains hashes, file length, custom data, etc. fileinfo = {} - fileinfo[RELEASE_FILENAME] = get_metadata_file_info(release_filename) + fileinfo[SNAPSHOT_FILENAME] = get_metadata_file_info(snapshot_filename) # Save the fileinfo of the compressed versions of 'timestamp.json' # in 'fileinfo'. Log the files included in 'fileinfo'. @@ -4126,7 +4127,7 @@ def generate_timestamp_metadata(release_filename, version, if not len(file_extension): continue - compressed_filename = release_filename + '.' + file_extension + compressed_filename = snapshot_filename + '.' + file_extension try: compressed_fileinfo = get_metadata_file_info(compressed_filename) @@ -4135,7 +4136,7 @@ def generate_timestamp_metadata(release_filename, version, else: logger.info('Including fileinfo about '+str(compressed_filename)) - fileinfo[RELEASE_FILENAME+'.'+file_extension] = compressed_fileinfo + fileinfo[SNAPSHOT_FILENAME+'.'+file_extension] = compressed_fileinfo # Generate the timestamp metadata object. timestamp_metadata = tuf.formats.TimestampFile.make_metadata(version, diff --git a/tuf/roledb.py b/tuf/roledb.py index a716575b..4941f663 100755 --- a/tuf/roledb.py +++ b/tuf/roledb.py @@ -117,7 +117,7 @@ def add_role(rolename, roleinfo, require_parent=True): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). roleinfo: An object representing the role associated with 'rolename', conformant to @@ -193,7 +193,7 @@ def update_roleinfo(rolename, roleinfo): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). roleinfo: An object representing the role associated with 'rolename', conformant to @@ -255,7 +255,7 @@ def get_parent_rolename(rolename): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. @@ -295,7 +295,7 @@ def get_all_parent_roles(rolename): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. @@ -347,7 +347,7 @@ def role_exists(rolename): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. @@ -383,7 +383,7 @@ def remove_role(rolename): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. @@ -422,7 +422,7 @@ def remove_delegated_roles(rolename): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. @@ -493,7 +493,7 @@ def get_roleinfo(rolename): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' is improperly formatted. @@ -529,7 +529,7 @@ def get_role_keyids(rolename): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. @@ -564,7 +564,7 @@ def get_role_threshold(rolename): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. @@ -599,7 +599,7 @@ def get_role_paths(rolename): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. @@ -640,7 +640,7 @@ def get_delegated_rolenames(rolename): rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' - (e.g., 'root', 'release', 'timestamp'). + (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. diff --git a/tuf/sig.py b/tuf/sig.py index d8ef7f0c..d789aa5e 100755 --- a/tuf/sig.py +++ b/tuf/sig.py @@ -27,7 +27,7 @@ keys into different categories. As keys are added and removed, the system must securely and efficiently verify the status of these signatures. For instance, a bunch of keys have recently expired. How many valid keys - are now available to the Release role? This question can be answered by + are now available to the Snapshot role? This question can be answered by get_signature_status(), which will return a full 'status report' of these 'signable' dicts. This module also provides a convenient verify() function that will determine if a role still has a sufficient number of valid keys. @@ -62,7 +62,7 @@ def get_signature_status(signable, role=None): Conformant to tuf.formats.SIGNABLE_SCHEMA. role: - TUF role (e.g., 'root', 'targets', 'release'). + TUF role (e.g., 'root', 'targets', 'snapshot'). tuf.FormatError, if 'signable' does not have the correct format. @@ -180,7 +180,7 @@ def verify(signable, role): signable = {'signed':, 'signatures': [{'keyid':, 'method':, 'sig':}]} role: - TUF role (e.g., 'root', 'targets', 'release'). + TUF role (e.g., 'root', 'targets', 'snapshot'). tuf.UnknownRoleError, if 'role' is not recognized. From 36b59f922ecdde88c1bad351557bf1ed68ef18a2 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Thu, 30 Jan 2014 08:11:35 -0500 Subject: [PATCH 2/3] Continue updating unit tests and modify ROOT_SCHEMA. --- docs/tuf-spec.txt | 4 +- tests/unit/aggregate_tests.py | 2 - tests/unit/{ => deprecated}/test_keystore.py | 0 tests/unit/{ => deprecated}/test_push.py | 0 .../{ => deprecated}/test_pushtoolslib.py | 0 .../unit/{ => deprecated}/test_quickstart.py | 0 tests/unit/{ => deprecated}/test_signercli.py | 0 tests/unit/{ => deprecated}/test_signerlib.py | 0 tests/unit/test_formats.py | 78 ++++++++------ tuf/formats.py | 14 +-- tuf/repository_tool.py | 102 +++++++++--------- 11 files changed, 104 insertions(+), 96 deletions(-) rename tests/unit/{ => deprecated}/test_keystore.py (100%) rename tests/unit/{ => deprecated}/test_push.py (100%) rename tests/unit/{ => deprecated}/test_pushtoolslib.py (100%) rename tests/unit/{ => deprecated}/test_quickstart.py (100%) rename tests/unit/{ => deprecated}/test_signercli.py (100%) rename tests/unit/{ => deprecated}/test_signerlib.py (100%) diff --git a/docs/tuf-spec.txt b/docs/tuf-spec.txt index 6e05e68c..2540cc1a 100644 --- a/docs/tuf-spec.txt +++ b/docs/tuf-spec.txt @@ -792,7 +792,7 @@ it is updated. In the next subsection, we will see how clients will reproduce the name of the intended file. - Finally, the root metadata should write the Boolean "consistent_snapshots" + Finally, the root metadata should write the Boolean "consistent_snapshot" attribute at the root level of its keys of attributes. If consistent snapshots are not written by the repository, then the attribute may either be left unspecified or be set to the False value. Otherwise, it must be @@ -807,7 +807,7 @@ snapshot. If the root metadata (root.txt) is either missing the Boolean - "consistent_snapshots" attribute or the attribute is set to False, then the + "consistent_snapshot" attribute or the attribute is set to False, then the client should do nothing different from the workflow in Section 5.1. Otherwise, the client must perform as follows: diff --git a/tests/unit/aggregate_tests.py b/tests/unit/aggregate_tests.py index 620c54de..0713e217 100755 --- a/tests/unit/aggregate_tests.py +++ b/tests/unit/aggregate_tests.py @@ -21,10 +21,8 @@ Run all the unit tests from every .py file beginning with "test_" in 'tuf/tests'. Use --random to run the tests in random order. - """ - import sys import unittest import glob diff --git a/tests/unit/test_keystore.py b/tests/unit/deprecated/test_keystore.py similarity index 100% rename from tests/unit/test_keystore.py rename to tests/unit/deprecated/test_keystore.py diff --git a/tests/unit/test_push.py b/tests/unit/deprecated/test_push.py similarity index 100% rename from tests/unit/test_push.py rename to tests/unit/deprecated/test_push.py diff --git a/tests/unit/test_pushtoolslib.py b/tests/unit/deprecated/test_pushtoolslib.py similarity index 100% rename from tests/unit/test_pushtoolslib.py rename to tests/unit/deprecated/test_pushtoolslib.py diff --git a/tests/unit/test_quickstart.py b/tests/unit/deprecated/test_quickstart.py similarity index 100% rename from tests/unit/test_quickstart.py rename to tests/unit/deprecated/test_quickstart.py diff --git a/tests/unit/test_signercli.py b/tests/unit/deprecated/test_signercli.py similarity index 100% rename from tests/unit/test_signercli.py rename to tests/unit/deprecated/test_signercli.py diff --git a/tests/unit/test_signerlib.py b/tests/unit/deprecated/test_signerlib.py similarity index 100% rename from tests/unit/test_signerlib.py rename to tests/unit/deprecated/test_signerlib.py diff --git a/tests/unit/test_formats.py b/tests/unit/test_formats.py index 49b8c608..2b9dbda0 100755 --- a/tests/unit/test_formats.py +++ b/tests/unit/test_formats.py @@ -108,7 +108,7 @@ def test_schemas(self): 'custom': {'type': 'paintjob'}}), 'FILEDICT_SCHEMA': (tuf.formats.FILEDICT_SCHEMA, - {'metadata/root.txt': {'length': 1024, + {'metadata/root.json': {'length': 1024, 'hashes': {'sha256': 'ABCD123'}, 'custom': {'type': 'metadata'}}}), @@ -156,7 +156,7 @@ def test_schemas(self): 'SCPCONFIG_SCHEMA': (tuf.formats.SCPCONFIG_SCHEMA, {'general': {'transfer_module': 'scp', - 'metadata_path': '/path/meta.txt', + 'metadata_path': '/path/meta.json', 'targets_directory': '/targets'}, 'scp': {'host': 'http://localhost:8001', 'user': 'McFly', @@ -184,6 +184,7 @@ def test_schemas(self): 'ROOT_SCHEMA': (tuf.formats.ROOT_SCHEMA, {'_type': 'Root', 'version': 8, + 'consistent_snapshot': False, 'expires': '2012-10-16 06:42:12 UTC', 'keys': {'123abc': {'keytype': 'rsa', 'keyval': {'public': 'pubkey', @@ -196,7 +197,7 @@ def test_schemas(self): {'_type': 'Targets', 'version': 8, 'expires': '2012-10-16 06:42:12 UTC', - 'targets': {'metadata/targets.txt': {'length': 1024, + 'targets': {'metadata/targets.json': {'length': 1024, 'hashes': {'sha256': 'ABCD123'}, 'custom': {'type': 'metadata'}}}, 'delegations': {'keys': {'123abc': {'keytype':'rsa', @@ -206,11 +207,11 @@ def test_schemas(self): 'threshold': 1, 'paths': ['path1/', 'path2']}]}}), - 'RELEASE_SCHEMA': (tuf.formats.RELEASE_SCHEMA, - {'_type': 'Release', + 'SNAPSHOT_SCHEMA': (tuf.formats.SNAPSHOT_SCHEMA, + {'_type': 'Snapshot', 'version': 8, 'expires': '2012-10-16 06:42:12 UTC', - 'meta': {'metadata/release.txt': {'length': 1024, + 'meta': {'metadata/snapshot.json': {'length': 1024, 'hashes': {'sha256': 'ABCD123'}, 'custom': {'type': 'metadata'}}}}), @@ -218,7 +219,7 @@ def test_schemas(self): {'_type': 'Timestamp', 'version': 8, 'expires': '2012-10-16 06:42:12 UTC', - 'meta': {'metadata/timestamp.txt': {'length': 1024, + 'meta': {'metadata/timestamp.json': {'length': 1024, 'hashes': {'sha256': 'ABCD123'}, 'custom': {'type': 'metadata'}}}}), @@ -246,8 +247,8 @@ def test_schemas(self): 'confined_target_dirs': ['path1/', 'path2/'], 'custom': {'type': 'mirror'}}]})} - # Iterate through 'valid_schemas', ensuring each 'valid_schema' correctly - # matches its respective 'schema_type'. + # Iterate 'valid_schemas', ensuring each 'valid_schema' correctly matches + # its respective 'schema_type'. for schema_name, (schema_type, valid_schema) in valid_schemas.items(): self.assertEqual(True, schema_type.matches(valid_schema)) @@ -290,7 +291,7 @@ def test_TimestampFile(self): # Test conditions for valid instances of 'tuf.formats.TimestampFile'. version = 8 expires = '2012-10-16 06:42:12 UTC' - filedict = {'metadata/timestamp.txt': {'length': 1024, + filedict = {'metadata/timestamp.json': {'length': 1024, 'hashes': {'sha256': 'ABCD123'}, 'custom': {'type': 'metadata'}}} @@ -322,7 +323,8 @@ def test_TimestampFile(self): def test_RootFile(self): # Test conditions for valid instances of 'tuf.formats.RootFile'. version = 8 - expiration_seconds = 691200 + consistent_snapshot = False + expires = '2018-10-16 06:42:12 UTC' keydict = {'123abc': {'keytype': 'rsa', 'keyval': {'public': 'pubkey', 'private': 'privkey'}}} @@ -335,50 +337,56 @@ def test_RootFile(self): from_metadata = tuf.formats.RootFile.from_metadata ROOT_SCHEMA = tuf.formats.ROOT_SCHEMA - self.assertTrue(ROOT_SCHEMA.matches(make_metadata(version, expiration_seconds, - keydict, roledict))) - metadata = make_metadata(version, expiration_seconds, keydict, roledict,) + self.assertTrue(ROOT_SCHEMA.matches(make_metadata(version, expires, + keydict, roledict, + consistent_snapshot))) + metadata = make_metadata(version, expires, keydict, roledict, + consistent_snapshot) self.assertTrue(isinstance(from_metadata(metadata), tuf.formats.RootFile)) # Test conditions for invalid arguments. bad_version = '8' - bad_expiration_seconds = 'eight' + bad_expires = 'eight' bad_keydict = 123 bad_roledict = 123 self.assertRaises(tuf.FormatError, make_metadata, bad_version, - expiration_seconds, - keydict, roledict) + expires, + keydict, roledict, + consistent_snapshot) self.assertRaises(tuf.FormatError, make_metadata, version, - bad_expiration_seconds, - keydict, roledict) + bad_expires, + keydict, roledict, + consistent_snapshot) self.assertRaises(tuf.FormatError, make_metadata, version, - expiration_seconds, - bad_keydict, roledict) + expires, + bad_keydict, roledict, + consistent_snapshot) self.assertRaises(tuf.FormatError, make_metadata, version, - expiration_seconds, - keydict, bad_roledict) + expires, + keydict, bad_roledict, + consistent_snapshot) self.assertRaises(tuf.FormatError, from_metadata, 'bad') - def test_ReleaseFile(self): - # Test conditions for valid instances of 'tuf.formats.ReleaseFile'. + def test_SnapshotFile(self): + # Test conditions for valid instances of 'tuf.formats.SnapshotFile'. version = 8 expires = '2012-10-16 06:42:12 UTC' - filedict = {'metadata/release.txt': {'length': 1024, + filedict = {'metadata/snapshot.json': {'length': 1024, 'hashes': {'sha256': 'ABCD123'}, 'custom': {'type': 'metadata'}}} - make_metadata = tuf.formats.ReleaseFile.make_metadata - from_metadata = tuf.formats.ReleaseFile.from_metadata - RELEASE_SCHEMA = tuf.formats.RELEASE_SCHEMA + make_metadata = tuf.formats.SnapshotFile.make_metadata + from_metadata = tuf.formats.SnapshotFile.from_metadata + SNAPSHOT_SCHEMA = tuf.formats.SNAPSHOT_SCHEMA - self.assertTrue(RELEASE_SCHEMA.matches(make_metadata(version, expires, + self.assertTrue(SNAPSHOT_SCHEMA.matches(make_metadata(version, expires, filedict))) metadata = make_metadata(version, expires, filedict) - self.assertTrue(isinstance(from_metadata(metadata), tuf.formats.ReleaseFile)) + self.assertTrue(isinstance(from_metadata(metadata), tuf.formats.SnapshotFile)) # Test conditions for invalid arguments. bad_version = '8' @@ -399,7 +407,7 @@ def test_TargetsFile(self): # Test conditions for valid instances of 'tuf.formats.TargetsFile'. version = 8 expires = '2012-10-16 06:42:12 UTC' - filedict = {'metadata/targets.txt': {'length': 1024, + filedict = {'metadata/targets.json': {'length': 1024, 'hashes': {'sha256': 'ABCD123'}, 'custom': {'type': 'metadata'}}} @@ -490,6 +498,7 @@ def test_make_signable(self): # Test conditions for expected make_signable() behavior. root = {'_type': 'Root', 'version': 8, + 'consistent_snapshot': False, 'expires': '2012-10-16 06:42:12 UTC', 'keys': {'123abc': {'keytype': 'rsa', 'keyval': {'public': 'pubkey', @@ -580,7 +589,7 @@ def test_get_role_class(self): self.assertEqual(tuf.formats.RootFile, get_role_class('Root')) self.assertEqual(tuf.formats.TargetsFile, get_role_class('Targets')) - self.assertEqual(tuf.formats.ReleaseFile, get_role_class('Release')) + self.assertEqual(tuf.formats.SnapshotFile, get_role_class('Snapshot')) self.assertEqual(tuf.formats.TimestampFile, get_role_class('Timestamp')) self.assertEqual(tuf.formats.MirrorsFile, get_role_class('Mirrors')) @@ -599,7 +608,7 @@ def test_expected_meta_rolename(self): self.assertEqual('Root', expected_rolename('root')) self.assertEqual('Targets', expected_rolename('targets')) - self.assertEqual('Release', expected_rolename('release')) + self.assertEqual('Snapshot', expected_rolename('snapshot')) self.assertEqual('Timestamp', expected_rolename('timestamp')) self.assertEqual('Mirrors', expected_rolename('mirrors')) self.assertEqual('Targets Role', expected_rolename('targets role')) @@ -616,6 +625,7 @@ def test_check_signable_object_format(self): # Test condition for a valid argument. root = {'_type': 'Root', 'version': 8, + 'consistent_snapshot': False, 'expires': '2012-10-16 06:42:12 UTC', 'keys': {'123abc': {'keytype': 'rsa', 'keyval': {'public': 'pubkey', diff --git a/tuf/formats.py b/tuf/formats.py index 76939ccc..65935171 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -393,7 +393,7 @@ object_name = 'ROOT_SCHEMA', _type = SCHEMA.String('Root'), version = METADATAVERSION_SCHEMA, - consistent_snapshots = BOOLEAN_SCHEMA, + consistent_snapshot = BOOLEAN_SCHEMA, expires = TIME_SCHEMA, keys = KEYDICT_SCHEMA, roles = ROLEDICT_SCHEMA) @@ -531,13 +531,13 @@ def make_metadata(version, expiration_date, filedict): class RootFile(MetaFile): - def __init__(self, version, expires, keys, roles, consistent_snapshots): + def __init__(self, version, expires, keys, roles, consistent_snapshot): self.info = {} self.info['version'] = version self.info['expires'] = expires self.info['keys'] = keys self.info['roles'] = roles - self.info['consistent_snapshots'] = consistent_snapshots + self.info['consistent_snapshot'] = consistent_snapshot @staticmethod @@ -550,20 +550,20 @@ def from_metadata(object): expires = parse_time(object['expires']) keys = object['keys'] roles = object['roles'] - consistent_snapshots = object['consistent_snapshots'] + consistent_snapshot = object['consistent_snapshot'] - return RootFile(version, expires, keys, roles, consistent_snapshots) + return RootFile(version, expires, keys, roles, consistent_snapshot) @staticmethod def make_metadata(version, expiration_date, keydict, roledict, - consistent_snapshots): + consistent_snapshot): result = {'_type' : 'Root'} result['version'] = version result['expires'] = expiration_date result['keys'] = keydict result['roles'] = roledict - result['consistent_snapshots'] = consistent_snapshots + result['consistent_snapshot'] = consistent_snapshot # Is 'result' a Root metadata file? # Raise 'tuf.FormatError' if not. diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index 325a814a..9bad7283 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -171,7 +171,7 @@ def __init__(self, repository_directory, metadata_directory, targets_directory): - def write(self, write_partial=False, consistent_snapshots=False): + def write(self, write_partial=False, consistent_snapshot=False): """ Write all the JSON Metadata objects to their corresponding files. @@ -187,7 +187,7 @@ def write(self, write_partial=False, consistent_snapshots=False): exception if a metadata role cannot be written due to not having enough signatures. - consistent_snapshots: + 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 @@ -211,7 +211,7 @@ def write(self, write_partial=False, consistent_snapshots=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_snapshots) + tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) # At this point the tuf.keydb and tuf.roledb stores must be fully # populated, otherwise write() throwns a 'tuf.Repository' exception if @@ -243,7 +243,7 @@ def write(self, write_partial=False, consistent_snapshots=False): _generate_and_write_metadata(delegated_rolename, delegated_filename, write_partial, self._targets_directory, self._metadata_directory, - consistent_snapshots) + consistent_snapshot) # Generate the 'root.json' metadata file. # _generate_and_write_metadata() raises a 'tuf.Error' exception if the @@ -254,7 +254,7 @@ def write(self, write_partial=False, consistent_snapshots=False): _generate_and_write_metadata('root', root_filename, write_partial, self._targets_directory, self._metadata_directory, - consistent_snapshots) + consistent_snapshot) # Generate the 'targets.json' metadata file. targets_filename = 'targets' + METADATA_EXTENSION @@ -263,7 +263,7 @@ def write(self, write_partial=False, consistent_snapshots=False): _generate_and_write_metadata('targets', targets_filename, write_partial, self._targets_directory, self._metadata_directory, - consistent_snapshots) + consistent_snapshot) # Generate the 'snapshot.json' metadata file. snapshot_filename = os.path.join(self._metadata_directory, 'snapshot') @@ -274,7 +274,7 @@ def write(self, write_partial=False, consistent_snapshots=False): _generate_and_write_metadata('snapshot', snapshot_filename, write_partial, self._targets_directory, self._metadata_directory, - consistent_snapshots, filenames) + consistent_snapshot, filenames) # Generate the 'timestamp.json' metadata file. timestamp_filename = 'timestamp' + METADATA_EXTENSION @@ -282,13 +282,13 @@ def write(self, write_partial=False, consistent_snapshots=False): filenames = {'snapshot': snapshot_filename} _generate_and_write_metadata('timestamp', timestamp_filename, write_partial, self._targets_directory, - self._metadata_directory, consistent_snapshots, + self._metadata_directory, consistent_snapshot, filenames) # Delete the metadata of roles no longer in 'tuf.roledb'. Obsolete roles # may have been revoked. _delete_obsolete_metadata(self._metadata_directory, - snapshot_signable['signed'], consistent_snapshots) + snapshot_signable['signed'], consistent_snapshot) @@ -1251,7 +1251,7 @@ def __init__(self): expiration = tuf.formats.format_time(time.time()+ROOT_EXPIRATION) roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1, - 'signatures': [], 'version': 0, 'consistent_snapshots': False, + 'signatures': [], 'version': 0, 'consistent_snapshot': False, 'compressions': [''], 'expires': expiration, 'partial_loaded': False} try: @@ -2278,7 +2278,7 @@ def delegations(self): def _generate_and_write_metadata(rolename, metadata_filename, write_partial, targets_directory, metadata_directory, - consistent_snapshots=False, filenames=None): + consistent_snapshot=False, filenames=None): """ Non-public function that can generate and write the metadata of the specified top-level 'rolename'. It also increments version numbers if: @@ -2299,7 +2299,7 @@ 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_snapshots) + roleinfo['expires'], consistent_snapshot) # Check for the Targets role, including delegated roles. elif rolename.startswith('targets'): @@ -2308,7 +2308,7 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, roleinfo['version'], roleinfo['expires'], roleinfo['delegations'], - consistent_snapshots) + consistent_snapshot) elif rolename == 'snapshot': root_filename = filenames['root'] @@ -2317,7 +2317,7 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, roleinfo['version'], roleinfo['expires'], root_filename, targets_filename, - consistent_snapshots ) + consistent_snapshot ) elif rolename == 'timestamp': snapshot_filename = filenames['snapshot'] @@ -2355,14 +2355,14 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, _remove_invalid_and_duplicate_signatures(signable) compressions = roleinfo['compressions'] filename = write_metadata_file(signable, metadata_filename, compressions, - consistent_snapshots) + 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_snapshots=False) + consistent_snapshot=False) # 'signable' contains an invalid threshold of signatures. @@ -2661,7 +2661,7 @@ def _remove_invalid_and_duplicate_signatures(signable): def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, - consistent_snapshots): + consistent_snapshot): """ Non-public function that deletes metadata files marked as removed by repository_tool.py. Metadata files marked as removed are not actually deleted @@ -2687,11 +2687,11 @@ def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, metadata_name = \ metadata_path[len(metadata_directory):].lstrip(os.path.sep) - # Strip the digest if 'consistent_snapshots' is True. + # Strip the digest if 'consistent_snapshot' is True. # Example: 'targets/unclaimed/13df98ab0.django.json' --> # 'targets/unclaimed/django.json' metadata_name, embeded_digest = \ - _strip_consistent_snapshots_digest(metadata_name, consistent_snapshots) + _strip_consistent_snapshot_digest(metadata_name, consistent_snapshot) # Strip filename extensions. The role database does not include the # metadata extension. @@ -2709,7 +2709,7 @@ def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, # Delete outdated consistent snapshots. snapshot metadata includes # the file extension of roles. - if consistent_snapshots: + if consistent_snapshot: #metadata_name_extension = metadata_name + METADATA_EXTENSION file_hashes = snapshot_metadata['meta'][metadata_name] \ ['hashes'].values() @@ -2742,7 +2742,7 @@ def _get_written_metadata_and_digests(metadata_signable): -def _strip_consistent_snapshots_digest(metadata_filename, consistent_snapshots): +def _strip_consistent_snapshot_digest(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. @@ -2750,10 +2750,10 @@ def _strip_consistent_snapshots_digest(metadata_filename, consistent_snapshots): embeded_digest = '' - # Strip the digest if 'consistent_snapshots' is True. + # Strip the digest if 'consistent_snapshot' is True. # Example: 'targets/unclaimed/13df98ab0.django.json' --> # 'targets/unclaimed/django.json' - if consistent_snapshots: + if consistent_snapshot: dirname, basename = os.path.split(metadata_filename) embeded_digest = basename[:basename.find('.')] basename = basename[basename.find('.'):] @@ -2904,14 +2904,14 @@ def load_repository(repository_directory): filenames = get_metadata_filenames(metadata_directory) # The Root file is always available without a consistent snapshots digest - # attached to the filename. Store the 'consistent_snapshots' value read the + # attached to the filename. Store the 'consistent_snapshot' value read the # loaded Root file so that other metadata files may be located. - # 'consistent_snapshots' value. - consistent_snapshots = False + # 'consistent_snapshot' value. + consistent_snapshot = False # Load the metadata of the top-level roles (i.e., Root, Timestamp, Targets, # and Snapshot). - repository, consistent_snapshots = _load_top_level_metadata(repository, + repository, consistent_snapshot = _load_top_level_metadata(repository, filenames) # Load delegated targets metadata. @@ -2933,11 +2933,11 @@ def load_repository(repository_directory): metadata_name = \ metadata_path[len(metadata_directory):].lstrip(os.path.sep) - # Strip the digest if 'consistent_snapshots' is True. + # Strip the digest if 'consistent_snapshot' is True. # Example: 'targets/unclaimed/13df98ab0.django.json' --> # 'targets/unclaimed/django.json' metadata_name, digest_junk = \ - _strip_consistent_snapshots_digest(metadata_name, consistent_snapshots) + _strip_consistent_snapshot_digest(metadata_name, consistent_snapshot) if metadata_name.endswith(METADATA_EXTENSION): extension_length = len(METADATA_EXTENSION) @@ -2947,7 +2947,7 @@ def load_repository(repository_directory): # Keep a store metadata previously loaded metadata to prevent # re-loading duplicate versions. Duplicate versions may occur with - # consistent_snapshots, 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 @@ -3051,8 +3051,8 @@ def _load_top_level_metadata(repository, top_level_filenames): _check_if_partial_loaded('root', signable, roleinfo) tuf.roledb.update_roleinfo('root', roleinfo) - # Ensure the 'consistent_snapshots' field is extracted. - consistent_snapshots = root_metadata['consistent_snapshots'] + # Ensure the 'consistent_snapshot' field is extracted. + consistent_snapshot = root_metadata['consistent_snapshot'] else: message = 'Cannot load the required root file: '+repr(root_filename) @@ -3080,8 +3080,8 @@ def _load_top_level_metadata(repository, top_level_filenames): pass # Load SNAPSHOT.json. A consistent snapshot of Snapshot must be calculated - # if 'consistent_snapshots' is True. - if consistent_snapshots: + # if 'consistent_snapshot' is True. + if consistent_snapshot: snapshot_hashes = timestamp_metadata['meta'][SNAPSHOT_FILENAME]['hashes'] snapshot_digest = random.choice(snapshot_hashes.values()) dirname, basename = os.path.split(snapshot_filename) @@ -3108,8 +3108,8 @@ def _load_top_level_metadata(repository, top_level_filenames): pass # Load TARGETS.json. A consistent snapshot of Targets must be calculated if - # 'consistent_snapshots' is True. - if consistent_snapshots: + # 'consistent_snapshot' is True. + if consistent_snapshot: targets_hashes = snapshot_metadata['meta'][TARGETS_FILENAME]['hashes'] targets_digest = random.choice(targets_hashes.values()) dirname, basename = os.path.split(targets_filename) @@ -3152,7 +3152,7 @@ def _load_top_level_metadata(repository, top_level_filenames): else: pass - return repository, consistent_snapshots + return repository, consistent_snapshot @@ -3733,7 +3733,7 @@ def get_target_hash(target_filepath): -def generate_root_metadata(version, expiration_date, consistent_snapshots): +def generate_root_metadata(version, expiration_date, consistent_snapshot): """ Create the root metadata. 'tuf.roledb.py' and 'tuf.keydb.py' are read and @@ -3750,7 +3750,7 @@ def generate_root_metadata(version, expiration_date, consistent_snapshots): The expiration date, in UTC, of the metadata file. Conformant to 'tuf.formats.TIME_SCHEMA'. - consistent_snapshots: + consistent_snapshot: tuf.FormatError, if the generated root metadata object could not @@ -3772,7 +3772,7 @@ def generate_root_metadata(version, expiration_date, consistent_snapshots): # Raise 'tuf.FormatError' if any of the arguments are improperly formatted. tuf.formats.METADATAVERSION_SCHEMA.check_match(version) tuf.formats.TIME_SCHEMA.check_match(expiration_date) - tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshots) + tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) # The role and key dictionaries to be saved in the root metadata object. # Conformant to 'ROLEDICT_SCHEMA' and 'KEYDICT_SCHEMA', respectively. @@ -3830,7 +3830,7 @@ def generate_root_metadata(version, expiration_date, consistent_snapshots): # Generate the root metadata object. root_metadata = tuf.formats.RootFile.make_metadata(version, expiration_date, keydict, roledict, - consistent_snapshots) + consistent_snapshot) return root_metadata @@ -3954,7 +3954,7 @@ def generate_targets_metadata(targets_directory, target_files, version, def generate_snapshot_metadata(metadata_directory, version, expiration_date, root_filename, targets_filename, - consistent_snapshots): + consistent_snapshot): """ Create the snapshot metadata. The minimum metadata must exist @@ -3980,7 +3980,7 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date, targets_filename: - consistent_snapshots: + consistent_snapshot: tuf.FormatError, if 'metadata_directory' is improperly formatted. @@ -4041,11 +4041,11 @@ 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_snapshots' is True. + # Strip the digest if 'consistent_snapshot' is True. # Example: 'targets/unclaimed/13df98ab0.django.json' --> # 'targets/unclaimed/django.json' metadata_name, digest_junk = \ - _strip_consistent_snapshots_digest(metadata_name, consistent_snapshots) + _strip_consistent_snapshot_digest(metadata_name, consistent_snapshot) # All delegated roles are added to the snapshot file, including # compressed versions. @@ -4233,7 +4233,7 @@ def sign_metadata(metadata_object, keyids, filename): -def write_metadata_file(metadata, filename, compressions, consistent_snapshots): +def write_metadata_file(metadata, filename, compressions, consistent_snapshot): """ If necessary, write the 'metadata' signable object to 'filename', and the @@ -4278,7 +4278,7 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshots): tuf.formats.SIGNABLE_SCHEMA.check_match(metadata) tuf.formats.PATH_SCHEMA.check_match(filename) tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) - tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshots) + tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) # Verify the directory of 'filename', and convert 'filename' to its absolute # path so that temporary files are moved to their expected destinations. @@ -4293,7 +4293,7 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshots): # if re-saving is required. file_content, new_digests = _get_written_metadata_and_digests(metadata) - if consistent_snapshots: + if consistent_snapshot: for new_digest in new_digests.values(): dirname, basename = os.path.split(filename) digest_and_filename = new_digest + '.' + basename @@ -4366,7 +4366,7 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshots): # Re-savign the same compressed version may cause its digest to unexpectedly # change (gzip includes a timestamp) even though content has not changed. _write_compressed_metadata(file_object, compressed_filename, - consistent_snapshots) + consistent_snapshot) return written_filename @@ -4374,7 +4374,7 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshots): def _write_compressed_metadata(file_object, compressed_filename, - consistent_snapshots): + consistent_snapshot): """ Write compressed versions of metadata, ensuring compressed file that have not changed are not re-written, the digest of the compressed file is properly @@ -4385,7 +4385,7 @@ def _write_compressed_metadata(file_object, compressed_filename, # If a consistent snapshot is unneeded, 'file_object' may be simply moved # 'compressed_filename' if not already written. - if not consistent_snapshots: + if not consistent_snapshot: if not os.path.exists(compressed_filename): file_object.move(compressed_filename) From 7b27fcec645db1c387f51e8e87afbdbda092803c Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Thu, 30 Jan 2014 13:06:33 -0500 Subject: [PATCH 3/3] Continue unit test updates. --- tests/unit/test_formats.py | 1 - tests/unit/test_keydb.py | 15 +-- tests/unit/test_roledb.py | 19 ++-- tests/unit/test_updater.py | 150 ++++++++++++++--------------- tests/unit/test_util_test_tools.py | 12 +-- tuf/repo/signerlib.py | 118 +++++++++++------------ tuf/tests/repository_setup.py | 6 +- tuf/tests/unittest_toolbox.py | 2 +- tuf/tests/util_test_tools.py | 48 ++++----- 9 files changed, 187 insertions(+), 184 deletions(-) diff --git a/tests/unit/test_formats.py b/tests/unit/test_formats.py index 2b9dbda0..387c1a25 100755 --- a/tests/unit/test_formats.py +++ b/tests/unit/test_formats.py @@ -15,7 +15,6 @@ Unit test for 'formats.py' - """ import unittest diff --git a/tests/unit/test_keydb.py b/tests/unit/test_keydb.py index fe57b776..7fd0d293 100755 --- a/tests/unit/test_keydb.py +++ b/tests/unit/test_keydb.py @@ -170,11 +170,13 @@ def test_create_keydb_from_root_metadata(self): roledict = {'Root': {'keyids': [keyid], 'threshold': 1}, 'Targets': {'keyids': [keyid2], 'threshold': 1}} version = 8 - expiration_seconds = 200 + consistent_snapshot = False + expires = '2012-10-16 06:42:12 UTC' root_metadata = tuf.formats.RootFile.make_metadata(version, - expiration_seconds, - keydict, roledict) + expires, + keydict, roledict, + consistent_snapshot) self.assertEqual(None, tuf.keydb.create_keydb_from_root_metadata(root_metadata)) # Ensure 'keyid' and 'keyid2' were added to the keydb database. self.assertEqual(rsakey, tuf.keydb.get_key(keyid)) @@ -206,11 +208,12 @@ def test_create_keydb_from_root_metadata(self): rsakey3['keytype'] = 'bad_keytype' keydict[keyid3] = rsakey3 version = 8 - expiration_seconds = 200 + expires = '2012-10-16 06:42:12 UTC' root_metadata = tuf.formats.RootFile.make_metadata(version, - expiration_seconds, - keydict, roledict) + expires, + keydict, roledict, + consistent_snapshot) 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/unit/test_roledb.py b/tests/unit/test_roledb.py index cb048854..630ccaef 100755 --- a/tests/unit/test_roledb.py +++ b/tests/unit/test_roledb.py @@ -319,11 +319,13 @@ def test_create_roledb_from_root_metadata(self): roledict = {'root': {'keyids': [keyid], 'threshold': 1}, 'targets': {'keyids': [keyid2], 'threshold': 1}} version = 8 - expiration_seconds = 200 + consistent_snapshot = False + expires = '2012-10-16 06:42:12 UTC' root_metadata = tuf.formats.RootFile.make_metadata(version, - expiration_seconds, - keydict, roledict) + expires, + keydict, roledict, + consistent_snapshot) self.assertEqual(None, tuf.roledb.create_roledb_from_root_metadata(root_metadata)) # Ensure 'Root' and 'Targets' were added to the role database. @@ -354,22 +356,23 @@ def test_create_roledb_from_root_metadata(self): 'targets/role1': {'keyids': [keyid2], 'threshold': 1}, 'release': {'keyids': [keyid3], 'threshold': 1}} version = 8 - expiration_seconds = 200 # Add a third key for 'release'. keydict[keyid3] = rsakey3 root_metadata = tuf.formats.RootFile.make_metadata(version, - expiration_seconds, - keydict, roledict) + expires, + keydict, roledict, + consistent_snapshot) 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 # other two roles. del roledict['targets/role1'] root_metadata = tuf.formats.RootFile.make_metadata(version, - expiration_seconds, - keydict, roledict) + expires, + keydict, roledict, + consistent_snapshot) self.assertEqual(None, tuf.roledb.create_roledb_from_root_metadata(root_metadata)) diff --git a/tests/unit/test_updater.py b/tests/unit/test_updater.py index 0b8385ab..cc9178af 100755 --- a/tests/unit/test_updater.py +++ b/tests/unit/test_updater.py @@ -20,7 +20,6 @@ unittest_toolbox module was created to provide additional testing tools for tuf's modules. For more info see unittest_toolbox.py. - Unit tests must follow a specific structure i.e. independent methods should be tested prior to dependent methods. More accurately: least dependent @@ -32,7 +31,6 @@ class guarantees the order of unit tests. So that, 'test_something_A' a number will be placed after 'test' and before methods name like so: 'test_1_check_directory'. The number is a measure of dependence, where 1 is less dependent than 2. - """ import os @@ -51,7 +49,7 @@ class guarantees the order of unit tests. So that, 'test_something_A' import tuf.formats import tuf.keydb import tuf.repo.keystore as keystore -import tuf.repo.signerlib as signerlib +import tuf.repository_tool as repo_tool import tuf.roledb import tuf.tests.repository_setup as setup import tuf.tests.unittest_toolbox as unittest_toolbox @@ -108,7 +106,7 @@ def test__init__exceptions(self): for directory, junk, role_list in os.walk(client_current_dir): for role_filepath in role_list: role_filepath = os.path.join(directory, role_filepath) - if role_filepath.endswith('root.txt'): + if role_filepath.endswith('root.json'): continue os.remove(role_filepath) updater.Updater('Repo_Name', self.mirrors) @@ -134,18 +132,18 @@ def setUpClass(cls): # Server side references. cls.server_repo_dir = cls.repositories['server_repository'] cls.server_meta_dir = os.path.join(cls.server_repo_dir, 'metadata') - cls.root_filepath = os.path.join(cls.server_meta_dir, 'root.txt') - cls.timestamp_filepath = os.path.join(cls.server_meta_dir, 'timestamp.txt') - cls.targets_filepath = os.path.join(cls.server_meta_dir, 'targets.txt') - cls.release_filepath = os.path.join(cls.server_meta_dir, 'release.txt') + cls.root_filepath = os.path.join(cls.server_meta_dir, 'root.json') + cls.timestamp_filepath = os.path.join(cls.server_meta_dir, 'timestamp.json') + cls.targets_filepath = os.path.join(cls.server_meta_dir, 'targets.json') + cls.snapshot_filepath = os.path.join(cls.server_meta_dir, 'snapshot.json') # References to delegated metadata paths and directories. cls.delegated_dir1 = os.path.join(cls.server_meta_dir, 'targets') cls.delegated_filepath1 = os.path.join(cls.delegated_dir1, - 'delegated_role1.txt') + 'delegated_role1.json') cls.delegated_dir2 = os.path.join(cls.delegated_dir1, 'delegated_role1') cls.delegated_filepath2 = os.path.join(cls.delegated_dir2, - 'delegated_role2.txt') + 'delegated_role2.json') cls.targets_dir = os.path.join(cls.server_repo_dir, 'targets') # Client side references. @@ -172,7 +170,7 @@ def setUp(self): # used as an optional argument to 'download_url_to_tempfileobj' patch # function. self.all_role_paths = [self.timestamp_filepath, - self.release_filepath, + self.snapshot_filepath, self.root_filepath, self.targets_filepath, self.delegated_filepath1, @@ -248,7 +246,7 @@ def _remove_filepath(self, filepath): def _add_target_to_targets_dir(self, targets_keyids): """ Adds a file to server's 'targets' directory and rebuilds - targets metadata (targets.txt). + targets metadata (targets.json). """ targets_sub_dir = os.path.join(self.targets_dir, 'targets_sub_dir') @@ -274,7 +272,7 @@ def _add_target_to_targets_dir(self, targets_keyids): def _remove_target_from_targets_dir(self, target_filename, remove_all=True): """ Remove a target 'target_filename' from server's targets directory and - rebuild 'targets', 'release', 'timestamp' metadata files. + rebuild 'targets', 'snapshot', 'timestamp' metadata files. 'target_filename' is relative to targets directory. Example of 'target_filename': 'targets_sub_dir/somefile.txt'. @@ -345,8 +343,8 @@ def _update_top_level_roles(self): # Reference self.Repository._update_metadata_if_changed(). update_if_changed = self.Repository._update_metadata_if_changed - self._mock_download_url_to_tempfileobj(self.release_filepath) - update_if_changed('release', referenced_metadata = 'timestamp') + self._mock_download_url_to_tempfileobj(self.snapshot_filepath) + update_if_changed('snapshot', referenced_metadata = 'timestamp') self._mock_download_url_to_tempfileobj(self.root_filepath) update_if_changed('root') @@ -363,9 +361,9 @@ def _update_top_level_roles(self): def test_1__load_metadata_from_file(self): # Setup - # Get root.txt file path. Extract root metadata, + # Get root.json file path. Extract root metadata, # it will be compared with content of loaded root metadata. - root_filepath = os.path.join(self.client_current_dir, 'root.txt') + root_filepath = os.path.join(self.client_current_dir, 'root.json') root_meta = tuf.util.load_json_file(root_filepath) @@ -414,16 +412,16 @@ def test_1__update_fileinfo(self): # Load file info for top level roles. This populates the fileinfo # dictionary. for role in self.role_list: - self.Repository._update_fileinfo(role+'.txt') + self.Repository._update_fileinfo(role+'.json') # Verify that fileinfo has been populated and contains appropriate data. self.assertTrue(self.Repository.fileinfo) for role in self.role_list: - role_filepath = os.path.join(self.client_current_dir, role+'.txt') + role_filepath = os.path.join(self.client_current_dir, role+'.json') role_info = tuf.util.get_file_details(role_filepath) role_info_dict = {'length':role_info[0], 'hashes':role_info[1]} - self.assertTrue(role+'.txt' in self.Repository.fileinfo.keys()) - self.assertEqual(self.Repository.fileinfo[role+'.txt'], role_info_dict) + self.assertTrue(role+'.json' in self.Repository.fileinfo.keys()) + self.assertEqual(self.Repository.fileinfo[role+'.json'], role_info_dict) @@ -477,7 +475,7 @@ def test_2__ensure_all_targets_allowed(self): targets_meta_dir = os.path.join(self.server_meta_dir, 'targets') role1_meta_dir = os.path.join(targets_meta_dir, 'delegated_role1') - role1_path = os.path.join(targets_meta_dir, 'delegated_role1.txt') + role1_path = os.path.join(targets_meta_dir, 'delegated_role1.json') role1_metadata_signable = tuf.util.load_json_file(role1_path) role1_metadata = role1_metadata_signable['signed'] @@ -495,7 +493,7 @@ def test_2__ensure_all_targets_allowed(self): # delegated role's metadata are not indicated in the metadata of the # delegated role's parent, we need to modify delegated role's 'targets' # field. - target = self.random_string()+'.txt' + target = self.random_string()+'.json' deleg_target_path = os.path.join('delegated_level', target) role1_metadata['targets'][deleg_target_path] = self.random_string() @@ -511,26 +509,26 @@ def test_2__ensure_all_targets_allowed(self): def test_2__fileinfo_has_changed(self): # Verify that the method returns 'False' if file info was not changed. for role in self.role_list: - role_filepath = os.path.join(self.client_current_dir, role+'.txt') + role_filepath = os.path.join(self.client_current_dir, role+'.json') role_info = tuf.util.get_file_details(role_filepath) role_info_dict = {'length':role_info[0], 'hashes':role_info[1]} - self.assertFalse(self.Repository._fileinfo_has_changed(role+'.txt', + self.assertFalse(self.Repository._fileinfo_has_changed(role+'.json', role_info_dict)) # Verify that the method returns 'True' if length or hashes were changed. for role in self.role_list: - role_filepath = os.path.join(self.client_current_dir, role+'.txt') + role_filepath = os.path.join(self.client_current_dir, role+'.json') role_info = tuf.util.get_file_details(role_filepath) role_info_dict = {'length':8, 'hashes':role_info[1]} - self.assertTrue(self.Repository._fileinfo_has_changed(role+'.txt', + self.assertTrue(self.Repository._fileinfo_has_changed(role+'.json', role_info_dict)) for role in self.role_list: - role_filepath = os.path.join(self.client_current_dir, role+'.txt') + role_filepath = os.path.join(self.client_current_dir, role+'.json') role_info = tuf.util.get_file_details(role_filepath) role_info_dict = {'length':role_info[0], 'hashes':{'sha256':self.random_string()}} - self.assertTrue(self.Repository._fileinfo_has_changed(role+'.txt', + self.assertTrue(self.Repository._fileinfo_has_changed(role+'.json', role_info_dict)) @@ -540,13 +538,13 @@ def test_2__move_current_to_previous(self): # The test will consist of removing a metadata file from client's # {client_repository}/metadata/previous directory, executing the method # and then verifying that the 'previous' directory contains - # the release file. - release_meta_path = os.path.join(self.client_previous_dir, 'release.txt') - os.remove(release_meta_path) - self.assertFalse(os.path.exists(release_meta_path)) - self.Repository._move_current_to_previous('release') - self.assertTrue(os.path.exists(release_meta_path)) - shutil.copy(release_meta_path, self.client_current_dir) + # the snapshot file. + snapshot_meta_path = os.path.join(self.client_previous_dir, 'snapshot.json') + os.remove(snapshot_meta_path) + self.assertFalse(os.path.exists(snapshot_meta_path)) + self.Repository._move_current_to_previous('snapshot') + self.assertTrue(os.path.exists(snapshot_meta_path)) + shutil.copy(snapshot_meta_path, self.client_current_dir) @@ -561,7 +559,7 @@ def test_2__delete_metadata(self): self.Repository._delete_metadata('timestamp') self.assertFalse('timestamp' in self.Repository.metadata['current']) timestamp_meta_path = os.path.join(self.client_previous_dir, - 'timestamp.txt') + 'timestamp.json') shutil.copy(timestamp_meta_path, self.client_current_dir) @@ -614,7 +612,7 @@ def test_3__update_metadata(self): # Test: Invalid file downloaded. # Patch 'download.download_url_to_tempfileobj' function. - self._mock_download_url_to_tempfileobj(self.release_filepath) + self._mock_download_url_to_tempfileobj(self.snapshot_filepath) # TODO: Is this the original intent of this test? self.assertRaises(TypeError, _update_metadata, 'targets', None) @@ -624,7 +622,7 @@ def test_3__update_metadata(self): # Patch 'download.download_url_to_tempfileobj' function. self._mock_download_url_to_tempfileobj(self.targets_filepath) uncompressed_fileinfo = \ - signerlib.get_metadata_file_info(self.targets_filepath) + repo_tool.get_metadata_file_info(self.targets_filepath) _update_metadata('targets', uncompressed_fileinfo) list_of_targets = self.Repository.metadata['current']['targets']['targets'] @@ -637,12 +635,12 @@ def test_3__update_metadata(self): # Add a file to targets directory and rebuild targets metadata. added_target_2 = self._add_target_to_targets_dir(targets_keyids) uncompressed_fileinfo = \ - signerlib.get_metadata_file_info(self.targets_filepath) + repo_tool.get_metadata_file_info(self.targets_filepath) # To test compressed file handling, compress targets metadata file. targets_filepath_compressed = self._compress_file(self.targets_filepath) compressed_fileinfo = \ - signerlib.get_metadata_file_info(targets_filepath_compressed) + repo_tool.get_metadata_file_info(targets_filepath_compressed) # Re-patch 'download.download_url_to_tempfileobj' function. self._mock_download_url_to_tempfileobj(targets_filepath_compressed) @@ -663,7 +661,7 @@ def test_3__update_metadata(self): # Restoring server's repository to the initial state. os.remove(targets_filepath_compressed) - os.remove(os.path.join(self.client_current_dir,'targets.txt')) + os.remove(os.path.join(self.client_current_dir,'targets.json')) self._remove_target_from_targets_dir(added_target_1) @@ -688,24 +686,24 @@ def test_3__update_metadata_if_changed(self): update_if_changed = self.Repository._update_metadata_if_changed - # Test: normal case. Update 'release' metadata. + # Test: normal case. Update 'snapshot' metadata. # Patch download_file. self._mock_download_url_to_tempfileobj(self.timestamp_filepath) - # Update timestamp metadata, it will indicate change in release metadata. + # Update timestamp metadata, it will indicate change in snapshot metadata. self.Repository._update_metadata('timestamp', DEFAULT_TIMESTAMP_FILEINFO) - # Save current release metadata before updating. It will be used to + # Save current snapshot metadata before updating. It will be used to # verify the update. - old_release_meta = self.Repository.metadata['current']['release'] - self._mock_download_url_to_tempfileobj(self.release_filepath) + old_snapshot_meta = self.Repository.metadata['current']['snapshot'] + self._mock_download_url_to_tempfileobj(self.snapshot_filepath) - # Update release metadata, it will indicate change in targets metadata. - update_if_changed(metadata_role='release', referenced_metadata='timestamp') - current_release_meta = self.Repository.metadata['current']['release'] - previous_release_meta = self.Repository.metadata['previous']['release'] - self.assertEqual(old_release_meta, previous_release_meta) - self.assertNotEqual(old_release_meta, current_release_meta) + # Update snapshot metadata, it will indicate change in targets metadata. + update_if_changed(metadata_role='snapshot', referenced_metadata='timestamp') + current_snapshot_meta = self.Repository.metadata['current']['snapshot'] + previous_snapshot_meta = self.Repository.metadata['previous']['snapshot'] + self.assertEqual(old_snapshot_meta, previous_snapshot_meta) + self.assertNotEqual(old_snapshot_meta, current_snapshot_meta) # Test: normal case. Update 'targets' metadata. @@ -719,37 +717,37 @@ def test_3__update_metadata_if_changed(self): self.fail('\nFailed to update targets metadata.') - # Test: normal case. Update compressed release file. - release_filepath_compressed = self._compress_file(self.release_filepath) + # Test: normal case. Update compressed snapshot file. + snapshot_filepath_compressed = self._compress_file(self.snapshot_filepath) # Since client's '.../metadata/current' will need to have separate # gzipped metadata file in order to test compressed file handling, # we need to copy it there. - shutil.copy(release_filepath_compressed, self.client_current_dir) + shutil.copy(snapshot_filepath_compressed, self.client_current_dir) # Add a target file and rebuild metadata files at the server side. added_target_2 = self._add_target_to_targets_dir(targets_keyids) - # Since release file was updated, update compressed release file. - release_filepath_compressed = self._compress_file(self.release_filepath) + # Since snapshot file was updated, update compressed snapshot file. + snapshot_filepath_compressed = self._compress_file(self.snapshot_filepath) # Patch download_file. self._mock_download_url_to_tempfileobj(self.timestamp_filepath) - # Update timestamp metadata, it will indicate change in release metadata. + # Update timestamp metadata, it will indicate change in snapshot metadata. self.Repository._update_metadata('timestamp', DEFAULT_TIMESTAMP_FILEINFO) - # Save current release metadata before updating. It will be used to + # Save current snapshot metadata before updating. It will be used to # verify the update. - old_release_meta = self.Repository.metadata['current']['release'] - self._mock_download_url_to_tempfileobj(self.release_filepath) + old_snapshot_meta = self.Repository.metadata['current']['snapshot'] + self._mock_download_url_to_tempfileobj(self.snapshot_filepath) - # Update release metadata, and verify the change. - update_if_changed(metadata_role='release', referenced_metadata='timestamp') - current_release_meta = self.Repository.metadata['current']['release'] - previous_release_meta = self.Repository.metadata['previous']['release'] - self.assertEqual(old_release_meta, previous_release_meta) - self.assertNotEqual(old_release_meta, current_release_meta) + # Update snapshot metadata, and verify the change. + update_if_changed(metadata_role='snapshot', referenced_metadata='timestamp') + current_snapshot_meta = self.Repository.metadata['current']['snapshot'] + previous_snapshot_meta = self.Repository.metadata['previous']['snapshot'] + self.assertEqual(old_snapshot_meta, previous_snapshot_meta) + self.assertNotEqual(old_snapshot_meta, current_snapshot_meta) # Test: Invalid targets metadata file downloaded. @@ -764,8 +762,8 @@ def test_3__update_metadata_if_changed(self): assert isinstance(mirror_error, tuf.DownloadLengthMismatchError) # Restoring repositories to the initial state. - os.remove(release_filepath_compressed) - os.remove(os.path.join(self.client_current_dir, 'release.txt.gz')) + os.remove(snapshot_filepath_compressed) + os.remove(os.path.join(self.client_current_dir, 'snapshot.json.gz')) self._remove_target_from_targets_dir(added_target_1) @@ -786,7 +784,7 @@ def test_3__targets_of_role(self): for target in range(len(targets_list)): targets_filepaths.append(targets_list[target]['filepath']) for dir_target in targets_dir_content: - if dir_target.endswith('.txt'): + if dir_target.endswith('.json'): self.assertTrue(dir_target in targets_filepaths) @@ -843,9 +841,9 @@ def test_4__refresh_targets_metadata(self): # Delegated roles paths. role1_dir = os.path.join(self.server_meta_dir, 'targets') - role1_filepath = os.path.join(role1_dir, 'delegated_role1.txt') + role1_filepath = os.path.join(role1_dir, 'delegated_role1.json') role2_dir = os.path.join(role1_dir, 'delegated_role1') - role2_filepath = os.path.join(role2_dir, 'delegated_role2.txt') + role2_filepath = os.path.join(role2_dir, 'delegated_role2.json') # Create a file in the delegated targets directory. deleg_target_filepath2 = self._add_file_to_directory(targets_deleg_dir2) @@ -914,7 +912,7 @@ def test_5_all_targets(self): # Verify that there is a correct number of records in 'all_targets' list. # On the repository there are 4 target files, 2 of which are delegated. # The targets role lists all targets, for a total of 4. The two delegated - # roles each list 1 of the already listed targets in 'targets.txt', for a + # roles each list 1 of the already listed targets in 'targets.json', for a # total of 2 (the delegated targets are listed twice). The total number of # targets in 'all_targets' should then be 6. self.assertTrue(len(all_targets) is 6) @@ -937,7 +935,7 @@ def test_5_targets_of_role(self): for target in range(len(targets_list)): targets_filepaths.append(targets_list[target]['filepath']) for dir_target in targets_dir_content: - if dir_target.endswith('.txt'): + if dir_target.endswith('.json'): self.assertTrue(dir_target in targets_filepaths) @@ -960,7 +958,7 @@ def test_6_target(self): # Test: normal case. for _target in targets_dir_content: - if _target.endswith('.txt'): + if _target.endswith('.json'): target_info = target(_target) # Verify that 'target_info' corresponds to 'TARGETFILE_SCHEMA'. self.assertTrue(tuf.formats.TARGETFILE_SCHEMA.matches(target_info)) diff --git a/tests/unit/test_util_test_tools.py b/tests/unit/test_util_test_tools.py index 0acf5131..bfd86d7f 100755 --- a/tests/unit/test_util_test_tools.py +++ b/tests/unit/test_util_test_tools.py @@ -93,14 +93,14 @@ def test_correct_directory_structure(self): metadata_dir = os.path.join(tuf_repo, 'metadata') current_dir = os.path.join(tuf_client, 'metadata', 'current') - # Verify '{root_repo}/tuf_repo/metadata/role.txt' paths exists. - for role in ['root', 'targets', 'release', 'timestamp']: + # Verify '{root_repo}/tuf_repo/metadata/role.json' paths exists. + for role in ['root', 'targets', 'snapshot', 'timestamp']: # Repository side. - role_file = os.path.join(metadata_dir, role+'.txt') + role_file = os.path.join(metadata_dir, role+'.json') self.assertTrue(os.path.isfile(role_file)) # Client side. - role_file = os.path.join(current_dir, role+'.txt') + role_file = os.path.join(current_dir, role+'.json') self.assertTrue(os.path.isfile(role_file)) # Verify '{root_repo}/tuf_repo/keystore/keyid.key' exists. @@ -126,8 +126,8 @@ def test_methods(self): Note: here file at the 'filepath' and the 'target' file at tuf-targets directory are identical files. - Ex: filepath = '{root_repo}/reg_repo/file.txt' - target = '{root_repo}/tuf_repo/targets/file.txt' + Ex: filepath = '{root_repo}/reg_repo/file.json' + target = '{root_repo}/tuf_repo/targets/file.json' """ reg_repo = os.path.join(self.root_repo, 'reg_repo') diff --git a/tuf/repo/signerlib.py b/tuf/repo/signerlib.py index c2a71f63..1947320e 100755 --- a/tuf/repo/signerlib.py +++ b/tuf/repo/signerlib.py @@ -47,7 +47,7 @@ # The metadata filenames for the top-level roles. ROOT_FILENAME = 'root.txt' TARGETS_FILENAME = 'targets.txt' -RELEASE_FILENAME = 'release.txt' +SNAPSHOT_FILENAME = 'snapshot.txt' TIMESTAMP_FILENAME = 'timestamp.txt' # The filename for the repository configuration file. @@ -183,7 +183,7 @@ def get_metadata_filenames(metadata_directory=None): filenames = {'root': 'metadata/root.txt', 'targets': 'metadata/targets.txt', - 'release': 'metadata/release.txt', + 'snapshot': 'metadata/snapshot.txt', 'timestamp': 'metadata/timestamp.txt'} If the metadata directory is not set by the caller, the current @@ -201,7 +201,7 @@ def get_metadata_filenames(metadata_directory=None): A dictionary containing the expected filenames of the top-level - metadata files, such as 'root.txt' and 'release.txt'. + metadata files, such as 'root.txt' and 'snapshot.txt'. """ if metadata_directory is None: @@ -214,7 +214,7 @@ def get_metadata_filenames(metadata_directory=None): filenames = {} filenames['root'] = os.path.join(metadata_directory, ROOT_FILENAME) filenames['targets'] = os.path.join(metadata_directory, TARGETS_FILENAME) - filenames['release'] = os.path.join(metadata_directory, RELEASE_FILENAME) + filenames['snapshot'] = os.path.join(metadata_directory, SNAPSHOT_FILENAME) filenames['timestamp'] = os.path.join(metadata_directory, TIMESTAMP_FILENAME) return filenames @@ -268,7 +268,7 @@ def generate_root_metadata(config_filepath, version): # Extract the role, threshold, and keyid information from the config. # The necessary role metadata is generated from this information. - for rolename in ['root', 'targets', 'release', 'timestamp']: + for rolename in ['root', 'targets', 'snapshot', 'timestamp']: # If a top-level role is missing from the config, raise an exception. if rolename not in config: raise tuf.Error('No '+rolename+' section found in config file.') @@ -312,7 +312,7 @@ def generate_root_metadata(config_filepath, version): # Generate the root metadata object. expiration_date = tuf.formats.format_time(time.time()+expiration_seconds) root_metadata = tuf.formats.RootFile.make_metadata(version, expiration_date, - keydict, roledict) + keydict, roledict, False) # Note: make_signable() returns the following dictionary: # {'signed' : role_metadata, 'signatures' : []} @@ -399,13 +399,13 @@ def generate_targets_metadata(repository_directory, target_files, version, -def generate_release_metadata(metadata_directory, version, expiration_date): +def generate_snapshot_metadata(metadata_directory, version, expiration_date): """ - Create the release metadata. The minimum metadata must exist + Create the snapshot metadata. The minimum metadata must exist (i.e., 'root.txt' and 'targets.txt'). This will also look through the 'targets/' directory in 'metadata_directory' and the resulting - release file will list all the delegated roles. + snapshot file will list all the delegated roles. metadata_directory: @@ -423,14 +423,14 @@ def generate_release_metadata(metadata_directory, version, expiration_date): tuf.FormatError, if 'metadata_directory' is improperly formatted. - tuf.Error, if an error occurred trying to generate the release metadata + tuf.Error, if an error occurred trying to generate the snapshot metadata object. The 'root.txt' and 'targets.txt' files are read. - The release 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + The snapshot 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. """ # Does 'metadata_directory' have the correct format? @@ -453,7 +453,7 @@ def generate_release_metadata(metadata_directory, version, expiration_date): # Walk the 'targets/' directory and generate the file info for all # the files listed there. This information is stored in the 'meta' - # field of the release metadata object. + # 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, files in os.walk(targets_metadata): @@ -463,26 +463,26 @@ def generate_release_metadata(metadata_directory, version, expiration_date): metadata_name = metadata_path[len(metadata_directory):].lstrip(os.path.sep) filedict[metadata_name] = get_metadata_file_info(metadata_path) - # Generate the release metadata object. - release_metadata = tuf.formats.ReleaseFile.make_metadata(version, - expiration_date, - filedict) + # Generate the snapshot metadata object. + snapshot_metadata = tuf.formats.SnapshotFile.make_metadata(version, + expiration_date, + filedict) - return tuf.formats.make_signable(release_metadata) + return tuf.formats.make_signable(snapshot_metadata) -def generate_timestamp_metadata(release_filename, version, +def generate_timestamp_metadata(snapshot_filename, version, expiration_date, compressions=()): """ - Generate the timestamp metadata object. The 'release.txt' file must exist. + Generate the timestamp metadata object. The 'snapshot.txt' file must exist. - release_filename: - The required filename of the release metadata file. + snapshot_filename: + The required filename of the snapshot metadata file. version: The metadata version number. Clients use the version number to @@ -493,7 +493,7 @@ def generate_timestamp_metadata(release_filename, version, Conformant to 'tuf.formats.TIME_SCHEMA'. compressions: - Compression extensions (e.g., 'gz'). If 'release.txt' is also saved in + Compression extensions (e.g., 'gz'). If 'snapshot.txt' 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. @@ -511,25 +511,25 @@ def generate_timestamp_metadata(release_filename, version, # Do the arguments have the correct format? # Raise 'tuf.FormatError' if there is mismatch. - tuf.formats.PATH_SCHEMA.check_match(release_filename) + tuf.formats.PATH_SCHEMA.check_match(snapshot_filename) tuf.formats.METADATAVERSION_SCHEMA.check_match(version) tuf.formats.TIME_SCHEMA.check_match(expiration_date) - # Retrieve the file info for the release metadata file. + # Retrieve the file info for the snapshot metadata file. # This file information contains hashes, file length, custom data, etc. fileinfo = {} - fileinfo['release.txt'] = get_metadata_file_info(release_filename) + fileinfo['snapshot.txt'] = get_metadata_file_info(snapshot_filename) # Save the file info of the compressed versions of 'timestamp.txt'. for file_extension in compressions: - compressed_filename = release_filename + '.' + file_extension + compressed_filename = snapshot_filename + '.' + file_extension try: compressed_fileinfo = get_metadata_file_info(compressed_filename) except: logger.warn('Could not get fileinfo about '+str(compressed_filename)) else: logger.info('Including fileinfo about '+str(compressed_filename)) - fileinfo['release.txt.' + file_extension] = compressed_fileinfo + fileinfo['snapshot.txt.' + file_extension] = compressed_fileinfo # Generate the timestamp metadata object. timestamp_metadata = tuf.formats.TimestampFile.make_metadata(version, @@ -893,7 +893,7 @@ def get_target_keyids(metadata_directory): # Walk the 'targets/' directory and generate the file info for all # the targets. This information is stored in the 'meta' field of - # the release metadata object. The keyids for the optional + # the snapshot metadata object. The keyids for the optional # delegated roles will now be extracted. targets_metadata = os.path.join(metadata_directory, 'targets') if os.path.exists(targets_metadata) and os.path.isdir(targets_metadata): @@ -969,7 +969,7 @@ def build_config_file(config_file_directory, timeout, role_info): # Verify that only the top-level roles are presented. for role in role_info.keys(): - if role not in ['root', 'targets', 'release', 'timestamp']: + if role not in ['root', 'targets', 'snapshot', 'timestamp']: msg = ('\nCannot build configuration file: role '+repr(role)+ ' is not a top-level role.') raise tuf.Error(msg) @@ -1158,19 +1158,19 @@ def build_targets_file(target_paths, targets_keyids, metadata_directory, -def build_release_file(release_keyids, metadata_directory, +def build_snapshot_file(snapshot_keyids, metadata_directory, version, expiration_date, compress=False): """ - Build the release metadata file using the signing keys in 'release_keyids'. + Build the snapshot metadata file using the signing keys in 'snapshot_keyids'. The generated metadata file is saved in 'metadata_directory'. - release_keyids: - The list of keyids to be used as the signing keys for the release file. + snapshot_keyids: + The list of keyids to be used as the signing keys for the snapshot file. metadata_directory: - The directory (absolute path) to save the release metadata file. + The directory (absolute path) to save the snapshot metadata file. version: The metadata version number. Clients use the version number to @@ -1181,48 +1181,48 @@ def build_release_file(release_keyids, metadata_directory, Conformant to 'tuf.formats.TIME_SCHEMA'. compress: - Should we *include* a compressed version of the release file? By default, + Should we *include* a compressed version of the snapshot file? By default, the answer is no. tuf.FormatError, if any of the arguments are improperly formatted. - tuf.Error, if there was an error while building the release file. + tuf.Error, if there was an error while building the snapshot file. - The release metadata file is written to a file. + The snapshot metadata file is written to a file. - The path for the written release metadata file. + The path for the written snapshot metadata file. """ # Do the arguments have the correct format? # Raise 'tuf.FormatError' if there is a mismatch. - tuf.formats.KEYIDS_SCHEMA.check_match(release_keyids) + tuf.formats.KEYIDS_SCHEMA.check_match(snapshot_keyids) tuf.formats.PATH_SCHEMA.check_match(metadata_directory) tuf.formats.METADATAVERSION_SCHEMA.check_match(version) tuf.formats.TIME_SCHEMA.check_match(expiration_date) metadata_directory = check_directory(metadata_directory) - # Generate the file path of the release metadata. - release_filepath = os.path.join(metadata_directory, RELEASE_FILENAME) + # Generate the file path of the snapshot metadata. + snapshot_filepath = os.path.join(metadata_directory, SNAPSHOT_FILENAME) - # Generate and sign the release metadata. - release_metadata = generate_release_metadata(metadata_directory, + # Generate and sign the snapshot metadata. + snapshot_metadata = generate_snapshot_metadata(metadata_directory, version, expiration_date) - signable = sign_metadata(release_metadata, release_keyids, release_filepath) + signable = sign_metadata(snapshot_metadata, snapshot_keyids, snapshot_filepath) - # Should we also include a compressed version of release.txt? + # Should we also include a compressed version of snapshot.txt? if compress: - # If so, write a gzip version of release.txt. + # If so, write a gzip version of snapshot.txt. compressed_written_filepath = \ - write_metadata_file(signable, release_filepath, compression='gz') + write_metadata_file(signable, snapshot_filepath, compression='gz') logger.info('Wrote '+str(compressed_written_filepath)) else: - logger.debug('No compressed version of release metadata will be included.') + logger.debug('No compressed version of snapshot metadata will be included.') - written_filepath = write_metadata_file(signable, release_filepath) + written_filepath = write_metadata_file(signable, snapshot_filepath) logger.info('Wrote '+str(written_filepath)) return written_filepath @@ -1233,7 +1233,7 @@ def build_release_file(release_keyids, metadata_directory, def build_timestamp_file(timestamp_keyids, metadata_directory, version, expiration_date, - include_compressed_release=True): + include_compressed_snapshot=True): """ Build the timestamp metadata file using the signing keys in 'timestamp_keyids'. @@ -1254,8 +1254,8 @@ def build_timestamp_file(timestamp_keyids, metadata_directory, The expiration date, in UTC, of the metadata file. Conformant to 'tuf.formats.TIME_SCHEMA'. - include_compressed_release: - Should the timestamp role *include* compression versions of the release + include_compressed_snapshot: + Should the timestamp role *include* compression versions of the snapshot metadata, if any? We do this by default. @@ -1279,23 +1279,23 @@ def build_timestamp_file(timestamp_keyids, metadata_directory, metadata_directory = check_directory(metadata_directory) - # Generate the file path of the release and timestamp metadata. - release_filepath = os.path.join(metadata_directory, RELEASE_FILENAME) + # Generate the file path of the snapshot and timestamp metadata. + snapshot_filepath = os.path.join(metadata_directory, SNAPSHOT_FILENAME) timestamp_filepath = os.path.join(metadata_directory, TIMESTAMP_FILENAME) - # Should we include compressed versions of release in timestamp? + # Should we include compressed versions of snapshot in timestamp? compressions = () - if include_compressed_release: + if include_compressed_snapshot: # Presently, we include only gzip versions by default. compressions = ('gz',) - logger.info('Including '+str(compressions)+' versions of release in '\ + logger.info('Including '+str(compressions)+' versions of snapshot in '\ 'timestamp.') else: - logger.warn('No compressed versions of release will be included in '\ + logger.warn('No compressed versions of snapshot will be included in '\ 'timestamp.') # Generate and sign the timestamp metadata. - timestamp_metadata = generate_timestamp_metadata(release_filepath, + timestamp_metadata = generate_timestamp_metadata(snapshot_filepath, version, expiration_date, compressions=compressions) diff --git a/tuf/tests/repository_setup.py b/tuf/tests/repository_setup.py index 778621d8..60161438 100644 --- a/tuf/tests/repository_setup.py +++ b/tuf/tests/repository_setup.py @@ -25,11 +25,11 @@ import tuf.formats import tuf.repo.keystore as keystore import tuf.repo.signerlib as signerlib +import tuf.repository_tool as repo_tool import tuf.repo.signercli as signercli import tuf.tests.unittest_toolbox as unittest_toolbox - # Role:keyids dictionary. role_keyids = {} @@ -215,8 +215,8 @@ def _mock_get_keyids(junk): keystore._keystore = unittest_toolbox.Modified_TestCase.rsa_keystore keystore._derived_keys = unittest_toolbox.Modified_TestCase.rsa_passwords - # Build release file. - signerlib.build_release_file(role_keyids['release'], server_metadata_dir, + # Build snapshot file. + signerlib.build_snapshot_file(role_keyids['snapshot'], server_metadata_dir, version, expiration_date+' UTC') # Build timestamp file. diff --git a/tuf/tests/unittest_toolbox.py b/tuf/tests/unittest_toolbox.py index 785fffcb..d425fe76 100644 --- a/tuf/tests/unittest_toolbox.py +++ b/tuf/tests/unittest_toolbox.py @@ -102,7 +102,7 @@ def setUp(): """ # List of all top level roles. - role_list = ['root', 'targets', 'release', 'timestamp'] + role_list = ['root', 'targets', 'snapshot', 'timestamp'] # List of delegated roles. delegated_role_list = ['targets/delegated_role1', diff --git a/tuf/tests/util_test_tools.py b/tuf/tests/util_test_tools.py index 43ea80fb..796663a2 100644 --- a/tuf/tests/util_test_tools.py +++ b/tuf/tests/util_test_tools.py @@ -53,7 +53,7 @@ | | | keystore metadata targets | | | - key1.key ... role.txt ... file(1) ... + key1.key ... role.json ... file(1) ... '{root_repo}/tuf_repo/': developer's tuf-repository directory containing following subdirectories: @@ -72,7 +72,7 @@ | | current previous | | - role.txt ... role.txt ... + role.json ... role.json ... '{root_repo}/tuf_client/': client directory containing tuf metadata. '{root_repo}/tuf_client/metadata/current': directory where client stores @@ -113,14 +113,14 @@ and keys. tuf_refresh_repo(): - Refreshes metadata files at the 'tuf_repo' directory i.e. role.txt's at + Refreshes metadata files at the 'tuf_repo' directory i.e. role.json's at '{root_repo}/tuf_repo/metadata/'. Following roles are refreshed: - targets, release and timestamp. Also, the whole 'reg_repo' directory is + targets, snapshot and timestamp. Also, the whole 'reg_repo' directory is copied to targets directory i.e. '{root_repo}/tuf_repo/targets/'. -Note: metadata files are root.txt, targets.txt, release.txt and -timestamp.txt (denoted as 'role.txt in the diagrams'). There could be -more metadata files such us mirrors.txt. The metadata files are signed +Note: metadata files are root.json, targets.json, snapshot.json and +timestamp.json (denoted as 'role.json in the diagrams'). There could be +more metadata files such us mirrors.json. The metadata files are signed by their corresponding roles i.e. root, targets etc. More documentation is provided in the comment and doc blocks. @@ -174,8 +174,8 @@ def init_repo(using_tuf=False, port=None): server_proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - # Tailor url for the repository. In order to download a 'file.txt' - # from 'reg_repo' do: url+'reg_repo/file.txt' + # Tailor url for the repository. In order to download a 'file.json' + # from 'reg_repo' do: url+'reg_repo/file.json' relpath = os.path.basename(root_repo) url = 'http://localhost:'+str(port)+'/'+relpath+'/' @@ -326,7 +326,7 @@ def init_tuf(root_repo): # In our case 'role_info[keyids]' will only have on entry since only one # is being used. role_info = {} - role_list = ['root', 'targets', 'release', 'timestamp'] + role_list = ['root', 'targets', 'snapshot', 'timestamp'] for role in role_list: role_info[role] = info @@ -336,17 +336,17 @@ def init_tuf(root_repo): # Build the configuration file. conf_path = signerlib.build_config_file(metadata_dir, 365, role_info) - # Generate the 'root.txt' metadata file. + # Generate the 'root.json' metadata file. signerlib.build_root_file(conf_path, keyids, metadata_dir, version) - # Generate the 'targets.txt' metadata file. + # Generate the 'targets.json' metadata file. signerlib.build_targets_file([targets_dir], keyids, metadata_dir, version, expiration) - # Generate the 'release.txt' metadata file. - signerlib.build_release_file(keyids, metadata_dir, version, expiration) + # Generate the 'snapshot.json' metadata file. + signerlib.build_snapshot_file(keyids, metadata_dir, version, expiration) - # Generate the 'timestamp.txt' metadata file. + # Generate the 'timestamp.json' metadata file. signerlib.build_timestamp_file(keyids, metadata_dir, version, expiration) # Move the metadata to the client's 'current' and 'previous' directories. @@ -435,14 +435,14 @@ def tuf_refresh_repo(root_repo, keyids): shutil.copytree(reg_repo, targets_dir) version = version+1 - # Regenerate the 'targets.txt' metadata file. + # Regenerate the 'targets.json' metadata file. signerlib.build_targets_file([targets_dir], keyids, metadata_dir, version, expiration) - # Regenerate the 'release.txt' metadata file. - signerlib.build_release_file(keyids, metadata_dir, version, expiration) + # Regenerate the 'snapshot.json' metadata file. + signerlib.build_snapshot_file(keyids, metadata_dir, version, expiration) - # Regenerate the 'timestamp.txt' metadata file. + # Regenerate the 'timestamp.json' metadata file. signerlib.build_timestamp_file(keyids, metadata_dir, version, expiration) @@ -450,9 +450,9 @@ def tuf_refresh_repo(root_repo, keyids): -def tuf_refresh_release_timestamp(metadata_dir, keyids): - # Regenerate the 'release.txt' metadata file. - signerlib.build_release_file(keyids, metadata_dir) +def tuf_refresh_snapshot_timestamp(metadata_dir, keyids): + # Regenerate the 'snapshot.json' metadata file. + signerlib.build_snapshot_file(keyids, metadata_dir) def tuf_refresh_and_download(): """ @@ -543,8 +543,8 @@ def make_targets_meta(root_repo): _make_role_metadata_wrapper(root_repo, signercli.make_targets_metadata) -def make_release_meta(root_repo): - _make_role_metadata_wrapper(root_repo, signercli.make_release_metadata) +def make_snapshot_meta(root_repo): + _make_role_metadata_wrapper(root_repo, signercli.make_snapshot_metadata) def make_timestamp_meta(root_repo):