#!/usr/bin/env python # Copyright 2012 - 2017, New York University and the TUF contributors # SPDX-License-Identifier: MIT OR Apache-2.0 """ formats.py Geremy Condra Vladimir Diaz Refactored April 30, 2012. -vladimir.v.diaz See LICENSE-MIT OR LICENSE for licensing information. A central location for all format-related checking of TUF objects. Some crypto-related formats may also be defined in securesystemslib. Note: 'formats.py' depends heavily on 'schema.py', so the 'schema.py' module should be read and understood before tackling this module. 'formats.py' can be broken down into three sections. (1) Schemas and object matching. (2) Classes that represent Role Metadata and help produce correctly formatted files. (3) Functions that help produce or verify TUF objects. The first section deals with schemas and object matching based on format. There are two ways of checking the format of objects. The first method raises a 'securesystemslib.exceptions.FormatError' exception if the match fails and the other returns a Boolean result. tuf.formats..check_match(object) tuf.formats..matches(object) Example: rsa_key = {'keytype': 'rsa' 'keyid': 34892fc465ac76bc3232fab 'keyval': {'public': 'public_key', 'private': 'private_key'} securesystemslib.formats.RSAKEY_SCHEMA.check_match(rsa_key) securesystemslib.formats.RSAKEY_SCHEMA.matches(rsa_key) In this example, if a dict key or dict value is missing or incorrect, the match fails. There are numerous variations of object checking provided by 'formats.py' and 'schema.py'. The second section deals with the role metadata classes. There are multiple top-level roles, each with differing metadata formats. Example: root_object = tuf.formats.RootFile.from_metadata(root_metadata_file) targets_metadata = tuf.formats.TargetsFile.make_metadata(...) The input and output of these classes are checked against their respective schema to ensure correctly formatted metadata. The last section contains miscellaneous functions related to the format of TUF objects. Example: signable_object = make_signable(unsigned_object) """ # Help with Python 3 compatibility, where the print statement is a function, an # implicit relative import is invalid, and the '/' operator performs true # division. Example: print 'hello world' raises a 'SyntaxError' exception. from __future__ import print_function from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import binascii import calendar import datetime import time import tuf import securesystemslib.formats import securesystemslib.schema as SCHEMA import six # TUF specification version. The constant should be updated when the version # number of the specification changes. All metadata should list this version # number. TUF_VERSION_NUMBER = '1.0' SPECIFICATION_VERSION_SCHEMA = SCHEMA.AnyString() # A datetime in 'YYYY-MM-DDTHH:MM:SSZ' ISO 8601 format. The "Z" zone designator # for the zero UTC offset is always used (i.e., a numerical offset is not # supported.) Example: '2015-10-21T13:20:00Z'. Note: This is a simple format # check, and an ISO8601 string should be fully verified when it is parsed. ISO8601_DATETIME_SCHEMA = SCHEMA.RegularExpression(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z') # A dict holding the version or file information for a particular metadata # role. The dict keys hold the relative file paths, and the dict values the # corresponding version numbers and/or file information. FILEINFODICT_SCHEMA = SCHEMA.DictOf( key_schema = securesystemslib.formats.RELPATH_SCHEMA, value_schema = SCHEMA.OneOf([securesystemslib.formats.VERSIONINFO_SCHEMA, securesystemslib.formats.FILEINFO_SCHEMA])) # A string representing a role's name. ROLENAME_SCHEMA = SCHEMA.AnyString() # Role object in {'keyids': [keydids..], 'name': 'ABC', 'threshold': 1, # 'paths':[filepaths..]} format. ROLE_SCHEMA = SCHEMA.Object( object_name = 'ROLE_SCHEMA', name = SCHEMA.Optional(securesystemslib.formats.ROLENAME_SCHEMA), keyids = securesystemslib.formats.KEYIDS_SCHEMA, threshold = securesystemslib.formats.THRESHOLD_SCHEMA, terminating = SCHEMA.Optional(securesystemslib.formats.BOOLEAN_SCHEMA), paths = SCHEMA.Optional(securesystemslib.formats.RELPATHS_SCHEMA), path_hash_prefixes = SCHEMA.Optional(securesystemslib.formats.PATH_HASH_PREFIXES_SCHEMA)) # A dict of roles where the dict keys are role names and the dict values holding # the role data/information. ROLEDICT_SCHEMA = SCHEMA.DictOf( key_schema = ROLENAME_SCHEMA, value_schema = ROLE_SCHEMA) # A dictionary of ROLEDICT, where dictionary keys can be repository names, and # dictionary values containing information for each role available on the # repository (corresponding to the repository belonging to named repository in # the dictionary key) ROLEDICTDB_SCHEMA = SCHEMA.DictOf( key_schema = securesystemslib.formats.NAME_SCHEMA, value_schema = ROLEDICT_SCHEMA) # Command argument list, as used by the CLI tool. # Example: {'keytype': ed25519, 'expires': 365,} COMMAND_SCHEMA = SCHEMA.DictOf( key_schema = securesystemslib.formats.NAME_SCHEMA, value_schema = SCHEMA.Any()) # A dictionary holding version information. VERSION_SCHEMA = SCHEMA.Object( object_name = 'VERSION_SCHEMA', major = SCHEMA.Integer(lo=0), minor = SCHEMA.Integer(lo=0), fix = SCHEMA.Integer(lo=0)) # An integer representing the numbered version of a metadata file. # Must be 1, or greater. METADATAVERSION_SCHEMA = SCHEMA.Integer(lo=0) # A value that is either True or False, on or off, etc. BOOLEAN_SCHEMA = SCHEMA.Boolean() # A string representing a role's name. ROLENAME_SCHEMA = SCHEMA.AnyString() # A role's threshold value (i.e., the minimum number # of signatures required to sign a metadata file). # Must be 1 and greater. THRESHOLD_SCHEMA = SCHEMA.Integer(lo=1) # A hexadecimal value in '23432df87ab..' format. HASH_SCHEMA = SCHEMA.RegularExpression(r'[a-fA-F0-9]+') # A hexadecimal value in '23432df87ab..' format. HEX_SCHEMA = SCHEMA.RegularExpression(r'[a-fA-F0-9]+') # A key identifier (e.g., a hexadecimal value identifying an RSA key). KEYID_SCHEMA = HASH_SCHEMA # A list of KEYID_SCHEMA. KEYIDS_SCHEMA = SCHEMA.ListOf(KEYID_SCHEMA) # The actual values of a key, as opposed to meta data such as a key type and # key identifier ('rsa', 233df889cb). For RSA keys, the key value is a pair of # public and private keys in PEM Format stored as strings. KEYVAL_SCHEMA = SCHEMA.Object( object_name = 'KEYVAL_SCHEMA', public = SCHEMA.AnyString(), private = SCHEMA.Optional(SCHEMA.AnyString())) # A generic TUF key. All TUF keys should be saved to metadata files in this # format. KEY_SCHEMA = SCHEMA.Object( object_name = 'KEY_SCHEMA', keytype = SCHEMA.AnyString(), keyval = KEYVAL_SCHEMA, expires = SCHEMA.Optional(ISO8601_DATETIME_SCHEMA)) # A dict where the dict keys hold a keyid and the dict values a key object. KEYDICT_SCHEMA = SCHEMA.DictOf( key_schema = KEYID_SCHEMA, value_schema = KEY_SCHEMA) # A relative file path (e.g., 'metadata/root/'). RELPATH_SCHEMA = SCHEMA.AnyString() RELPATHS_SCHEMA = SCHEMA.ListOf(RELPATH_SCHEMA) # A path hash prefix is a hexadecimal string. PATH_HASH_PREFIX_SCHEMA = HEX_SCHEMA # A list of path hash prefixes. PATH_HASH_PREFIXES_SCHEMA = SCHEMA.ListOf(PATH_HASH_PREFIX_SCHEMA) # Role object in {'keyids': [keydids..], 'name': 'ABC', 'threshold': 1, # 'paths':[filepaths..]} format. ROLE_SCHEMA = SCHEMA.Object( object_name = 'ROLE_SCHEMA', name = SCHEMA.Optional(ROLENAME_SCHEMA), keyids = KEYIDS_SCHEMA, threshold = THRESHOLD_SCHEMA, backtrack = SCHEMA.Optional(BOOLEAN_SCHEMA), paths = SCHEMA.Optional(RELPATHS_SCHEMA), path_hash_prefixes = SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA)) # A dict of roles where the dict keys are role names and the dict values holding # the role data/information. ROLEDICT_SCHEMA = SCHEMA.DictOf( key_schema = ROLENAME_SCHEMA, value_schema = ROLE_SCHEMA) # An integer representing length. Must be 0, or greater. LENGTH_SCHEMA = SCHEMA.Integer(lo=0) # A dict in {'sha256': '23432df87ab..', 'sha512': '34324abc34df..', ...} format. HASHDICT_SCHEMA = SCHEMA.DictOf( key_schema = SCHEMA.AnyString(), value_schema = HASH_SCHEMA) # Information about target files, like file length and file hash(es). This # schema allows the storage of multiple hashes for the same file (e.g., sha256 # and sha512 may be computed for the same file and stored). FILEINFO_SCHEMA = SCHEMA.Object( object_name = 'FILEINFO_SCHEMA', length = LENGTH_SCHEMA, hashes = HASHDICT_SCHEMA, version = SCHEMA.Optional(METADATAVERSION_SCHEMA), custom = SCHEMA.Optional(SCHEMA.Object())) # A dict holding the information for a particular target / file. The dict keys # hold the relative file paths, and the dict values the corresponding file # information. FILEDICT_SCHEMA = SCHEMA.DictOf( key_schema = RELPATH_SCHEMA, value_schema = FILEINFO_SCHEMA) # A dict holding a target info. TARGETINFO_SCHEMA = SCHEMA.Object( object_name = 'TARGETINFO_SCHEMA', filepath = RELPATH_SCHEMA, fileinfo = FILEINFO_SCHEMA) # A list of TARGETINFO_SCHEMA. TARGETINFOS_SCHEMA = SCHEMA.ListOf(TARGETINFO_SCHEMA) # A string representing a named oject. NAME_SCHEMA = SCHEMA.AnyString() # A dict of repository names to mirrors. REPO_NAMES_TO_MIRRORS_SCHEMA = SCHEMA.DictOf( key_schema = NAME_SCHEMA, value_schema = SCHEMA.ListOf(securesystemslib.formats.URL_SCHEMA)) # An object containing the map file's "mapping" attribute. MAPPING_SCHEMA = SCHEMA.ListOf(SCHEMA.Object( paths = RELPATHS_SCHEMA, repositories = SCHEMA.ListOf(NAME_SCHEMA), terminating = BOOLEAN_SCHEMA, threshold = THRESHOLD_SCHEMA)) # A dict containing the map file (named 'map.json', by default). The format of # the map file is covered in TAP 4: Multiple repository consensus on entrusted # targets. MAPFILE_SCHEMA = SCHEMA.Object( repositories = REPO_NAMES_TO_MIRRORS_SCHEMA, mapping = MAPPING_SCHEMA) # Like ROLEDICT_SCHEMA, except that ROLE_SCHEMA instances are stored in order. ROLELIST_SCHEMA = SCHEMA.ListOf(ROLE_SCHEMA) # The delegated roles of a Targets role (a parent). DELEGATIONS_SCHEMA = SCHEMA.Object( keys = KEYDICT_SCHEMA, roles = ROLELIST_SCHEMA) # The number of hashed bins, or the number of delegated roles. See # delegate_hashed_bins() in 'repository_tool.py' for an example. Note: # Tools may require further restrictions on the number of bins, such # as requiring them to be a power of 2. NUMBINS_SCHEMA = SCHEMA.Integer(lo=1) # The fileinfo format of targets specified in the repository and # developer tools. The second element of this list holds custom data about the # target, such as file permissions, author(s), last modified, etc. CUSTOM_SCHEMA = SCHEMA.Object() PATH_FILEINFO_SCHEMA = SCHEMA.DictOf( key_schema = RELPATH_SCHEMA, value_schema = CUSTOM_SCHEMA) # TUF roledb ROLEDB_SCHEMA = SCHEMA.Object( object_name = 'ROLEDB_SCHEMA', keyids = SCHEMA.Optional(KEYIDS_SCHEMA), signing_keyids = SCHEMA.Optional(KEYIDS_SCHEMA), previous_keyids = SCHEMA.Optional(KEYIDS_SCHEMA), threshold = SCHEMA.Optional(THRESHOLD_SCHEMA), previous_threshold = SCHEMA.Optional(THRESHOLD_SCHEMA), version = SCHEMA.Optional(METADATAVERSION_SCHEMA), expires = SCHEMA.Optional(ISO8601_DATETIME_SCHEMA), signatures = SCHEMA.Optional(securesystemslib.formats.SIGNATURES_SCHEMA), paths = SCHEMA.Optional(SCHEMA.OneOf([RELPATHS_SCHEMA, PATH_FILEINFO_SCHEMA])), path_hash_prefixes = SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA), delegations = SCHEMA.Optional(DELEGATIONS_SCHEMA), partial_loaded = SCHEMA.Optional(BOOLEAN_SCHEMA)) # A signable object. Holds the signing role and its associated signatures. SIGNABLE_SCHEMA = SCHEMA.Object( object_name = 'SIGNABLE_SCHEMA', signed = SCHEMA.Any(), signatures = SCHEMA.ListOf(securesystemslib.formats.SIGNATURE_SCHEMA)) # Root role: indicates root keys and top-level roles. ROOT_SCHEMA = SCHEMA.Object( object_name = 'ROOT_SCHEMA', _type = SCHEMA.String('root'), spec_version = SPECIFICATION_VERSION_SCHEMA, version = METADATAVERSION_SCHEMA, consistent_snapshot = BOOLEAN_SCHEMA, expires = ISO8601_DATETIME_SCHEMA, keys = KEYDICT_SCHEMA, roles = ROLEDICT_SCHEMA) # Targets role: Indicates targets and delegates target paths to other roles. TARGETS_SCHEMA = SCHEMA.Object( object_name = 'TARGETS_SCHEMA', _type = SCHEMA.String('targets'), spec_version = SPECIFICATION_VERSION_SCHEMA, version = METADATAVERSION_SCHEMA, expires = ISO8601_DATETIME_SCHEMA, targets = FILEDICT_SCHEMA, delegations = SCHEMA.Optional(DELEGATIONS_SCHEMA)) # Snapshot role: indicates the latest versions of all metadata (except # timestamp). SNAPSHOT_SCHEMA = SCHEMA.Object( object_name = 'SNAPSHOT_SCHEMA', _type = SCHEMA.String('snapshot'), version = securesystemslib.formats.METADATAVERSION_SCHEMA, expires = securesystemslib.formats.ISO8601_DATETIME_SCHEMA, spec_version = SPECIFICATION_VERSION_SCHEMA, meta = FILEINFODICT_SCHEMA) # Timestamp role: indicates the latest version of the snapshot file. TIMESTAMP_SCHEMA = SCHEMA.Object( object_name = 'TIMESTAMP_SCHEMA', _type = SCHEMA.String('timestamp'), version = securesystemslib.formats.METADATAVERSION_SCHEMA, expires = securesystemslib.formats.ISO8601_DATETIME_SCHEMA, meta = securesystemslib.formats.FILEDICT_SCHEMA) # project.cfg file: stores information about the project in a json dictionary PROJECT_CFG_SCHEMA = SCHEMA.Object( object_name = 'PROJECT_CFG_SCHEMA', project_name = SCHEMA.AnyString(), layout_type = SCHEMA.OneOf([SCHEMA.String('repo-like'), SCHEMA.String('flat')]), targets_location = securesystemslib.formats.PATH_SCHEMA, metadata_location = securesystemslib.formats.PATH_SCHEMA, prefix = securesystemslib.formats.PATH_SCHEMA, public_keys = securesystemslib.formats.KEYDICT_SCHEMA, threshold = SCHEMA.Integer(lo = 0, hi = 2) ) # A schema containing information a repository mirror may require, # such as a url, the path of the directory metadata files, etc. MIRROR_SCHEMA = SCHEMA.Object( object_name = 'MIRROR_SCHEMA', url_prefix = securesystemslib.formats.URL_SCHEMA, metadata_path = securesystemslib.formats.RELPATH_SCHEMA, targets_path = securesystemslib.formats.RELPATH_SCHEMA, confined_target_dirs = securesystemslib.formats.RELPATHS_SCHEMA, custom = SCHEMA.Optional(SCHEMA.Object())) # A dictionary of mirrors where the dict keys hold the mirror's name and # and the dict values the mirror's data (i.e., 'MIRROR_SCHEMA'). # The repository class of 'updater.py' accepts dictionaries # of this type provided by the TUF client. MIRRORDICT_SCHEMA = SCHEMA.DictOf( key_schema = SCHEMA.AnyString(), value_schema = MIRROR_SCHEMA) # A Mirrorlist: indicates all the live mirrors, and what documents they # serve. MIRRORLIST_SCHEMA = SCHEMA.Object( object_name = 'MIRRORLIST_SCHEMA', _type = SCHEMA.String('mirrors'), version = METADATAVERSION_SCHEMA, expires = securesystemslib.formats.ISO8601_DATETIME_SCHEMA, mirrors = SCHEMA.ListOf(MIRROR_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]) # The format of the resulting "scp config dict" after extraction from the # push configuration file (i.e., push.cfg). In the case of a config file # utilizing the scp transfer module, it must contain the 'general' and 'scp' # sections, where 'general' must contain a 'transfer_module' and # 'metadata_path' entry, and 'scp' the 'host', 'user', 'identity_file', and # 'remote_directory' entries. SCPCONFIG_SCHEMA = SCHEMA.Object( object_name = 'SCPCONFIG_SCHEMA', general = SCHEMA.Object( object_name = '[general]', transfer_module = SCHEMA.String('scp'), metadata_path = securesystemslib.formats.PATH_SCHEMA, targets_directory = securesystemslib.formats.PATH_SCHEMA), scp=SCHEMA.Object( object_name = '[scp]', host = securesystemslib.formats.URL_SCHEMA, user = securesystemslib.formats.NAME_SCHEMA, identity_file = securesystemslib.formats.PATH_SCHEMA, remote_directory = securesystemslib.formats.PATH_SCHEMA)) # The format of the resulting "receive config dict" after extraction from the # receive configuration file (i.e., receive.cfg). The receive config file # must contain a 'general' section, and this section the 'pushroots', # 'repository_directory', 'metadata_directory', 'targets_directory', and # 'backup_directory' entries. RECEIVECONFIG_SCHEMA = SCHEMA.Object( object_name = 'RECEIVECONFIG_SCHEMA', general=SCHEMA.Object( object_name = '[general]', pushroots = SCHEMA.ListOf(securesystemslib.formats.PATH_SCHEMA), repository_directory = securesystemslib.formats.PATH_SCHEMA, metadata_directory = securesystemslib.formats.PATH_SCHEMA, targets_directory = securesystemslib.formats.PATH_SCHEMA, backup_directory = securesystemslib.formats.PATH_SCHEMA)) def make_signable(role_schema): """ Return the role metadata 'role_schema' in 'SIGNABLE_SCHEMA' format. 'role_schema' is added to the 'signed' key, and an empty list initialized to the 'signatures' key. The caller adds signatures to this second field. Note: check_signable_object_format() should be called after make_signable() and signatures added to ensure the final signable object has a valid format (i.e., a signable containing a supported role metadata). role_schema: A role schema dict (e.g., 'ROOT_SCHEMA', 'SNAPSHOT_SCHEMA'). None. None. A dict in 'SIGNABLE_SCHEMA' format. """ if not isinstance(role_schema, dict) or 'signed' not in role_schema: return { 'signed' : role_schema, 'signatures' : [] } else: return role_schema class MetaFile(object): """ Base class for all metadata file classes. Classes representing metadata files such as RootFile and SnapshotFile all inherit from MetaFile. The __eq__, __ne__, perform 'equal' and 'not equal' comparisons between Metadata File objects. """ info = {} def __eq__(self, other): return isinstance(other, MetaFile) and self.info == other.info __hash__ = None def __ne__(self, other): return not self.__eq__(other) def __getattr__(self, name): """ Allow all metafile objects to have their interesting attributes referred to directly without the info dict. The info dict is just to be able to do the __eq__ comparison generically. """ if name in self.info: return self.info[name] else: raise AttributeError(name) class TimestampFile(MetaFile): def __init__(self, version, expires, filedict): self.info = {} self.info['version'] = version self.info['expires'] = expires self.info['meta'] = filedict @staticmethod def from_metadata(timestamp_metadata): # Is 'timestamp_metadata' a Timestamp metadata file? # Raise securesystemslib.exceptions.FormatError if not. TIMESTAMP_SCHEMA.check_match(timestamp_metadata) version = timestamp_metadata['version'] expires = timestamp_metadata['expires'] filedict = timestamp_metadata['meta'] return TimestampFile(version, expires, filedict) @staticmethod def make_metadata(version, expiration_date, filedict): result = {'_type' : 'timestamp'} result['spec_version'] = TUF_VERSION_NUMBER result['version'] = version result['expires'] = expiration_date result['meta'] = filedict # Is 'result' a Timestamp metadata file? # Raise 'securesystemslib.exceptions.FormatError' if not. TIMESTAMP_SCHEMA.check_match(result) return result class RootFile(MetaFile): 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_snapshot'] = consistent_snapshot @staticmethod def from_metadata(root_metadata): # Is 'root_metadata' a Root metadata file? # Raise 'securesystemslib.exceptions.FormatError' if not. tuf.formats.ROOT_SCHEMA.check_match(root_metadata) version = root_metadata['version'] expires = root_metadata['expires'] keys = root_metadata['keys'] roles = root_metadata['roles'] consistent_snapshot = root_metadata['consistent_snapshot'] return RootFile(version, expires, keys, roles, consistent_snapshot) @staticmethod def make_metadata(version, expiration_date, keydict, roledict, consistent_snapshot): result = {'_type' : 'root'} result['spec_version'] = TUF_VERSION_NUMBER result['version'] = version result['expires'] = expiration_date result['keys'] = keydict result['roles'] = roledict result['consistent_snapshot'] = consistent_snapshot # Is 'result' a Root metadata file? # Raise 'securesystemslib.exceptions.FormatError' if not. ROOT_SCHEMA.check_match(result) return result class SnapshotFile(MetaFile): def __init__(self, version, expires, versiondict): self.info = {} self.info['version'] = version self.info['expires'] = expires self.info['meta'] = versiondict @staticmethod def from_metadata(snapshot_metadata): # Is 'snapshot_metadata' a Snapshot metadata file? # Raise 'securesystemslib.exceptions.FormatError' if not. SNAPSHOT_SCHEMA.check_match(snapshot_metadata) version = snapshot_metadata['version'] expires = snapshot_metadata['expires'] versiondict = snapshot_metadata['meta'] return SnapshotFile(version, expires, versiondict) @staticmethod def make_metadata(version, expiration_date, versiondict): result = {'_type' : 'snapshot'} result['spec_version'] = TUF_VERSION_NUMBER result['version'] = version result['expires'] = expiration_date result['meta'] = versiondict # Is 'result' a Snapshot metadata file? # Raise 'securesystemslib.exceptions.FormatError' if not. SNAPSHOT_SCHEMA.check_match(result) return result class TargetsFile(MetaFile): def __init__(self, version, expires, filedict=None, delegations=None): if filedict is None: filedict = {} if delegations is None: delegations = {} self.info = {} self.info['version'] = version self.info['expires'] = expires self.info['targets'] = filedict self.info['delegations'] = delegations @staticmethod def from_metadata(targets_metadata): # Is 'targets_metadata' a Targets metadata file? # Raise securesystemslib.exceptions.FormatError if not. tuf.formats.TARGETS_SCHEMA.check_match(targets_metadata) version = targets_metadata['version'] expires = targets_metadata['expires'] filedict = targets_metadata.get('targets') delegations = targets_metadata.get('delegations') return TargetsFile(version, expires, filedict, delegations) @staticmethod def make_metadata(version, expiration_date, filedict=None, delegations=None): if filedict is None and delegations is None: raise securesystemslib.exceptions.Error('We don\'t allow completely' ' empty targets metadata.') result = {'_type' : 'targets'} result['spec_version'] = TUF_VERSION_NUMBER result['version'] = version result['expires'] = expiration_date result['targets'] = {} if filedict is not None: result['targets'] = filedict if delegations is not None: result['delegations'] = delegations # Is 'result' a Targets metadata file? # Raise 'securesystemslib.exceptions.FormatError' if not. tuf.formats.TARGETS_SCHEMA.check_match(result) return result class MirrorsFile(MetaFile): def __init__(self, version, expires): self.info = {} self.info['version'] = version self.info['expires'] = expires @staticmethod def from_metadata(mirrors_metadata): raise NotImplementedError @staticmethod def make_metadata(): raise NotImplementedError # A dict holding the recognized schemas for the top-level roles. SCHEMAS_BY_TYPE = { 'root' : ROOT_SCHEMA, 'targets' : TARGETS_SCHEMA, 'snapshot' : SNAPSHOT_SCHEMA, 'timestamp' : TIMESTAMP_SCHEMA, 'mirrors' : MIRRORLIST_SCHEMA} # A dict holding the recognized class names for the top-level roles. # That is, the role classes listed in this module (e.g., class TargetsFile()). ROLE_CLASSES_BY_TYPE = { 'Root' : RootFile, 'Targets' : TargetsFile, 'Snapshot' : SnapshotFile, 'Timestamp' : TimestampFile, 'Mirrors' : MirrorsFile} def datetime_to_unix_timestamp(datetime_object): """ Convert 'datetime_object' (in datetime.datetime()) format) to a Unix/POSIX timestamp. For example, Python's time.time() returns a Unix timestamp, and includes the number of microseconds. 'datetime_object' is converted to UTC. >>> datetime_object = datetime.datetime(1985, 10, 26, 1, 22) >>> timestamp = datetime_to_unix_timestamp(datetime_object) >>> timestamp 499137720 datetime_object: The datetime.datetime() object to convert to a Unix timestamp. securesystemslib.exceptions.FormatError, if 'datetime_object' is not a datetime.datetime() object. None. A unix (posix) timestamp (e.g., 499137660). """ # Is 'datetime_object' a datetime.datetime() object? # Raise 'securesystemslib.exceptions.FormatError' if not. if not isinstance(datetime_object, datetime.datetime): message = repr(datetime_object) + ' is not a datetime.datetime() object.' raise securesystemslib.exceptions.FormatError(message) unix_timestamp = calendar.timegm(datetime_object.timetuple()) return unix_timestamp def unix_timestamp_to_datetime(unix_timestamp): """ Convert 'unix_timestamp' (i.e., POSIX time, in UNIX_TIMESTAMP_SCHEMA format) to a datetime.datetime() object. 'unix_timestamp' is the number of seconds since the epoch (January 1, 1970.) >>> datetime_object = unix_timestamp_to_datetime(1445455680) >>> datetime_object datetime.datetime(2015, 10, 21, 19, 28) unix_timestamp: An integer representing the time (e.g., 1445455680). Conformant to 'securesystemslib.formats.UNIX_TIMESTAMP_SCHEMA'. securesystemslib.exceptions.FormatError, if 'unix_timestamp' is improperly formatted. None. A datetime.datetime() object corresponding to 'unix_timestamp'. """ # Is 'unix_timestamp' properly formatted? # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. securesystemslib.formats.UNIX_TIMESTAMP_SCHEMA.check_match(unix_timestamp) # Convert 'unix_timestamp' to a 'time.struct_time', in UTC. The Daylight # Savings Time (DST) flag is set to zero. datetime.fromtimestamp() is not # used because it returns a local datetime. struct_time = time.gmtime(unix_timestamp) # Extract the (year, month, day, hour, minutes, seconds) arguments for the # datetime object to be returned. datetime_object = datetime.datetime(*struct_time[:6]) return datetime_object def format_base64(data): """ Return the base64 encoding of 'data' with whitespace and '=' signs omitted. data: Binary or buffer of data to convert. securesystemslib.exceptions.FormatError, if the base64 encoding fails or the argument is invalid. None. A base64-encoded string. """ try: return binascii.b2a_base64(data).decode('utf-8').rstrip('=\n ') except (TypeError, binascii.Error) as e: raise securesystemslib.exceptions.FormatError('Invalid base64' ' encoding: ' + str(e)) def parse_base64(base64_string): """ Parse a base64 encoding with whitespace and '=' signs omitted. base64_string: A string holding a base64 value. securesystemslib.exceptions.FormatError, if 'base64_string' cannot be parsed due to an invalid base64 encoding. None. A byte string representing the parsed based64 encoding of 'base64_string'. """ if not isinstance(base64_string, six.string_types): message = 'Invalid argument: '+repr(base64_string) raise securesystemslib.exceptions.FormatError(message) extra = len(base64_string) % 4 if extra: padding = '=' * (4 - extra) base64_string = base64_string + padding try: return binascii.a2b_base64(base64_string.encode('utf-8')) except (TypeError, binascii.Error) as e: raise securesystemslib.exceptions.FormatError('Invalid base64' ' encoding: ' + str(e)) def make_fileinfo(length, hashes, version=None, custom=None): """ Create a dictionary conformant to 'FILEINFO_SCHEMA'. This dict describes both metadata and target files. length: An integer representing the size of the file. hashes: A dict of hashes in 'HASHDICT_SCHEMA' format, which has the form: {'sha256': 123df8a9b12, 'sha512': 324324dfc121, ...} version: An optional integer representing the version of the file. custom: An optional object providing additional information about the file. securesystemslib.exceptions.FormatError, if the 'FILEINFO_SCHEMA' to be returned does not have the correct format. If any of the arguments are incorrectly formatted, the dict returned will be checked for formatting errors, and if found, will raise a 'securesystemslib.exceptions.FormatError' exception. A dictionary conformant to 'FILEINFO_SCHEMA', representing the file information of a metadata or target file. """ fileinfo = {'length' : length, 'hashes' : hashes} if version is not None: fileinfo['version'] = version if custom is not None: fileinfo['custom'] = custom # Raise 'securesystemslib.exceptions.FormatError' if the check fails. securesystemslib.formats.FILEINFO_SCHEMA.check_match(fileinfo) return fileinfo def make_versioninfo(version_number): """ Create a dictionary conformant to 'VERSIONINFO_SCHEMA'. This dict describes both metadata and target files. version_number: An integer representing the version of a particular metadata role. The dictionary returned by this function is expected to be included in Snapshot metadata. securesystemslib.exceptions.FormatError, if the dict to be returned does not have the correct format (i.e., VERSIONINFO_SCHEMA). None. A dictionary conformant to 'VERSIONINFO_SCHEMA', containing the version information of a metadata role. """ versioninfo = {'version': version_number} # Raise 'securesystemslib.exceptions.FormatError' if 'versioninfo' is # improperly formatted. securesystemslib.formats.VERSIONINFO_SCHEMA.check_match(versioninfo) return versioninfo def make_role_metadata(keyids, threshold, name=None, paths=None, path_hash_prefixes=None): """ Create a dictionary conforming to 'tuf.formats.ROLE_SCHEMA', representing the role with 'keyids', 'threshold', and 'paths' as field values. 'paths' is optional (i.e., used only by the 'Target' role). keyids: a list of key ids. threshold: An integer denoting the number of required keys for the signing role. name: A string that is the name of this role. paths: The 'Target' role stores the paths of target files in its metadata file. 'paths' is a list of file paths. path_hash_prefixes: The 'Target' role stores the paths of target files in its metadata file. 'path_hash_prefixes' is a succint way to describe a set of paths to target files. securesystemslib.exceptions.FormatError, if the returned role meta is formatted incorrectly. If any of the arguments do not have a proper format, a securesystemslib.exceptions.FormatError exception is raised when the 'ROLE_SCHEMA' dict is created. A properly formatted role meta dict, conforming to 'ROLE_SCHEMA'. """ role_meta = {} role_meta['keyids'] = keyids role_meta['threshold'] = threshold if name is not None: role_meta['name'] = name # According to the specification, the 'paths' and 'path_hash_prefixes' must # be mutually exclusive. However, at the time of writing we do not always # ensure that this is the case with the schema checks (see #83). Therefore, # we must do it for ourselves. if paths is not None and path_hash_prefixes is not None: raise securesystemslib.exceptions.FormatError('Both "paths" and' ' "path_hash_prefixes" are specified.') if path_hash_prefixes is not None: role_meta['path_hash_prefixes'] = path_hash_prefixes elif paths is not None: role_meta['paths'] = paths # Does 'role_meta' have the correct type? # This check ensures 'role_meta' conforms to tuf.formats.ROLE_SCHEMA. ROLE_SCHEMA.check_match(role_meta) return role_meta def get_role_class(expected_rolename): """ Return the role class corresponding to 'expected_rolename'. The role name returned by expected_meta_rolename() should be the name passed as an argument to this function. If 'expected_rolename' is 'Root', the class RootFile is returned. expected_rolename: The role name used to determine which role class to return. securesystemslib.exceptions.FormatError, if 'expected_rolename' is not a supported role. None. The class corresponding to 'expected_rolename'. E.g., 'Snapshot' as an argument to this function causes SnapshotFile' to be returned. """ # Does 'expected_rolename' have the correct type? # This check ensures 'expected_rolename' conforms to # 'securesystemslib.formats.NAME_SCHEMA'. # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. securesystemslib.formats.NAME_SCHEMA.check_match(expected_rolename) try: role_class = ROLE_CLASSES_BY_TYPE[expected_rolename] except KeyError: raise securesystemslib.exceptions.FormatError(repr(expected_rolename) + ' ' 'not supported.') else: return role_class def expected_meta_rolename(meta_rolename): """ Ensure 'meta_rolename' is properly formatted. 'targets' is returned as 'Targets'. 'targets role1' is returned as 'Targets Role1'. The words in the string (i.e., separated by whitespace) are capitalized. meta_rolename: A string representing the rolename. E.g., 'root', 'targets'. securesystemslib.exceptions.FormatError, if 'meta_rolename' is improperly formatted. None. A string (e.g., 'Root', 'Targets'). """ # Does 'meta_rolename' have the correct type? # This check ensures 'meta_rolename' conforms to # 'securesystemslib.formats.NAME_SCHEMA'. # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. securesystemslib.formats.NAME_SCHEMA.check_match(meta_rolename) return meta_rolename.lower() def check_signable_object_format(signable): """ Ensure 'signable' is properly formatted, conformant to 'SIGNABLE_SCHEMA'. Return the signing role on success. Note: The 'signed' field of a 'SIGNABLE_SCHEMA' is checked against securesystemslib.schema.Any(). The 'signed' field, however, should actually hold one of the supported role schemas (e.g., 'ROOT_SCHEMA', 'TARGETS_SCHEMA'). The role schemas all differ in their format, so this function determines exactly which schema is listed in the 'signed' field. signable: The signable object compared against 'SIGNABLE.SCHEMA'. securesystemslib.exceptions.FormatError, if 'signable' does not have the correct format. None. A string representing the signing role (e.g., 'root', 'targets'). The role string is returned with characters all lower case. """ # Does 'signable' have the correct type? # This check ensures 'signable' conforms to # 'SIGNABLE_SCHEMA'. SIGNABLE_SCHEMA.check_match(signable) try: role_type = signable['signed']['_type'] except (KeyError, TypeError): raise securesystemslib.exceptions.FormatError('Untyped signable object.') try: schema = SCHEMAS_BY_TYPE[role_type] except KeyError: raise securesystemslib.exceptions.FormatError('Unrecognized' ' type ' + repr(role_type)) # 'securesystemslib.exceptions.FormatError' raised if 'signable' does not # have a properly formatted role schema. schema.check_match(signable['signed']) return role_type.lower() if __name__ == '__main__': # The interactive sessions of the documentation strings can # be tested by running formats.py as a standalone module. # python -B formats.py import doctest doctest.testmod()