From 8a805bdc3fa3f0361bfcd0e70bce60db9198b299 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Fri, 20 Dec 2013 12:47:27 -0500 Subject: [PATCH] Fix pycrypto_keys.py header block, libtuf.py doc update, and minor formats.py schemas edit --- tuf/formats.py | 18 +-- tuf/libtuf.py | 295 ++++++++++++++++++++++++++++++++++--------- tuf/pycrypto_keys.py | 4 +- 3 files changed, 245 insertions(+), 72 deletions(-) diff --git a/tuf/formats.py b/tuf/formats.py index 69695a9e..340d199a 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -261,11 +261,11 @@ SIGNATURESTATUS_SCHEMA = SCHEMA.Object( object_name = 'SIGNATURESTATUS_SCHEMA', threshold = SCHEMA.Integer(), - good_sigs = SCHEMA.ListOf(KEYID_SCHEMA), - bad_sigs = SCHEMA.ListOf(KEYID_SCHEMA), - unknown_sigs = SCHEMA.ListOf(KEYID_SCHEMA), - untrusted_sigs = SCHEMA.ListOf(KEYID_SCHEMA), - unknown_method_sigs = SCHEMA.ListOf(KEYID_SCHEMA)) + good_sigs = KEYIDS_SCHEMA, + bad_sigs = KEYIDS_SCHEMA, + unknown_sigs = KEYIDS_SCHEMA, + untrusted_sigs = KEYIDS_SCHEMA, + unknown_method_sigs = KEYIDS_SCHEMA) # A signable object. Holds the signing role and its associated signatures. SIGNABLE_SCHEMA = SCHEMA.Object( @@ -332,7 +332,7 @@ ROLE_SCHEMA = SCHEMA.Object( object_name = 'ROLE_SCHEMA', name = SCHEMA.Optional(ROLENAME_SCHEMA), - keyids = SCHEMA.ListOf(KEYID_SCHEMA), + keyids = KEYIDS_SCHEMA, threshold = THRESHOLD_SCHEMA, paths = SCHEMA.Optional(RELPATHS_SCHEMA), path_hash_prefixes = SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA)) @@ -366,12 +366,12 @@ # tuf.roledb ROLEDB_SCHEMA = SCHEMA.Object( object_name = 'ROLEDB_SCHEMA', - keyids = SCHEMA.ListOf(KEYID_SCHEMA), - signing_keyids = SCHEMA.Optional(SCHEMA.ListOf(KEYID_SCHEMA)), + keyids = KEYIDS_SCHEMA, + signing_keyids = SCHEMA.Optional(KEYIDS_SCHEMA), threshold = THRESHOLD_SCHEMA, version = SCHEMA.Optional(METADATAVERSION_SCHEMA), expires = SCHEMA.Optional(SCHEMA.OneOf([EXPIRATION_SCHEMA, TIME_SCHEMA])), - signatures = SCHEMA.Optional(SCHEMA.ListOf(SIGNATURE_SCHEMA)), + signatures = SCHEMA.Optional(SIGNATURES_SCHEMA), compressions = SCHEMA.Optional(COMPRESSIONS_SCHEMA), paths = SCHEMA.Optional(RELPATHS_SCHEMA), path_hash_prefixes = SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA), diff --git a/tuf/libtuf.py b/tuf/libtuf.py index 0b8f4b44..8367de45 100755 --- a/tuf/libtuf.py +++ b/tuf/libtuf.py @@ -12,7 +12,7 @@ See LICENSE for licensing information. - See 'tuf.README' for a complete guide on using 'tuf.libtuf.py'. + See 'tuf/README' for a complete guide on using 'tuf.libtuf.py'. """ # Help with Python 3 compatibility, where the print statement is a function, an @@ -59,7 +59,8 @@ RELEASE_FILENAME = 'release.txt' TIMESTAMP_FILENAME = 'timestamp.txt' -# The targets and metadata directory names. +# The targets and metadata directory names. Metadata files are written +# to the staged metadata directory instead of the "live" one. METADATA_STAGED_DIRECTORY_NAME = 'metadata.staged' METADATA_DIRECTORY_NAME = 'metadata' TARGETS_DIRECTORY_NAME = 'targets' @@ -90,11 +91,6 @@ # Initial 'timestamp.txt' expiration time of 1 day. TIMESTAMP_EXPIRATION = 86400 -# The suffix added to metadata filenames of partially written metadata. -# Partial metadata may contain insufficient number of signatures and require -# multiple repository maintainers to independently sign them. -#PARTIAL_METADATA_SUFFIX = '.partial' - class Repository(object): """ @@ -102,18 +98,29 @@ class Repository(object): repository_directory: + The root folder of the repository that contains the metadata and targets + sub-directories. metadata_directory: + The metadata sub-directory contains the files of the top-level + roles, including all roles delegated from 'targets.txt'. targets_directory: + The targets sub-directory contains all the target files that are + downloaded by clients and are referenced in TUF Metadata. The hashes and + file lengths are listed in Metadata files so that they are securely + downloaded. Metadata files are similarly referenced in the top-level + metadata. tuf.FormatError, if the arguments are improperly formatted. + Creates top-level role objects and assigns them as attributes. - Repository object. + A Repository object that contains default Metadata objects for the top-level + roles. """ def __init__(self, repository_directory, metadata_directory, targets_directory): @@ -141,13 +148,21 @@ def __init__(self, repository_directory, metadata_directory, targets_directory): def write(self, write_partial=False): """ - Write all the JSON Metadata objects to their corresponding files. + Write all the JSON Metadata objects to their corresponding files. + write() raises an exception if any of the role metadata to be written to + disk is invalid, such as an insufficient threshold of signatures, missing + private keys, etc. write_partial: + A boolean indicating whether partial metadata should be written to + disk. Partial metadata may be written to allow multiple maintainters + to independently sign and update role metadata. write() raises an + exception if a metadata role cannot be written due to not having enough + signatures. - tuf.RepositoryError, if any of the top-level roles do not have a minimum + tuf.Error, if any of the top-level roles do not have a minimum threshold of signatures. @@ -219,8 +234,7 @@ def write_partial(self): None. - tuf.RepositoryError, if any of the top-level roles do not have a minimum - threshold of signatures. + None. Creates metadata files in the repository's metadata directory. @@ -236,13 +250,16 @@ def write_partial(self): def status(self): """ - + Determine + None. + None. + Generates and writes temporary metadata files. None. @@ -372,7 +389,7 @@ def get_filepaths_in_directory(self, files_directory, recursive_walk=False, followlinks=True): """ - Walk the given files_directory to build a list of target files in it. + Walk the given 'files_directory' and build a list of target files found. files_directory: @@ -387,7 +404,8 @@ def get_filepaths_in_directory(self, files_directory, recursive_walk=False, tuf.FormatError, if the arguments are improperly formatted. - tuf.Error, if + tuf.Error, if 'file_directory' is not a valid directory. + Python IO exceptions. @@ -405,10 +423,12 @@ def get_filepaths_in_directory(self, files_directory, recursive_walk=False, tuf.formats.BOOLEAN_SCHEMA.check_match(recursive_walk) tuf.formats.BOOLEAN_SCHEMA.check_match(followlinks) + # Ensure a valid directory is given. if not os.path.isdir(files_directory): message = repr(files_directory)+' is not a directory.' raise tuf.Error(message) - + + # A list of the target filepaths found in 'file_directory'. targets = [] # FIXME: We need a way to tell Python 2, but not Python 3, to return @@ -434,15 +454,24 @@ def get_filepaths_in_directory(self, files_directory, recursive_walk=False, class Metadata(object): """ - Write all the Metadata objects' JSON contents to the corresponding files. - + Provide a base class to represent a TUF Metadata role. There are four + top-level roles: Root, Targets, Release, 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 + compressions, is also provided by the Metadata base class. + + None. + None. + None. + None. """ def __init__(self): @@ -453,6 +482,11 @@ def __init__(self): def add_key(self, key): """ + Add 'key' to the role. Adding a key, which should contain only the public + portion, signifies the corresponding private key and signatures the role + is expected to provide. A threshold of signatures is required for a role + to be considered properly signed. If a metadata file contains an + insufficient threshold of signatures, it must not be accepted. >>> >>> @@ -460,11 +494,16 @@ def add_key(self, key): key: - tuf.formats.ANYKEY_SCHEMA + The role key to be added, conformant to 'tuf.formats.ANYKEY_SCHEMA'. + Adding a public key to a role means that its corresponding private key + must generate and add its signature to the role. A threshold number of + signatures is required for a role to be fully signed. + tuf.FormatError, if the 'key' argument is improperly formatted. + The role's entries in 'tuf.keydb.py' and 'tuf.roledb.py' are updated. None. @@ -476,6 +515,8 @@ def add_key(self, key): # Raise 'tuf.FormatError' if any are improperly formatted. tuf.formats.ANYKEY_SCHEMA.check_match(key) + # Ensure 'key', which should contain the public portion, is added to + # 'tuf.keydb.py'. try: tuf.keydb.add_key(key) except tuf.KeyAlreadyExistsError, e: @@ -483,8 +524,11 @@ def add_key(self, key): keyid = key['keyid'] roleinfo = tuf.roledb.get_roleinfo(self.rolename) + + # Add 'key' to the role's entry in 'tuf.roledb.py' and avoid duplicates. if keyid not in roleinfo['keyids']: roleinfo['keyids'].append(keyid) + tuf.roledb.update_roleinfo(self._rolename, roleinfo) @@ -492,6 +536,8 @@ def add_key(self, key): def remove_key(self, key): """ + Remove 'key' from the role's currently recognized list of role keys. + The role expects a threshold number of signatures >>> >>> @@ -499,13 +545,15 @@ def remove_key(self, key): key: - tuf.formats.ANYKEY_SCHEMA + 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'. - tuf.FormatError, if 'key' is improperly formatted. + tuf.FormatError, if the 'key' argument is improperly formatted. - Updates 'tuf.keydb.py'. + Updates the role's 'tuf.roledb.py' entry. None. @@ -519,8 +567,10 @@ def remove_key(self, key): keyid = key['keyid'] roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if keyid in roleinfo['keyids']: roleinfo['keyids'].remove(keyid) + tuf.roledb.update_roleinfo(self._rolename, roleinfo) @@ -528,6 +578,9 @@ def remove_key(self, key): def load_signing_key(self, key): """ + Load the role key, which must contain the private portion, so that role + signatures may be generated when the role's metadata file is eventually + written to disk. >>> >>> @@ -535,15 +588,18 @@ def load_signing_key(self, key): key: - tuf.formats.ANYKEY_SCHEMA + The role's key, conformant to 'tuf.formats.ANYKEY_SCHEMA'. It must + contain the private key, so that role signatures may be generated when + write() or write_partial() is eventually called to generate valid + metadata files. tuf.FormatError, if 'key' is improperly formatted. - tuf.Error, if the private key is unavailable in 'key'. + tuf.Error, if the private key is not found in 'key'. - Updates 'tuf.keydb.py'. + Updates the role's 'tuf.keydb.py' and 'tuf.roledb.py' entries. None. @@ -554,21 +610,26 @@ def load_signing_key(self, key): # types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if any are improperly formatted. tuf.formats.ANYKEY_SCHEMA.check_match(key) - + + # Ensure the private portion of the key is available, otherwise signatures + # cannot be generated when the metadata file is written to disk. if not len(key['keyval']['private']): message = 'The private key is unavailable.' raise tuf.Error(message) + # Has the key, with the private portion included, been added to the keydb? + # The public version of the key may have been previously added. try: tuf.keydb.add_key(key) except tuf.KeyAlreadyExistsError, e: tuf.keydb.remove_key(key['keyid']) tuf.keydb.add_key(key) - # Update 'signing_keys' in roledb. + # Update the role's 'signing_keys' field in 'tuf.roledb.py'. roleinfo = tuf.roledb.get_roleinfo(self.rolename) if key['keyid'] not in roleinfo['signing_keyids']: roleinfo['signing_keyids'].append(key['keyid']) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) @@ -576,6 +637,8 @@ def load_signing_key(self, key): def unload_signing_key(self, key): """ + Remove a previously loaded role private key (i.e., load_signing_key()). + The keyid of the 'key' is removed the list of signing keys recognized. >>> >>> @@ -583,15 +646,13 @@ def unload_signing_key(self, key): key: - tuf.formats.ANYKEY_SCHEMA + The role key to be unloaded, conformant to 'tuf.formats.ANYKEY_SCHEMA'. - tuf.FormatError, if 'key' is improperly formatted. - - tuf.Error, if the private key is unavailable in 'key'. + tuf.FormatError, if the 'key' argument is improperly formatted. - Updates 'tuf.keydb.py'. + Updates the signing keys of the role in 'tuf.roledb.py'. None. @@ -603,10 +664,12 @@ def unload_signing_key(self, key): # Raise 'tuf.FormatError' if any are improperly formatted. tuf.formats.ANYKEY_SCHEMA.check_match(key) - # Update 'signing_keys' in roledb. + # Update the role's 'signing_keys' field in 'tuf.roledb.py'. roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if key['keyid'] in roleinfo['signing_keyids']: roleinfo['signing_keyids'].remove(key['keyid']) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) @@ -614,22 +677,26 @@ def unload_signing_key(self, key): def add_signature(self, signature): """ + Add a signature to the role. A role is considered fully signed if it + contains a threshold of signatures. The 'signature' should have been + generated by the private key corresponding to one of the role's expected + keys. >>> >>> >>> - key: - tuf.formats.ANYKEY_SCHEMA + signature: + The signature to be added to the role, conformant to + 'tuf.formats.SIGNATURE_SCHEMA'. - tuf.FormatError, if 'key' is improperly formatted. - - tuf.Error, if the private key is unavailable in 'key'. + tuf.FormatError, if the 'signature' argument is improperly formatted. - Updates 'tuf.keydb.py'. + Adds 'signature', if not already added, to the role's 'signatures' field + in 'tuf.roledb.py'. None. @@ -642,9 +709,13 @@ def add_signature(self, signature): tuf.formats.SIGNATURE_SCHEMA.check_match(signature) roleinfo = tuf.roledb.get_roleinfo(self.rolename) + + # Ensure the roleinf contains a 'signatures' field. if 'signatures' not in roleinfo: roleinfo['signatures'] = [] - + + # Update the role's roleinfo by adding 'signature', if it has not been + # added. if signature not in roleinfo['signatures']: roleinfo['signatures'].append(signature) tuf.roledb.update_roleinfo(self.rolename, roleinfo) @@ -654,22 +725,23 @@ def add_signature(self, signature): def remove_signature(self, signature): """ + Remove a previously loaded, or added, role 'signature'. A role must + contain a threshold number of signatures to be considered fully signed. >>> >>> >>> - key: - tuf.formats.ANYKEY_SCHEMA + signature: + The role signature to remove, conformant to + 'tuf.formats.SIGNATURE_SCHEMA'. - tuf.FormatError, if 'key' is improperly formatted. - - tuf.Error, if the private key is unavailable in 'key'. + tuf.FormatError, if the 'signature' argument is improperly formatted. - Updates 'tuf.keydb.py'. + Updates the 'signatures' field of the role in 'tuf.roledb.py'. None. @@ -682,8 +754,10 @@ def remove_signature(self, signature): tuf.formats.SIGNATURE_SCHEMA.check_match(signature) roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if signature in roleinfo['signatures']: roleinfo['signatures'].remove(signature) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) @@ -691,6 +765,23 @@ def remove_signature(self, signature): @property 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. + + + None. + + + None. + + + None. + + + A list of signatures, conformant to 'tuf.formats.SIGNATURES_SCHEMA'. """ roleinfo = tuf.roledb.get_roleinfo(self.rolename) @@ -703,6 +794,22 @@ def signatures(self): @property def keys(self): """ + + A getter method that returns the role's keyids of the keys. The role + is expected to eventually contain a threshold of signatures generated + by the private keys of each of the role's keys (returned here as a keyid). + + + None. + + + None. + + + None. + + + A list of the role's keyids (i.e., keyids of the keys). """ roleinfo = tuf.roledb.get_roleinfo(self.rolename) @@ -715,6 +822,22 @@ def keys(self): @property def rolename(self): """ + + Return the role's name. + Examples: 'root', 'timestamp', 'targets/unclaimed/django'. + + + None. + + + None. + + + None. + + + The role's name, conformant to 'tuf.formats.ROLENAME_SCHEMA'. + Examples: 'root', 'timestamp', 'targets/unclaimed/django'. """ return self._rolename @@ -724,6 +847,21 @@ def rolename(self): @property def version(self): """ + + A getter method that returns the role's version number, conformant to + 'tuf.formats.VERSION_SCHEMA'. + + + None. + + + None. + + + None. + + + The role's version number, conformant to 'tuf.formats.VERSION_SCHEMA'. """ roleinfo = tuf.roledb.get_roleinfo(self.rolename) @@ -737,20 +875,31 @@ def version(self): def version(self, version): """ + A setter method that updates the role's version number. TUF clients + download new metadata with version number greater than the version + currently trusted. New metadata start at version 1 when either write() + or write_partial() is called. Version numbers are automatically + incremented, when the write methods are called, as follows: + + 1. write_partial==True and the metadata is the first to be written. + + 2. write_partial=False (i.e., write()), the metadata was not loaded as + partially written, and a write_partial is not needed. >>> >>> >>> - threshold: - tuf.formats.THRESHOLD_SCHEMA + version: + The role's version number, conformant to 'tuf.formats.VERSION_SCHEMA'. - tuf.FormatError, if the argument is improperly formatted. + tuf.FormatError, if the 'version' argument is improperly formatted. - Modifies the threshold attribute of the Repository object. + Modifies the 'version' attribute of the Repository object and updates + the role's version in 'tuf.roledb.py'. None. @@ -772,6 +921,21 @@ def version(self, version): @property def threshold(self): """ + + Return the role's threshold value. A role is considered fully signed if + a threshold number of signatures is available. + + + None. + + + None. + + + None. + + + The role's threshold value, conformant to 'tuf.formats.THRESHOLD_SCHEMA'. """ roleinfo = tuf.roledb.get_roleinfo(self._rolename) @@ -785,6 +949,9 @@ def threshold(self): def threshold(self, threshold): """ + A setter method that modified the threshold value of the role. Metadata + is considered fully signed if a 'threshold' number of signatures is + available. >>> >>> @@ -792,13 +959,16 @@ def threshold(self, threshold): threshold: - tuf.formats.THRESHOLD_SCHEMA + An integer value that sets the role's threshold value, or the miminum + number of signatures needed for metadata to be considered fully + signed. Conformant to 'tuf.formats.THRESHOLD_SCHEMA'. - tuf.FormatError, if the argument is improperly formatted. + tuf.FormatError, if the 'threshold' argument is improperly formatted. - Modifies the threshold attribute of the Repository object. + Modifies the threshold attribute of the Repository object and updates + the roles threshold in 'tuf.roledb.py'. None. @@ -814,13 +984,13 @@ def threshold(self, threshold): roleinfo['threshold'] = threshold tuf.roledb.update_roleinfo(self._rolename, roleinfo) - self._threshold = threshold @property def expiration(self): """ + A getter method that returns the role's expiration datetime. >>> >>> @@ -836,7 +1006,8 @@ def expiration(self): None. - The role's expiration datetime, conformant to tuf.formats.DATETIME_SCHEMA. + The role's expiration datetime, conformant to + 'tuf.formats.DATETIME_SCHEMA'. """ roleinfo = tuf.roledb.get_roleinfo(self.rolename) @@ -849,9 +1020,9 @@ def expiration(self): def expiration(self, expiration_datetime_utc): """ - A setter function for the role's expiration datetime. The top-level + A setter method for the role's expiration datetime. The top-level roles have a default expiration (e.g., ROOT_EXPIRATION), but may later - be modified by this setter function. + be modified by this setter method. TODO: expiration_datetime_utc in ISO 8601 format. @@ -906,7 +1077,7 @@ def expiration(self, expiration_datetime_utc): def signing_keys(self): """ - A getter function that returns a list of the role's signing keys. + A getter method that returns a list of the role's signing keys. >>> >>> @@ -937,7 +1108,7 @@ def signing_keys(self): def compressions(self): """ - A getter function that returns a list of the file compression algorithms + A getter method that returns a list of the file compression algorithms used when the metadata is written to disk. If ['gz'] is set for the 'targets.txt' role, the metadata files 'targets.txt' and 'targets.txt.gz' are written. @@ -971,7 +1142,7 @@ def compressions(self): def compressions(self, compression_list): """ - A setter function for the file compression algorithms used when the + A setter method for the file compression algorithms used when the metadata is written to disk. If ['gz'] is set for the 'targets.txt' role the metadata files 'targets.txt' and 'targets.txt.gz' are written. @@ -1301,7 +1472,7 @@ def add_target(self, filepath): Add a filepath (must be under the repository's targets directory) to the Targets object. - This function does not actually create 'filepath' on the file + This method does not actually create 'filepath' on the file system. 'filepath' must already exist on the file system. >>> @@ -1367,7 +1538,7 @@ def add_targets(self, list_of_targets): """ Add a list of target filepaths (all relative to 'self.targets_directory'). - This function does not actually create files on the file system. The + This method does not actually create files on the file system. The list of target must already exist. >>> diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index b08a4b7b..ba6533bc 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -1,4 +1,6 @@ -""" pycrypto_keys.py +""" + + pycrypto_keys.py Vladimir Diaz