Пример #1
0
def _role_paths_needs_update(role, relative_delegated_paths,
                             path_hash_prefixes):

    # By default, we will assume that the delegator needs no update.
    needs_update = False
    role_paths = role.get('paths')

    # relative_delegated_paths are relative to 'repository'.
    # relative_role_paths are relative to 'repository/targets'.
    # This is because role_paths are relative to 'repository/targets'.
    relative_role_paths = []
    for relative_delegated_path in relative_delegated_paths:
        assert relative_delegated_path.startswith('targets/')
        relative_role_paths.append(relative_delegated_path[8:])

    # Check role paths.
    if role_paths is None:
        logger.warn('No role paths!')
    else:
        if relative_delegated_paths is not None:
            if set(role_paths) == set(relative_role_paths):
                logger.debug('Role paths are the same.')
            else:
                needs_update = True
                logger.info('Role paths have changed!')
        else:
            assert path_hash_prefixes is not None
            needs_update = True
            logger.info(
                'Role paths have been substituted with path_hash_prefixes!')

    return needs_update
Пример #2
0
def get_delegatee_role_from_delegator(delegator_targets_role_name,
                                      relative_delegatee_targets_role_name):

    # Extract metadata from the delegator targets role.
    delegator_filename = \
      os.path.join(METADATA_DIRECTORY,
                   '{0}.txt'.format(delegator_targets_role_name))
    delegator_metadata_dict = \
      signerlib.read_metadata_file(delegator_filename)
    # TODO: Verify signature on delegator metadata!
    delegator_signed_metadata = delegator_metadata_dict['signed']

    delegations = delegator_signed_metadata.get('delegations', {})
    roles = delegations.get('roles', [])

    # Find the delegatee, if it exists, in the delegator.
    absolute_delegatee_targets_role_name = \
      '{0}/{1}'.format(delegator_targets_role_name,
                       relative_delegatee_targets_role_name)
    role_index = \
      signerlib.find_delegated_role(roles, absolute_delegatee_targets_role_name)
    role = None

    if role_index is None:
        logger.info('{0} does not know about {1}'.format(
            delegator_targets_role_name, relative_delegatee_targets_role_name))
    else:
        role = roles[role_index]

    return role
def get_delegatee_role_from_delegator(delegator_targets_role_name,
                                      relative_delegatee_targets_role_name):

  # Extract metadata from the delegator targets role.
  delegator_filename = \
    os.path.join(METADATA_DIRECTORY,
                 '{0}.txt'.format(delegator_targets_role_name))
  delegator_metadata_dict = \
    signerlib.read_metadata_file(delegator_filename)
  # TODO: Verify signature on delegator metadata!
  delegator_signed_metadata = delegator_metadata_dict['signed']

  delegations = delegator_signed_metadata.get('delegations', {})
  roles = delegations.get('roles', [])

  # Find the delegatee, if it exists, in the delegator.
  absolute_delegatee_targets_role_name = \
    '{0}/{1}'.format(delegator_targets_role_name,
                     relative_delegatee_targets_role_name)
  role_index = \
    signerlib.find_delegated_role(roles, absolute_delegatee_targets_role_name)
  role = None

  if role_index is None:
    logger.info('{0} does not know about {1}'.format(
                delegator_targets_role_name,
                relative_delegatee_targets_role_name))
  else:
    role = roles[role_index]

  return role
def _role_paths_needs_update(role, relative_delegated_paths,
                             path_hash_prefixes):

  # By default, we will assume that the delegator needs no update.
  needs_update = False
  role_paths = role.get('paths')

  # relative_delegated_paths are relative to 'repository'.
  # relative_role_paths are relative to 'repository/targets'.
  # This is because role_paths are relative to 'repository/targets'.
  relative_role_paths = []
  for relative_delegated_path in relative_delegated_paths:
    assert relative_delegated_path.startswith('targets/')
    relative_role_paths.append(relative_delegated_path[8:])

  # Check role paths.
  if role_paths is None:
    logger.warn('No role paths!')
  else:
    if relative_delegated_paths is not None:
      if set(role_paths) == set(relative_role_paths):
        logger.debug('Role paths are the same.')
      else:
        needs_update = True
        logger.info('Role paths have changed!')
    else:
      assert path_hash_prefixes is not None
      needs_update = True
      logger.info('Role paths have been substituted with path_hash_prefixes!')

  return needs_update
def update_delegator_metadata(delegator_targets_role_name,
                              relative_delegatee_targets_role_name,
                              delegator_targets_role_keys,
                              delegatee_targets_role_keys,
                              relative_delegated_paths=None,
                              path_hash_prefixes=None):

  # relative_delegated_paths XOR path_hash_prefixes
  assert (relative_delegated_paths is None and path_hash_prefixes is not None) \
          or \
         (relative_delegated_paths is not None and path_hash_prefixes is None)

  if delegator_needs_update(delegator_targets_role_name,
                            relative_delegatee_targets_role_name,
                            relative_delegated_paths=relative_delegated_paths,
                            path_hash_prefixes=path_hash_prefixes):
    signercli._update_parent_metadata(METADATA_DIRECTORY,
                                      relative_delegatee_targets_role_name,
                                      delegatee_targets_role_keys,
                                      delegator_targets_role_name,
                                      delegator_targets_role_keys,
                                      delegated_paths=relative_delegated_paths,
                                      path_hash_prefixes=path_hash_prefixes)
  else:
    logger.info('{0} does not need to be updated about {1}'.format(
                delegator_targets_role_name,
                relative_delegatee_targets_role_name))
Пример #6
0
def update_delegator_metadata(delegator_targets_role_name,
                              relative_delegatee_targets_role_name,
                              delegator_targets_role_keys,
                              delegatee_targets_role_keys,
                              relative_delegated_paths=None,
                              path_hash_prefixes=None):

    # relative_delegated_paths XOR path_hash_prefixes
    assert (relative_delegated_paths is None and path_hash_prefixes is not None) \
            or \
           (relative_delegated_paths is not None and path_hash_prefixes is None)

    if delegator_needs_update(
            delegator_targets_role_name,
            relative_delegatee_targets_role_name,
            relative_delegated_paths=relative_delegated_paths,
            path_hash_prefixes=path_hash_prefixes):
        signercli._update_parent_metadata(
            METADATA_DIRECTORY,
            relative_delegatee_targets_role_name,
            delegatee_targets_role_keys,
            delegator_targets_role_name,
            delegator_targets_role_keys,
            delegated_paths=relative_delegated_paths,
            path_hash_prefixes=path_hash_prefixes)
    else:
        logger.info('{0} does not need to be updated about {1}'.format(
            delegator_targets_role_name, relative_delegatee_targets_role_name))
Пример #7
0
def compress_metadata(metadata_filename):
    compressed_metadata_filename = '{0}.gz'.format(metadata_filename)

    with open(metadata_filename) as metadata_file, \
         gzip.open(compressed_metadata_filename, 'w') as compressed_metadata_file:
        shutil.copyfileobj(metadata_file, compressed_metadata_file)

    logger.info('Compressed {0} as {1}'.format(metadata_filename,
                                               compressed_metadata_filename))
def compress_metadata(metadata_filename):
  compressed_metadata_filename = '{0}.gz'.format(metadata_filename)

  with open(metadata_filename) as metadata_file, \
       gzip.open(compressed_metadata_filename, 'w') as compressed_metadata_file:
    shutil.copyfileobj(metadata_file, compressed_metadata_file)

  logger.info('Compressed {0} as {1}'.format(metadata_filename,
              compressed_metadata_filename))
def update_release(time_delta, compress=False):
  expiration_date = get_expiration_date(time_delta)
  release_role_keys = get_keys_for_top_level_role(RELEASE_ROLE_NAME)
  version_number = signercli._get_metadata_version(RELEASE_ROLE_FILE)

  # Generate and write the signed release metadata.
  release_role_filename = \
    signerlib.build_release_file(release_role_keys, METADATA_DIRECTORY,
                                 version_number, expiration_date,
                                 compress=compress)

  logger.info('Release written to {0}'.format(release_role_filename))
Пример #10
0
def update_release(time_delta, compress=False):
    expiration_date = get_expiration_date(time_delta)
    release_role_keys = get_keys_for_top_level_role(RELEASE_ROLE_NAME)
    version_number = signercli._get_metadata_version(RELEASE_ROLE_FILE)

    # Generate and write the signed release metadata.
    release_role_filename = \
      signerlib.build_release_file(release_role_keys, METADATA_DIRECTORY,
                                   version_number, expiration_date,
                                   compress=compress)

    logger.info('Release written to {0}'.format(release_role_filename))
Пример #11
0
def update_targets_metadata(targets_role_name,
                            relative_delegated_paths,
                            time_delta,
                            targets_role_keys=None):

    # TODO: Ensure that targets_role_name is of the correct form.
    assert targets_role_name == 'targets' or \
           targets_role_name.startswith('targets/')

    # The first time a parent role creates a delegation, a directory containing
    # the parent role's name is created in the metadata directory.  For example,
    # if the targets roles creates a delegated role 'role1', the metadata
    # directory would then contain:
    # '{METADATA_DIRECTORY}/targets/role1.txt', where 'role1.txt' is the
    # delegated role's metadata file.
    # If delegated role 'role1' creates its own delegated role 'role2', the
    # metadata directory would then contain:
    # '{METADATA_DIRECTORY}/targets/role1/role2.txt'.
    # When creating a delegated role, if the parent directory already exists,
    # this means a prior delegation has been performed by the parent.

    parent_role_name = os.path.dirname(targets_role_name)
    child_role_name = os.path.basename(targets_role_name)

    parent_role_directory = os.path.join(METADATA_DIRECTORY, parent_role_name)
    if not os.path.isdir(parent_role_directory):
        os.mkdir(parent_role_directory)

    # Set the filename of the targets role metadata.
    targets_role_filename = os.path.join(parent_role_directory,
                                         '{0}.txt'.format(child_role_name))

    expiration_date = get_expiration_date(time_delta)
    version_number = signercli._get_metadata_version(targets_role_filename)

    # Prepare the targets metadata.
    targets_metadata = \
      signerlib.generate_targets_metadata(REPOSITORY_DIRECTORY,
                                          relative_delegated_paths,
                                          version_number, expiration_date)

    # Where are our signing keys?
    if targets_role_keys is None:
        logger.info('Looking up keys for {0}'.format(targets_role_name))
        targets_role_keys = get_keys_for_targets_role(targets_role_name,
                                                      create_missing_keys=True)
    else:
        logger.info('Using given keys for {0}'.format(targets_role_name))

    # Sign and write the targets role metadata.
    signercli._sign_and_write_metadata(targets_metadata, targets_role_keys,
                                       targets_role_filename)
Пример #12
0
def delegator_needs_update(delegator_targets_role_name,
                           relative_delegatee_targets_role_name,
                           relative_delegated_paths=None,
                           path_hash_prefixes=None):

    # relative_delegated_paths XOR path_hash_prefixes
    assert (relative_delegated_paths is None and path_hash_prefixes is not None) \
            or \
           (relative_delegated_paths is not None and path_hash_prefixes is None)

    # By default, we will assume that the delegator needs no update.
    needs_update = False
    role = \
      get_delegatee_role_from_delegator(delegator_targets_role_name,
                                        relative_delegatee_targets_role_name)

    if role is None:
        needs_update = True
        logger.info('Role needs to be added.')
    else:
        if _role_path_hash_prefixes_needs_update(role,
                                                 relative_delegated_paths,
                                                 path_hash_prefixes):
            needs_update = True
            logger.info('Role needs update.')
        elif _role_paths_needs_update(role, relative_delegated_paths,
                                      path_hash_prefixes):
            needs_update = True
            logger.info('Role needs update.')
        else:
            logger.info('Role does not need update.')

    return needs_update
def update_targets_metadata(targets_role_name, relative_delegated_paths,
                            time_delta, targets_role_keys=None):

  # TODO: Ensure that targets_role_name is of the correct form.
  assert targets_role_name == 'targets' or \
         targets_role_name.startswith('targets/')

  # The first time a parent role creates a delegation, a directory containing
  # the parent role's name is created in the metadata directory.  For example,
  # if the targets roles creates a delegated role 'role1', the metadata
  # directory would then contain:
  # '{METADATA_DIRECTORY}/targets/role1.txt', where 'role1.txt' is the
  # delegated role's metadata file.
  # If delegated role 'role1' creates its own delegated role 'role2', the
  # metadata directory would then contain:
  # '{METADATA_DIRECTORY}/targets/role1/role2.txt'.
  # When creating a delegated role, if the parent directory already exists,
  # this means a prior delegation has been performed by the parent.

  parent_role_name = os.path.dirname(targets_role_name)
  child_role_name = os.path.basename(targets_role_name)

  parent_role_directory = os.path.join(METADATA_DIRECTORY, parent_role_name)
  if not os.path.isdir(parent_role_directory):
    os.mkdir(parent_role_directory)

  # Set the filename of the targets role metadata.
  targets_role_filename = os.path.join(parent_role_directory,
                                       '{0}.txt'.format(child_role_name))

  expiration_date = get_expiration_date(time_delta)
  version_number = signercli._get_metadata_version(targets_role_filename)

  # Prepare the targets metadata.
  targets_metadata = \
    signerlib.generate_targets_metadata(REPOSITORY_DIRECTORY,
                                        relative_delegated_paths,
                                        version_number, expiration_date)

  # Where are our signing keys?
  if targets_role_keys is None:
    logger.info('Looking up keys for {0}'.format(targets_role_name))
    targets_role_keys = get_keys_for_targets_role(targets_role_name,
                                                  create_missing_keys=True)
  else:
    logger.info('Using given keys for {0}'.format(targets_role_name))

  # Sign and write the targets role metadata.
  signercli._sign_and_write_metadata(targets_metadata, targets_role_keys,
                                     targets_role_filename)
def delegator_needs_update(delegator_targets_role_name,
                           relative_delegatee_targets_role_name,
                           relative_delegated_paths=None,
                           path_hash_prefixes=None):

  # relative_delegated_paths XOR path_hash_prefixes
  assert (relative_delegated_paths is None and path_hash_prefixes is not None) \
          or \
         (relative_delegated_paths is not None and path_hash_prefixes is None)

  # By default, we will assume that the delegator needs no update.
  needs_update = False
  role = \
    get_delegatee_role_from_delegator(delegator_targets_role_name,
                                      relative_delegatee_targets_role_name)

  if role is None:
    needs_update = True
    logger.info('Role needs to be added.')
  else:
    if _role_path_hash_prefixes_needs_update(role, relative_delegated_paths,
                                             path_hash_prefixes):
      needs_update = True
      logger.info('Role needs update.')
    elif _role_paths_needs_update(role, relative_delegated_paths,
                                  path_hash_prefixes):
      needs_update = True
      logger.info('Role needs update.')
    else:
      logger.info('Role does not need update.')

  return needs_update
Пример #15
0
def get_keys_for_targets_role(targets_role_name, create_missing_keys=False):
    """Given the name of a targets role (targets_role_name) and its list of
  passwords (targets_role_passwords), return the RSA key IDs for a targets role.

  Side effect: Load the aforementioned RSA keys into the global TUF
  keystore."""

    # Get the list of passwords for this role.
    targets_role_passwords = ROLE_NAME_TO_PASSWORDS[targets_role_name]

    if targets_role_name == 'targets':
        # Ask root about targets' keys.
        root_metadata_dict = signerlib.read_metadata_file(ROOT_ROLE_FILE)
        # TODO: Verify signature on root metadata!
        signed_root_metadata = root_metadata_dict['signed']
        targets_role_keys = signed_root_metadata['roles']['targets']['keyids']
    else:
        # Ask the targets role itself.
        targets_role_filename = os.path.join(
            METADATA_DIRECTORY, '{0}.txt'.format(targets_role_name))
        # If the targets role exists...
        if os.path.exists(targets_role_filename):
            targets_role_metadata_dict = \
              signerlib.read_metadata_file(targets_role_filename)
            # TODO: Verify signature on targets role metadata!
            targets_role_metadata_signatures = \
              targets_role_metadata_dict['signatures']
            targets_role_keys = []
            # Assume keys are listed in the same order as passwords.
            for targets_role_metadata_signature in targets_role_metadata_signatures:
                targets_role_keys.append(
                    targets_role_metadata_signature['keyid'])
            # Assume that there are as many keys as there are passwords.
            assert len(targets_role_keys) == len(targets_role_passwords)
        # Otherwise...
        else:
            if create_missing_keys:
                logger.info('Creating keys for {0}'.format(targets_role_name))
                targets_role_keys = generate_rsa_keys(targets_role_passwords)
            else:
                raise MissingKeys(targets_role_name)

    # Decrypt and load the keys of the targets role.
    loaded_targets_role_keys = \
      keystore.load_keystore_from_keyfiles(KEYSTORE_DIRECTORY, targets_role_keys,
                                           targets_role_passwords)
    assert targets_role_keys == loaded_targets_role_keys

    return targets_role_keys
def get_keys_for_targets_role(targets_role_name, create_missing_keys=False):
  """Given the name of a targets role (targets_role_name) and its list of
  passwords (targets_role_passwords), return the RSA key IDs for a targets role.

  Side effect: Load the aforementioned RSA keys into the global TUF
  keystore."""

  # Get the list of passwords for this role.
  targets_role_passwords = ROLE_NAME_TO_PASSWORDS[targets_role_name]

  if targets_role_name == 'targets':
    # Ask root about targets' keys.
    root_metadata_dict = signerlib.read_metadata_file(ROOT_ROLE_FILE)
    # TODO: Verify signature on root metadata!
    signed_root_metadata = root_metadata_dict['signed']
    targets_role_keys = signed_root_metadata['roles']['targets']['keyids']
  else:
    # Ask the targets role itself.
    targets_role_filename = os.path.join(METADATA_DIRECTORY,
                                         '{0}.txt'.format(targets_role_name))
    # If the targets role exists...
    if os.path.exists(targets_role_filename):
      targets_role_metadata_dict = \
        signerlib.read_metadata_file(targets_role_filename)
      # TODO: Verify signature on targets role metadata!
      targets_role_metadata_signatures = \
        targets_role_metadata_dict['signatures']
      targets_role_keys = []
      # Assume keys are listed in the same order as passwords.
      for targets_role_metadata_signature in targets_role_metadata_signatures:
        targets_role_keys.append(targets_role_metadata_signature['keyid'])
      # Assume that there are as many keys as there are passwords.
      assert len(targets_role_keys) == len(targets_role_passwords)
    # Otherwise...
    else:
      if create_missing_keys:
        logger.info('Creating keys for {0}'.format(targets_role_name))
        targets_role_keys = generate_rsa_keys(targets_role_passwords)
      else:
        raise MissingKeys(targets_role_name)

  # Decrypt and load the keys of the targets role.
  loaded_targets_role_keys = \
    keystore.load_keystore_from_keyfiles(KEYSTORE_DIRECTORY, targets_role_keys,
                                         targets_role_passwords)
  assert targets_role_keys == loaded_targets_role_keys

  return targets_role_keys
def _role_path_hash_prefixes_needs_update(role, relative_delegated_paths,
                                          path_hash_prefixes):

  # By default, we will assume that the delegator needs no update.
  needs_update = False
  role_path_hash_prefixes = role.get('path_hash_prefixes')

  # Otherwise, check role path_hash_prefixes.
  if role_path_hash_prefixes is None:
    logger.warn('No role path_hash_prefixes!')
  else:
    if path_hash_prefixes is not None:
      if set(role_path_hash_prefixes) == set(path_hash_prefixes):
        logger.debug('Role path_hash_prefixes is the same.')
      else:
        needs_update = True
        logger.debug('Role path_hash_prefixes has changed!')
    else:
      assert relative_delegated_paths is not None
      needs_update = True
      logger.info('Role path_hash_prefixes has been substituted with paths!')

  return needs_update
Пример #18
0
def _role_path_hash_prefixes_needs_update(role, relative_delegated_paths,
                                          path_hash_prefixes):

    # By default, we will assume that the delegator needs no update.
    needs_update = False
    role_path_hash_prefixes = role.get('path_hash_prefixes')

    # Otherwise, check role path_hash_prefixes.
    if role_path_hash_prefixes is None:
        logger.warn('No role path_hash_prefixes!')
    else:
        if path_hash_prefixes is not None:
            if set(role_path_hash_prefixes) == set(path_hash_prefixes):
                logger.debug('Role path_hash_prefixes is the same.')
            else:
                needs_update = True
                logger.debug('Role path_hash_prefixes has changed!')
        else:
            assert relative_delegated_paths is not None
            needs_update = True
            logger.info(
                'Role path_hash_prefixes has been substituted with paths!')

    return needs_update
def metadata_matches_data(metadata_directory, targets_directory, full_role_name,
                          files_directory, recursive_walk=False,
                          followlinks=True,
                          file_predicate=signerlib.accept_any_file):
  """
  Return True if metadata matches data for the target role; False otherwise.
  """

  # Assume that metadata lives in a file specified by the full role name.
  metadata_filename = full_role_name + ".txt"
  metadata_filename = os.path.join(metadata_directory, metadata_filename)

  try:
    metadata_file = open(metadata_filename)
  except:
    raise MissingTargetMetadataError(metadata_filename)
  else:
    all_metadata = json.load(metadata_file)
    metadata_file.close()

    # TODO: Use TUF to verify that all_metadata is correctly signed.
    signed_metadata = all_metadata["signed"]
    # Check that the metadata is well-formed.
    signed_metadata = tuf.formats.TargetsFile.from_metadata(signed_metadata)
    expected_targets = signed_metadata.info["targets"]

    # We begin by assuming that everything is all right.
    matched = True

    # For expected_file in metadata, does it match the observed_file in targets?
    for expected_file in expected_targets:
      observed_file = os.path.join(targets_directory, expected_file)
      if os.path.exists(observed_file):
        # Does expected_file describe observed_file?
        expected_file_metadata = expected_targets[expected_file]

        # Compare every hash of the expected file with the equivalent hash of
        # the observed file.
        expected_file_hashes = expected_file_metadata["hashes"]
        for hash_algorithm, expected_file_digest in expected_file_hashes.iteritems():
          observed_file_digest_object = \
            tuf.hash.digest_filename(observed_file, algorithm=hash_algorithm)
          observed_file_digest = observed_file_digest_object.hexdigest()
          if observed_file_digest != expected_file_digest:
            # Metadata has probably diverged from data.
            logger.info("{0} != {1}".format(observed_file, expected_file))
            matched = False
            break

        # Break out of the outer for loop in case we found a mismatch.
        if not matched: break
      else:
        # expected_file was deleted, so metadata has diverged from data.
        logger.info("{0} has been deleted".format(expected_file))
        matched = False
        break

    # For observed_file in targets, does it match the expected_file in metadata?
    if matched:
      # FIXME: Temporary hack to workaround Python 2 not returning filenames in
      # Unicode unless you do tricks like this:
      # http://docs.python.org/2/howto/unicode.html#unicode-filenames
      files_directory = unicode(files_directory, encoding="utf-8")
      # Get the list of observed target files.
      observed_targets = signerlib.get_targets(files_directory,
                                               recursive_walk=recursive_walk,
                                               followlinks=followlinks,
                                               file_predicate=file_predicate)

      for observed_file in observed_targets:
        # Ensure that form of observed_file conforms to that of expected_file.
        # Presently, this means that they do not share the "targets/" prefix.
        assert observed_file.startswith(targets_directory)
        observed_file = observed_file[len(targets_directory)+1:]
        # observed_file was added, so metadata has diverged from data.
        if observed_file not in expected_targets:
          logger.info("{0} is new".format(observed_file))
          matched = False
          break

    return matched
Пример #20
0
def update_unclaimed_targets():
    # For simplicity, ensure that we can evenly distribute MAX_NUMBER_OF_BINS
    # over NUMBER_OF_BINS.
    assert MAX_NUMBER_OF_BINS % NUMBER_OF_BINS == 0

    # Get all possible targets.
    absolute_delegated_paths = signerlib.get_targets(
        delegate.TARGETS_DIRECTORY, recursive_walk=True, followlinks=True)

    logger.info('There are {0} total PyPI targets.'.format(
        len(absolute_delegated_paths)))

    # Record the absolute delegated paths that fall into each bin.
    absolute_delegated_paths_in_bin = \
      {bin_index: [] for bin_index in xrange(MAX_NUMBER_OF_BINS)}

    # Assign every path to its bin.
    for absolute_delegated_path in absolute_delegated_paths:
        assert absolute_delegated_path.startswith(delegate.TARGETS_DIRECTORY +
                                                  '/')

        relative_delegated_path = \
          absolute_delegated_path[len(delegate.TARGETS_DIRECTORY)+1:]
        relative_delegated_path_digest = hasher.digest(algorithm=HASH_FUNCTION)
        relative_delegated_path_digest.update(relative_delegated_path)
        relative_delegated_path_hash = relative_delegated_path_digest.hexdigest(
        )
        relative_delegated_path_hash_prefix = \
          relative_delegated_path_hash[:PREFIX_LENGTH]

        # Convert a base-16 (hex) number to a base-10 (dec) number.
        bin_index = int(relative_delegated_path_hash_prefix, 16)
        assert bin_index > -1
        assert bin_index < MAX_NUMBER_OF_BINS

        absolute_delegated_paths_in_bin[bin_index] += [absolute_delegated_path]

    # Delegate every target to the "unclaimed" targets role.
    unclaimed_relative_delegated_paths = ["targets/gems/"]

    # TODO: Update delegatee only if necessary to do so.
    unclaimed_relative_target_paths = []
    delegate.update_targets_metadata(delegate.UNCLAIMED_TARGETS_ROLE_NAME,
                                     unclaimed_relative_target_paths,
                                     datetime.timedelta(days=365))

    # TODO: Comment on shared keys, and why this must come after
    # update_targets_metadata.
    unclaimed_targets_role_keys = \
      delegate.get_keys_for_targets_role(delegate.UNCLAIMED_TARGETS_ROLE_NAME)

    delegate.make_delegation(
        delegate.TARGETS_ROLE_NAME,
        delegate.UNCLAIMED_TARGETS_ROLE_NAME,
        relative_delegated_paths=unclaimed_relative_delegated_paths)

    # Delegate from the "unclaimed" targets role to each bin.
    bin_offset = MAX_NUMBER_OF_BINS // NUMBER_OF_BINS
    for global_bin_index in xrange(0, MAX_NUMBER_OF_BINS, bin_offset):
        # The bin index in hex padded from the left with zeroes for up to the
        # PREFIX_LENGTH.
        global_bin_index_in_hex = hex(global_bin_index)[2:].zfill(
            PREFIX_LENGTH)

        relative_binned_targets_role_name = global_bin_index_in_hex
        absolute_binned_targets_role_name = \
          os.path.join(delegate.UNCLAIMED_TARGETS_ROLE_NAME,
                       relative_binned_targets_role_name)
        absolute_delegated_paths_in_this_bin = []
        path_hash_prefixes = []

        for local_bin_index in xrange(global_bin_index,
                                      global_bin_index + bin_offset):
            absolute_delegated_paths_in_this_bin.extend(
                absolute_delegated_paths_in_bin[local_bin_index])
            local_bin_index_in_hex = hex(local_bin_index)[2:].zfill(
                PREFIX_LENGTH)
            path_hash_prefixes.append(local_bin_index_in_hex)

        relative_delegated_paths_in_this_bin = \
          delegate.get_relative_delegated_paths(absolute_delegated_paths_in_this_bin)

        # TODO: Update delegator only if necessary to do so.
        delegate.update_delegator_metadata(
            delegate.UNCLAIMED_TARGETS_ROLE_NAME,
            relative_binned_targets_role_name,
            unclaimed_targets_role_keys,
            unclaimed_targets_role_keys,
            path_hash_prefixes=path_hash_prefixes)

        logger.info('Delegated from {0} to {1}'.format(
            delegate.UNCLAIMED_TARGETS_ROLE_NAME,
            absolute_binned_targets_role_name))

        # TODO: Update delegatee only if necessary to do so.
        delegate.update_targets_metadata(
            absolute_binned_targets_role_name,
            relative_delegated_paths_in_this_bin,
            datetime.timedelta(days=90),
            targets_role_keys=unclaimed_targets_role_keys)

        logger.info('Wrote {0}'.format(absolute_binned_targets_role_name))

    # Compress "unclaimed" targets role metadata.
    unclaimed_targets_role_filename = \
      os.path.join(delegate.METADATA_DIRECTORY,
                   '{0}.txt'.format(delegate.UNCLAIMED_TARGETS_ROLE_NAME))
    delegate.compress_metadata(unclaimed_targets_role_filename)

    # Compute statistics and check sanity.

    expected_number_of_paths_per_bin = \
      len(absolute_delegated_paths)/MAX_NUMBER_OF_BINS

    observed_number_of_paths_in_all_bins = \
      sum((len(paths) for paths in absolute_delegated_paths_in_bin.values()))

    observed_number_of_bins = len(absolute_delegated_paths_in_bin)

    observed_number_of_paths_per_bin = \
      observed_number_of_paths_in_all_bins/observed_number_of_bins

    # Each of the delegated paths must have been assigned to a bin.
    assert observed_number_of_paths_in_all_bins == len(
        absolute_delegated_paths)
    # The observed number of bins must be equal to the expected number of bins.
    assert observed_number_of_bins == MAX_NUMBER_OF_BINS

    logger.info('Expected number of paths per bin: {0}'.format(
        expected_number_of_paths_per_bin))
    logger.info('Observed number of paths per bin: {0}'.format(
        observed_number_of_paths_per_bin))
def update_unclaimed_targets():
  # For simplicity, ensure that we can evenly distribute MAX_NUMBER_OF_BINS
  # over NUMBER_OF_BINS.
  assert MAX_NUMBER_OF_BINS % NUMBER_OF_BINS == 0

  # Get all possible targets.
  absolute_delegated_paths = signerlib.get_targets(delegate.TARGETS_DIRECTORY,
                                                   recursive_walk=True,
                                                   followlinks=True)

  logger.info('There are {0} total PyPI targets.'.format(len(
              absolute_delegated_paths)))


  # Record the absolute delegated paths that fall into each bin.
  absolute_delegated_paths_in_bin = \
    {bin_index: [] for bin_index in xrange(MAX_NUMBER_OF_BINS)}

  # Assign every path to its bin.
  for absolute_delegated_path in absolute_delegated_paths:
    assert absolute_delegated_path.startswith(delegate.TARGETS_DIRECTORY+'/')

    relative_delegated_path = \
      absolute_delegated_path[len(delegate.TARGETS_DIRECTORY)+1:]
    relative_delegated_path_digest = hasher.digest(algorithm=HASH_FUNCTION)
    relative_delegated_path_digest.update(relative_delegated_path)
    relative_delegated_path_hash = relative_delegated_path_digest.hexdigest()
    relative_delegated_path_hash_prefix = \
      relative_delegated_path_hash[:PREFIX_LENGTH]

    # Convert a base-16 (hex) number to a base-10 (dec) number.
    bin_index = int(relative_delegated_path_hash_prefix, 16)
    assert bin_index > -1
    assert bin_index < MAX_NUMBER_OF_BINS

    absolute_delegated_paths_in_bin[bin_index] += [absolute_delegated_path] 


  # Delegate every target to the "unclaimed" targets role.
  # Presently, every target falls under the "simple/" and "packages/"
  # directories.
  unclaimed_relative_delegated_paths = ["targets/simple/", "targets/packages/"]

  # TODO: Update delegatee only if necessary to do so.
  unclaimed_relative_target_paths = []
  delegate.update_targets_metadata(delegate.UNCLAIMED_TARGETS_ROLE_NAME,
                                   unclaimed_relative_target_paths,
                                   datetime.timedelta(days=365))

  # TODO: Comment on shared keys, and why this must come after
  # update_targets_metadata.
  unclaimed_targets_role_keys = \
    delegate.get_keys_for_targets_role(delegate.UNCLAIMED_TARGETS_ROLE_NAME)

  delegate.make_delegation(delegate.TARGETS_ROLE_NAME,
                           delegate.UNCLAIMED_TARGETS_ROLE_NAME,
                           relative_delegated_paths=unclaimed_relative_delegated_paths)

  # Delegate from the "unclaimed" targets role to each bin.

  bin_offset = MAX_NUMBER_OF_BINS // NUMBER_OF_BINS
  for global_bin_index in xrange(0, MAX_NUMBER_OF_BINS, bin_offset):
    # The bin index in hex padded from the left with zeroes for up to the
    # PREFIX_LENGTH.
    global_bin_index_in_hex = hex(global_bin_index)[2:].zfill(PREFIX_LENGTH)

    relative_binned_targets_role_name = global_bin_index_in_hex
    absolute_binned_targets_role_name = \
      os.path.join(delegate.UNCLAIMED_TARGETS_ROLE_NAME,
                   relative_binned_targets_role_name)
    absolute_delegated_paths_in_this_bin = []
    path_hash_prefixes = []

    for local_bin_index in xrange(global_bin_index,
                                  global_bin_index+bin_offset):
      absolute_delegated_paths_in_this_bin.extend(
        absolute_delegated_paths_in_bin[local_bin_index])
      local_bin_index_in_hex = hex(local_bin_index)[2:].zfill(PREFIX_LENGTH)
      path_hash_prefixes.append(local_bin_index_in_hex)

    relative_delegated_paths_in_this_bin = \
      delegate.get_relative_delegated_paths(absolute_delegated_paths_in_this_bin)

    # TODO: Update delegator only if necessary to do so.
    delegate.update_delegator_metadata(delegate.UNCLAIMED_TARGETS_ROLE_NAME,
                                       relative_binned_targets_role_name,
                                       unclaimed_targets_role_keys,
                                       unclaimed_targets_role_keys,
                                       path_hash_prefixes=path_hash_prefixes)

    logger.info('Delegated from {0} to {1}'.format(
                delegate.UNCLAIMED_TARGETS_ROLE_NAME,
                absolute_binned_targets_role_name))

    # TODO: Update delegatee only if necessary to do so.
    delegate.update_targets_metadata(absolute_binned_targets_role_name,
                                     relative_delegated_paths_in_this_bin,
                                     datetime.timedelta(days=90),
                                     targets_role_keys=unclaimed_targets_role_keys)

    logger.info('Wrote {0}'.format(absolute_binned_targets_role_name))

  # Compress "unclaimed" targets role metadata.
  unclaimed_targets_role_filename = \
    os.path.join(delegate.METADATA_DIRECTORY,
                 '{0}.txt'.format(delegate.UNCLAIMED_TARGETS_ROLE_NAME))
  delegate.compress_metadata(unclaimed_targets_role_filename)

  # Compute statistics and check sanity.

  expected_number_of_paths_per_bin = \
    len(absolute_delegated_paths)/MAX_NUMBER_OF_BINS

  observed_number_of_paths_in_all_bins = \
    sum((len(paths) for paths in absolute_delegated_paths_in_bin.values()))

  observed_number_of_bins = len(absolute_delegated_paths_in_bin)

  observed_number_of_paths_per_bin = \
    observed_number_of_paths_in_all_bins/observed_number_of_bins

  # Each of the delegated paths must have been assigned to a bin.
  assert observed_number_of_paths_in_all_bins == len(absolute_delegated_paths)
  # The observed number of bins must be equal to the expected number of bins.
  assert observed_number_of_bins == MAX_NUMBER_OF_BINS

  logger.info('Expected number of paths per bin: {0}'.format(
              expected_number_of_paths_per_bin))
  logger.info('Observed number of paths per bin: {0}'.format(
              observed_number_of_paths_per_bin))