
#!/usr/bin/env python

# Copyright 2013 - 2017, New York University and the TUF contributors
# SPDX-License-Identifier: MIT OR Apache-2.0

"""
<Program Name>
  repository_tool.py

<Author>
  Vladimir Diaz <vladimir.v.diaz@gmail.com>

<Started>
  October 19, 2013

<Copyright>
  See LICENSE-MIT OR LICENSE for licensing information.

<Purpose>
  Provide a tool that can create a TUF repository.  It can be used with the
  Python interpreter in interactive mode, or imported directly into a Python
  module.  See 'tuf/README' for the complete guide to using
  'tuf.repository_tool.py'.
"""

import os
import time
import datetime
import logging
import tempfile
import shutil
import json

from collections import deque

from securesystemslib import exceptions as sslib_exceptions
from securesystemslib import formats as sslib_formats
from securesystemslib import util as sslib_util
from securesystemslib import storage as sslib_storage

from tuf import exceptions
from tuf import formats
from tuf import keydb
from tuf import log
from tuf import repository_lib as repo_lib
from tuf import roledb


# Copy API
# pylint: disable=unused-import

# Copy generic repository API functions to be used via `repository_tool`
from tuf.repository_lib import (
    create_tuf_client_directory,
    disable_console_log_messages)


# Copy key-related API functions to be used via `repository_tool`
from tuf.repository_lib import (
    import_rsa_privatekey_from_file,
    import_ed25519_privatekey_from_file)

from securesystemslib.interface import (
    generate_and_write_rsa_keypair,
    generate_and_write_rsa_keypair_with_prompt,
    generate_and_write_unencrypted_rsa_keypair,
    generate_and_write_ecdsa_keypair,
    generate_and_write_ecdsa_keypair_with_prompt,
    generate_and_write_unencrypted_ecdsa_keypair,
    generate_and_write_ed25519_keypair,
    generate_and_write_ed25519_keypair_with_prompt,
    generate_and_write_unencrypted_ed25519_keypair,
    import_rsa_publickey_from_file,
    import_ecdsa_publickey_from_file,
    import_ed25519_publickey_from_file,
    import_ecdsa_privatekey_from_file)

from securesystemslib.keys import (
    format_metadata_to_key,
    generate_rsa_key,
    generate_ecdsa_key,
    generate_ed25519_key,
    import_rsakey_from_pem,
    import_ecdsakey_from_pem)


# See 'log.py' to learn how logging is handled in TUF.
logger = logging.getLogger(__name__)

# Add a console handler so that users are aware of potentially unintended
# states, such as multiple roles that share keys.
log.add_console_handler()
log.set_console_log_level(logging.INFO)

# Recommended RSA key sizes:
# https://en.wikipedia.org/wiki/Key_size#Asymmetric_algorithm_key_lengths
# Based on the above, RSA keys of size 3072 are expected to provide security
# through 2031 and beyond.
DEFAULT_RSA_KEY_BITS=3072

# The default number of hashed bin delegations
DEFAULT_NUM_BINS=1024

# 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'

# The extension of TUF metadata.
METADATA_EXTENSION = '.json'

# Expiration date delta, in seconds, of the top-level roles.  A metadata
# expiration date is set by taking the current time and adding the expiration
# seconds listed below.

# Initial 'root.json' expiration time of 1 year.
ROOT_EXPIRATION = 31556900

# Initial 'targets.json' expiration time of 3 months.
TARGETS_EXPIRATION = 7889230

# Initial 'snapshot.json' expiration time of 1 week.
SNAPSHOT_EXPIRATION = 604800

# Initial 'timestamp.json' expiration time of 1 day.
TIMESTAMP_EXPIRATION = 86400


class Repository(object):
  """
  <Purpose>
    Represent a TUF repository that contains the metadata of the top-level
    roles, including all those delegated from the 'targets.json' role.  The
    repository object returned provides access to the top-level roles, and any
    delegated targets that are added as the repository is modified.  For
    example, a Repository object named 'repository' provides the following
    access by default:

    repository.root.version = 2
    repository.timestamp.expiration = datetime.datetime(2015, 8, 8, 12, 0)
    repository.snapshot.add_verification_key(...)
    repository.targets.delegate('unclaimed', ...)

    Delegating a role from 'targets' updates the attributes of the parent
    delegation, which then provides:

    repository.targets('unclaimed').add_verification_key(...)


  <Arguments>
    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.json'.

    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.

    storage_backend:
      An object which implements
      securesystemslib.storage.StorageBackendInterface.

    repository_name:
      The name of the repository.  If not supplied, 'rolename' is added to the
      'default' repository.

    use_timestamp_length:
      Whether to include the optional length attribute of the snapshot
      metadata file in the timestamp metadata.
      Default is True.

    use_timestamp_hashes:
      Whether to include the optional hashes attribute of the snapshot
      metadata file in the timestamp metadata.
      Default is True.

    use_snapshot_length:
      Whether to include the optional length attribute for targets
      metadata files in the snapshot metadata.
      Default is False to save bandwidth but without losing security
      from rollback attacks.
      Read more at section 5.6 from the Mercury paper:
      https://www.usenix.org/conference/atc17/technical-sessions/presentation/kuppusamy

    use_snapshot_hashes:
      Whether to include the optional hashes attribute for targets
      metadata files in the snapshot metadata.
      Default is False to save bandwidth but without losing security
      from rollback attacks.
      Read more at section 5.6 from the Mercury paper:
      https://www.usenix.org/conference/atc17/technical-sessions/presentation/kuppusamy

  <Exceptions>
    securesystemslib.exceptions.FormatError, if the arguments are improperly
    formatted.

  <Side Effects>
    Creates top-level role objects and assigns them as attributes.

  <Returns>
    A Repository object that contains default Metadata objects for the top-level
    roles.
  """

  def __init__(self, repository_directory, metadata_directory,
      targets_directory, storage_backend, repository_name='default',
      use_timestamp_length=True, use_timestamp_hashes=True,
      use_snapshot_length=False, use_snapshot_hashes=False):

    # Do the arguments have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    sslib_formats.PATH_SCHEMA.check_match(repository_directory)
    sslib_formats.PATH_SCHEMA.check_match(metadata_directory)
    sslib_formats.PATH_SCHEMA.check_match(targets_directory)
    sslib_formats.NAME_SCHEMA.check_match(repository_name)
    sslib_formats.BOOLEAN_SCHEMA.check_match(use_timestamp_length)
    sslib_formats.BOOLEAN_SCHEMA.check_match(use_timestamp_hashes)
    sslib_formats.BOOLEAN_SCHEMA.check_match(use_snapshot_length)
    sslib_formats.BOOLEAN_SCHEMA.check_match(use_snapshot_hashes)

    self._repository_directory = repository_directory
    self._metadata_directory = metadata_directory
    self._targets_directory = targets_directory
    self._repository_name = repository_name
    self._storage_backend = storage_backend
    self._use_timestamp_length = use_timestamp_length
    self._use_timestamp_hashes = use_timestamp_hashes
    self._use_snapshot_length = use_snapshot_length
    self._use_snapshot_hashes = use_snapshot_hashes

    try:
      roledb.create_roledb(repository_name)
      keydb.create_keydb(repository_name)

    except sslib_exceptions.InvalidNameError:
      logger.debug(repr(repository_name) + ' already exists.  Overwriting'
          ' its contents.')

    # Set the top-level role objects.
    self.root = Root(self._repository_name)
    self.snapshot = Snapshot(self._repository_name)
    self.timestamp = Timestamp(self._repository_name)
    self.targets = Targets(self._targets_directory, 'targets',
        repository_name=self._repository_name)



  def writeall(self, consistent_snapshot=False, use_existing_fileinfo=False):
    """
    <Purpose>
      Write all the JSON Metadata objects to their corresponding files for
      roles which have changed.
      writeall() 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.

    <Arguments>
      consistent_snapshot:
        A boolean indicating whether role metadata files should have their
        version numbers as filename prefix when written to disk, i.e
        'VERSION.ROLENAME.json', and target files should be copied to a
        filename that has their hex digest as filename prefix, i.e
        'HASH.FILENAME'. Note that:
        - root metadata is always written with a version prefix, independently
          of 'consistent_snapshot'
        - the latest version of each metadata file is always also written
          without version prefix
        - target files are only copied to a hash-prefixed filename if
          'consistent_snapshot' is True and 'use_existing_fileinfo' is False.
          If both are True hash-prefixed target file copies must be created
          out-of-band.

      use_existing_fileinfo:
        Boolean indicating whether the fileinfo dicts in the roledb should be
        written as-is (True) or whether hashes should be generated (False,
        requires access to the targets files on-disk).

    <Exceptions>
      tuf.exceptions.UnsignedMetadataError, if any of the top-level
      and delegated roles do not have the minimum threshold of signatures.

    <Side Effects>
      Creates metadata files in the repository's metadata directory.

    <Returns>
      None.
    """

    # Do the arguments have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly
    # formatted.
    sslib_formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot)

    # At this point, keydb and roledb must be fully populated,
    # otherwise writeall() throws a 'tuf.exceptions.UnsignedMetadataError' for
    # the top-level roles.  exception if any of the top-level roles are missing
    # signatures, keys, etc.

    # Write the metadata files of all the Targets roles that are dirty (i.e.,
    # have been modified via roledb.update_roleinfo()).
    filenames = {'root': os.path.join(self._metadata_directory,
        repo_lib.ROOT_FILENAME), 'targets': os.path.join(self._metadata_directory,
        repo_lib.TARGETS_FILENAME), 'snapshot': os.path.join(self._metadata_directory,
        repo_lib.SNAPSHOT_FILENAME), 'timestamp': os.path.join(self._metadata_directory,
        repo_lib.TIMESTAMP_FILENAME)}

    snapshot_signable = None
    dirty_rolenames = roledb.get_dirty_roles(self._repository_name)

    for dirty_rolename in dirty_rolenames:

      # Ignore top-level roles, they will be generated later in this method.
      if dirty_rolename in roledb.TOP_LEVEL_ROLES:
        continue

      dirty_filename = os.path.join(self._metadata_directory,
          dirty_rolename + METADATA_EXTENSION)
      repo_lib._generate_and_write_metadata(dirty_rolename, dirty_filename,
          self._targets_directory, self._metadata_directory,
          self._storage_backend, consistent_snapshot, filenames,
          repository_name=self._repository_name,
          use_existing_fileinfo=use_existing_fileinfo)

    # Metadata should be written in (delegated targets -> root -> targets ->
    # snapshot -> timestamp) order.  Begin by generating the 'root.json'
    # metadata file.  _generate_and_write_metadata() raises a
    # 'securesystemslib.exceptions.Error' exception if the metadata cannot be
    # written.
    root_roleinfo = roledb.get_roleinfo('root', self._repository_name)
    old_consistent_snapshot = root_roleinfo['consistent_snapshot']
    if 'root' in dirty_rolenames or consistent_snapshot != old_consistent_snapshot:
      repo_lib._generate_and_write_metadata('root', filenames['root'],
          self._targets_directory, self._metadata_directory,
          self._storage_backend, consistent_snapshot, filenames,
          repository_name=self._repository_name)

    # Generate the 'targets.json' metadata file.
    if 'targets' in dirty_rolenames:
      repo_lib._generate_and_write_metadata('targets', filenames['targets'],
          self._targets_directory, self._metadata_directory,
          self._storage_backend, consistent_snapshot,
          repository_name=self._repository_name,
          use_existing_fileinfo=use_existing_fileinfo)

    # Generate the 'snapshot.json' metadata file.
    if 'snapshot' in dirty_rolenames:
      snapshot_signable, junk = repo_lib._generate_and_write_metadata('snapshot',
          filenames['snapshot'], self._targets_directory,
          self._metadata_directory, self._storage_backend,
          consistent_snapshot, filenames,
          repository_name=self._repository_name,
          use_snapshot_length=self._use_snapshot_length,
          use_snapshot_hashes=self._use_snapshot_hashes)

    # Generate the 'timestamp.json' metadata file.
    if 'timestamp' in dirty_rolenames:
      repo_lib._generate_and_write_metadata('timestamp', filenames['timestamp'],
          self._targets_directory, self._metadata_directory,
          self._storage_backend, consistent_snapshot,
          filenames, repository_name=self._repository_name,
          use_timestamp_length=self._use_timestamp_length,
          use_timestamp_hashes=self._use_timestamp_hashes)

    roledb.unmark_dirty(dirty_rolenames, self._repository_name)

    # Delete the metadata of roles no longer in 'roledb'.  Obsolete roles
    # may have been revoked and should no longer have their metadata files
    # available on disk, otherwise loading a repository may unintentionally
    # load them.
    if snapshot_signable is not None:
      repo_lib._delete_obsolete_metadata(self._metadata_directory,
          snapshot_signable['signed'], consistent_snapshot, self._repository_name,
          self._storage_backend)



  def write(self, rolename, consistent_snapshot=False, increment_version_number=True,
      use_existing_fileinfo=False):
    """
    <Purpose>
      Write the JSON metadata for 'rolename' to its corresponding file on disk.
      Unlike writeall(), write() allows the metadata file to contain an invalid
      threshold of signatures.

    <Arguments>
      rolename:
        The name of the role to be written to disk.

      consistent_snapshot:
        A boolean indicating whether the role metadata file should have its
        version number as filename prefix when written to disk, i.e
        'VERSION.ROLENAME.json'. Note that:
        - root metadata is always written with a version prefix, independently
          of 'consistent_snapshot'
        - the latest version of the metadata file is always also written
          without version prefix
        - if the metadata is targets metadata and 'consistent_snapshot' is
          True, the corresponding target files are copied to a filename with
          their hex digest as filename prefix, i.e 'HASH.FILENAME', unless
          'use_existing_fileinfo' is also True.
          If 'consistent_snapshot' and 'use_existing_fileinfo' both are True,
          hash-prefixed target file copies must be created out-of-band.

      increment_version_number:
        Boolean indicating whether the version number of 'rolename' should be
        automatically incremented.

      use_existing_fileinfo:
        Boolean indicating whether the fileinfo dicts in the roledb should be
        written as-is (True) or whether hashes should be generated (False,
        requires access to the targets files on-disk).

    <Exceptions>
      None.

    <Side Effects>
      Creates metadata files in the repository's metadata directory.

    <Returns>
      None.
    """

    rolename_filename = os.path.join(self._metadata_directory,
                                     rolename + METADATA_EXTENSION)

    filenames = {'root': os.path.join(self._metadata_directory, repo_lib.ROOT_FILENAME),
        'targets': os.path.join(self._metadata_directory, repo_lib.TARGETS_FILENAME),
         'snapshot': os.path.join(self._metadata_directory, repo_lib.SNAPSHOT_FILENAME),
         'timestamp': os.path.join(self._metadata_directory, repo_lib.TIMESTAMP_FILENAME)}

    repo_lib._generate_and_write_metadata(rolename, rolename_filename,
        self._targets_directory, self._metadata_directory,
        self._storage_backend, consistent_snapshot,
        filenames=filenames, allow_partially_signed=True,
        increment_version_number=increment_version_number,
        repository_name=self._repository_name,
        use_existing_fileinfo=use_existing_fileinfo)

    # Ensure 'rolename' is no longer marked as dirty after the successful write().
    roledb.unmark_dirty([rolename], self._repository_name)





  def status(self):
    """
    <Purpose>
      Determine the status of the top-level roles.  status() checks if each
      role provides sufficient public and private keys, signatures, and that a
      valid metadata file is generated if writeall() or write() were to be
      called.  Metadata files are temporarily written so that file hashes and
      lengths may be verified, determine if delegated role trust is fully
      obeyed, and target paths valid according to parent roles.  status() does
      not do a simple check for number of threshold keys and signatures.

    <Arguments>
      None.

    <Exceptions>
      None.

    <Side Effects>
      Generates and writes temporary metadata files.

    <Returns>
      None.
    """

    temp_repository_directory = None

    # Generate and write temporary metadata so that full verification of
    # metadata is possible, such as verifying signatures, digests, and file
    # content.  Ensure temporary files are removed after verification results
    # are completed.
    try:
      temp_repository_directory = tempfile.mkdtemp()
      targets_directory = self._targets_directory
      metadata_directory = os.path.join(temp_repository_directory,
          METADATA_STAGED_DIRECTORY_NAME)
      os.mkdir(metadata_directory)

      # Verify the top-level roles and log the results.
      repo_lib._log_status_of_top_level_roles(targets_directory,
          metadata_directory, self._repository_name, self._storage_backend)

    finally:
      shutil.rmtree(temp_repository_directory, ignore_errors=True)



  def dirty_roles(self):
    """
    <Purpose>
      Print/log the roles that have been modified.  For example, if some role's
      version number is changed (repository.timestamp.version = 2), it is
      considered dirty and will be included in the list of dirty roles
      printed/logged here.  Unlike status(), signatures, public keys, targets,
      etc. are not verified.  status() should be called instead if the caller
      would like to verify if a valid role file is generated if writeall() were
      to be called.

    <Arguments>
      None.

    <Exceptions>
      None.

    <Side Effects>
      None.

    <Returns>
      None.
    """

    logger.info('Dirty roles: ' + str(roledb.get_dirty_roles(self._repository_name)))



  def mark_dirty(self, roles):
    """
    <Purpose>
      Mark the list of 'roles' as dirty.

    <Arguments>
      roles:
        A list of roles to mark as dirty.  on the next write, these roles
        will be written to disk.

    <Exceptions>
      None.

    <Side Effects>
      None.

    <Returns>
      None.
    """

    roledb.mark_dirty(roles, self._repository_name)



  def unmark_dirty(self, roles):
    """
    <Purpose>
      No longer mark the list of 'roles' as dirty.

    <Arguments>
      roles:
        A list of roles to mark as dirty.  on the next write, these roles
        will be written to disk.

    <Exceptions>
      None.

    <Side Effects>
      None.

    <Returns>
      None.
    """

    roledb.unmark_dirty(roles, self._repository_name)



  @staticmethod
  def get_filepaths_in_directory(files_directory, recursive_walk=False,
      followlinks=True):
    """
    <Purpose>
      Walk the given 'files_directory' and build a list of target files found.

    <Arguments>
      files_directory:
        The path to a directory of target files.

      recursive_walk:
        To recursively walk the directory, set recursive_walk=True.

      followlinks:
        To follow symbolic links, set followlinks=True.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the arguments are improperly
      formatted.

      securesystemslib.exceptions.Error, if 'file_directory' is not a valid
      directory.

      Python IO exceptions.

    <Side Effects>
      None.

    <Returns>
      A list of absolute paths to target files in the given 'files_directory'.
    """

    # Do the arguments have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    sslib_formats.PATH_SCHEMA.check_match(files_directory)
    sslib_formats.BOOLEAN_SCHEMA.check_match(recursive_walk)
    sslib_formats.BOOLEAN_SCHEMA.check_match(followlinks)

    # Ensure a valid directory is given.
    if not os.path.isdir(files_directory):
      raise sslib_exceptions.Error(repr(files_directory) + ' is not'
        ' a directory.')

    # A list of the target filepaths found in 'files_directory'.
    targets = []

    # FIXME: We need a way to tell Python 2, but not Python 3, to return
    # filenames in Unicode; see #61 and:
    # http://docs.python.org/howto/unicode.html#unicode-filenames
    for dirpath, dirnames, filenames in os.walk(files_directory,
                                                followlinks=followlinks):
      for filename in filenames:
        full_target_path = os.path.join(os.path.abspath(dirpath), filename)
        targets.append(full_target_path)

      # Prune the subdirectories to walk right now if we do not wish to
      # recursively walk 'files_directory'.
      if recursive_walk is False:
        del dirnames[:]

      else:
        logger.debug('Not pruning subdirectories ' + repr(dirnames))

    return targets





class Metadata(object):
  """
  <Purpose>
    Provide a base class to represent a TUF Metadata role.  There are four
    top-level roles: Root, Targets, Snapshot, and Timestamp.  The Metadata
    class provides methods that are needed by all top-level roles, such as
    adding and removing public keys, private keys, and signatures.  Metadata
    attributes, such as rolename, version, threshold, expiration, and key list
    are also provided by the Metadata base class.

  <Arguments>
    None.

  <Exceptions>
    None.

  <Side Effects>
    None.

  <Returns>
    None.
  """

  def __init__(self):
    self._rolename = None
    self._repository_name = None


  def add_verification_key(self, key, expires=None):
    """
    <Purpose>
      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.

      >>>
      >>>
      >>>

    <Arguments>
      key:
        The role key to be added, conformant to
        'securesystemslib.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.

      expires:
        The date in which 'key' expires.  'expires' is a datetime.datetime()
        object.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if any of the arguments are
      improperly formatted.

      securesystemslib.exceptions.Error, if the 'expires' datetime has already
      expired.

    <Side Effects>
      The role's entries in 'keydb' and 'roledb' are updated.

    <Returns>
      None.
    """

    # Does 'key' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    sslib_formats.ANYKEY_SCHEMA.check_match(key)

    # If 'expires' is unset, choose a default expiration for 'key'.  By
    # default, Root, Targets, Snapshot, and Timestamp keys are set to expire
    # 1 year, 3 months, 1 week, and 1 day from the current time, respectively.
    if expires is None:
      if self.rolename == 'root':
        expires = \
          formats.unix_timestamp_to_datetime(int(time.time() + ROOT_EXPIRATION))

      elif self.rolename == 'Targets':
        expires = \
          formats.unix_timestamp_to_datetime(int(time.time() + TARGETS_EXPIRATION))

      elif self.rolename == 'Snapshot':
        expires = \
          formats.unix_timestamp_to_datetime(int(time.time() + SNAPSHOT_EXPIRATION))

      elif self.rolename == 'Timestamp':
        expires = \
          formats.unix_timestamp_to_datetime(int(time.time() + TIMESTAMP_EXPIRATION))

      else:
        expires = \
          formats.unix_timestamp_to_datetime(int(time.time() + TIMESTAMP_EXPIRATION))

    # Is 'expires' a datetime.datetime() object?
    # Raise 'securesystemslib.exceptions.FormatError' if not.
    if not isinstance(expires, datetime.datetime):
      raise sslib_exceptions.FormatError(repr(expires) + ' is not a'
          ' datetime.datetime() object.')

    # Truncate the microseconds value to produce a correct schema string
    # of the form 'yyyy-mm-ddThh:mm:ssZ'.
    expires = expires.replace(microsecond = 0)

    # Ensure the expiration has not already passed.
    current_datetime = \
      formats.unix_timestamp_to_datetime(int(time.time()))

    if expires < current_datetime:
      raise sslib_exceptions.Error(repr(key) + ' has already'
          ' expired.')

    # Update the key's 'expires' entry.
    expires = expires.isoformat() + 'Z'
    key['expires'] = expires

    # Ensure 'key', which should contain the public portion, is added to
    # 'keydb'.  Add 'key' to the list of recognized keys.
    # Keys may be shared, so do not raise an exception if 'key' has already
    # been loaded.
    try:
      keydb.add_key(key, repository_name=self._repository_name)

    except exceptions.KeyAlreadyExistsError:
      logger.warning('Adding a verification key that has already been used.')

    keyid = key['keyid']
    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)

    # Save the keyids that are being replaced since certain roles will need to
    # re-sign metadata with these keys (e.g., root).  Use list() to make a copy
    # of roleinfo['keyids'] to ensure we're modifying distinct lists.
    previous_keyids = list(roleinfo['keyids'])

    # Add 'key' to the role's entry in 'roledb', and avoid duplicates.
    if keyid not in roleinfo['keyids']:
      roleinfo['keyids'].append(keyid)
      roleinfo['previous_keyids'] = previous_keyids

      roledb.update_roleinfo(self._rolename, roleinfo,
          repository_name=self._repository_name)



  def remove_verification_key(self, key):
    """
    <Purpose>
      Remove 'key' from the role's currently recognized list of role keys.
      The role expects a threshold number of signatures.

      >>>
      >>>
      >>>

    <Arguments>
      key:
        The role's key, conformant to 'securesystemslib.formats.ANYKEY_SCHEMA'.
        'key' should contain only the public portion, as only the public key is
        needed.  The 'add_verification_key()' method should have previously
        added 'key'.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the 'key' argument is
      improperly formatted.

      securesystemslib.exceptions.Error, if the 'key' argument has not been
      previously added.

    <Side Effects>
      Updates the role's 'roledb' entry.

    <Returns>
      None.
    """

    # Does 'key' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    sslib_formats.ANYKEY_SCHEMA.check_match(key)

    keyid = key['keyid']
    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)

    if keyid in roleinfo['keyids']:
      roleinfo['keyids'].remove(keyid)

      roledb.update_roleinfo(self._rolename, roleinfo,
          repository_name=self._repository_name)

    else:
      raise sslib_exceptions.Error('Verification key not found.')



  def load_signing_key(self, key):
    """
    <Purpose>
      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.

      >>>
      >>>
      >>>

    <Arguments>
      key:
        The role's key, conformant to 'securesystemslib.formats.ANYKEY_SCHEMA'.
        It must contain the private key, so that role signatures may be
        generated when writeall() or write() is eventually called to generate
        valid metadata files.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if 'key' is improperly formatted.

      securesystemslib.exceptions.Error, if the private key is not found in 'key'.

    <Side Effects>
      Updates the role's 'keydb' and 'roledb' entries.

    <Returns>
      None.
    """

    # Does 'key' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    sslib_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 'private' not in key['keyval'] or not len(key['keyval']['private']):
      raise sslib_exceptions.Error('This is not a private key.')

    # 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:
      keydb.add_key(key, repository_name=self._repository_name)

    except exceptions.KeyAlreadyExistsError:
      keydb.remove_key(key['keyid'], self._repository_name)
      keydb.add_key(key, repository_name=self._repository_name)

    # Update the role's 'signing_keys' field in 'roledb'.
    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)
    if key['keyid'] not in roleinfo['signing_keyids']:
      roleinfo['signing_keyids'].append(key['keyid'])

      roledb.update_roleinfo(self.rolename, roleinfo,
          repository_name=self._repository_name)



  def unload_signing_key(self, key):
    """
    <Purpose>
      Remove a previously loaded role private key (i.e., load_signing_key()).
      The keyid of the 'key' is removed from the list of recognized signing
      keys.

      >>>
      >>>
      >>>

    <Arguments>
      key:
        The role key to be unloaded, conformant to
        'securesystemslib.formats.ANYKEY_SCHEMA'.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the 'key' argument is
      improperly formatted.

      securesystemslib.exceptions.Error, if the 'key' argument has not been
      previously loaded.

    <Side Effects>
      Updates the signing keys of the role in 'roledb'.

    <Returns>
      None.
    """

    # Does 'key' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    sslib_formats.ANYKEY_SCHEMA.check_match(key)

    # Update the role's 'signing_keys' field in 'roledb'.
    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)

    # TODO: Should we consider removing keys from keydb that are no longer
    # associated with any roles?  There could be many no-longer-used keys
    # stored in the keydb if not.  For now, just unload the key.
    if key['keyid'] in roleinfo['signing_keyids']:
      roleinfo['signing_keyids'].remove(key['keyid'])

      roledb.update_roleinfo(self.rolename, roleinfo,
          repository_name=self._repository_name)

    else:
      raise sslib_exceptions.Error('Signing key not found.')



  def add_signature(self, signature, mark_role_as_dirty=True):
    """
    <Purpose>
      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.

      >>>
      >>>
      >>>

    <Arguments>
      signature:
        The signature to be added to the role, conformant to
        'securesystemslib.formats.SIGNATURE_SCHEMA'.

      mark_role_as_dirty:
        A boolean indicating whether the updated 'roleinfo' for 'rolename'
        should be marked as dirty.  The caller might not want to mark
        'rolename' as dirty if it is loading metadata from disk and only wants
        to populate roledb.py.  Likewise, add_role() would support a similar
        boolean to allow the repository tools to successfully load roles via
        load_repository() without needing to mark these roles as dirty (default
        behavior).

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the 'signature' argument is
      improperly formatted.

    <Side Effects>
      Adds 'signature', if not already added, to the role's 'signatures' field
      in 'roledb'.

    <Returns>
      None.
    """

    # Does 'signature' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    sslib_formats.SIGNATURE_SCHEMA.check_match(signature)
    sslib_formats.BOOLEAN_SCHEMA.check_match(mark_role_as_dirty)

    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)

    # Ensure the roleinfo 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)
      roledb.update_roleinfo(self.rolename, roleinfo, mark_role_as_dirty,
          repository_name=self._repository_name)

    else:
      logger.debug('Signature already exists for role: ' + repr(self.rolename))



  def remove_signature(self, signature):
    """
    <Purpose>
      Remove a previously loaded, or added, role 'signature'.  A role must
      contain a threshold number of signatures to be considered fully signed.

      >>>
      >>>
      >>>

    <Arguments>
      signature:
        The role signature to remove, conformant to
        'securesystemslib.formats.SIGNATURE_SCHEMA'.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the 'signature' argument is
      improperly formatted.

      securesystemslib.exceptions.Error, if 'signature' has not been previously
      added to this role.

    <Side Effects>
      Updates the 'signatures' field of the role in 'roledb'.

    <Returns>
      None.
    """

    # Does 'signature' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    sslib_formats.SIGNATURE_SCHEMA.check_match(signature)

    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)

    if signature in roleinfo['signatures']:
      roleinfo['signatures'].remove(signature)

      roledb.update_roleinfo(self.rolename, roleinfo,
          repository_name=self._repository_name)

    else:
      raise sslib_exceptions.Error('Signature not found.')



  @property
  def signatures(self):
    """
    <Purpose>
      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_verification_key() method.

    <Arguments>
      None.

    <Exceptions>
      None.

    <Side Effects>
      None.

    <Returns>
      A list of signatures, conformant to
      'securesystemslib.formats.SIGNATURES_SCHEMA'.
    """

    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)
    signatures = roleinfo['signatures']

    return signatures



  @property
  def keys(self):
    """
    <Purpose>
      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.)

    <Arguments>
      None.

    <Exceptions>
      None.

    <Side Effects>
      None.

    <Returns>
      A list of the role's keyids (i.e., keyids of the keys).
    """

    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)
    keyids = roleinfo['keyids']

    return keyids



  @property
  def rolename(self):
    """
    <Purpose>
      Return the role's name.
      Examples: 'root', 'timestamp', 'targets/unclaimed/django'.

    <Arguments>
      None.

    <Exceptions>
      None.

    <Side Effects>
      None.

    <Returns>
      The role's name, conformant to 'tuf.formats.ROLENAME_SCHEMA'.
      Examples: 'root', 'timestamp', 'targets/unclaimed/django'.
    """

    return self._rolename



  @property
  def version(self):
    """
    <Purpose>
      A getter method that returns the role's version number, conformant to
      'tuf.formats.VERSION_SCHEMA'.

    <Arguments>
      None.

    <Exceptions>
      None.

    <Side Effects>
      None.

    <Returns>
      The role's version number, conformant to
      'tuf.formats.VERSION_SCHEMA'.
    """

    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)
    version = roleinfo['version']

    return version



  @version.setter
  def version(self, version):
    """
    <Purpose>
      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.

      >>>
      >>>
      >>>

    <Arguments>
      version:
        The role's version number, conformant to
        'tuf.formats.VERSION_SCHEMA'.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the 'version' argument is
      improperly formatted.

    <Side Effects>
      Modifies the 'version' attribute of the Repository object and updates the
      role's version in 'roledb'.

    <Returns>
      None.
    """

    # Does 'version' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    formats.METADATAVERSION_SCHEMA.check_match(version)

    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)
    roleinfo['version'] = version

    roledb.update_roleinfo(self._rolename, roleinfo,
        repository_name=self._repository_name)



  @property
  def threshold(self):
    """
    <Purpose>
      Return the role's threshold value.  A role is considered fully signed if
      a threshold number of signatures is available.

    <Arguments>
      None.

    <Exceptions>
      None.

    <Side Effects>
      None.

    <Returns>
      The role's threshold value, conformant to
      'tuf.formats.THRESHOLD_SCHEMA'.
    """

    roleinfo = roledb.get_roleinfo(self._rolename, self._repository_name)
    threshold = roleinfo['threshold']

    return threshold



  @threshold.setter
  def threshold(self, threshold):
    """
    <Purpose>
      A setter method that modified the threshold value of the role.  Metadata
      is considered fully signed if a 'threshold' number of signatures is
      available.

      >>>
      >>>
      >>>

    <Arguments>
      threshold:
        An integer value that sets the role's threshold value, or the minimum
        number of signatures needed for metadata to be considered fully
        signed.  Conformant to 'tuf.formats.THRESHOLD_SCHEMA'.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the 'threshold' argument is
      improperly formatted.

    <Side Effects>
      Modifies the threshold attribute of the Repository object and updates
      the roles threshold in 'roledb'.

    <Returns>
      None.
    """

    # Does 'threshold' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    formats.THRESHOLD_SCHEMA.check_match(threshold)

    roleinfo = roledb.get_roleinfo(self._rolename, self._repository_name)
    roleinfo['previous_threshold'] = roleinfo['threshold']
    roleinfo['threshold'] = threshold

    roledb.update_roleinfo(self._rolename, roleinfo,
        repository_name=self._repository_name)


  @property
  def expiration(self):
    """
    <Purpose>
      A getter method that returns the role's expiration datetime.

    <Arguments>
      None.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the expiration cannot be
      parsed correctly

    <Side Effects>
      None.

    <Returns>
      The role's expiration datetime, a datetime.datetime() object.
    """

    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)
    expires = roleinfo['expires']

    return formats.expiry_string_to_datetime(expires)



  @expiration.setter
  def expiration(self, datetime_object):
    """
    <Purpose>
      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 method.

      >>>
      >>>
      >>>

    <Arguments>
      datetime_object:
        The datetime expiration of the role, a datetime.datetime() object.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if 'datetime_object' is not a
      datetime.datetime() object.

      securesystemslib.exceptions.Error, if 'datetime_object' has already
      expired.

    <Side Effects>
      Modifies the expiration attribute of the Repository object.
      The datetime given will be truncated to microseconds = 0

    <Returns>
      None.
    """

    # Is 'datetime_object' a datetime.datetime() object?
    # Raise 'securesystemslib.exceptions.FormatError' if not.
    if not isinstance(datetime_object, datetime.datetime):
      raise sslib_exceptions.FormatError(
          repr(datetime_object) + ' is not a datetime.datetime() object.')

    # truncate the microseconds value to produce a correct schema string
    # of the form yyyy-mm-ddThh:mm:ssZ
    datetime_object = datetime_object.replace(microsecond = 0)

    # Ensure the expiration has not already passed.
    current_datetime_object = \
      formats.unix_timestamp_to_datetime(int(time.time()))

    if datetime_object < current_datetime_object:
      raise sslib_exceptions.Error(repr(self.rolename) + ' has'
        ' already expired.')

    # Update the role's 'expires' entry in 'roledb'.
    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)
    expires = datetime_object.isoformat() + 'Z'
    roleinfo['expires'] = expires

    roledb.update_roleinfo(self.rolename, roleinfo,
        repository_name=self._repository_name)



  @property
  def signing_keys(self):
    """
    <Purpose>
      A getter method that returns a list of the role's signing keys.

      >>>
      >>>
      >>>

    <Arguments>
      None.

    <Exceptions>
      None.

    <Side Effects>
      None.

    <Returns>
      A list of keyids of the role's signing keys, conformant to
      'securesystemslib.formats.KEYIDS_SCHEMA'.
    """

    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)
    signing_keyids = roleinfo['signing_keyids']

    return signing_keyids





class Root(Metadata):
  """
  <Purpose>
    Represent a Root role object.  The root role is responsible for
    listing the public keys and threshold of all the top-level roles, including
    itself.  Top-level metadata is rejected if it does not comply with what is
    specified by the Root role.

    This Root object sub-classes Metadata, so the expected Metadata
    operations like adding/removing public keys, signatures, private keys, and
    updating metadata attributes (e.g., version and expiration) is supported.
    Since Root is a top-level role and must exist, a default Root object
    is instantiated when a new Repository object is created.

    >>>
    >>>
    >>>

  <Arguments>
    repository_name:
      The name of the repository.  If not supplied, 'rolename' is added to the
      'default' repository.

  <Exceptions>
    tuf.exceptions.FormatError, if the argument is improperly formatted.

  <Side Effects>
    A 'root' role is added to 'roledb'.

  <Returns>
    None.
  """

  def __init__(self, repository_name):

    super(Root, self).__init__()

    self._rolename = 'root'
    self._repository_name = repository_name

    # Is 'repository_name' properly formatted?  Otherwise, raise a
    # tuf.exceptions.FormatError exception.
    formats.ROLENAME_SCHEMA.check_match(repository_name)

    # By default, 'snapshot' metadata is set to expire 1 week from the current
    # time.  The expiration may be modified.
    expiration = formats.unix_timestamp_to_datetime(
        int(time.time() + ROOT_EXPIRATION))
    expiration = expiration.isoformat() + 'Z'

    roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1,
                'signatures': [], 'version': 0, 'consistent_snapshot': False,
                'expires': expiration, 'partial_loaded': False}
    try:
      roledb.add_role(self._rolename, roleinfo, self._repository_name)

    except exceptions.RoleAlreadyExistsError:
      pass





class Timestamp(Metadata):
  """
  <Purpose>
    Represent a Timestamp role object.  The timestamp role is responsible for
    referencing the latest version of the Snapshot role.  Under normal
    conditions, it is the only role to be downloaded from a remote repository
    without a known file length and hash.  An upper length limit is set, though.
    Also, its signatures are also verified to be valid according to the Root
    role.  If invalid metadata can only be downloaded by the client, Root
    is the only other role that is downloaded without a known length and hash.
    This case may occur if a role's signing keys have been revoked and a newer
    Root file is needed to list the updated keys.

    This Timestamp object sub-classes Metadata, so the expected Metadata
    operations like adding/removing public keys, signatures, private keys, and
    updating metadata attributes (e.g., version and expiration) is supported.
    Since Snapshot is a top-level role and must exist, a default Timestamp
    object is instantiated when a new Repository object is created.

    >>>
    >>>
    >>>

  <Arguments>
    repository_name:
      The name of the repository.  If not supplied, 'rolename' is added to the
      'default' repository.

  <Exceptions>
    tuf.exceptions.FormatError, if the argument is improperly formatted.

  <Side Effects>
    A 'timestamp' role is added to 'roledb'.

  <Returns>
    None.
  """

  def __init__(self, repository_name):

    super(Timestamp, self).__init__()

    self._rolename = 'timestamp'
    self._repository_name = repository_name

    # Is 'repository_name' properly formatted?  Otherwise, raise a
    # tuf.exceptions.FormatError exception.
    sslib_formats.NAME_SCHEMA.check_match(repository_name)

    # By default, 'root' metadata is set to expire 1 year from the current
    # time.  The expiration may be modified.
    expiration = formats.unix_timestamp_to_datetime(
        int(time.time() + TIMESTAMP_EXPIRATION))
    expiration = expiration.isoformat() + 'Z'

    roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1,
                'signatures': [], 'version': 0, 'expires': expiration,
                'partial_loaded': False}

    try:
      roledb.add_role(self.rolename, roleinfo, self._repository_name)

    except exceptions.RoleAlreadyExistsError:
      pass





class Snapshot(Metadata):
  """
  <Purpose>
    Represent a Snapshot role object.  The snapshot role is responsible for
    referencing the other top-level roles (excluding Timestamp) and all
    delegated roles.

    This Snapshot object sub-classes Metadata, so the expected
    Metadata operations like adding/removing public keys, signatures, private
    keys, and updating metadata attributes (e.g., version and expiration) is
    supported.  Since Snapshot is a top-level role and must exist, a default
    Snapshot object is instantiated when a new Repository object is created.

    >>>
    >>>
    >>>

  <Arguments>
    repository_name:
      The name of the repository.  If not supplied, 'rolename' is added to the
      'default' repository.

  <Exceptions>
    tuf.exceptions.FormatError, if the argument is improperly formatted.

  <Side Effects>
    A 'snapshot' role is added to 'roledb'.

  <Returns>
    None.
  """

  def __init__(self, repository_name):

    super(Snapshot, self).__init__()

    self._rolename = 'snapshot'
    self._repository_name = repository_name

    # Is 'repository_name' properly formatted?  Otherwise, raise a
    # tuf.exceptions.FormatError exception.
    sslib_formats.NAME_SCHEMA.check_match(repository_name)

    # By default, 'snapshot' metadata is set to expire 1 week from the current
    # time.  The expiration may be modified.
    expiration = formats.unix_timestamp_to_datetime(
        int(time.time() + SNAPSHOT_EXPIRATION))
    expiration = expiration.isoformat() + 'Z'

    roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1,
                'signatures': [], 'version': 0, 'expires': expiration,
                'partial_loaded': False}

    try:
      roledb.add_role(self._rolename, roleinfo, self._repository_name)

    except exceptions.RoleAlreadyExistsError:
      pass





class Targets(Metadata):
  """
  <Purpose>
    Represent a Targets role object.  Targets roles include the top-level role
    'targets.json' and all delegated roles (e.g., 'targets/unclaimed/django').
    The expected operations of Targets metadata is included, such as adding
    and removing repository target files, making and revoking delegations, and
    listing the target files provided by it.

    Adding or removing a delegation causes the attributes of the Targets object
    to be updated.  That is, if the 'django' Targets object is delegated by
    'targets/unclaimed', a new attribute is added so that the following
    code statement is supported:
    repository.targets('unclaimed')('django').version = 2

    Likewise, revoking a delegation causes removal of the delegation attribute.

    This Targets object sub-classes Metadata, so the expected Metadata
    operations like adding/removing public keys, signatures, private keys, and
    updating metadata attributes (e.g., version and expiration) is supported.
    Since Targets is a top-level role and must exist, a default Targets object
    (for 'targets.json', not delegated roles) is instantiated when a new
    Repository object is created.

    >>>
    >>>
    >>>

  <Arguments>
    targets_directory:
      The targets directory of the Repository object.

    rolename:
      The rolename of this Targets object.

    roleinfo:
      An already populated roleinfo object of 'rolename'.  Conformant to
      'tuf.formats.ROLEDB_SCHEMA'.

    repository_name:
      The name of the repository.  If not supplied, 'rolename' is added to the
      'default' repository.

  <Exceptions>
    securesystemslib.exceptions.FormatError, if the arguments are improperly
    formatted.

  <Side Effects>
    Modifies the roleinfo of the targets role in 'roledb', or creates
    a default one named 'targets'.

  <Returns>
    None.
  """

  def __init__(self, targets_directory, rolename='targets', roleinfo=None,
               parent_targets_object=None, repository_name='default'):

    # Do the arguments have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    sslib_formats.PATH_SCHEMA.check_match(targets_directory)
    formats.ROLENAME_SCHEMA.check_match(rolename)
    sslib_formats.NAME_SCHEMA.check_match(repository_name)

    if roleinfo is not None:
      formats.ROLEDB_SCHEMA.check_match(roleinfo)

    super(Targets, self).__init__()
    self._targets_directory = targets_directory
    self._rolename = rolename
    self._target_files = []
    self._delegated_roles = {}
    self._parent_targets_object = self
    self._repository_name = repository_name

    # Keep a reference to the top-level 'targets' object.  Any delegated roles
    # that may be created, can be added to and accessed via the top-level
    # 'targets' object.
    if parent_targets_object is not None:
      self._parent_targets_object = parent_targets_object

    # By default, Targets objects are set to expire 3 months from the current
    # time.  May be later modified.
    expiration = formats.unix_timestamp_to_datetime(
        int(time.time() + TARGETS_EXPIRATION))
    expiration = expiration.isoformat() + 'Z'

    # If 'roleinfo' is not provided, set an initial default.
    if roleinfo is None:
      roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1,
                  'version': 0, 'expires': expiration,
                  'signatures': [], 'paths': {}, 'path_hash_prefixes': [],
                  'partial_loaded': False, 'delegations': {'keys': {},
                                                           'roles': []}}

    # Add the new role to the 'roledb'.
    try:
      roledb.add_role(self.rolename, roleinfo, self._repository_name)

    except exceptions.RoleAlreadyExistsError:
      pass



  def __call__(self, rolename):
    """
    <Purpose>
      Allow callable Targets object so that delegated roles may be referenced
      by their string rolenames.  Rolenames may include characters like '-' and
      are not restricted to Python identifiers.

    <Arguments>
      rolename:
        The rolename of the delegated role.  'rolename' must be a role
        previously delegated by this Targets role.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the arguments are improperly
      formatted.

      tuf.exceptions.UnknownRoleError, if 'rolename' has not been
      delegated by this Targets object.

    <Side Effects>
      Modifies the roleinfo of the targets role in 'roledb'.

    <Returns>
      The Targets object of 'rolename'.
    """

    # Do the arguments have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    formats.ROLENAME_SCHEMA.check_match(rolename)

    if rolename in self._delegated_roles:
      return self._delegated_roles[rolename]

    else:
      raise exceptions.UnknownRoleError(repr(rolename) + ' has'
          ' not been delegated by ' + repr(self.rolename))



  def add_delegated_role(self, rolename, targets_object):
    """
    <Purpose>
      Add 'targets_object' to this Targets object's list of known delegated
      roles.  Specifically, delegated Targets roles should call 'super(Targets,
      self).add_delegated_role(...)' so that the top-level 'targets' role
      contains a dictionary of all the available roles on the repository.

    <Arguments>
      rolename:
        The rolename of the delegated role.  'rolename' must be a role
        previously delegated by this Targets role.

      targets_object:
        A Targets() object.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the arguments are improperly
      formatted.

    <Side Effects>
      Updates the Target object's dictionary of delegated targets.

    <Returns>
      The Targets object of 'rolename'.
    """

    # Do the arguments have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    formats.ROLENAME_SCHEMA.check_match(rolename)

    if not isinstance(targets_object, Targets):
      raise sslib_exceptions.FormatError(repr(targets_object) + ' is'
          ' not a Targets object.')


    if rolename in self._delegated_roles:
      logger.debug(repr(rolename) + ' already exists.')

    else:
      self._delegated_roles[rolename] = targets_object



  def remove_delegated_role(self, rolename):
    """
      Remove 'rolename' from this Targets object's list of delegated roles.
      This method does not update roledb and others.

    <Arguments>
      rolename:
        The rolename of the delegated role to remove.  'rolename' should be a
        role previously delegated by this Targets role.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the argument is improperly
      formatted.

    <Side Effects>
      Updates the Target object's dictionary of delegated targets.

    <Returns>
      None.
    """

    # Do the arguments have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if any are improperly formatted.
    formats.ROLENAME_SCHEMA.check_match(rolename)

    if rolename not in self._delegated_roles:
      logger.debug(repr(rolename) + ' has not been delegated.')
      return

    else:
      del self._delegated_roles[rolename]



  @property
  def target_files(self):
    """
    <Purpose>
      A getter method that returns the target files added thus far to this
      Targets object.

      >>>
      >>>
      >>>

    <Arguments>
      None.

    <Exceptions>
      None.

    <Side Effects>
      None.

    <Returns>
      None.
    """

    target_files = roledb.get_roleinfo(self._rolename,
        self._repository_name)['paths']
    return target_files



  def add_paths(self, paths, child_rolename):
    """
    <Purpose>
      Add 'paths' to the delegated paths of 'child_rolename'.  'paths' can be a
      list of either file paths or glob patterns.  The updater client verifies
      the target paths specified by child roles, and searches for targets by
      visiting these delegated paths.  A child role may only provide targets
      specifically listed in the delegations field of the delegating role, or a
      target that matches a delegated path.

      >>>
      >>>
      >>>

    <Arguments>
      paths:
        A list of glob patterns, or file paths, that 'child_rolename' is
        trusted to provide.

      child_rolename:
        The child delegation that requires an update to its delegated or
        trusted paths, as listed in the parent role's delegations (e.g.,
        'Django' in 'unclaimed').

    <Exceptions>
      securesystemslib.exceptions.FormatError, if a path or glob pattern in
      'paths' is not a string, or if 'child_rolename' is not a formatted
      rolename.

      securesystemslib.exceptions.Error, if 'child_rolename' has not been
      delegated yet.

      tuf.exceptions.InvalidNameError, if any path in 'paths' does not match
      pattern.

    <Side Effects>
      Modifies this Targets' delegations field.

    <Returns>
      None.
    """

    # Do the argument have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.
    # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
    sslib_formats.PATHS_SCHEMA.check_match(paths)
    formats.ROLENAME_SCHEMA.check_match(child_rolename)

    # Ensure that 'child_rolename' exists, otherwise it will not have an entry
    # in the parent role's delegations field.
    if not roledb.role_exists(child_rolename, self._repository_name):
      raise sslib_exceptions.Error(repr(child_rolename) + ' does'
          ' not exist.')

    for path in paths:
      # Check if the delegated paths or glob patterns are relative and use
      # forward slash as a separator or raise an exception. Paths' existence
      # on the file system is not verified. If the path is incorrect,
      # the targetfile won't be matched successfully during a client update.
      self._check_path(path)

    # Get the current role's roleinfo, so that its delegations field can be
    # updated.
    roleinfo = roledb.get_roleinfo(self._rolename, self._repository_name)

    # Update the delegated paths of 'child_rolename' to add relative paths.
    for role in roleinfo['delegations']['roles']:
      if role['name'] == child_rolename:
        for relative_path in paths:
          if relative_path not in role['paths']:
            role['paths'].append(relative_path)

          else:
            logger.debug(repr(relative_path) + ' is already a delegated path.')
      else:
        logger.debug(repr(role['name']) + ' does not match child rolename.')

    roledb.update_roleinfo(self._rolename, roleinfo,
        repository_name=self._repository_name)



  def add_target(self, filepath, custom=None, fileinfo=None):
    """
    <Purpose>
      Add a filepath (must be relative to the repository's targets directory)
      to the Targets object.

      If 'filepath' has already been added, it will be replaced with any new
      file or 'custom' information.

      >>>
      >>>
      >>>

    <Arguments>
      filepath:
        The path of the target file.  It must be relative to the repository's
        targets directory.

      custom:
        An optional dictionary providing additional information about the file.
        NOTE: if a custom value is passed, the fileinfo parameter must be None.
        This parameter will be deprecated in a future release of tuf, use of
        the fileinfo parameter is preferred.

      fileinfo:
        An optional fileinfo dictionary, conforming to
        tuf.formats.TARGETS_FILEINFO_SCHEMA, providing full information about the
        file, i.e:
          { 'length': 101,
            'hashes': { 'sha256': '123EDF...' },
            'custom': { 'permissions': '600'} # optional
          }
        NOTE: if a custom value is passed, the fileinfo parameter must be None.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if 'filepath' is improperly
      formatted.

      tuf.exceptions.InvalidNameError, if 'filepath' does not match pattern.

    <Side Effects>
      Adds 'filepath' to this role's list of targets.  This role's
      'roledb' entry is also updated.

    <Returns>
      None.
    """

    # Does 'filepath' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if there is a mismatch.
    formats.RELPATH_SCHEMA.check_match(filepath)

    if fileinfo and custom:
      raise sslib_exceptions.Error("Can only take one of"
          " custom or fileinfo, not both.")

    if fileinfo:
      formats.TARGETS_FILEINFO_SCHEMA.check_match(fileinfo)

    if custom is None:
      custom = {}
    else:
      formats.CUSTOM_SCHEMA.check_match(custom)

    # Add 'filepath' (i.e., relative to the targets directory) to the role's
    # list of targets.  'filepath' will not be verified as an allowed path
    # according to some delegating role.  Not verifying 'filepath' here allows
    # freedom to add targets and parent restrictions in any order, minimize
    # the number of times these checks are performed, and allow any role to
    # delegate trust of packages to this Targets role.

    # Check if the target is relative and uses forward slash as a separator
    # or raise an exception. File's existence on the file system is not
    # verified. If the file does not exist relative to the targets directory,
    # later calls to write() will fail.
    self._check_path(filepath)

    # Update the role's 'roledb' entry and avoid duplicates.
    roleinfo = roledb.get_roleinfo(self._rolename, self._repository_name)

    if filepath not in roleinfo['paths']:
      logger.debug('Adding new target: ' + repr(filepath))

    else:
      logger.debug('Replacing target: ' + repr(filepath))

    if fileinfo:
      roleinfo['paths'].update({filepath: fileinfo})
    else:
      roleinfo['paths'].update({filepath: {'custom': custom}})

    roledb.update_roleinfo(self._rolename, roleinfo,
        repository_name=self._repository_name)



  def add_targets(self, list_of_targets):
    """
    <Purpose>
      Add a list of target filepaths (all relative to 'self.targets_directory').
      This method does not actually create files on the file system.  The
      list of targets must already exist on disk.

      >>>
      >>>
      >>>

    <Arguments>
      list_of_targets:
        A list of target filepaths that are added to the paths of this Targets
        object.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the arguments are improperly
      formatted.

      tuf.exceptions.InvalidNameError, if any target in 'list_of_targets'
      does not match pattern.

    <Side Effects>
      This Targets' roleinfo is updated with the paths in 'list_of_targets'.

    <Returns>
      None.
    """

    # Does 'list_of_targets' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.
    # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
    formats.RELPATHS_SCHEMA.check_match(list_of_targets)

    # Ensure the paths in 'list_of_targets' are relative and use forward slash
    # as a separator or raise an exception. The paths of 'list_of_targets'
    # will be verified as existing and allowed paths according to this Targets
    # parent role when write() or writeall() is called.  Not verifying
    # filepaths here allows the freedom to add targets and parent restrictions
    # in any order and minimize the number of times these checks are performed.
    for target in list_of_targets:
      self._check_path(target)

    # Update this Targets 'roledb' entry.
    roleinfo = roledb.get_roleinfo(self._rolename, self._repository_name)
    for relative_target in list_of_targets:
      if relative_target not in roleinfo['paths']:
        logger.debug('Adding new target: ' + repr(relative_target))
      else:
        logger.debug('Replacing target: ' + repr(relative_target))
      roleinfo['paths'].update({relative_target: {}})

    roledb.update_roleinfo(self.rolename, roleinfo,
        repository_name=self._repository_name)



  def remove_target(self, filepath):
    """
    <Purpose>
      Remove the target 'filepath' from this Targets' 'paths' field.  'filepath'
      is relative to the targets directory.

      >>>
      >>>
      >>>

    <Arguments>
      filepath:
        The target to remove from this Targets object, relative to the
        repository's targets directory.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if 'filepath' is improperly
      formatted.

      securesystemslib.exceptions.Error, if 'filepath' is not located in the
      repository's targets directory, or not found.

    <Side Effects>
      Modifies this Targets 'roledb' entry.

    <Returns>
      None.
    """

    # Does 'filepath' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if there is a mismatch.
    formats.RELPATH_SCHEMA.check_match(filepath)

    # Remove 'relative_filepath', if found, and update this Targets roleinfo.
    fileinfo = roledb.get_roleinfo(self.rolename, self._repository_name)
    if filepath in fileinfo['paths']:
      del fileinfo['paths'][filepath]
      roledb.update_roleinfo(self.rolename, fileinfo,
          repository_name=self._repository_name)

    else:
      raise sslib_exceptions.Error('Target file path not found.')



  def clear_targets(self):
    """
    <Purpose>
      Remove all the target filepaths in the "paths" field of this Targets.

      >>>
      >>>
      >>>

    <Arguments>
      None

    <Exceptions>
      None.

    <Side Effects>
      Modifies this Targets' 'roledb' entry.

    <Returns>
      None.
    """

    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)
    roleinfo['paths'] = {}

    roledb.update_roleinfo(self.rolename, roleinfo,
        repository_name=self._repository_name)





  def get_delegated_rolenames(self):
    """
    <Purpose>
      Return all delegations of a role.  If ['a/b/', 'a/b/c/', 'a/b/c/d'] have
      been delegated by the delegated role 'django',
      repository.targets('django').get_delegated_rolenames() returns: ['a/b',
      'a/b/c', 'a/b/c/d'].

    <Arguments>
      None.

    <Exceptions>
      None.

    <Side Effects>
      None.

    <Returns>
     A list of rolenames.
    """

    return roledb.get_delegated_rolenames(self.rolename, self._repository_name)





  def _create_delegated_target(self, rolename, keyids, threshold, paths):
    """
    Create a new Targets object for the 'rolename' delegation.  An initial
    expiration is set (3 months from the current time).
    """

    expiration = formats.unix_timestamp_to_datetime(
        int(time.time() + TARGETS_EXPIRATION))
    expiration = expiration.isoformat() + 'Z'

    roleinfo = {'name': rolename, 'keyids': keyids, 'signing_keyids': [],
                'threshold': threshold, 'version': 0,
                'expires': expiration, 'signatures': [], 'partial_loaded': False,
                'paths': paths, 'delegations': {'keys': {}, 'roles': []}}

    # The new targets object is added as an attribute to this Targets object.
    new_targets_object = Targets(self._targets_directory, rolename, roleinfo,
        parent_targets_object=self._parent_targets_object,
        repository_name=self._repository_name)

    return new_targets_object





  def _update_roledb_delegations(self, keydict, delegations_roleinfo):
    """
    Update the roledb to include delegations of the keys in keydict and the
    roles in delegations_roleinfo
    """

    current_roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)
    current_roleinfo['delegations']['keys'].update(keydict)

    for roleinfo in delegations_roleinfo:
      current_roleinfo['delegations']['roles'].append(roleinfo)

    roledb.update_roleinfo(self.rolename, current_roleinfo,
        repository_name=self._repository_name)





  def delegate(self, rolename, public_keys, paths, threshold=1,
      terminating=False, list_of_targets=None, path_hash_prefixes=None):
    """
    <Purpose>
      Create a new delegation, where 'rolename' is a child delegation of this
      Targets object.  The keys and roles database is updated, including the
      delegations field of this Targets.  The delegation of 'rolename' is added
      and accessible (i.e., repository.targets(rolename)).

      Actual metadata files are not created, only when repository.writeall() or
      repository.write() is called.

      >>>
      >>>
      >>>

    <Arguments>
      rolename:
        The name of the delegated role, as in 'django' or 'unclaimed'.

      public_keys:
        A list of TUF key objects in 'ANYKEYLIST_SCHEMA' format.  The list
        may contain any of the supported key types: RSAKEY_SCHEMA,
        ED25519KEY_SCHEMA, etc.

      paths:
        The paths, or glob patterns, delegated to 'rolename'.  Any targets
        added to 'rolename', via add_targets() or 'list_of_targets', must
        match one of the paths or glob patterns in 'paths'.  Apart from the
        public keys of 'rolename', the delegated 'paths' is often known and
        specified when a delegation is first performed.  If the delegator
        is unsure of which 'paths' to delegate, 'paths' can be set to [''].

      threshold:
        The threshold number of keys of 'rolename'.

      terminating:
        Boolean that indicates whether this role allows the updater client to
        continue searching for targets (target files it is trusted to list but
        has not yet specified) in other delegations.  If 'terminating' is True
        and 'updater.target()' does not find 'example_target.tar.gz' in this
        role, a 'tuf.exceptions.UnknownTargetError' exception should be raised.
        If 'terminating' is False (default), and 'target/other_role' is also
        trusted with 'example_target.tar.gz' and has listed it,
        updater.target() should backtrack and return the target file specified
        by 'target/other_role'.

      list_of_targets:
        A list of target filepaths that are added to 'rolename'.
        'list_of_targets' is a list of target filepaths, can be empty, and each
        filepath must be located in the repository's targets directory.  The
        list of targets should also exist at the specified paths, otherwise
        non-existent target paths might not be added when the targets file is
        written to disk with writeall() or write().

      path_hash_prefixes:
        A list of hash prefixes in
        'tuf.formats.PATH_HASH_PREFIXES_SCHEMA' format, used in
        hashed bin delegations.  Targets may be located and stored in hashed
        bins by calculating the target path's hash prefix.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if any of the arguments are
      improperly formatted.

      securesystemslib.exceptions.Error, if the delegated role already exists.

      tuf.exceptions.InvalidNameError, if any path in 'paths' or target in
      'list_of_targets' does not match pattern.

    <Side Effects>
      A new Target object is created for 'rolename' that is accessible to the
      caller (i.e., targets.<rolename>).  The 'keydb' and
      'roledb' stores are updated with 'public_keys'.

    <Returns>
      None.
    """

    # Do the arguments have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.
    # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
    formats.ROLENAME_SCHEMA.check_match(rolename)
    sslib_formats.ANYKEYLIST_SCHEMA.check_match(public_keys)
    formats.RELPATHS_SCHEMA.check_match(paths)
    formats.THRESHOLD_SCHEMA.check_match(threshold)
    sslib_formats.BOOLEAN_SCHEMA.check_match(terminating)

    if list_of_targets is not None:
      formats.RELPATHS_SCHEMA.check_match(list_of_targets)

    if path_hash_prefixes is not None:
      formats.PATH_HASH_PREFIXES_SCHEMA.check_match(path_hash_prefixes)

    # Keep track of the valid keyids (added to the new Targets object) and
    # their keydicts (added to this Targets delegations).
    keyids, keydict = repo_lib.keys_to_keydict(public_keys)

    # Ensure the paths of 'list_of_targets' are located in the repository's
    # targets directory.
    relative_targetpaths = {}

    if list_of_targets:
      for target in list_of_targets:
        # Check if the target path is relative or raise an exception. File's
        # existence on the file system is not verified. If the file does not
        # exist relative to the targets directory, later calls to write()
        # will fail.
        self._check_path(target)
        relative_targetpaths.update({target: {}})

    for path in paths:
      # Check if the delegated paths or glob patterns are relative or
      # raise an exception. Paths' existence on the file system is not
      # verified. If the path is incorrect, the targetfile won't be matched
      # successfully during a client update.
      self._check_path(path)

    # The new targets object is added as an attribute to this Targets object.
    new_targets_object = self._create_delegated_target(rolename, keyids,
        threshold, relative_targetpaths)

    # Update the roleinfo of this role.  A ROLE_SCHEMA object requires only
    # 'keyids', 'threshold', and 'paths'.
    roleinfo = {'name': rolename,
                'keyids': keyids,
                'threshold': threshold,
                'terminating': terminating,
                'paths': list(relative_targetpaths.keys())}

    if paths:
      roleinfo['paths'] = paths

    if path_hash_prefixes:
      roleinfo['path_hash_prefixes'] = path_hash_prefixes
      # A role in a delegations must list either 'path_hash_prefixes'
      # or 'paths'.
      del roleinfo['paths']

    # Update the public keys of 'new_targets_object'.
    for key in public_keys:
      new_targets_object.add_verification_key(key)

    # Add the new delegation to the top-level 'targets' role object (i.e.,
    # 'repository.targets()').  For example, 'django', which was delegated by
    # repository.target('claimed'), is added to 'repository.targets('django')).
    if self.rolename != 'targets':
      self._parent_targets_object.add_delegated_role(rolename,
          new_targets_object)

    # Add 'new_targets_object' to the delegating role object (this object).
    self.add_delegated_role(rolename, new_targets_object)

    # Update the 'delegations' field of the current role.
    self._update_roledb_delegations(keydict, [roleinfo])





  def revoke(self, rolename):
    """
    <Purpose>
      Revoke this Targets' 'rolename' delegation.  Its 'rolename' attribute is
      deleted, including the entries in its 'delegations' field and in
      'roledb'.

      Actual metadata files are not updated, only when repository.write() or
      repository.write() is called.

      >>>
      >>>
      >>>

    <Arguments>
      rolename:
        The rolename (e.g., 'Django' in 'django') of the child delegation the
        parent role (this role) wants to revoke.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if 'rolename' is improperly
      formatted.

    <Side Effects>
      The delegations dictionary of 'rolename' is modified, and its 'roledb'
      entry is updated.  This Targets' 'rolename' delegation attribute is also
      deleted.

    <Returns>
      None.
    """

    # Does 'rolename' have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.
    # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
    formats.ROLENAME_SCHEMA.check_match(rolename)

    # Remove 'rolename' from this Target's delegations dict.
    roleinfo = roledb.get_roleinfo(self.rolename, self._repository_name)

    for role in roleinfo['delegations']['roles']:
      if role['name'] == rolename:
        roleinfo['delegations']['roles'].remove(role)

    roledb.update_roleinfo(self.rolename, roleinfo,
        repository_name=self._repository_name)

    # Remove 'rolename' from 'roledb'.
    try:
      roledb.remove_role(rolename, self._repository_name)
      # Remove the rolename delegation from the current role.  For example, the
      # 'django' role is removed from repository.targets('django').
      del self._delegated_roles[rolename]
      self._parent_targets_object.remove_delegated_role(rolename)

    except (exceptions.UnknownRoleError, KeyError):
      pass



  def delegate_hashed_bins(self, list_of_targets, keys_of_hashed_bins,
      number_of_bins=DEFAULT_NUM_BINS):
    """
    <Purpose>
      Distribute a large number of target files over multiple delegated roles
      (hashed bins).  The metadata files of delegated roles will be nearly
      equal in size (i.e., 'list_of_targets' is uniformly distributed by
      calculating the target filepath's hash and determining which bin it should
      reside in.  The updater client will use "lazy bin walk" to find a target
      file's hashed bin destination.  The parent role lists a range of path
      hash prefixes each hashed bin contains.  This method is intended for
      repositories with a large number of target files, a way of easily
      distributing and managing the metadata that lists the targets, and
      minimizing the number of metadata files (and their size) downloaded by
      the client.  See tuf-spec.txt and the following link for more
      information:
      http://www.python.org/dev/peps/pep-0458/#metadata-scalability

      >>>
      >>>
      >>>

    <Arguments>
      list_of_targets:
        The target filepaths of the targets that should be stored in hashed
        bins created (i.e., delegated roles).  A repository object's
        get_filepaths_in_directory() can generate a list of valid target
        paths.

      keys_of_hashed_bins:
        The initial public keys of the delegated roles.  Public keys may be
        later added or removed by calling the usual methods of the delegated
        Targets object.  For example:
        repository.targets('000-003').add_verification_key()

      number_of_bins:
        The number of delegated roles, or hashed bins, that should be generated
        and contain the target file attributes listed in 'list_of_targets'.
        'number_of_bins' must be a power of 2.  Each bin may contain a
        range of path hash prefixes (e.g., target filepath digests that range
        from [000]... - [003]..., where the series of digits in brackets is
        considered the hash prefix).

    <Exceptions>
      securesystemslib.exceptions.FormatError, if the arguments are improperly
      formatted.

      securesystemslib.exceptions.Error, if 'number_of_bins' is not a power of
      2, or one of the targets in 'list_of_targets' is not relative to the
      repository's targets directory.

      tuf.exceptions.InvalidNameError, if any target in 'list_of_targets'
      does not match pattern.

    <Side Effects>
      Delegates multiple target roles from the current parent role.

    <Returns>
      None.
    """

    # Do the arguments have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.
    # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
    sslib_formats.PATHS_SCHEMA.check_match(list_of_targets)
    sslib_formats.ANYKEYLIST_SCHEMA.check_match(keys_of_hashed_bins)
    formats.NUMBINS_SCHEMA.check_match(number_of_bins)

    prefix_length, prefix_count, bin_size = repo_lib.get_bin_numbers(number_of_bins)

    logger.info('Creating hashed bin delegations.\n' +
        repr(len(list_of_targets)) + ' total targets.\n' +
        repr(number_of_bins) + ' hashed bins.\n' +
        repr(prefix_count) + ' total hash prefixes.\n' +
        'Each bin ranges over ' + repr(bin_size) + ' hash prefixes.')

    # Generate a list of bin names, the range of prefixes to be delegated to
    # that bin, along with the corresponding full list of target prefixes
    # to be delegated to that bin
    ordered_roles = []
    for idx in range(0, prefix_count, bin_size):
      high = idx + bin_size - 1
      name = repo_lib.create_bin_name(idx, high, prefix_length)
      if bin_size == 1:
        target_hash_prefixes = [name]
      else:
        target_hash_prefixes = []
        for idy in range(idx, idx+bin_size):
          target_hash_prefixes.append("{prefix:0{len}x}".format(prefix=idy,
              len=prefix_length))

      role = {"name": name,
          "target_paths": [],
          "target_hash_prefixes": target_hash_prefixes}
      ordered_roles.append(role)

    for target_path in list_of_targets:
      # Check if the target path is relative or raise an exception. File's
      # existence on the file system is not verified. If the file does not
      # exist relative to the targets directory, later calls to write() and
      # writeall() will fail.
      self._check_path(target_path)

      # Determine the hash prefix of 'target_path' by computing the digest of
      # its path relative to the targets directory.
      # We must hash a target path as it appears in the metadata
      hash_prefix = repo_lib.get_target_hash(target_path)[:prefix_length]
      ordered_roles[int(hash_prefix, 16) // bin_size]["target_paths"].append(target_path)

    keyids, keydict = repo_lib.keys_to_keydict(keys_of_hashed_bins)

    # A queue of roleinfo's that need to be updated in the roledb
    delegated_roleinfos = []

    for bin_role in ordered_roles:
      # TODO: originally we just called self.delegate() for each item in this
      # iteration. However, this is *extremely* slow when creating a large
      # number of hashed bins, i.e. 16k as is recommended for PyPI usage in
      # PEP 458: https://www.python.org/dev/peps/pep-0458/
      # The source of the slowness is the interactions with the roledb, which
      # causes several deep copies of roleinfo dictionaries:
      # https://github.com/theupdateframework/python-tuf/issues/1005
      # Once the underlying issues in #1005 are resolved, i.e. some combination
      # of the intermediate and long-term fixes, we may simplify here by
      # switching back to just calling self.delegate(), but until that time we
      # queue roledb interactions and perform all updates to the roledb in one
      # operation at the end of the iteration.

      relative_paths = {}
      for path in bin_role['target_paths']:
        relative_paths.update({path: {}})

      # Delegate from the "unclaimed" targets role to each 'bin_role'
      target = self._create_delegated_target(bin_role['name'], keyids, 1,
          relative_paths)

      roleinfo = {'name': bin_role['name'],
                  'keyids': keyids,
                  'threshold': 1,
                  'terminating': False,
                  'path_hash_prefixes': bin_role['target_hash_prefixes']}
      delegated_roleinfos.append(roleinfo)

      for key in keys_of_hashed_bins:
        target.add_verification_key(key)

      # Add the new delegation to the top-level 'targets' role object (i.e.,
      # 'repository.targets()').
      if self.rolename != 'targets':
        self._parent_targets_object.add_delegated_role(bin_role['name'],
            target)

      # Add 'new_targets_object' to the 'targets' role object (this object).
      self.add_delegated_role(bin_role['name'], target)
      logger.debug('Delegated from ' + repr(self.rolename) + ' to ' + repr(bin_role))


    self._update_roledb_delegations(keydict, delegated_roleinfos)




  def add_target_to_bin(self, target_filepath, number_of_bins=DEFAULT_NUM_BINS,
      fileinfo=None):
    """
    <Purpose>
      Add the fileinfo of 'target_filepath' to the expected hashed bin, if the
      bin is available.  The hashed bin should have been created by
      {targets_role}.delegate_hashed_bins().  Assuming the target filepath is
      located in the repository's targets directory, determine the filepath's
      hash prefix, locate the expected bin (if any), and then add the fileinfo
      to the expected bin.  Example:  'targets/foo.tar.gz' may be added to the
      'targets/unclaimed/58-5f.json' role's list of targets by calling this
      method.

    <Arguments>
      target_filepath:
        The filepath of the target to be added to a hashed bin.  The filepath
        must be located in the repository's targets directory.

      number_of_bins:
        The number of delegated roles, or hashed bins, in use by the repository.
        Note: 'number_of_bins' must be a power of 2.

      fileinfo:
        An optional fileinfo object, conforming to tuf.formats.TARGETS_FILEINFO_SCHEMA,
        providing full information about the file.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if 'target_filepath' is
      improperly formatted.

      securesystemslib.exceptions.Error, if 'target_filepath' cannot be added to
      a hashed bin (e.g., an invalid target filepath, or the expected hashed
      bin does not exist.)

    <Side Effects>
      The fileinfo of 'target_filepath' is added to a hashed bin of this Targets
      object.

    <Returns>
      The name of the hashed bin that the target was added to.
    """

    # Do the arguments have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.
    # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
    sslib_formats.PATH_SCHEMA.check_match(target_filepath)
    formats.NUMBINS_SCHEMA.check_match(number_of_bins)

    # TODO: check target_filepath is sane

    path_hash = repo_lib.get_target_hash(target_filepath)
    bin_name = repo_lib.find_bin_for_target_hash(path_hash, number_of_bins)

    # Ensure the Targets object has delegated to hashed bins
    if not self._delegated_roles.get(bin_name, None):
      raise sslib_exceptions.Error(self.rolename + ' does not have'
          ' a delegated role ' + bin_name)

    self._delegated_roles[bin_name].add_target(target_filepath,
        fileinfo=fileinfo)

    return bin_name



  def remove_target_from_bin(self, target_filepath,
      number_of_bins=DEFAULT_NUM_BINS):
    """
    <Purpose>
      Remove the fileinfo of 'target_filepath' from the expected hashed bin, if
      the bin is available.  The hashed bin should have been created by
      {targets_role}.delegate_hashed_bins().  Assuming the target filepath is
      located in the repository's targets directory, determine the filepath's
      hash prefix, locate the expected bin (if any), and then remove the
      fileinfo from the expected bin.  Example:  'targets/foo.tar.gz' may be
      removed from the '58-5f.json' role's list of targets by calling this
      method.

    <Arguments>
      target_filepath:
        The filepath of the target to be added to a hashed bin.  The filepath
        must be located in the repository's targets directory.

      number_of_bins:
        The number of delegated roles, or hashed bins, in use by the repository.
        Note: 'number_of_bins' must be a power of 2.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if 'target_filepath' is
      improperly formatted.

      securesystemslib.exceptions.Error, if 'target_filepath' cannot be removed
      from a hashed bin (e.g., an invalid target filepath, or the expected
      hashed bin does not exist.)

    <Side Effects>
      The fileinfo of 'target_filepath' is removed from a hashed bin of this
      Targets object.

    <Returns>
      The name of the hashed bin that the target was added to.
    """

    # Do the arguments have the correct format?
    # Ensure the arguments have the appropriate number of objects and object
    # types, and that all dict keys are properly named.
    # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
    sslib_formats.PATH_SCHEMA.check_match(target_filepath)
    formats.NUMBINS_SCHEMA.check_match(number_of_bins)

    # TODO: check target_filepath is sane?

    path_hash = repo_lib.get_target_hash(target_filepath)
    bin_name = repo_lib.find_bin_for_target_hash(path_hash, number_of_bins)

    # Ensure the Targets object has delegated to hashed bins
    if not self._delegated_roles.get(bin_name, None):
      raise sslib_exceptions.Error(self.rolename + ' does not have'
          ' a delegated role ' + bin_name)

    self._delegated_roles[bin_name].remove_target(target_filepath)

    return bin_name


  @property
  def delegations(self):
    """
    <Purpose>
      A getter method that returns the delegations made by this Targets role.

      >>>
      >>>
      >>>

    <Arguments>
      None.

    <Exceptions>
      tuf.exceptions.UnknownRoleError, if this Targets' rolename
      does not exist in 'roledb'.

    <Side Effects>
      None.

    <Returns>
      A list containing the Targets objects of this Targets' delegations.
    """

    return list(self._delegated_roles.values())





  def _check_path(self, pathname):
    """
    <Purpose>
      Check if a path matches the definition of a PATHPATTERN or a
      TARGETPATH (uses the forward slash (/) as directory separator and
      does not start with a directory separator). Checks are performed only
      on the path string, without accessing the file system.

    <Arguments>
      pathname:
        A file path or a glob pattern.

    <Exceptions>
      securesystemslib.exceptions.FormatError, if 'pathname' is improperly
      formatted.

      tuf.exceptions.InvalidNameError, if 'pathname' does not match pattern.

    <Returns>
      None.
    """

    formats.RELPATH_SCHEMA.check_match(pathname)

    if '\\' in pathname:
      raise exceptions.InvalidNameError('Path ' + repr(pathname)
          + ' does not use the forward slash (/) as directory separator.')

    if pathname.startswith('/'):
      raise exceptions.InvalidNameError('Path ' + repr(pathname)
          + ' starts with a directory separator. All paths should be relative'
          '  to targets directory.')




def create_new_repository(repository_directory, repository_name='default',
    storage_backend=None, use_timestamp_length=True, use_timestamp_hashes=True,
    use_snapshot_length=False, use_snapshot_hashes=False):
  """
  <Purpose>
    Create a new repository, instantiate barebones metadata for the top-level
    roles, and return a Repository object.  On disk, create_new_repository()
    only creates the directories needed to hold the metadata and targets files.
    The repository object returned may be modified to update the newly created
    repository.  The methods of the returned object may be called to create
    actual repository files (e.g., repository.write()).

  <Arguments>
    repository_directory:
      The directory that will eventually hold the metadata and target files of
      the TUF repository.

    repository_name:
      The name of the repository.  If not supplied, 'rolename' is added to the
      'default' repository.

    storage_backend:
      An object which implements
      securesystemslib.storage.StorageBackendInterface. When no object is
      passed a FilesystemBackend will be instantiated and used.

    use_timestamp_length:
      Whether to include the optional length attribute of the snapshot
      metadata file in the timestamp metadata.
      Default is True.

    use_timestamp_hashes:
      Whether to include the optional hashes attribute of the snapshot
      metadata file in the timestamp metadata.
      Default is True.

    use_snapshot_length:
      Whether to include the optional length attribute for targets
      metadata files in the snapshot metadata.
      Default is False to save bandwidth but without losing security
      from rollback attacks.
      Read more at section 5.6 from the Mercury paper:
      https://www.usenix.org/conference/atc17/technical-sessions/presentation/kuppusamy

    use_snapshot_hashes:
      Whether to include the optional hashes attribute for targets
      metadata files in the snapshot metadata.
      Default is False to save bandwidth but without losing security
      from rollback attacks.
      Read more at section 5.6 from the Mercury paper:
      https://www.usenix.org/conference/atc17/technical-sessions/presentation/kuppusamy

  <Exceptions>
    securesystemslib.exceptions.FormatError, if the arguments are improperly
    formatted.

  <Side Effects>
    The 'repository_directory' is created if it does not exist, including its
    metadata and targets sub-directories.

  <Returns>
    A 'tuf.repository_tool.Repository' object.
  """

  # Does 'repository_directory' have the correct format?
  # Ensure the arguments have the appropriate number of objects and object
  # types, and that all dict keys are properly named.
  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
  sslib_formats.PATH_SCHEMA.check_match(repository_directory)
  sslib_formats.NAME_SCHEMA.check_match(repository_name)

  if storage_backend is None:
    storage_backend = sslib_storage.FilesystemBackend()

  # Set the repository, metadata, and targets directories.  These directories
  # are created if they do not exist.
  repository_directory = os.path.abspath(repository_directory)
  metadata_directory = None
  targets_directory = None

  # Ensure the 'repository_directory' exists
  logger.info('Creating ' + repr(repository_directory))
  storage_backend.create_folder(repository_directory)

  # Set the metadata and targets directories.  The metadata directory is a
  # staged one so that the "live" repository is not affected.  The
  # staged metadata changes may be moved over to "live" after all updated
  # have been completed.
  metadata_directory = os.path.join(repository_directory,
      METADATA_STAGED_DIRECTORY_NAME)
  targets_directory = os.path.join(repository_directory, TARGETS_DIRECTORY_NAME)

  # Ensure the metadata directory exists
  logger.info('Creating ' + repr(metadata_directory))
  storage_backend.create_folder(metadata_directory)

  # Ensure the targets directory exists
  logger.info('Creating ' + repr(targets_directory))
  storage_backend.create_folder(targets_directory)

  # Create the bare bones repository object, where only the top-level roles
  # have been set and contain default values (e.g., Root roles has a threshold
  # of 1, expires 1 year into the future, etc.)
  repository = Repository(repository_directory, metadata_directory,
      targets_directory, storage_backend, repository_name, use_timestamp_length,
      use_timestamp_hashes, use_snapshot_length, use_snapshot_hashes)

  return repository





def load_repository(repository_directory, repository_name='default',
    storage_backend=None, use_timestamp_length=True, use_timestamp_hashes=True,
    use_snapshot_length=False, use_snapshot_hashes=False):
  """
  <Purpose>
    Return a repository object containing the contents of metadata files loaded
    from the repository.

  <Arguments>
    repository_directory:
      The root folder of the repository that contains the metadata and targets
      sub-directories.

    repository_name:
      The name of the repository.  If not supplied, 'default' is used as the
      repository name.

    storage_backend:
      An object which implements
      securesystemslib.storage.StorageBackendInterface. When no object is
      passed a FilesystemBackend will be instantiated and used.

    use_timestamp_length:
      Whether to include the optional length attribute of the snapshot
      metadata file in the timestamp metadata.
      Default is True.

    use_timestamp_hashes:
      Whether to include the optional hashes attribute of the snapshot
      metadata file in the timestamp metadata.
      Default is True.

    use_snapshot_length:
      Whether to include the optional length attribute for targets
      metadata files in the snapshot metadata.
      Default is False to save bandwidth but without losing security
      from rollback attacks.
      Read more at section 5.6 from the Mercury paper:
      https://www.usenix.org/conference/atc17/technical-sessions/presentation/kuppusamy

    use_snapshot_hashes:
      Whether to include the optional hashes attribute for targets
      metadata files in the snapshot metadata.
      Default is False to save bandwidth but without losing security
      from rollback attacks.
      Read more at section 5.6 from the Mercury paper:
      https://www.usenix.org/conference/atc17/technical-sessions/presentation/kuppusamy

  <Exceptions>
    securesystemslib.exceptions.FormatError, if 'repository_directory' or any of
    the metadata files are improperly formatted.

    tuf.exceptions.RepositoryError, if the Root role cannot be
    found.  At a minimum, a repository must contain 'root.json'

  <Side Effects>
   All the metadata files found in the repository are loaded and their contents
   stored in a repository_tool.Repository object.

  <Returns>
    repository_tool.Repository object.
  """

  # Does 'repository_directory' have the correct format?
  # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
  sslib_formats.PATH_SCHEMA.check_match(repository_directory)
  sslib_formats.NAME_SCHEMA.check_match(repository_name)

  if storage_backend is None:
    storage_backend = sslib_storage.FilesystemBackend()

  repository_directory = os.path.abspath(repository_directory)
  metadata_directory = os.path.join(repository_directory,
      METADATA_STAGED_DIRECTORY_NAME)
  targets_directory = os.path.join(repository_directory, TARGETS_DIRECTORY_NAME)

  # The Repository() object loaded (i.e., containing all the metadata roles
  # found) and returned.
  repository = Repository(repository_directory, metadata_directory,
      targets_directory, storage_backend, repository_name, use_timestamp_length,
      use_timestamp_hashes, use_snapshot_length, use_snapshot_hashes)

  filenames = repo_lib.get_top_level_metadata_filenames(metadata_directory)

  # The Root file is always available without a version number (a consistent
  # snapshot) attached to the filename.  Store the 'consistent_snapshot' value
  # and read the loaded Root file so that other metadata files may be located.
  consistent_snapshot = False

  # Load the metadata of the top-level roles (i.e., Root, Timestamp, Targets,
  # and Snapshot).
  repository, consistent_snapshot = repo_lib._load_top_level_metadata(repository,
    filenames, repository_name)

  delegated_roles_filenames = repo_lib.get_delegated_roles_metadata_filenames(
      metadata_directory, consistent_snapshot, storage_backend)

  # Load the delegated targets metadata and their fileinfo.
  # The delegated targets roles form a tree/graph which is traversed in a
  # breadth-first-search manner starting from 'targets' in order to correctly
  # load the delegations hierarchy.
  parent_targets_object = repository.targets

  # Keep the next delegations to be loaded in a deque structure which
  # has the properties of a list but is designed to have fast appends
  # and pops from both ends
  delegations = deque()
  # A set used to keep the already loaded delegations and avoid an infinite
  # loop in case of cycles in the delegations graph
  loaded_delegations = set()

  # Top-level roles are already loaded, fetch targets and get its delegations.
  # Store the delegations in the form of delegated-delegating role tuples,
  # starting from the top-level targets:
  # [('role1', 'targets'), ('role2', 'targets'), ... ]
  roleinfo = roledb.get_roleinfo('targets', repository_name)
  for role in roleinfo['delegations']['roles']:
    delegations.append((role, 'targets'))

  # Traverse the graph by appending the next delegation to the deque and
  # 'pop'-ing and loading the left-most element.
  while delegations:
    delegation_info, delegating_role = delegations.popleft()

    rolename = delegation_info['name']
    if (rolename, delegating_role) in loaded_delegations:
      logger.warning('Detected cycle in the delegation graph: ' +
          repr(delegating_role) + ' -> ' +
          repr(rolename) +
          ' is reached more than once.')
      continue

    # Instead of adding only rolename to the set, store the already loaded
    # delegated-delegating role tuples. This way a delegated role is added
    # to each of its delegating roles but when the role is reached twice
    # from the same delegating role an infinite loop is avoided.
    loaded_delegations.add((rolename, delegating_role))

    metadata_path = delegated_roles_filenames[rolename]
    signable = None

    try:
      signable = sslib_util.load_json_file(metadata_path)

    except (sslib_exceptions.Error, ValueError, IOError):
      logger.debug('Tried to load metadata with invalid JSON'
          ' content: ' + repr(metadata_path))
      continue

    metadata_object = signable['signed']

    # Extract the metadata attributes of 'metadata_object' and update its
    # corresponding roleinfo.
    roleinfo = {'name': rolename,
                'signing_keyids': [],
                'signatures': [],
                'partial_loaded': False
               }

    roleinfo['signatures'].extend(signable['signatures'])
    roleinfo['version'] = metadata_object['version']
    roleinfo['expires'] = metadata_object['expires']
    roleinfo['paths'] = metadata_object['targets']
    roleinfo['delegations'] = metadata_object['delegations']
    roleinfo['threshold'] = delegation_info['threshold']
    roleinfo['keyids'] = delegation_info['keyids']

    # Generate the Targets object of the delegated role,
    # add it to the top-level 'targets' object and to its
    # direct delegating role object.
    new_targets_object = Targets(targets_directory, rolename,
         roleinfo, parent_targets_object=parent_targets_object,
         repository_name=repository_name)

    parent_targets_object.add_delegated_role(rolename,
        new_targets_object)
    if delegating_role != 'targets':
      parent_targets_object(delegating_role).add_delegated_role(rolename,
          new_targets_object)

    # Append the next level delegations to the deque:
    # the 'delegated' role becomes the 'delegating'
    for delegation in metadata_object['delegations']['roles']:
      delegations.append((delegation, rolename))

    # Extract the keys specified in the delegations field of the Targets
    # role.  Add 'key_object' to the list of recognized keys.  Keys may be
    # shared, so do not raise an exception if 'key_object' has already been
    # added.  In contrast to the methods that may add duplicate keys, do not
    # log a warning here as there may be many such duplicate key warnings.
    # The repository maintainer should have also been made aware of the
    # duplicate key when it was added.
    for key_metadata in metadata_object['delegations']['keys'].values():

      # The repo may have used hashing algorithms for the generated keyids
      # that doesn't match the client's set of hash algorithms.  Make sure
      # to only used the repo's selected hashing algorithms.
      key_object, keyids = format_metadata_to_key(key_metadata,
          keyid_hash_algorithms=key_metadata['keyid_hash_algorithms'])
      try:
        for keyid in keyids: # pragma: no branch
          key_object['keyid'] = keyid
          keydb.add_key(key_object, keyid=None,
              repository_name=repository_name)

      except exceptions.KeyAlreadyExistsError:
        pass

  return repository





def dump_signable_metadata(metadata_filepath):
  """
  <Purpose>
    Dump the "signed" portion of metadata. It is the portion that is normally
    signed by the repository tool, which is in canonicalized JSON form.
    This function is intended for external tools that wish to independently
    sign metadata.

    The normal workflow for this use case is to:
    (1) call dump_signable_metadata(metadata_filepath)
    (2) sign the output with an external tool
    (3) call append_signature(signature, metadata_filepath)

  <Arguments>
    metadata_filepath:
      The path to the metadata file.  For example,
      repository/metadata/root.json.

  <Exceptions>
    securesystemslib.exceptions.FormatError, if the arguments are improperly
    formatted.

    IOError, if 'metadata_filepath' cannot be opened.

  <Side Effects>
    None.

  <Returns>
    Metadata content that is normally signed by the repository tool (i.e., the
    "signed" portion of a metadata file).
  """

  # Are the argument properly formatted?
  sslib_formats.PATH_SCHEMA.check_match(metadata_filepath)

  signable = sslib_util.load_json_file(metadata_filepath)

  # Is 'signable' a valid metadata file?
  formats.SIGNABLE_SCHEMA.check_match(signable)

  return sslib_formats.encode_canonical(signable['signed'])





def append_signature(signature, metadata_filepath):
  """
  <Purpose>
    Append 'signature' to the metadata at 'metadata_filepath'.  The signature
    is assumed to be valid, and externally generated by signing the output of
    dump_signable_metadata(metadata_filepath).  This function is intended for
    external tools that wish to independently sign metadata.

    The normal workflow for this use case is to:
    (1) call dump_signable_metadata(metadata_filepath)
    (2) sign the output with an external tool
    (3) call append_signature(signature, metadata_filepath)

  <Arguments>
    signature:
      A TUF signature structure that contains the KEYID, signing method, and
      the signature.  It conforms to securesystemslib.formats.SIGNATURE_SCHEMA.

      For example:

      {
       "keyid": "a0a0f0cf08...",
       "method": "ed25519",
       "sig": "14f6e6566ec13..."
      }

    metadata_filepath:
      The path to the metadata file.  For example,
      repository/metadata/root.json.

  <Exceptions>
    securesystemslib.exceptions.FormatError, if the arguments are improperly
    formatted.

  <Side Effects>
    'metadata_filepath' is overwritten.

  <Returns>
    None.
  """

  # Are the arguments properly formatted?
  sslib_formats.SIGNATURE_SCHEMA.check_match(signature)
  sslib_formats.PATH_SCHEMA.check_match(metadata_filepath)

  signable = sslib_util.load_json_file(metadata_filepath)

  # Is 'signable' a valid metadata file?
  formats.SIGNABLE_SCHEMA.check_match(signable)

  signable['signatures'].append(signature)

  file_object = tempfile.TemporaryFile()

  written_metadata_content = json.dumps(signable, indent=1,
      separators=(',', ': '), sort_keys=True).encode('utf-8')

  file_object.write(written_metadata_content)
  sslib_util.persist_temp_file(file_object, metadata_filepath)





if __name__ == '__main__':
  # The interactive sessions of the documentation strings can
  # be tested by running repository_tool.py as a standalone module:
  # $ python3 repository_tool.py.
  import doctest
  doctest.testmod()
