diff --git a/tests/repository_simulator.py b/tests/repository_simulator.py index 6f6ca32ecd..3565fbe07f 100644 --- a/tests/repository_simulator.py +++ b/tests/repository_simulator.py @@ -56,6 +56,7 @@ from typing import Dict, Iterator, List, Optional, Tuple from urllib import parse +from tuf.api.metadata import TOP_LEVEL_ROLE_NAMES from tuf.api.serialization.json import JSONSerializer from tuf.exceptions import FetcherHTTPError from tuf.api.metadata import ( @@ -152,10 +153,11 @@ def _initialize(self): timestamp = Timestamp(1, SPEC_VER, self.safe_expiry, snapshot_meta) self.md_timestamp = Metadata(timestamp, OrderedDict()) - root = Root(1, SPEC_VER, self.safe_expiry, {}, {}, True) - for role in ["root", "timestamp", "snapshot", "targets"]: + roles = {role_name: Role([], 1) for role_name in TOP_LEVEL_ROLE_NAMES} + root = Root(1, SPEC_VER, self.safe_expiry, {}, roles, True) + + for role in TOP_LEVEL_ROLE_NAMES: key, signer = self.create_key() - root.roles[role] = Role([], 1) root.add_key(role, key) # store the private key if role not in self.signers: diff --git a/tests/test_metadata_serialization.py b/tests/test_metadata_serialization.py index 13bb55003a..1ae70e1fcc 100644 --- a/tests/test_metadata_serialization.py +++ b/tests/test_metadata_serialization.py @@ -142,23 +142,38 @@ def test_role_serialization(self, test_case_data: str): "keyid1" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"}}, \ "keyid2" : {"keytype": "ed25519", "scheme": "ed25519", "keyval": {"public": "bar"}}}, \ "roles": { \ + "root": {"keyids": ["keyid1"], "threshold": 1}, \ + "timestamp": {"keyids": ["keyid2"], "threshold": 1}, \ "targets": {"keyids": ["keyid1"], "threshold": 1}, \ "snapshot": {"keyids": ["keyid2"], "threshold": 1}} \ }', "no consistent_snapshot": '{ "_type": "root", "spec_version": "1.0.0", "version": 1, \ "expires": "2030-01-01T00:00:00Z", \ "keys": {"keyid" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"} }}, \ - "roles": { "targets": {"keyids": ["keyid"], "threshold": 3} } \ + "roles": { \ + "root": {"keyids": ["keyid"], "threshold": 1}, \ + "timestamp": {"keyids": ["keyid"], "threshold": 1}, \ + "targets": {"keyids": ["keyid"], "threshold": 1}, \ + "snapshot": {"keyids": ["keyid"], "threshold": 1}} \ }', - "empty keys and roles": '{"_type": "root", "spec_version": "1.0.0", "version": 1, \ + "empty keys": '{"_type": "root", "spec_version": "1.0.0", "version": 1, \ "expires": "2030-01-01T00:00:00Z", "consistent_snapshot": false, \ "keys": {}, \ - "roles": {} \ + "roles": { \ + "root": {"keyids": [], "threshold": 1}, \ + "timestamp": {"keyids": [], "threshold": 1}, \ + "targets": {"keyids": [], "threshold": 1}, \ + "snapshot": {"keyids": [], "threshold": 1}} \ }', "unrecognized field": '{"_type": "root", "spec_version": "1.0.0", "version": 1, \ "expires": "2030-01-01T00:00:00Z", "consistent_snapshot": false, \ "keys": {"keyid" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"}}}, \ - "roles": { "targets": {"keyids": ["keyid"], "threshold": 3}}, \ + "roles": { \ + "root": {"keyids": ["keyid"], "threshold": 1}, \ + "timestamp": {"keyids": ["keyid"], "threshold": 1}, \ + "targets": {"keyids": ["keyid"], "threshold": 1}, \ + "snapshot": {"keyids": ["keyid"], "threshold": 1} \ + }, \ "foo": "bar"}', } @@ -169,6 +184,48 @@ def test_root_serialization(self, test_case_data: str): self.assertDictEqual(case_dict, root.to_dict()) + invalid_roots: utils.DataSet = { + "invalid role name": '{"_type": "root", "spec_version": "1.0.0", "version": 1, \ + "expires": "2030-01-01T00:00:00Z", "consistent_snapshot": false, \ + "keys": { \ + "keyid1" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"}}, \ + "keyid2" : {"keytype": "ed25519", "scheme": "ed25519", "keyval": {"public": "bar"}}}, \ + "roles": { \ + "bar": {"keyids": ["keyid1"], "threshold": 1}, \ + "timestamp": {"keyids": ["keyid2"], "threshold": 1}, \ + "targets": {"keyids": ["keyid1"], "threshold": 1}, \ + "snapshot": {"keyids": ["keyid2"], "threshold": 1}} \ + }', + "missing root role": '{"_type": "root", "spec_version": "1.0.0", "version": 1, \ + "expires": "2030-01-01T00:00:00Z", "consistent_snapshot": false, \ + "keys": { \ + "keyid1" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"}}, \ + "keyid2" : {"keytype": "ed25519", "scheme": "ed25519", "keyval": {"public": "bar"}}}, \ + "roles": { \ + "timestamp": {"keyids": ["keyid2"], "threshold": 1}, \ + "targets": {"keyids": ["keyid1"], "threshold": 1}, \ + "snapshot": {"keyids": ["keyid2"], "threshold": 1}} \ + }', + "one additional role": '{"_type": "root", "spec_version": "1.0.0", "version": 1, \ + "expires": "2030-01-01T00:00:00Z", "consistent_snapshot": false, \ + "keys": { \ + "keyid1" : {"keytype": "rsa", "scheme": "rsassa-pss-sha256", "keyval": {"public": "foo"}}, \ + "keyid2" : {"keytype": "ed25519", "scheme": "ed25519", "keyval": {"public": "bar"}}}, \ + "roles": { \ + "root": {"keyids": ["keyid1"], "threshold": 1}, \ + "timestamp": {"keyids": ["keyid2"], "threshold": 1}, \ + "targets": {"keyids": ["keyid1"], "threshold": 1}, \ + "snapshot": {"keyids": ["keyid2"], "threshold": 1}, \ + "foo": {"keyids": ["keyid2"], "threshold": 1}} \ + }', + } + + @utils.run_sub_tests_with_dataset(invalid_roots) + def test_invalid_root_serialization(self, test_case_data: Dict[str, str]): + case_dict = json.loads(test_case_data) + with self.assertRaises(ValueError): + Root.from_dict(copy.deepcopy(case_dict)) + invalid_metafiles: utils.DataSet = { "wrong length type": '{"version": 1, "length": "a", "hashes": {"sha256" : "abc"}}', "length 0": '{"version": 1, "length": 0, "hashes": {"sha256" : "abc"}}', diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 4d376921aa..25f14fe772 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -68,6 +68,7 @@ # We aim to support SPECIFICATION_VERSION and require the input metadata # files to have the same major version (the first number) as ours. SPECIFICATION_VERSION = ["1", "0", "19"] +TOP_LEVEL_ROLE_NAMES = {"root", "timestamp", "snapshot", "targets"} # T is a Generic type constraint for Metadata.signed T = TypeVar("T", "Root", "Timestamp", "Snapshot", "Targets") @@ -728,6 +729,9 @@ def __init__( super().__init__(version, spec_version, expires, unrecognized_fields) self.consistent_snapshot = consistent_snapshot self.keys = keys + if set(roles) != TOP_LEVEL_ROLE_NAMES: + raise ValueError("Role names must be the top-level metadata roles") + self.roles = roles @classmethod