def check_signable_object_format(signable): """ <Purpose> 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. <Arguments> signable: The signable object compared against 'SIGNABLE.SCHEMA'. <Exceptions> securesystemslib.exceptions.FormatError, if 'signable' does not have the correct format. tuf.exceptions.UnsignedMetadataError, if 'signable' does not have any signatures <Side Effects> None. <Returns> 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) as error: six.raise_from( sslib_exceptions.FormatError('Untyped signable object.'), error) try: schema = SCHEMAS_BY_TYPE[role_type] except KeyError as error: six.raise_from( sslib_exceptions.FormatError('Unrecognized type ' + repr(role_type)), error) if not signable['signatures']: raise exceptions.UnsignedMetadataError( 'Signable object of type ' + repr(role_type) + ' has no signatures ', signable) # 'securesystemslib.exceptions.FormatError' raised if 'signable' does not # have a properly formatted role schema. schema.check_match(signable['signed']) return role_type.lower()
def parse_base64(base64_string): """ <Purpose> Parse a base64 encoding with whitespace and '=' signs omitted. <Arguments> base64_string: A string holding a base64 value. <Exceptions> securesystemslib.exceptions.FormatError, if 'base64_string' cannot be parsed due to an invalid base64 encoding. <Side Effects> None. <Returns> A byte string representing the parsed based64 encoding of 'base64_string'. """ if not isinstance(base64_string, str): message = 'Invalid argument: ' + repr(base64_string) raise 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 exceptions.FormatError('Invalid base64' ' encoding: ' + str(e))
def check_match(self, object): if not isinstance(object, dict): raise exceptions.FormatError('Wanted a ' + repr(self._object_name) + '.') # (key, schema) = (a, AnyString()) = (a=AnyString()) for key, schema in self._required: # Check if 'object' has all the required dict keys. If not one of the # required keys, check if it is an Optional(). try: item = object[key] except KeyError: # If not an Optional schema, raise an exception. if not isinstance(schema, Optional): raise exceptions.FormatError('Missing key ' + repr(key) + ' in ' + repr(self._object_name)) # Check that 'object's schema matches Object()'s schema for this # particular 'key'. else: try: schema.check_match(item) except exceptions.FormatError as e: raise exceptions.FormatError( str(e) + ' in ' + self._object_name + '.' + key)
def check_match(self, object): if not isinstance(object, (list, tuple)): raise exceptions.FormatError('Expected ' + repr(self._struct_name) + '; but got ' + repr(object)) elif len(object) < self._min: raise exceptions.FormatError('Too few fields in ' + self._struct_name) elif len(object) > len(self._sub_schemas) and not self._allow_more: raise exceptions.FormatError('Too many fields in ' + self._struct_name) # Iterate through the items of 'object', checking against each schema in # the list of schemas allowed (i.e., the sub-schemas and also any optional # schemas. The lenth of 'object' must be less than the length of the # required schemas + the optional schemas. However, 'object' is allowed to # be only as large as the length of the required schemas. In the while # loop below, we check against these two cases. index = 0 while index < len(object) and index < len(self._sub_schemas): item = object[index] schema = self._sub_schemas[index] schema.check_match(item) index = index + 1
def __init__(self, sub_schemas, optional_schemas=None, allow_more=False, struct_name='list'): """ <Purpose> Create a new Struct schema. <Arguments> sub_schemas: The sub-schemas recognized. optional_schemas: Optional list. If none is given, it will be "[]". allow_more: Specifies that an optional list of types is allowed. struct_name: A string identifier for the Struct object. """ if optional_schemas is None: optional_schemas = [] # Ensure each item of the list contains the expected object type. if not isinstance(sub_schemas, (list, tuple)): raise exceptions.FormatError('Expected Schema but got ' + repr(sub_schemas)) for schema in sub_schemas: if not isinstance(schema, Schema): raise exceptions.FormatError('Expected Schema but' ' got ' + repr(schema)) self._sub_schemas = sub_schemas + optional_schemas self._min = len(sub_schemas) self._allow_more = allow_more self._struct_name = struct_name
def check_match(self, object): if not isinstance(object, six.binary_type): raise exceptions.FormatError('Expected a byte but' ' got ' + repr(object)) if len(object) != self._bytes_length: raise exceptions.FormatError('Expected a byte of' ' length ' + repr(self._bytes_length))
def check_match(self, object): if not isinstance(object, six.string_types): raise exceptions.FormatError('Expected a string but' ' got ' + repr(object)) if len(object) != self._string_length: raise exceptions.FormatError('Expected a string of' ' length ' + repr(self._string_length))
def import_publickeys_from_file(filepaths, key_types=None): """Imports multiple public keys from files. NOTE: The default signing scheme 'rsassa-pss-sha256' is assigned to RSA keys. Use 'import_rsa_publickey_from_file' to specify any other than the default signing scheme for an RSA key. ed25519 and ecdsa keys have the signing scheme included in the custom key format (see generate functions). Arguments: filepaths: A list of paths to public key files. key_types (optional): A list of types of keys to be imported associated with filepaths by index. Must be one of KEY_TYPE_RSA, KEY_TYPE_ED25519 or KEY_TYPE_ECDSA. If not specified, all keys are assumed to be KEY_TYPE_RSA. Raises: TypeError: filepaths or 'key_types' (if passed) is not iterable. FormatError: Argument are malformed, or 'key_types' is passed and does not have the same length as 'filepaths' or contains an unsupported type. UnsupportedLibraryError: pyca/cryptography is not available. StorageError: Key file cannot be read. Error: Public key is malformed. Returns: A dict of public keys in KEYDICT_SCHEMA format. """ if key_types is None: key_types = [KEY_TYPE_RSA] * len(filepaths) if len(key_types) != len(filepaths): raise exceptions.FormatError( "Pass equal amount of 'filepaths' (got {}) and 'key_types (got {}), " "or no 'key_types' at all to default to '{}'.".format( len(filepaths), len(key_types), KEY_TYPE_RSA)) key_dict = {} for idx, filepath in enumerate(filepaths): if key_types[idx] == KEY_TYPE_ED25519: key = import_ed25519_publickey_from_file(filepath) elif key_types[idx] == KEY_TYPE_RSA: key = import_rsa_publickey_from_file(filepath) elif key_types[idx] == KEY_TYPE_ECDSA: key = import_ecdsa_publickey_from_file(filepath) else: raise exceptions.FormatError( "Unsupported key type '{}'. Must be '{}', '{}' or '{}'.". format(key_types[idx], KEY_TYPE_RSA, KEY_TYPE_ED25519, KEY_TYPE_ECDSA)) key_dict[key["keyid"]] = key return key_dict
def check_match(self, object): if isinstance(object, bool) or not isinstance(object, int): # We need to check for bool as a special case, since bool # is for historical reasons a subtype of int. raise exceptions.FormatError('Got ' + repr(object) + ' instead of an integer.') elif not (self._lo <= object <= self._hi): int_range = '[' + repr(self._lo) + ', ' + repr(self._hi) + '].' raise exceptions.FormatError( repr(object) + ' not in range ' + int_range)
def __init__(self, required_schemas): # Ensure each item of the list contains the expected object type. if not isinstance(required_schemas, list): raise exceptions.FormatError('Expected a list but' ' got' + repr(required_schemas)) for schema in required_schemas: if not isinstance(schema, Schema): raise exceptions.FormatError('List contains an' ' invalid item ' + repr(schema)) self._required_schemas = required_schemas[:]
def __init__(self, alternatives): # Ensure each item of the list contains the expected object type. if not isinstance(alternatives, list): raise exceptions.FormatError('Expected a list but' ' got ' + repr(alternatives)) for alternative in alternatives: if not isinstance(alternative, Schema): raise exceptions.FormatError('List contains an' ' invalid item ' + repr(alternative)) self._alternatives = alternatives
def format_base64(data): """ <Purpose> Return the base64 encoding of 'data' with whitespace and '=' signs omitted. <Arguments> data: Binary or buffer of data to convert. <Exceptions> securesystemslib.exceptions.FormatError, if the base64 encoding fails or the argument is invalid. <Side Effects> None. <Returns> A base64-encoded string. """ try: return binascii.b2a_base64(data).decode('utf-8').rstrip('=\n ') except (TypeError, binascii.Error) as e: raise sslib_exceptions.FormatError('Invalid base64' ' encoding: ' + str(e))
def import_ed25519_publickey_from_file(filepath): """Imports custom JSON-formatted ed25519 public key from disk. NOTE: The signing scheme is set at key generation (see generate function). Arguments: filepath: The path to read the file from. Raises: FormatError: Argument is malformed. StorageError: Key file cannot be read. Error: Public key is malformed. Returns: An ed25519 public key object conformant with 'ED25519KEY_SCHEMA'. """ formats.PATH_SCHEMA.check_match(filepath) # Load custom on-disk JSON formatted key and convert to its custom in-memory # dict key representation ed25519_key_metadata = util.load_json_file(filepath) ed25519_key, _ = keys.format_metadata_to_key(ed25519_key_metadata) # Check that the generic loading functions indeed loaded an ed25519 key if ed25519_key['keytype'] != 'ed25519': message = 'Invalid key type loaded: ' + repr(ed25519_key['keytype']) raise exceptions.FormatError(message) return ed25519_key
def check_match(self, object): AnyString.check_match(self, object) if object == "": raise exceptions.FormatError( 'Expected a string' ' with at least one character but got ' + repr(object))
def expiry_string_to_datetime(expires): """ <Purpose> Convert an expiry string to a datetime object. <Arguments> expires: The expiry date-time string in the ISO8601 format that is defined in securesystemslib.ISO8601_DATETIME_SCHEMA. E.g. '2038-01-19T03:14:08Z' <Exceptions> securesystemslib.exceptions.FormatError, if 'expires' cannot be parsed correctly. <Side Effects> None. <Returns> A datetime object representing the expiry time. """ # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. sslib_formats.ISO8601_DATETIME_SCHEMA.check_match(expires) try: return datetime.datetime.strptime(expires, "%Y-%m-%dT%H:%M:%SZ") except ValueError as error: raise sslib_exceptions.FormatError('Failed to parse ' + repr(expires) + ' as an expiry time') from error
def __init__(self, length): if isinstance(length, bool) or not isinstance(length, int): # We need to check for bool as a special case, since bool # is for historical reasons a subtype of int. raise exceptions.FormatError('Got ' + repr(length) + ' instead of an integer.') self._bytes_length = length
def check_match(self, object): # Simply return as soon as we find a match. # Raise 'exceptions.FormatError' if no matches are found. for alternative in self._alternatives: if alternative.matches(object): return raise exceptions.FormatError('Object did not match a' ' recognized alternative.')
def check_match(self, object): if not isinstance(object, dict): raise exceptions.FormatError('Expected a dict but' ' got ' + repr(object)) for key, value in six.iteritems(object): self._key_schema.check_match(key) self._value_schema.check_match(value)
def import_privatekey_from_file(filepath, key_type=None, password=None, prompt=False): """Imports private key from file. If a password is passed or entered on the prompt, the private key is decrypted, otherwise it is treated as unencrypted. NOTE: The default signing scheme 'rsassa-pss-sha256' is assigned to RSA keys. Use 'import_rsa_privatekey_from_file' to specify any other than the default signing scheme for an RSA key. ed25519 and ecdsa keys have the signing scheme included in the custom key format (see generate functions). Arguments: filepath: The path to read the file from. key_type (optional): One of KEY_TYPE_RSA, KEY_TYPE_ED25519 or KEY_TYPE_ECDSA. Default is KEY_TYPE_RSA. password (optional): A password to decrypt the key. prompt (optional): A boolean indicating if the user should be prompted for a decryption password. If the user enters an empty password, the key is not decrypted. Raises: FormatError: Arguments are malformed or 'key_type' is not supported. ValueError: Both a 'password' is passed and 'prompt' is true. UnsupportedLibraryError: pyca/cryptography is not available. StorageError: Key file cannot be read. Error, CryptoError: Key cannot be parsed. Returns: A private key object conformant with one of 'ED25519KEY_SCHEMA', 'RSAKEY_SCHEMA' or 'ECDSAKEY_SCHEMA'. """ if key_type is None: key_type = KEY_TYPE_RSA if key_type == KEY_TYPE_ED25519: return import_ed25519_privatekey_from_file(filepath, password=password, prompt=prompt) elif key_type == KEY_TYPE_RSA: return import_rsa_privatekey_from_file(filepath, password=password, prompt=prompt) elif key_type == KEY_TYPE_ECDSA: return import_ecdsa_privatekey_from_file(filepath, password=password, prompt=prompt) else: raise exceptions.FormatError( "Unsupported key type '{}'. Must be '{}', '{}' or '{}'.".format( key_type, KEY_TYPE_RSA, KEY_TYPE_ED25519, KEY_TYPE_ECDSA))
def __init__(self, key_schema, value_schema): """ <Purpose> Create a new DictOf schema. <Arguments> key_schema: The dictionary's key. value_schema: The dictionary's value. """ if not isinstance(key_schema, Schema): raise exceptions.FormatError('Expected Schema but' ' got ' + repr(key_schema)) if not isinstance(value_schema, Schema): raise exceptions.FormatError('Expected Schema but' ' got ' + repr(value_schema)) self._key_schema = key_schema self._value_schema = value_schema
def check_match(self, object): if not isinstance(object, (list, tuple)): raise exceptions.FormatError( 'Expected object of type {} but got type {}'.format( self._list_name, type(object).__name__)) # Check if all the items in the 'object' list # match 'schema'. for item in object: try: self._schema.check_match(item) except exceptions.FormatError as e: raise exceptions.FormatError( str(e) + ' in ' + repr(self._list_name)) # Raise exception if the number of items in the list is # not within the expected range. if not (self._min_count <= len(object) <= self._max_count): raise exceptions.FormatError('Length of ' + repr(self._list_name) + ' out of range.')
def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None): """ <Purpose> Create a new regular expression schema. <Arguments> pattern: The pattern to match, or None if re_object is provided. modifiers: Flags to use when compiling the pattern. re_object: A compiled regular expression object. re_name: Identifier for the regular expression object. """ if not isinstance(pattern, six.string_types): if pattern is not None: raise exceptions.FormatError( repr(pattern) + ' is not a string.') if re_object is None: if pattern is None: raise exceptions.FormatError( 'Cannot compare against an unset regular expression') if not pattern.endswith('$'): pattern += '$' re_object = re.compile(pattern, modifiers) self._re_object = re_object if re_name is None: if pattern is not None: re_name = 'pattern /' + pattern + '/' else: re_name = 'pattern' self._re_name = re_name
def __init__(self, object_name='object', **required): """ <Purpose> Create a new Object schema. <Arguments> object_name: A string identifier for the object argument. A variable number of keyword arguments is accepted. """ # Ensure valid arguments. for key, schema in six.iteritems(required): if not isinstance(schema, Schema): raise exceptions.FormatError('Expected Schema but' ' got ' + repr(schema)) self._object_name = object_name self._required = list(required.items())
def _encode_canonical(object, output_function): # Helper for encode_canonical. Older versions of json.encoder don't # even let us replace the separators. if isinstance(object, str): output_function(_canonical_string_encoder(object)) elif object is True: output_function("true") elif object is False: output_function("false") elif object is None: output_function("null") elif isinstance(object, int): output_function(str(object)) elif isinstance(object, (tuple, list)): output_function("[") if len(object): for item in object[:-1]: _encode_canonical(item, output_function) output_function(",") _encode_canonical(object[-1], output_function) output_function("]") elif isinstance(object, dict): output_function("{") if len(object): items = sorted(object.items()) for key, value in items[:-1]: output_function(_canonical_string_encoder(key)) output_function(":") _encode_canonical(value, output_function) output_function(",") key, value = items[-1] output_function(_canonical_string_encoder(key)) output_function(":") _encode_canonical(value, output_function) output_function("}") else: raise exceptions.FormatError('I cannot encode ' + repr(object))
def datetime_to_unix_timestamp(datetime_object): """ <Purpose> 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 <Arguments> datetime_object: The datetime.datetime() object to convert to a Unix timestamp. <Exceptions> securesystemslib.exceptions.FormatError, if 'datetime_object' is not a datetime.datetime() object. <Side Effects> None. <Returns> 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 sslib_exceptions.FormatError(message) unix_timestamp = calendar.timegm(datetime_object.timetuple()) return unix_timestamp
def __init__(self, schema, min_count=0, max_count=sys.maxsize, list_name='list'): """ <Purpose> Create a new ListOf schema. <Arguments> schema: The pattern to match. min_count: The minimum number of sub-schema in 'schema'. max_count: The maximum number of sub-schema in 'schema'. list_name: A string identifier for the ListOf object. """ if not isinstance(schema, Schema): message = 'Expected Schema type but got ' + repr(schema) raise exceptions.FormatError(message) self._schema = schema self._min_count = min_count self._max_count = max_count self._list_name = list_name
def check_match(self, object): if not isinstance(object, bool): raise exceptions.FormatError('Got ' + repr(object) + ' instead of a boolean.')
def encode_canonical(object, output_function=None): """ <Purpose> Encode 'object' in canonical JSON form, as specified at http://wiki.laptop.org/go/Canonical_JSON . It's a restricted dialect of JSON in which keys are always lexically sorted, there is no whitespace, floats aren't allowed, and only quote and backslash get escaped. The result is encoded in UTF-8, and the resulting bits are passed to output_function (if provided), or joined into a string and returned. Note: This function should be called prior to computing the hash or signature of a JSON object in securesystemslib. For example, generating a signature of a signing role object such as 'ROOT_SCHEMA' is required to ensure repeatable hashes are generated across different json module versions and platforms. Code elsewhere is free to dump JSON objects in any format they wish (e.g., utilizing indentation and single quotes around object keys). These objects are only required to be in "canonical JSON" format when their hashes or signatures are needed. >>> encode_canonical("") '""' >>> encode_canonical([1, 2, 3]) '[1,2,3]' >>> encode_canonical([]) '[]' >>> encode_canonical({"A": [99]}) '{"A":[99]}' >>> encode_canonical({"x" : 3, "y" : 2}) '{"x":3,"y":2}' <Arguments> object: The object to be encoded. output_function: The result will be passed as arguments to 'output_function' (e.g., output_function('result')). <Exceptions> securesystemslib.exceptions.FormatError, if 'object' cannot be encoded or 'output_function' is not callable. <Side Effects> The results are fed to 'output_function()' if 'output_function' is set. <Returns> A string representing the 'object' encoded in canonical JSON form. """ result = None # If 'output_function' is unset, treat it as # appending to a list. if output_function is None: result = [] output_function = result.append try: _encode_canonical(object, output_function) except (TypeError, exceptions.FormatError) as e: message = 'Could not encode ' + repr(object) + ': ' + str(e) raise exceptions.FormatError(message) # Return the encoded 'object' as a string. # Note: Implies 'output_function' is None, # otherwise results are sent to 'output_function'. if result is not None: return ''.join(result)
def check_match(self, object): if not isinstance( object, six.string_types) or not self._re_object.match(object): raise exceptions.FormatError( repr(object) + ' did not match ' + repr(self._re_name))
def __init__(self, schema): if not isinstance(schema, Schema): raise exceptions.FormatError('Expected Schema, but' ' got ' + repr(schema)) self._schema = schema