""" roledb.py Vladimir Diaz March 21, 2012. Based on a previous version of this module by Geremy Condra. See LICENSE for licensing information. Represent a collection of roles and their organization. The caller may create a collection of roles from those found in the 'root.json' metadata file by calling 'create_roledb_from_rootmeta()', or individually by adding roles with 'add_role()'. There are many supplemental functions included here that yield useful information about the roles contained in the database, such as extracting all the parent rolenames for a specified rolename, deleting all the delegated roles, retrieving role paths, etc. The Update Framework process maintains a single roledb. The role database is a dictionary conformant to 'tuf.formats.ROLEDICT_SCHEMA' and has the form: {'rolename': {'keyids': ['34345df32093bd12...'], 'threshold': 1 'signatures': ['abcd3452...'], 'paths': ['path/to/role.json'], 'path_hash_prefixes': ['ab34df13'], 'delegations': {'keys': {}, 'roles': {}}} The 'name', 'paths', 'path_hash_prefixes', and 'delegations' dict keys are optional. """ # 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 logging import copy import tuf import tuf.formats import tuf.log import six # See 'tuf.log' to learn how logging is handled in TUF. logger = logging.getLogger('tuf.roledb') # The role database. _roledb_dict = {} def create_roledb_from_root_metadata(root_metadata): """ Create a role database containing all of the unique roles found in 'root_metadata'. root_metadata: A dictionary conformant to 'tuf.formats.ROOT_SCHEMA'. The roles found in the 'roles' field of 'root_metadata' is needed by this function. tuf.FormatError, if 'root_metadata' does not have the correct object format. tuf.Error, if one of the roles found in 'root_metadata' contains an invalid delegation (i.e., a nonexistent parent role). Calls add_role(). The old role database is replaced. None. """ # Does 'root_metadata' have the correct object format? # This check will ensure 'root_metadata' has the appropriate number of objects # and object types, and that all dict keys are properly named. # Raises tuf.FormatError. tuf.formats.ROOT_SCHEMA.check_match(root_metadata) # Clear the role database. _roledb_dict.clear() # Do not modify the contents of the 'root_metadata' argument. root_metadata = copy.deepcopy(root_metadata) # Iterate through the roles found in 'root_metadata' # and add them to '_roledb_dict'. Duplicates are avoided. for rolename, roleinfo in six.iteritems(root_metadata['roles']): if rolename == 'root': roleinfo['version'] = root_metadata['version'] roleinfo['expires'] = root_metadata['expires'] roleinfo['signatures'] = [] roleinfo['signing_keyids'] = [] roleinfo['compressions'] = [''] roleinfo['partial_loaded'] = False if rolename.startswith('targets'): roleinfo['paths'] = {} roleinfo['delegations'] = {'keys': {}, 'roles': []} try: add_role(rolename, roleinfo) # tuf.Error raised if the parent role of 'rolename' does not exist. except tuf.Error as e: logger.error(e) raise def add_role(rolename, roleinfo, require_parent=True): """ Add to the role database the 'roleinfo' associated with 'rolename'. rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). roleinfo: An object representing the role associated with 'rolename', conformant to ROLEDB_SCHEMA. 'roleinfo' has the form: {'keyids': ['34345df32093bd12...'], 'threshold': 1, 'signatures': ['ab23dfc32'] 'paths': ['path/to/target1', 'path/to/target2', ...], 'path_hash_prefixes': ['a324fcd...', ...], 'delegations': {'keys': } The 'paths', 'path_hash_prefixes', and 'delegations' dict keys are optional. The 'target' role has an additional 'paths' key. Its value is a list of strings representing the path of the target file(s). require_parent: A boolean indicating whether to check for a delegating role. add_role() will raise an exception if this parent role does not exist. tuf.FormatError, if 'rolename' or 'roleinfo' does not have the correct object format. tuf.RoleAlreadyExistsError, if 'rolename' has already been added. tuf.InvalidNameError, if 'rolename' is improperly formatted. The role database is modified. None. """ # Does 'rolename' have the correct object format? # This check will ensure 'rolename' has the appropriate number of objects # and object types, and that all dict keys are properly named. tuf.formats.ROLENAME_SCHEMA.check_match(rolename) # Does 'roleinfo' have the correct object format? tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo) # Does 'require_parent' have the correct format? tuf.formats.BOOLEAN_SCHEMA.check_match(require_parent) # Raises tuf.InvalidNameError. _validate_rolename(rolename) if rolename in _roledb_dict: raise tuf.RoleAlreadyExistsError('Role already exists: '+rolename) # Make sure that the delegating role exists. This should be just a # sanity check and not a security measure. if require_parent and '/' in rolename: # Get parent role. 'a/b/c/d' --> 'a/b/c'. parent_role = '/'.join(rolename.split('/')[:-1]) if parent_role not in _roledb_dict: raise tuf.Error('Parent role does not exist: '+parent_role) _roledb_dict[rolename] = copy.deepcopy(roleinfo) def update_roleinfo(rolename, roleinfo): """ rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). roleinfo: An object representing the role associated with 'rolename', conformant to ROLEDB_SCHEMA. 'roleinfo' has the form: {'name': 'role_name', 'keyids': ['34345df32093bd12...'], 'threshold': 1, 'paths': ['path/to/target1', 'path/to/target2', ...], 'path_hash_prefixes': ['a324fcd...', ...]} The 'name', 'paths', and 'path_hash_prefixes' dict keys are optional. The 'target' role has an additional 'paths' key. Its value is a list of strings representing the path of the target file(s). tuf.FormatError, if 'rolename' or 'roleinfo' does not have the correct object format. tuf.UnknownRoleError, if 'rolename' cannot be found in the role database. tuf.InvalidNameError, if 'rolename' is improperly formatted. The role database is modified. None. """ # Does 'rolename' have the correct object format? # This check will ensure 'rolename' has the appropriate number of objects # and object types, and that all dict keys are properly named. tuf.formats.ROLENAME_SCHEMA.check_match(rolename) # Does 'roleinfo' have the correct object format? tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo) # Raises tuf.InvalidNameError. _validate_rolename(rolename) if rolename not in _roledb_dict: raise tuf.UnknownRoleError('Role does not exist: '+rolename) _roledb_dict[rolename] = copy.deepcopy(roleinfo) def get_parent_rolename(rolename): """ Return the name of the parent role for 'rolename'. Given the rolename 'a/b/c/d', return 'a/b/c'. Given 'a', return ''. rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. tuf.UnknownRoleError, if 'rolename' cannot be found in the role database. tuf.InvalidNameError, if 'rolename' is incorrectly formatted. None. A string representing the name of the parent role. """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. _check_rolename(rolename) parts = rolename.split('/') parent_rolename = '/'.join(parts[:-1]) return parent_rolename def get_all_parent_roles(rolename): """ Return a list of roles that are parents of 'rolename'. Given the rolename 'a/b/c/d', return the list: ['a', 'a/b', 'a/b/c']. Given 'a', return ['a']. rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. tuf.UnknownRoleError, if 'rolename' cannot be found in the role database. tuf.InvalidNameError, if 'rolename' is improperly formatted. None. A list containing all the parent roles. """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. _check_rolename(rolename) # List of parent roles returned. parent_roles = [] parts = rolename.split('/') # Append the first role to the list. parent_roles.append(parts[0]) # The 'roles_added' string contains the roles already added. If 'a' and 'a/b' # have been added to 'parent_roles', 'roles_added' would contain 'a/b' roles_added = parts[0] # Add each subsequent role to the previous string (with a '/' separator). # This only goes to -1 because we only want to return the parents (so we # ignore the last element). for next_role in parts[1:-1]: parent_roles.append(roles_added+'/'+next_role) roles_added = roles_added+'/'+next_role return parent_roles def role_exists(rolename): """ Verify whether 'rolename' is stored in the role database. rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. tuf.InvalidNameError, if 'rolename' is incorrectly formatted. None. Boolean. True if 'rolename' is found in the role database, False otherwise. """ # Raise tuf.FormatError, tuf.InvalidNameError. try: _check_rolename(rolename) except (tuf.FormatError, tuf.InvalidNameError): raise except tuf.UnknownRoleError: return False return True def remove_role(rolename): """ Remove 'rolename', including its delegations. rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. tuf.UnknownRoleError, if 'rolename' cannot be found in the role database. tuf.InvalidNameError, if 'rolename' is incorrectly formatted. A role, or roles, may be removed from the role database. None. """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. _check_rolename(rolename) remove_delegated_roles(rolename) # remove_delegated_roles() should have left 'rolename' in the database, and # 'rolename' was verified to exist by _check_rolename(). # Remove 'rolename'. del _roledb_dict[rolename] def remove_delegated_roles(rolename): """ Remove a role's delegations (leaving the rest of the role alone). All levels of delegation are removed, not just the directly delegated roles. If 'rolename' is 'a/b/c' and the role database contains ['a/b/c/d/e', 'a/b/c/d', 'a/b/c', 'a/b', 'a'], return ['a/b/c', 'a/b', 'a']. rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. tuf.UnknownRoleError, if 'rolename' cannot be found in the role database. tuf.InvalidNameError, if 'rolename' is incorrectly formatted. Role(s) from the role database may be deleted. None. """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. _check_rolename(rolename) # Ensure that we only care about delegated roles! rolename_with_slash = rolename + '/' for name in get_rolenames(): if name.startswith(rolename_with_slash): del _roledb_dict[name] def get_rolenames(): """ Return a list of the rolenames found in the role database. None. None. None. A list of rolenames. """ return list(_roledb_dict.keys()) def get_roleinfo(rolename): """ Return the roleinfo of 'rolename'. {'keyids': ['34345df32093bd12...'], 'threshold': 1, 'signatures': ['ab453bdf...', ...], 'paths': ['path/to/target1', 'path/to/target2', ...], 'path_hash_prefixes': ['a324fcd...', ...], 'delegations': {'keys': {}, 'roles': []}} The 'signatures', 'paths', 'path_hash_prefixes', and 'delegations' dict keys are optional. rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' is improperly formatted. tuf.UnknownRoleError, if 'rolename' does not exist. None. The roleinfo of 'rolename'. """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. _check_rolename(rolename) return copy.deepcopy(_roledb_dict[rolename]) def get_role_keyids(rolename): """ Return a list of the keyids associated with 'rolename'. Keyids are used as identifiers for keys (e.g., rsa key). A list of keyids are associated with each rolename. Signing a metadata file, such as 'root.json' (Root role), involves signing or verifying the file with a list of keys identified by keyid. rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. tuf.UnknownRoleError, if 'rolename' cannot be found in the role database. tuf.InvalidNameError, if 'rolename' is incorrectly formatted. None. A list of keyids. """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. _check_rolename(rolename) roleinfo = _roledb_dict[rolename] return roleinfo['keyids'] def get_role_threshold(rolename): """ Return the threshold value of the role associated with 'rolename'. rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. tuf.UnknownRoleError, if 'rolename' cannot be found in in the role database. tuf.InvalidNameError, if 'rolename' is incorrectly formatted. None. A threshold integer value. """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. _check_rolename(rolename) roleinfo = _roledb_dict[rolename] return roleinfo['threshold'] def get_role_paths(rolename): """ Return the paths of the role associated with 'rolename'. rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. tuf.UnknownRoleError, if 'rolename' cannot be found in the role database. tuf.InvalidNameError, if 'rolename' is incorrectly formatted. None. A list of paths. """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. _check_rolename(rolename) roleinfo = _roledb_dict[rolename] # Paths won't exist for non-target roles. try: return roleinfo['paths'] except KeyError: return dict() def get_delegated_rolenames(rolename): """ Return the delegations of a role. If 'rolename' is 'a/b/c' and the role database contains ['a/b/c/d', 'a/b/c/d/e', 'a/b/c'], return ['a/b/c/d', 'a/b/c/d/e'] rolename: An object representing the role's name, conformant to 'ROLENAME_SCHEMA' (e.g., 'root', 'snapshot', 'timestamp'). tuf.FormatError, if 'rolename' does not have the correct object format. tuf.UnknownRoleError, if 'rolename' cannot be found in the role database. tuf.InvalidNameError, if 'rolename' is incorrectly formatted. None. A list of rolenames. Note that the rolenames are *NOT* sorted by order of delegation. """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. _check_rolename(rolename) # The list of delegated roles to be returned. delegated_roles = [] # Ensure that we only care about delegated roles! rolename_with_slash = rolename + '/' for name in get_rolenames(): if name.startswith(rolename_with_slash): delegated_roles.append(name) return delegated_roles def clear_roledb(): """ Reset the roledb database. None. None. None. None. """ _roledb_dict.clear() def _check_rolename(rolename): """ Raise tuf.FormatError if 'rolename' does not match 'tuf.formats.ROLENAME_SCHEMA', tuf.UnknownRoleError if 'rolename' is not found in the role database, or tuf.InvalidNameError if 'rolename' is not formatted correctly. """ # Does 'rolename' have the correct object format? # This check will ensure 'rolename' has the appropriate number of objects # and object types, and that all dict keys are properly named. tuf.formats.ROLENAME_SCHEMA.check_match(rolename) # Raises tuf.InvalidNameError. _validate_rolename(rolename) if rolename not in _roledb_dict: raise tuf.UnknownRoleError('Role name does not exist: '+rolename) def _validate_rolename(rolename): """ Raise tuf.InvalidNameError if 'rolename' is not formatted correctly. It is assumed 'rolename' has been checked against 'ROLENAME_SCHEMA' prior to calling this function. """ if rolename == '': raise tuf.InvalidNameError('Rolename must not be an empty string') if rolename != rolename.strip(): raise tuf.InvalidNameError( 'Invalid rolename. Cannot start or end with whitespace: '+rolename) if rolename.startswith('/') or rolename.endswith('/'): raise tuf.InvalidNameError( 'Invalid rolename. Cannot start or end with "/": '+rolename)