Skip to content

Commit d51dbb8

Browse files
authored
Merge pull request #1100 from sechkova/issue-1050
Raise an error on loading/writing unsigned metadata
2 parents 461663f + bebf19a commit d51dbb8

File tree

6 files changed

+78
-36
lines changed

6 files changed

+78
-36
lines changed

tests/test_formats.py

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@
3131
import unittest
3232
import datetime
3333
import sys
34+
import os
3435

3536
import tuf
3637
import tuf.formats
3738

3839
import utils
3940

4041
import securesystemslib
42+
import securesystemslib.util
4143
import six
4244

4345

@@ -778,20 +780,10 @@ def test_parse_base64(self):
778780

779781
def test_make_signable(self):
780782
# Test conditions for expected make_signable() behavior.
781-
root = {'_type': 'root',
782-
'spec_version': '1.0.0',
783-
'version': 8,
784-
'consistent_snapshot': False,
785-
'expires': '1985-10-21T13:20:00Z',
786-
'keys': {'123abc': {'keytype': 'rsa',
787-
'scheme': 'rsassa-pss-sha256',
788-
'keyval': {'public': 'pubkey',
789-
'private': 'privkey'}}},
790-
'roles': {'root': {'keyids': ['123abc'],
791-
'threshold': 1,
792-
'paths': ['path1/', 'path2']}}}
793-
794783
SIGNABLE_SCHEMA = tuf.formats.SIGNABLE_SCHEMA
784+
root_file = os.path.join('repository_data', 'repository', 'metadata',
785+
'root.json')
786+
root = securesystemslib.util.load_json_file(root_file)
795787
self.assertTrue(SIGNABLE_SCHEMA.matches(tuf.formats.make_signable(root)))
796788
signable = tuf.formats.make_signable(root)
797789
self.assertEqual('root', tuf.formats.check_signable_object_format(signable))
@@ -902,19 +894,9 @@ def test_expected_meta_rolename(self):
902894

903895
def test_check_signable_object_format(self):
904896
# Test condition for a valid argument.
905-
root = {'_type': 'root',
906-
'spec_version': '1.0.0',
907-
'version': 8,
908-
'consistent_snapshot': False,
909-
'expires': '1985-10-21T13:20:00Z',
910-
'keys': {'123abc': {'keytype': 'rsa',
911-
'scheme': 'rsassa-pss-sha256',
912-
'keyval': {'public': 'pubkey',
913-
'private': 'privkey'}}},
914-
'roles': {'root': {'keyids': ['123abc'],
915-
'threshold': 1,
916-
'paths': ['path1/', 'path2']}}}
917-
897+
root_file = os.path.join('repository_data', 'repository', 'metadata',
898+
'root.json')
899+
root = securesystemslib.util.load_json_file(root_file)
918900
root = tuf.formats.make_signable(root)
919901
self.assertEqual('root', tuf.formats.check_signable_object_format(root))
920902

tests/test_repository_lib.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,8 @@ def test__generate_and_write_metadata(self):
885885
# (specifically 'snapshot') and keys to be available in 'tuf.roledb'.
886886
tuf.roledb.create_roledb_from_root_metadata(root_signable['signed'],
887887
repository_name)
888+
tuf.keydb.create_keydb_from_root_metadata(root_signable['signed'],
889+
repository_name)
888890
temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory)
889891
targets_directory = os.path.join(temporary_directory, 'targets')
890892
os.mkdir(targets_directory)
@@ -897,6 +899,14 @@ def test__generate_and_write_metadata(self):
897899
securesystemslib.util.ensure_parent_dir(obsolete_metadata)
898900
shutil.copyfile(targets_metadata, obsolete_metadata)
899901

902+
keystore_path = os.path.join('repository_data', 'keystore')
903+
targets_private_keypath = os.path.join(keystore_path, 'targets_key')
904+
targets_private_key = repo_lib.import_ed25519_privatekey_from_file(targets_private_keypath,
905+
'password')
906+
tuf.keydb.remove_key(targets_private_key['keyid'],
907+
repository_name=repository_name)
908+
tuf.keydb.add_key(targets_private_key, repository_name=repository_name)
909+
900910
# Verify that obsolete metadata (a metadata file exists on disk, but the
901911
# role is unavailable in 'tuf.roledb'). First add the obsolete
902912
# 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):
906916
tuf.formats.unix_timestamp_to_datetime(int(time.time() + 86400))
907917
expiration = expiration.isoformat() + 'Z'
908918
targets_roleinfo['expires'] = expiration
919+
targets_roleinfo['signing_keyids'] = targets_roleinfo['keyids']
909920
tuf.roledb.add_role('obsolete_role', targets_roleinfo,
910921
repository_name=repository_name)
911922

@@ -1004,14 +1015,24 @@ def test__load_top_level_metadata(self):
10041015
roleinfo['version'] = 1
10051016
tuf.roledb.add_role('role1', roleinfo, repository_name)
10061017

1018+
keystore_path = os.path.join('repository_data', 'keystore')
1019+
root_privkey_path = os.path.join(keystore_path, 'root_key')
1020+
targets_privkey_path = os.path.join(keystore_path, 'targets_key')
1021+
snapshot_privkey_path = os.path.join(keystore_path, 'snapshot_key')
1022+
timestamp_privkey_path = os.path.join(keystore_path, 'timestamp_key')
1023+
1024+
repository.root.load_signing_key(repo_lib.import_rsa_privatekey_from_file(root_privkey_path, 'password'))
1025+
repository.targets.load_signing_key(repo_lib.import_ed25519_privatekey_from_file(targets_privkey_path, 'password'))
1026+
repository.snapshot.load_signing_key(repo_lib.import_ed25519_privatekey_from_file(snapshot_privkey_path, 'password'))
1027+
repository.timestamp.load_signing_key(repo_lib.import_ed25519_privatekey_from_file(timestamp_privkey_path, 'password'))
10071028

10081029
# Partially write all top-level roles (we increase the threshold of each
10091030
# top-level role so that they are flagged as partially written.
10101031
repository.root.threshold = repository.root.threshold + 1
10111032
repository.snapshot.threshold = repository.snapshot.threshold + 1
10121033
repository.targets.threshold = repository.targets.threshold + 1
10131034
repository.timestamp.threshold = repository.timestamp.threshold + 1
1014-
repository.write('root', )
1035+
repository.write('root')
10151036
repository.write('snapshot')
10161037
repository.write('targets')
10171038
repository.write('timestamp')

tests/test_updater_root_rotation_integration.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,10 +357,13 @@ def test_root_rotation_max(self):
357357
def test_root_rotation_missing_keys(self):
358358
repository = repo_tool.load_repository(self.repository_directory)
359359

360-
# A partially written root.json (threshold = 1, and not signed in this
361-
# case) causes an invalid root chain later.
360+
# A partially written root.json (threshold = 2, and signed with only 1 key)
361+
# causes an invalid root chain later.
362+
repository.root.threshold = 2
363+
repository.root.load_signing_key(self.role_keys['root']['private'])
362364
repository.snapshot.load_signing_key(self.role_keys['snapshot']['private'])
363365
repository.timestamp.load_signing_key(self.role_keys['timestamp']['private'])
366+
364367
repository.write('root')
365368
repository.write('snapshot')
366369
repository.write('timestamp')
@@ -371,9 +374,9 @@ def test_root_rotation_missing_keys(self):
371374
os.path.join(self.repository_directory, 'metadata'))
372375

373376
# Create a new, valid root.json.
374-
repository.root.threshold = 2
377+
# Still not valid, because it is not written with a threshold of 2
378+
# previous keys
375379
repository.root.add_verification_key(self.role_keys['role1']['public'])
376-
repository.root.load_signing_key(self.role_keys['root']['private'])
377380
repository.root.load_signing_key(self.role_keys['role1']['private'])
378381

379382
repository.writeall()

tuf/developer_tool.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -855,7 +855,12 @@ def load_project(project_directory, prefix='', new_targets_location=None,
855855
targets_metadata_path = os.path.join(project_directory, metadata_directory,
856856
project_filename)
857857
signable = securesystemslib.util.load_json_file(targets_metadata_path)
858-
tuf.formats.check_signable_object_format(signable)
858+
try:
859+
tuf.formats.check_signable_object_format(signable)
860+
except tuf.exceptions.UnsignedMetadataError:
861+
# Downgrade the error to a warning because a use case exists where
862+
# metadata may be generated unsigned on one machine and signed on another.
863+
logger.warning('Unsigned metadata object: ' + repr(signable))
859864
targets_metadata = signable['signed']
860865

861866
# Remove the prefix from the metadata.

tuf/formats.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,9 @@ def check_signable_object_format(signable):
938938
securesystemslib.exceptions.FormatError, if 'signable' does not have the
939939
correct format.
940940
941+
tuf.exceptions.UnsignedMetadataError, if 'signable' does not have any
942+
signatures
943+
941944
<Side Effects>
942945
None.
943946
@@ -965,6 +968,10 @@ def check_signable_object_format(signable):
965968
six.raise_from(securesystemslib.exceptions.FormatError(
966969
'Unrecognized type ' + repr(role_type)), error)
967970

971+
if not signable['signatures']:
972+
raise tuf.exceptions.UnsignedMetadataError('Signable object of type ' +
973+
repr(role_type) + ' has no signatures ', signable)
974+
968975
# 'securesystemslib.exceptions.FormatError' raised if 'signable' does not
969976
# have a properly formatted role schema.
970977
schema.check_match(signable['signed'])

tuf/repository_lib.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,13 @@ def _load_top_level_metadata(repository, top_level_filenames, repository_name):
505505
try:
506506
# Initialize the key and role metadata of the top-level roles.
507507
signable = securesystemslib.util.load_json_file(root_filename)
508-
tuf.formats.check_signable_object_format(signable)
508+
try:
509+
tuf.formats.check_signable_object_format(signable)
510+
except tuf.exceptions.UnsignedMetadataError:
511+
# Downgrade the error to a warning because a use case exists where
512+
# metadata may be generated unsigned on one machine and signed on another.
513+
logger.warning('Unsigned metadata object: ' + repr(signable))
514+
509515
root_metadata = signable['signed']
510516
tuf.keydb.create_keydb_from_root_metadata(root_metadata, repository_name)
511517
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):
586592

587593
try:
588594
signable = securesystemslib.util.load_json_file(snapshot_filename)
589-
tuf.formats.check_signable_object_format(signable)
595+
try:
596+
tuf.formats.check_signable_object_format(signable)
597+
except tuf.exceptions.UnsignedMetadataError:
598+
# Downgrade the error to a warning because a use case exists where
599+
# metadata may be generated unsigned on one machine and signed on another.
600+
logger.warning('Unsigned metadata object: ' + repr(signable))
601+
590602
snapshot_metadata = signable['signed']
591603

592604
for signature in signable['signatures']:
@@ -622,7 +634,13 @@ def _load_top_level_metadata(repository, top_level_filenames, repository_name):
622634

623635
try:
624636
signable = securesystemslib.util.load_json_file(targets_filename)
625-
tuf.formats.check_signable_object_format(signable)
637+
try:
638+
tuf.formats.check_signable_object_format(signable)
639+
except tuf.exceptions.UnsignedMetadataError:
640+
# Downgrade the error to a warning because a use case exists where
641+
# metadata may be generated unsigned on one machine and signed on another.
642+
logger.warning('Unsigned metadata object: ' + repr(signable))
643+
626644
targets_metadata = signable['signed']
627645

628646
for signature in signable['signatures']:
@@ -1862,7 +1880,13 @@ def sign_metadata(metadata_object, keyids, filename, repository_name):
18621880

18631881
# Raise 'securesystemslib.exceptions.FormatError' if the resulting 'signable'
18641882
# is not formatted correctly.
1865-
tuf.formats.check_signable_object_format(signable)
1883+
try:
1884+
tuf.formats.check_signable_object_format(signable)
1885+
except tuf.exceptions.UnsignedMetadataError:
1886+
# Downgrade the error to a warning because a use case exists where
1887+
# metadata may be generated unsigned on one machine and signed on another.
1888+
logger.warning('Unsigned metadata object: ' + repr(signable))
1889+
18661890

18671891
return signable
18681892

0 commit comments

Comments
 (0)