From aafa75a0c7106cf9310e37d0df3186f6e5e09154 Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Wed, 20 Oct 2021 15:29:02 +0300 Subject: [PATCH] Metadata API: validate root role names Validate that root role names are 4 and that they are exactly "root", "snapshot", "targets" and "timestamp" as described in the spec: https://theupdateframework.github.io/specification/latest/#root-role Additionally, fix the valid_roots dataset, so each of the cases contains the top metadata role names inside the roles dictionary. Signed-off-by: Martin Vrachev --- tests/repository_simulator.py | 8 ++-- tests/test_metadata_serialization.py | 66 ++++++++++++++++++++++++++-- tuf/api/metadata.py | 4 ++ 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/tests/repository_simulator.py b/tests/repository_simulator.py index 6f6ca32ecd..5b472e3f1a 100644 --- a/tests/repository_simulator.py +++ b/tests/repository_simulator.py @@ -152,10 +152,12 @@ 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"]: + top_level_md = ["root", "timestamp", "snapshot", "targets"] + roles = {role_name: Role([], 1) for role_name in top_level_md} + root = Root(1, SPEC_VER, self.safe_expiry, {}, roles, True) + + for role in top_level_md: 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..3d1cafcc6e 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": ["keyid3"], "threshold": 1}, \ + "timestamp": {"keyids": ["keyid4"], "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": ["keyid1"], "threshold": 1}, \ + "timestamp": {"keyids": ["keyid2"], "threshold": 1}, \ + "targets": {"keyids": ["keyid3"], "threshold": 1}, \ + "snapshot": {"keyids": ["keyid4"], "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": ["keyid3"], "threshold": 1}, \ + "timestamp": {"keyids": ["keyid4"], "threshold": 1}, \ + "targets": {"keyids": ["keyid1"], "threshold": 1}, \ + "snapshot": {"keyids": ["keyid2"], "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": ["keyid1"], "threshold": 1}, \ + "timestamp": {"keyids": ["keyid2"], "threshold": 1}, \ + "targets": {"keyids": ["keyid3"], "threshold": 1}, \ + "snapshot": {"keyids": ["keyid4"], "threshold": 1} \ + }, \ "foo": "bar"}', } @@ -169,6 +184,49 @@ 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": ["keyid3"], "threshold": 1}, \ + "timestamp": {"keyids": ["keyid4"], "threshold": 1}, \ + "targets": {"keyids": ["keyid1"], "threshold": 1}, \ + "snapshot": {"keyids": ["keyid2"], "threshold": 1}, \ + "foo": {"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": ["keyid4"], "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": ["keyid3"], "threshold": 1}, \ + "timestamp": {"keyids": ["keyid4"], "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 0b88f4c0d2..b3a687a74f 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -726,6 +726,10 @@ def __init__( super().__init__(version, spec_version, expires, unrecognized_fields) self.consistent_snapshot = consistent_snapshot self.keys = keys + mandatory_roles = ["root", "timestamp", "snapshot", "timestamp"] + if not (len(roles) == 4 and all(r in roles for r in mandatory_roles)): + raise ValueError("Role names must be the top-level metadata roles") + self.roles = roles @classmethod