From de2644f3d0f695af9d6a17360fa6d1af82125d32 Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Fri, 14 May 2021 19:33:53 +0300 Subject: [PATCH] Breaking: new API: consistent_snapshot optional NOTE: making consistent_snapshot optional requires using a default value for the argument in __init__ in Root and thus consistent_snapshot should be rearranged in the end. Read more: https://github.com/theupdateframework/tuf/pull/1394#issuecomment-842134961 From chapter 7 in the spec (version 1.0.17) "Finally, the root metadata should write the Boolean "consistent_snapshot" attribute at the root level of its keys of attributes. If consistent snapshots are not written by the repository, then the attribute may either be left unspecified or be set to the False value. Otherwise, it must be set to the True value." We want to make sure we support repositories without consistent_snapshot set. Signed-off-by: Martin Vrachev --- tests/test_api.py | 6 ++++++ tuf/api/metadata.py | 13 +++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 540209f0fb..0760c46bf8 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -425,6 +425,12 @@ def test_metadata_root(self): with self.assertRaises(KeyError): root.signed.remove_key('root', 'nosuchkey') + # Test serializing and deserializing without consistent_snapshot. + root_dict = root.to_dict() + del root_dict["signed"]["consistent_snapshot"] + root = Root.from_dict(copy.deepcopy(root_dict["signed"])) + self.assertEqual(root_dict["signed"], root.to_dict()) + def test_delegated_role_class(self): roles = [ { diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 06de1125cd..85b46e26d0 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -503,8 +503,8 @@ class Root(Signed): """A container for the signed part of root metadata. Attributes: - consistent_snapshot: A boolean indicating whether the repository - supports consistent snapshots. + consistent_snapshot: An optional boolean indicating whether the + repository supports consistent snapshots. keys: A dictionary that contains a public key store used to verify top level roles metadata signatures:: @@ -534,9 +534,9 @@ def __init__( version: int, spec_version: str, expires: datetime, - consistent_snapshot: bool, keys: Dict[str, Key], roles: Dict[str, Role], + consistent_snapshot: Optional[bool] = None, unrecognized_fields: Optional[Mapping[str, Any]] = None, ) -> None: super().__init__(version, spec_version, expires, unrecognized_fields) @@ -548,7 +548,7 @@ def __init__( def from_dict(cls, root_dict: Dict[str, Any]) -> "Root": """Creates Root object from its dict representation.""" common_args = cls._common_fields_from_dict(root_dict) - consistent_snapshot = root_dict.pop("consistent_snapshot") + consistent_snapshot = root_dict.pop("consistent_snapshot", None) keys = root_dict.pop("keys") roles = root_dict.pop("roles") @@ -558,7 +558,7 @@ def from_dict(cls, root_dict: Dict[str, Any]) -> "Root": roles[role_name] = Role.from_dict(role_dict) # All fields left in the root_dict are unrecognized. - return cls(*common_args, consistent_snapshot, keys, roles, root_dict) + return cls(*common_args, keys, roles, consistent_snapshot, root_dict) def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self.""" @@ -567,10 +567,11 @@ def to_dict(self) -> Dict[str, Any]: roles = {} for role_name, role in self.roles.items(): roles[role_name] = role.to_dict() + if self.consistent_snapshot is not None: + root_dict["consistent_snapshot"] = self.consistent_snapshot root_dict.update( { - "consistent_snapshot": self.consistent_snapshot, "keys": keys, "roles": roles, }