From 59b5498918a4f5ff5a838d3ed4deb8f3f484f58a Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Mon, 25 Oct 2021 12:45:08 +0300 Subject: [PATCH 1/3] Add TOP_LEVEL_ROLE_NAMES constant This constant can be used across tuf without defining it each time. Signed-off-by: Martin Vrachev --- tuf/api/metadata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 4d376921aa..7f3bbeccf2 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") From 9bc55ee568691f5ff4ec76de87d75851236f492d Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Wed, 20 Oct 2021 15:29:02 +0300 Subject: [PATCH 2/3] 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 | 65 ++++++++++++++++++++++++++-- tuf/api/metadata.py | 4 ++ 3 files changed, 70 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..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 7f3bbeccf2..9a49f2789d 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -729,6 +729,10 @@ def __init__( super().__init__(version, spec_version, expires, unrecognized_fields) self.consistent_snapshot = consistent_snapshot self.keys = keys + mandatory_roles = ["root", "timestamp", "snapshot", "targets"] + 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 From 4158272a7adbcc458563e8b57ced29b7859c3077 Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Mon, 25 Oct 2021 13:20:13 +0300 Subject: [PATCH 3/3] Use TOP_LEVEL_ROLE_NAMES across TUF Signed-off-by: Martin Vrachev --- tests/repository_simulator.py | 6 +++--- tuf/api/metadata.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/repository_simulator.py b/tests/repository_simulator.py index 5b472e3f1a..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,11 +153,10 @@ def _initialize(self): timestamp = Timestamp(1, SPEC_VER, self.safe_expiry, snapshot_meta) self.md_timestamp = Metadata(timestamp, OrderedDict()) - top_level_md = ["root", "timestamp", "snapshot", "targets"] - roles = {role_name: Role([], 1) for role_name in top_level_md} + 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_md: + for role in TOP_LEVEL_ROLE_NAMES: key, signer = self.create_key() root.add_key(role, key) # store the private key diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 9a49f2789d..25f14fe772 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -729,8 +729,7 @@ def __init__( super().__init__(version, spec_version, expires, unrecognized_fields) self.consistent_snapshot = consistent_snapshot self.keys = keys - mandatory_roles = ["root", "timestamp", "snapshot", "targets"] - if not (len(roles) == 4 and all(r in roles for r in mandatory_roles)): + if set(roles) != TOP_LEVEL_ROLE_NAMES: raise ValueError("Role names must be the top-level metadata roles") self.roles = roles