diff --git a/tests/test_formats.py b/tests/test_formats.py index 9faa017633..dbff791c4e 100755 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -31,6 +31,7 @@ import unittest import datetime import sys +import os import tuf import tuf.formats @@ -38,6 +39,7 @@ import utils import securesystemslib +import securesystemslib.util import six @@ -778,20 +780,10 @@ def test_parse_base64(self): def test_make_signable(self): # Test conditions for expected make_signable() behavior. - root = {'_type': 'root', - 'spec_version': '1.0.0', - 'version': 8, - 'consistent_snapshot': False, - 'expires': '1985-10-21T13:20:00Z', - 'keys': {'123abc': {'keytype': 'rsa', - 'scheme': 'rsassa-pss-sha256', - 'keyval': {'public': 'pubkey', - 'private': 'privkey'}}}, - 'roles': {'root': {'keyids': ['123abc'], - 'threshold': 1, - 'paths': ['path1/', 'path2']}}} - SIGNABLE_SCHEMA = tuf.formats.SIGNABLE_SCHEMA + root_file = os.path.join('repository_data', 'repository', 'metadata', + 'root.json') + root = securesystemslib.util.load_json_file(root_file) self.assertTrue(SIGNABLE_SCHEMA.matches(tuf.formats.make_signable(root))) signable = tuf.formats.make_signable(root) self.assertEqual('root', tuf.formats.check_signable_object_format(signable)) @@ -902,19 +894,9 @@ def test_expected_meta_rolename(self): def test_check_signable_object_format(self): # Test condition for a valid argument. - root = {'_type': 'root', - 'spec_version': '1.0.0', - 'version': 8, - 'consistent_snapshot': False, - 'expires': '1985-10-21T13:20:00Z', - 'keys': {'123abc': {'keytype': 'rsa', - 'scheme': 'rsassa-pss-sha256', - 'keyval': {'public': 'pubkey', - 'private': 'privkey'}}}, - 'roles': {'root': {'keyids': ['123abc'], - 'threshold': 1, - 'paths': ['path1/', 'path2']}}} - + root_file = os.path.join('repository_data', 'repository', 'metadata', + 'root.json') + root = securesystemslib.util.load_json_file(root_file) root = tuf.formats.make_signable(root) self.assertEqual('root', tuf.formats.check_signable_object_format(root)) diff --git a/tests/test_repository_lib.py b/tests/test_repository_lib.py index e136787407..40cf6f964b 100755 --- a/tests/test_repository_lib.py +++ b/tests/test_repository_lib.py @@ -885,6 +885,8 @@ def test__generate_and_write_metadata(self): # (specifically 'snapshot') and keys to be available in 'tuf.roledb'. tuf.roledb.create_roledb_from_root_metadata(root_signable['signed'], repository_name) + tuf.keydb.create_keydb_from_root_metadata(root_signable['signed'], + repository_name) temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) targets_directory = os.path.join(temporary_directory, 'targets') os.mkdir(targets_directory) @@ -897,6 +899,14 @@ def test__generate_and_write_metadata(self): securesystemslib.util.ensure_parent_dir(obsolete_metadata) shutil.copyfile(targets_metadata, obsolete_metadata) + keystore_path = os.path.join('repository_data', 'keystore') + targets_private_keypath = os.path.join(keystore_path, 'targets_key') + targets_private_key = repo_lib.import_ed25519_privatekey_from_file(targets_private_keypath, + 'password') + tuf.keydb.remove_key(targets_private_key['keyid'], + repository_name=repository_name) + tuf.keydb.add_key(targets_private_key, repository_name=repository_name) + # Verify that obsolete metadata (a metadata file exists on disk, but the # role is unavailable in 'tuf.roledb'). First add the obsolete # role to 'tuf.roledb' so that its metadata file can be written to disk. @@ -906,6 +916,7 @@ def test__generate_and_write_metadata(self): tuf.formats.unix_timestamp_to_datetime(int(time.time() + 86400)) expiration = expiration.isoformat() + 'Z' targets_roleinfo['expires'] = expiration + targets_roleinfo['signing_keyids'] = targets_roleinfo['keyids'] tuf.roledb.add_role('obsolete_role', targets_roleinfo, repository_name=repository_name) @@ -1004,6 +1015,16 @@ def test__load_top_level_metadata(self): roleinfo['version'] = 1 tuf.roledb.add_role('role1', roleinfo, repository_name) + keystore_path = os.path.join('repository_data', 'keystore') + root_privkey_path = os.path.join(keystore_path, 'root_key') + targets_privkey_path = os.path.join(keystore_path, 'targets_key') + snapshot_privkey_path = os.path.join(keystore_path, 'snapshot_key') + timestamp_privkey_path = os.path.join(keystore_path, 'timestamp_key') + + repository.root.load_signing_key(repo_lib.import_rsa_privatekey_from_file(root_privkey_path, 'password')) + repository.targets.load_signing_key(repo_lib.import_ed25519_privatekey_from_file(targets_privkey_path, 'password')) + repository.snapshot.load_signing_key(repo_lib.import_ed25519_privatekey_from_file(snapshot_privkey_path, 'password')) + repository.timestamp.load_signing_key(repo_lib.import_ed25519_privatekey_from_file(timestamp_privkey_path, 'password')) # Partially write all top-level roles (we increase the threshold of each # top-level role so that they are flagged as partially written. @@ -1011,7 +1032,7 @@ def test__load_top_level_metadata(self): repository.snapshot.threshold = repository.snapshot.threshold + 1 repository.targets.threshold = repository.targets.threshold + 1 repository.timestamp.threshold = repository.timestamp.threshold + 1 - repository.write('root', ) + repository.write('root') repository.write('snapshot') repository.write('targets') repository.write('timestamp') diff --git a/tests/test_updater_root_rotation_integration.py b/tests/test_updater_root_rotation_integration.py index c101b496b5..a8e74616aa 100755 --- a/tests/test_updater_root_rotation_integration.py +++ b/tests/test_updater_root_rotation_integration.py @@ -357,10 +357,13 @@ def test_root_rotation_max(self): def test_root_rotation_missing_keys(self): repository = repo_tool.load_repository(self.repository_directory) - # A partially written root.json (threshold = 1, and not signed in this - # case) causes an invalid root chain later. + # A partially written root.json (threshold = 2, and signed with only 1 key) + # causes an invalid root chain later. + repository.root.threshold = 2 + repository.root.load_signing_key(self.role_keys['root']['private']) repository.snapshot.load_signing_key(self.role_keys['snapshot']['private']) repository.timestamp.load_signing_key(self.role_keys['timestamp']['private']) + repository.write('root') repository.write('snapshot') repository.write('timestamp') @@ -371,9 +374,9 @@ def test_root_rotation_missing_keys(self): os.path.join(self.repository_directory, 'metadata')) # Create a new, valid root.json. - repository.root.threshold = 2 + # Still not valid, because it is not written with a threshold of 2 + # previous keys repository.root.add_verification_key(self.role_keys['role1']['public']) - repository.root.load_signing_key(self.role_keys['root']['private']) repository.root.load_signing_key(self.role_keys['role1']['private']) repository.writeall() diff --git a/tuf/developer_tool.py b/tuf/developer_tool.py index 0db06fe847..0d4c3331c6 100755 --- a/tuf/developer_tool.py +++ b/tuf/developer_tool.py @@ -855,7 +855,12 @@ def load_project(project_directory, prefix='', new_targets_location=None, targets_metadata_path = os.path.join(project_directory, metadata_directory, project_filename) signable = securesystemslib.util.load_json_file(targets_metadata_path) - tuf.formats.check_signable_object_format(signable) + try: + tuf.formats.check_signable_object_format(signable) + except tuf.exceptions.UnsignedMetadataError: + # Downgrade the error to a warning because a use case exists where + # metadata may be generated unsigned on one machine and signed on another. + logger.warning('Unsigned metadata object: ' + repr(signable)) targets_metadata = signable['signed'] # Remove the prefix from the metadata. diff --git a/tuf/formats.py b/tuf/formats.py index d289ddb0b4..da0dc25c56 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -938,6 +938,9 @@ def check_signable_object_format(signable): securesystemslib.exceptions.FormatError, if 'signable' does not have the correct format. + tuf.exceptions.UnsignedMetadataError, if 'signable' does not have any + signatures + None. @@ -965,6 +968,10 @@ def check_signable_object_format(signable): six.raise_from(securesystemslib.exceptions.FormatError( 'Unrecognized type ' + repr(role_type)), error) + if not signable['signatures']: + raise tuf.exceptions.UnsignedMetadataError('Signable object of type ' + + repr(role_type) + ' has no signatures ', signable) + # 'securesystemslib.exceptions.FormatError' raised if 'signable' does not # have a properly formatted role schema. schema.check_match(signable['signed']) diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index 491ca09beb..9ffe8d977b 100644 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -505,7 +505,13 @@ def _load_top_level_metadata(repository, top_level_filenames, repository_name): try: # Initialize the key and role metadata of the top-level roles. signable = securesystemslib.util.load_json_file(root_filename) - tuf.formats.check_signable_object_format(signable) + try: + tuf.formats.check_signable_object_format(signable) + except tuf.exceptions.UnsignedMetadataError: + # Downgrade the error to a warning because a use case exists where + # metadata may be generated unsigned on one machine and signed on another. + logger.warning('Unsigned metadata object: ' + repr(signable)) + root_metadata = signable['signed'] tuf.keydb.create_keydb_from_root_metadata(root_metadata, repository_name) tuf.roledb.create_roledb_from_root_metadata(root_metadata, repository_name) @@ -586,7 +592,13 @@ def _load_top_level_metadata(repository, top_level_filenames, repository_name): try: signable = securesystemslib.util.load_json_file(snapshot_filename) - tuf.formats.check_signable_object_format(signable) + try: + tuf.formats.check_signable_object_format(signable) + except tuf.exceptions.UnsignedMetadataError: + # Downgrade the error to a warning because a use case exists where + # metadata may be generated unsigned on one machine and signed on another. + logger.warning('Unsigned metadata object: ' + repr(signable)) + snapshot_metadata = signable['signed'] for signature in signable['signatures']: @@ -622,7 +634,13 @@ def _load_top_level_metadata(repository, top_level_filenames, repository_name): try: signable = securesystemslib.util.load_json_file(targets_filename) - tuf.formats.check_signable_object_format(signable) + try: + tuf.formats.check_signable_object_format(signable) + except tuf.exceptions.UnsignedMetadataError: + # Downgrade the error to a warning because a use case exists where + # metadata may be generated unsigned on one machine and signed on another. + logger.warning('Unsigned metadata object: ' + repr(signable)) + targets_metadata = signable['signed'] for signature in signable['signatures']: @@ -1862,7 +1880,13 @@ def sign_metadata(metadata_object, keyids, filename, repository_name): # Raise 'securesystemslib.exceptions.FormatError' if the resulting 'signable' # is not formatted correctly. - tuf.formats.check_signable_object_format(signable) + try: + tuf.formats.check_signable_object_format(signable) + except tuf.exceptions.UnsignedMetadataError: + # Downgrade the error to a warning because a use case exists where + # metadata may be generated unsigned on one machine and signed on another. + logger.warning('Unsigned metadata object: ' + repr(signable)) + return signable