Exemplo n.º 1
0
    def serialize(self, signed_obj: Signed) -> bytes:
        """Serialize Signed object into utf-8 encoded OLPC Canonical JSON
        bytes.
        """
        try:
            signed_dict = signed_obj.to_dict()
            canonical_bytes = encode_canonical(signed_dict).encode("utf-8")

        except Exception as e:
            raise SerializationError from e

        return canonical_bytes
Exemplo n.º 2
0
def generate_rsa_signature(signed, rsakey_dict):
    """
  <Purpose>
    Generate a new signature dict presumably to be added to the 'signatures'
    field of 'signable'.  The 'signable' dict is of the form:

    {'signed': 'signer',
               'signatures': [{'keyid': keyid,
                               'method': 'evp',
                               'sig': sig}]}

    The 'signed' argument is needed here for the signing process.
    The 'rsakey_dict' argument is used to generate 'keyid', 'method', and 'sig'.

    The caller should ensure the returned signature is not already in
    'signable'.

  <Arguments>
    signed:
      The data used by 'securesystemslib.keys.create_signature()' to generate
      signatures.  It is stored in the 'signed' field of 'signable'.

    rsakey_dict:
      The RSA key, a 'securesystemslib.formats.RSAKEY_SCHEMA' dictionary.
      Used here to produce 'keyid', 'method', and 'sig'.

  <Exceptions>
    securesystemslib.exceptions.FormatError, if 'rsakey_dict' does not have the
    correct format.

    TypeError, if a private key is not defined for 'rsakey_dict'.

  <Side Effects>
    None.

  <Returns>
    Signature dictionary conformant to securesystemslib.formats.SIGNATURE_SCHEMA.
    Has the form:
    {'keyid': keyid, 'method': 'evp', 'sig': sig}
  """

    # We need 'signed' in canonical JSON format to generate
    # the 'method' and 'sig' fields of the signature.
    signed = sslib_formats.encode_canonical(signed).encode('utf-8')

    # Generate the RSA signature.
    # Raises securesystemslib.exceptions.FormatError and TypeError.
    signature = sslib_keys.create_signature(rsakey_dict, signed)

    return signature
Exemplo n.º 3
0
    def test_tutorial(self):
        """
    Run the TUTORIAL.md tutorial.
    Note that anywhere the tutorial provides a command that prompts for the
    user to enter a passphrase/password, this test is changed to simply provide
    that as an argument. It's not worth trying to arrange automated testing of
    the interactive password entry process here. Anywhere user entry has been
    skipped from the tutorial instructions, "# Skipping user entry of password"
    is written, with the original line below it, starting with ##.
    """

        # ----- Tutorial Section:  Keys

        generate_and_write_rsa_keypair('root_key',
                                       bits=2048,
                                       password='******')

        # Skipping user entry of password
        ## generate_and_write_rsa_keypair('root_key2')
        generate_and_write_rsa_keypair('root_key2', password='******')

        # Tutorial tells users to expect these files to exist:
        # ['root_key', 'root_key.pub', 'root_key2', 'root_key2.pub']
        for fname in [
                'root_key', 'root_key.pub', 'root_key2', 'root_key2.pub'
        ]:
            self.assertTrue(os.path.exists(fname))

        # Generate key pair at /path/to/KEYID
        fname = generate_and_write_rsa_keypair(password="******")
        self.assertTrue(os.path.exists(fname))

        # ----- Tutorial Section:  Import RSA Keys

        public_root_key = import_rsa_publickey_from_file('root_key.pub')

        # Skipping user entry of password
        ## private_root_key = import_rsa_privatekey_from_file('root_key')
        private_root_key = import_rsa_privatekey_from_file(
            'root_key', 'password')

        # Skipping user entry of password
        ## import_rsa_privatekey_from_file('root_key')
        with self.assertRaises(securesystemslib.exceptions.CryptoError):
            import_rsa_privatekey_from_file('root_key', 'not_the_real_pw')

        # ----- Tutorial Section: Create and Import Ed25519 Keys

        # Skipping user entry of password
        ## generate_and_write_ed25519_keypair('ed25519_key')
        generate_and_write_ed25519_keypair('ed25519_key', password='******')

        public_ed25519_key = import_ed25519_publickey_from_file(
            'ed25519_key.pub')

        # Skipping user entry of password
        ## private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key')
        private_ed25519_key = import_ed25519_privatekey_from_file(
            'ed25519_key', 'password')

        # ----- Tutorial Section: Create Top-level Metadata
        repository = create_new_repository('repository')
        repository.root.add_verification_key(public_root_key)
        self.assertTrue(repository.root.keys)

        public_root_key2 = import_rsa_publickey_from_file('root_key2.pub')
        repository.root.add_verification_key(public_root_key2)

        repository.root.threshold = 2
        private_root_key2 = import_rsa_privatekey_from_file(
            'root_key2', password='******')

        repository.root.load_signing_key(private_root_key)
        repository.root.load_signing_key(private_root_key2)

        # NOTE: The tutorial does not call dirty_roles anymore due to #964 and
        # #958. We still call it here to see if roles are dirty as expected.
        with mock.patch("tuf.repository_tool.logger") as mock_logger:
            repository.dirty_roles()
            # Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
            mock_logger.info.assert_called_with("Dirty roles: " +
                                                str(['root']))

        # Patch logger to assert that it accurately logs the repo's status. Since
        # the logger is called multiple times, we have to assert for the accurate
        # sequence of calls or rather its call arguments.
        with mock.patch("tuf.repository_lib.logger") as mock_logger:
            repository.status()
            # Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
            self.assertListEqual([
                repr('targets') + " role contains 0 / 1 public keys.",
                repr('snapshot') + " role contains 0 / 1 public keys.",
                repr('timestamp') + " role contains 0 / 1 public keys.",
                repr('root') + " role contains 2 / 2 signatures.",
                repr('targets') + " role contains 0 / 1 signatures."
            ], [args[0] for args, _ in mock_logger.info.call_args_list])

        generate_and_write_rsa_keypair('targets_key', password='******')
        generate_and_write_rsa_keypair('snapshot_key', password='******')
        generate_and_write_rsa_keypair('timestamp_key', password='******')

        repository.targets.add_verification_key(
            import_rsa_publickey_from_file('targets_key.pub'))
        repository.snapshot.add_verification_key(
            import_rsa_publickey_from_file('snapshot_key.pub'))
        repository.timestamp.add_verification_key(
            import_rsa_publickey_from_file('timestamp_key.pub'))

        # Skipping user entry of password
        ## private_targets_key = import_rsa_privatekey_from_file('targets_key')
        private_targets_key = import_rsa_privatekey_from_file(
            'targets_key', 'password')

        # Skipping user entry of password
        ## private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key')
        private_snapshot_key = import_rsa_privatekey_from_file(
            'snapshot_key', 'password')

        # Skipping user entry of password
        ## private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key')
        private_timestamp_key = import_rsa_privatekey_from_file(
            'timestamp_key', 'password')

        repository.targets.load_signing_key(private_targets_key)
        repository.snapshot.load_signing_key(private_snapshot_key)
        repository.timestamp.load_signing_key(private_timestamp_key)

        repository.timestamp.expiration = datetime.datetime(
            2080, 10, 28, 12, 8)

        # NOTE: The tutorial does not call dirty_roles anymore due to #964 and
        # #958. We still call it here to see if roles are dirty as expected.
        with mock.patch("tuf.repository_tool.logger") as mock_logger:
            repository.dirty_roles()
            # Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
            mock_logger.info.assert_called_with(
                "Dirty roles: " +
                str(['root', 'snapshot', 'targets', 'timestamp']))

        repository.writeall()

        # ----- Tutorial Section: Targets
        # These next commands in the tutorial are shown as bash commands, so I'll
        # just simulate this with some Python commands.
        ## $ cd repository/targets/
        ## $ echo 'file1' > file1.txt
        ## $ echo 'file2' > file2.txt
        ## $ echo 'file3' > file3.txt
        ## $ mkdir myproject; echo 'file4' > myproject/file4.txt
        ## $ cd ../../

        with open(os.path.join('repository', 'targets', 'file1.txt'),
                  'w') as fobj:
            fobj.write('file1')
        with open(os.path.join('repository', 'targets', 'file2.txt'),
                  'w') as fobj:
            fobj.write('file2')
        with open(os.path.join('repository', 'targets', 'file3.txt'),
                  'w') as fobj:
            fobj.write('file3')

        os.mkdir(os.path.join('repository', 'targets', 'myproject'))
        with open(
                os.path.join('repository', 'targets', 'myproject',
                             'file4.txt'), 'w') as fobj:
            fobj.write('file4')

        repository = load_repository('repository')
        list_of_targets = repository.get_filepaths_in_directory(
            os.path.join('repository', 'targets'),
            recursive_walk=False,
            followlinks=True)

        self.assertListEqual(sorted(list_of_targets), [
            os.path.abspath(os.path.join('repository', 'targets',
                                         'file1.txt')),
            os.path.abspath(os.path.join('repository', 'targets',
                                         'file2.txt')),
            os.path.abspath(os.path.join('repository', 'targets', 'file3.txt'))
        ])

        repository.targets.add_targets(list_of_targets)

        self.assertTrue('file1.txt' in repository.targets.target_files)
        self.assertTrue('file2.txt' in repository.targets.target_files)
        self.assertTrue('file3.txt' in repository.targets.target_files)

        target4_filepath = os.path.abspath(
            os.path.join('repository', 'targets', 'myproject', 'file4.txt'))
        octal_file_permissions = oct(os.stat(target4_filepath).st_mode)[4:]
        custom_file_permissions = {'file_permissions': octal_file_permissions}
        repository.targets.add_target(target4_filepath,
                                      custom_file_permissions)
        # Note that target filepaths specified in the repo use '/' even on Windows.
        # (This is important to make metadata platform-independent.)
        self.assertTrue(
            os.path.join('myproject/file4.txt') in
            repository.targets.target_files)

        # Skipping user entry of password
        ## private_targets_key = import_rsa_privatekey_from_file('targets_key')
        private_targets_key = import_rsa_privatekey_from_file(
            'targets_key', 'password')
        repository.targets.load_signing_key(private_targets_key)

        # Skipping user entry of password
        ## private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key')
        private_snapshot_key = import_rsa_privatekey_from_file(
            'snapshot_key', 'password')
        repository.snapshot.load_signing_key(private_snapshot_key)

        # Skipping user entry of password
        ## private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key')
        private_timestamp_key = import_rsa_privatekey_from_file(
            'timestamp_key', 'password')
        repository.timestamp.load_signing_key(private_timestamp_key)

        # NOTE: The tutorial does not call dirty_roles anymore due to #964 and
        # #958. We still call it here to see if roles are dirty as expected.
        with mock.patch("tuf.repository_tool.logger") as mock_logger:
            repository.dirty_roles()
            # Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
            mock_logger.info.assert_called_with(
                "Dirty roles: " + str(['snapshot', 'targets', 'timestamp']))

        repository.writeall()

        repository.targets.remove_target('myproject/file4.txt')
        self.assertTrue(
            os.path.exists(
                os.path.join('repository', 'targets', 'myproject',
                             'file4.txt')))

        # NOTE: The tutorial does not call dirty_roles anymore due to #964 and
        # #958. We still call it here to see if roles are dirty as expected.
        with mock.patch("tuf.repository_tool.logger") as mock_logger:
            repository.dirty_roles()
            # Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
            mock_logger.info.assert_called_with("Dirty roles: " +
                                                str(['targets']))

        repository.mark_dirty(['snapshot', 'timestamp'])
        repository.writeall()

        # ----- Tutorial Section: Excursion: Dump Metadata and Append Signature
        signable_content = dump_signable_metadata(
            os.path.join('repository', 'metadata.staged', 'timestamp.json'))

        # Skipping user entry of password
        ## private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key')
        private_ed25519_key = import_ed25519_privatekey_from_file(
            'ed25519_key', 'password')
        signature = create_signature(
            private_ed25519_key,
            encode_canonical(signable_content).encode())
        append_signature(
            signature,
            os.path.join('repository', 'metadata.staged', 'timestamp.json'))

        # ----- Tutorial Section: Delegations
        generate_and_write_rsa_keypair('unclaimed_key',
                                       bits=2048,
                                       password='******')
        public_unclaimed_key = import_rsa_publickey_from_file(
            'unclaimed_key.pub')
        repository.targets.delegate('unclaimed', [public_unclaimed_key],
                                    ['myproject/*.txt'])

        repository.targets("unclaimed").add_target("myproject/file4.txt")

        # Skipping user entry of password
        ## private_unclaimed_key = import_rsa_privatekey_from_file('unclaimed_key')
        private_unclaimed_key = import_rsa_privatekey_from_file(
            'unclaimed_key', 'password')
        repository.targets("unclaimed").load_signing_key(private_unclaimed_key)

        # NOTE: The tutorial does not call dirty_roles anymore due to #964 and
        # #958. We still call it here to see if roles are dirty as expected.
        with mock.patch("tuf.repository_tool.logger") as mock_logger:
            repository.dirty_roles()
            # Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
            mock_logger.info.assert_called_with("Dirty roles: " +
                                                str(['targets', 'unclaimed']))

        repository.mark_dirty(["snapshot", "timestamp"])
        repository.writeall()

        # Simulate the following shell command:
        ## $ cp -r "repository/metadata.staged/" "repository/metadata/"
        shutil.copytree(os.path.join('repository', 'metadata.staged'),
                        os.path.join('repository', 'metadata'))

        # ----- Tutorial Section: Delegate to Hashed Bins
        repository.targets('unclaimed').remove_target("myproject/file4.txt")

        targets = repository.get_filepaths_in_directory(os.path.join(
            'repository', 'targets', 'myproject'),
                                                        recursive_walk=True)

        # Patch logger to assert that it accurately logs the output of hashed bin
        # delegation. The logger is called multiple times, first with info level
        # then with warning level. So we have to assert for the accurate sequence
        # of calls or rather its call arguments.
        with mock.patch("tuf.repository_tool.logger") as mock_logger:
            repository.targets('unclaimed').delegate_hashed_bins(
                targets, [public_unclaimed_key], 32)

            self.assertListEqual(
                [
                    "Creating hashed bin delegations.", "1 total targets.",
                    "32 hashed bins.", "256 total hash prefixes.",
                    "Each bin ranges over 8 hash prefixes."
                ] +
                ["Adding a verification key that has already been used."] * 32,
                [
                    args[0] for args, _ in mock_logger.info.call_args_list +
                    mock_logger.warning.call_args_list
                ])

        for delegation in repository.targets('unclaimed').delegations:
            delegation.load_signing_key(private_unclaimed_key)

        # NOTE: The tutorial does not call dirty_roles anymore due to #964 and
        # #958. We still call it here to see if roles are dirty as expected.
        with mock.patch("tuf.repository_tool.logger") as mock_logger:
            repository.dirty_roles()
            # Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
            mock_logger.info.assert_called_with("Dirty roles: " + str([
                '00-07', '08-0f', '10-17', '18-1f', '20-27', '28-2f', '30-37',
                '38-3f', '40-47', '48-4f', '50-57', '58-5f', '60-67', '68-6f',
                '70-77', '78-7f', '80-87', '88-8f', '90-97', '98-9f', 'a0-a7',
                'a8-af', 'b0-b7', 'b8-bf', 'c0-c7', 'c8-cf', 'd0-d7', 'd8-df',
                'e0-e7', 'e8-ef', 'f0-f7', 'f8-ff', 'unclaimed'
            ]))

        repository.mark_dirty(["snapshot", "timestamp"])
        repository.writeall()

        # ----- Tutorial Section: How to Perform an Update

        # A separate tutorial is linked to for client use. That is not tested here.
        create_tuf_client_directory("repository/", "client/tufrepo/")
Exemplo n.º 4
0
def get_signature_status(signable,
                         role=None,
                         repository_name='default',
                         threshold=None,
                         keyids=None):
    """
  <Purpose>
    Return a dictionary representing the status of the signatures listed in
    'signable'. Signatures in the returned dictionary are identified by the
    signature keyid and can have a status of either:

    * bad -- Invalid signature
    * good -- Valid signature from key that is available in 'tuf.keydb', and is
      authorized for the passed role as per 'roledb' (authorization may be
      overwritten by passed 'keyids').
    * unknown -- Signature from key that is not available in 'tuf.keydb', or if
      'role' is None.
    * unknown signing schemes -- Signature from key with unknown signing
      scheme.
    * untrusted -- Valid signature from key that is available in 'tuf.keydb',
      but is not trusted for the passed role as per 'roledb' or the passed
      'keyids'.

    NOTE: The result may contain duplicate keyids or keyids that reference the
    same key, if 'signable' lists multiple signatures from the same key.

  <Arguments>
    signable:
      A dictionary containing a list of signatures and a 'signed' identifier.
      signable = {'signed': 'signer',
                  'signatures': [{'keyid': keyid,
                                  'sig': sig}]}

      Conformant to tuf.formats.SIGNABLE_SCHEMA.

    role:
      TUF role string (e.g. 'root', 'targets', 'snapshot' or timestamp).

    threshold:
      Rather than reference the role's threshold as set in roledb, use
      the given 'threshold' to calculate the signature status of 'signable'.
      'threshold' is an integer value that sets the role's threshold value, or
      the minimum number of signatures needed for metadata to be considered
      fully signed.

    keyids:
      Similar to the 'threshold' argument, use the supplied list of 'keyids'
      to calculate the signature status, instead of referencing the keyids
      in roledb for 'role'.

  <Exceptions>
    securesystemslib.exceptions.FormatError, if 'signable' does not have the
    correct format.

    tuf.exceptions.UnknownRoleError, if 'role' is not recognized.

  <Side Effects>
    None.

  <Returns>
    A dictionary representing the status of the signatures in 'signable'.
    Conformant to tuf.formats.SIGNATURESTATUS_SCHEMA.
  """

    # Do the arguments have the correct format?  This check will ensure that
    # arguments have the appropriate number of objects and object types, and that
    # all dict keys are properly named.  Raise
    # 'securesystemslib.exceptions.FormatError' if the check fails.
    formats.SIGNABLE_SCHEMA.check_match(signable)
    sslib_formats.NAME_SCHEMA.check_match(repository_name)

    if role is not None:
        formats.ROLENAME_SCHEMA.check_match(role)

    if threshold is not None:
        formats.THRESHOLD_SCHEMA.check_match(threshold)

    if keyids is not None:
        sslib_formats.KEYIDS_SCHEMA.check_match(keyids)

    # The signature status dictionary returned.
    signature_status = {}
    good_sigs = []
    bad_sigs = []
    unknown_sigs = []
    untrusted_sigs = []
    unknown_signing_schemes = []

    # Extract the relevant fields from 'signable' that will allow us to identify
    # the different classes of keys (i.e., good_sigs, bad_sigs, etc.).
    signed = sslib_formats.encode_canonical(signable['signed']).encode('utf-8')
    signatures = signable['signatures']

    # Iterate the signatures and enumerate the signature_status fields.
    # (i.e., good_sigs, bad_sigs, etc.).
    for signature in signatures:
        keyid = signature['keyid']

        # Does the signature use an unrecognized key?
        try:
            key = keydb.get_key(keyid, repository_name)

        except exceptions.UnknownKeyError:
            unknown_sigs.append(keyid)
            continue

        # Does the signature use an unknown/unsupported signing scheme?
        try:
            valid_sig = sslib_keys.verify_signature(key, signature, signed)

        except sslib_exceptions.UnsupportedAlgorithmError:
            unknown_signing_schemes.append(keyid)
            continue

        # We are now dealing with either a trusted or untrusted key...
        if valid_sig:
            if role is not None:

                # Is this an unauthorized key? (a keyid associated with 'role')
                # Note that if the role is not known, tuf.exceptions.UnknownRoleError
                # is raised here.
                if keyids is None:
                    keyids = roledb.get_role_keyids(role, repository_name)

                if keyid not in keyids:
                    untrusted_sigs.append(keyid)
                    continue

            # This is an unset role, thus an unknown signature.
            else:
                unknown_sigs.append(keyid)
                continue

            # Identify good/authorized key.
            good_sigs.append(keyid)

        else:
            # This is a bad signature for a trusted key.
            bad_sigs.append(keyid)

    # Retrieve the threshold value for 'role'.  Raise
    # tuf.exceptions.UnknownRoleError if we were given an invalid role.
    if role is not None:
        if threshold is None:
            # Note that if the role is not known, tuf.exceptions.UnknownRoleError is
            # raised here.
            threshold = roledb.get_role_threshold(
                role, repository_name=repository_name)

        else:
            logger.debug('Not using roledb.py\'s threshold for ' + repr(role))

    else:
        threshold = 0

    # Build the signature_status dict.
    signature_status['threshold'] = threshold
    signature_status['good_sigs'] = good_sigs
    signature_status['bad_sigs'] = bad_sigs
    signature_status['unknown_sigs'] = unknown_sigs
    signature_status['untrusted_sigs'] = untrusted_sigs
    signature_status['unknown_signing_schemes'] = unknown_signing_schemes

    return signature_status
Exemplo n.º 5
0
def canonical(_signed: Json) -> bytes:
    """Returns the UTF-8 encoded canonical JSON representation of _signed."""
    return encode_canonical(_signed).encode("utf-8")
Exemplo n.º 6
0
 def to_canonical_bytes(self) -> bytes:
     """Returns the UTF-8 encoded canonical JSON representation of self. """
     return encode_canonical(self.to_dict()).encode('UTF-8')