diff --git a/tests/test_api.py b/tests/test_api.py index 24f6ace24a..820e8dc0fe 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -330,6 +330,37 @@ def test_metadata_targets(self): # Verify that data is updated self.assertEqual(targets.signed.targets[filename], fileinfo) + def setup_dict_with_unrecognized_field(self, file_path, field, value): + json_dict = {} + with open(file_path) as f: + json_dict = json.loads(f.read()) + # We are changing the json dict without changing the signature. + # This could be a problem if we want to do verification on this dict. + json_dict["signed"][field] = value + return json_dict + + def test_support_for_unrecognized_fields(self): + for metadata in ["root", "timestamp", "snapshot", "targets"]: + path = os.path.join(self.repo_dir, "metadata", metadata + ".json") + dict1 = self.setup_dict_with_unrecognized_field(path, "f", "b") + # Test that the metadata classes store unrecognized fields when + # initializing and passes them when casting the instance to a dict. + + temp_copy = copy.deepcopy(dict1) + metadata_obj = Metadata.from_dict(temp_copy) + + self.assertEqual(dict1["signed"], metadata_obj.signed.to_dict()) + + # Test that two instances of the same class could have different + # unrecognized fields. + dict2 = self.setup_dict_with_unrecognized_field(path, "f2", "b2") + temp_copy2 = copy.deepcopy(dict2) + metadata_obj2 = Metadata.from_dict(temp_copy2) + self.assertNotEqual( + metadata_obj.signed.to_dict(), metadata_obj2.signed.to_dict() + ) + + # Run unit test. if __name__ == '__main__': utils.configure_test_logging(sys.argv) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 99efd9b510..bba2af1a96 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -300,6 +300,7 @@ class Signed: spec_version: The TUF specification version number (semver) the metadata format adheres to. expires: The metadata expiration datetime object. + unrecognized_fields: Dictionary of all unrecognized fields. """ @@ -307,7 +308,12 @@ class Signed: # we keep it to match spec terminology (I often refer to this as "payload", # or "inner metadata") def __init__( - self, _type: str, version: int, spec_version: str, expires: datetime + self, + _type: str, + version: int, + spec_version: str, + expires: datetime, + unrecognized_fields: Optional[Mapping[str, Any]] = None, ) -> None: self._type = _type @@ -318,6 +324,7 @@ def __init__( if version < 0: raise ValueError(f"version must be >= 0, got {version}") self.version = version + self.unrecognized_fields = unrecognized_fields or {} @staticmethod def _common_fields_from_dict(signed_dict: Mapping[str, Any]) -> list: @@ -349,6 +356,7 @@ def _common_fields_to_dict(self) -> Dict[str, Any]: "version": self.version, "spec_version": self.spec_version, "expires": self.expires.isoformat() + "Z", + **self.unrecognized_fields, } # Modification. @@ -409,8 +417,11 @@ def __init__( consistent_snapshot: bool, keys: Mapping[str, Any], roles: Mapping[str, Any], + unrecognized_fields: Optional[Mapping[str, Any]] = None, ) -> None: - super().__init__(_type, version, spec_version, expires) + super().__init__( + _type, version, spec_version, expires, unrecognized_fields + ) # TODO: Add classes for keys and roles self.consistent_snapshot = consistent_snapshot self.keys = keys @@ -423,7 +434,8 @@ def from_dict(cls, root_dict: Mapping[str, Any]) -> "Root": consistent_snapshot = root_dict.pop("consistent_snapshot") keys = root_dict.pop("keys") roles = root_dict.pop("roles") - return cls(*common_args, consistent_snapshot, keys, roles) + # All fields left in the root_dict are unrecognized. + return cls(*common_args, consistent_snapshot, keys, roles, root_dict) def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """ @@ -485,8 +497,11 @@ def __init__( spec_version: str, expires: datetime, meta: Mapping[str, Any], + unrecognized_fields: Optional[Mapping[str, Any]] = None, ) -> None: - super().__init__(_type, version, spec_version, expires) + super().__init__( + _type, version, spec_version, expires, unrecognized_fields + ) # TODO: Add class for meta self.meta = meta @@ -495,7 +510,8 @@ def from_dict(cls, timestamp_dict: Mapping[str, Any]) -> "Timestamp": """Creates Timestamp object from its dict representation. """ common_args = cls._common_fields_from_dict(timestamp_dict) meta = timestamp_dict.pop("meta") - return cls(*common_args, meta) + # All fields left in the timestamp_dict are unrecognized. + return cls(*common_args, meta, timestamp_dict) def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """ @@ -549,8 +565,11 @@ def __init__( spec_version: str, expires: datetime, meta: Mapping[str, Any], + unrecognized_fields: Optional[Mapping[str, Any]] = None, ) -> None: - super().__init__(_type, version, spec_version, expires) + super().__init__( + _type, version, spec_version, expires, unrecognized_fields + ) # TODO: Add class for meta self.meta = meta @@ -559,7 +578,8 @@ def from_dict(cls, snapshot_dict: Mapping[str, Any]) -> "Snapshot": """Creates Snapshot object from its dict representation. """ common_args = cls._common_fields_from_dict(snapshot_dict) meta = snapshot_dict.pop("meta") - return cls(*common_args, meta) + # All fields left in the snapshot_dict are unrecognized. + return cls(*common_args, meta, snapshot_dict) def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """ @@ -652,8 +672,11 @@ def __init__( expires: datetime, targets: Mapping[str, Any], delegations: Mapping[str, Any], + unrecognized_fields: Optional[Mapping[str, Any]] = None, ) -> None: - super().__init__(_type, version, spec_version, expires) + super().__init__( + _type, version, spec_version, expires, unrecognized_fields + ) # TODO: Add class for meta self.targets = targets self.delegations = delegations @@ -664,7 +687,8 @@ def from_dict(cls, targets_dict: Mapping[str, Any]) -> "Targets": common_args = cls._common_fields_from_dict(targets_dict) targets = targets_dict.pop("targets") delegations = targets_dict.pop("delegations") - return cls(*common_args, targets, delegations) + # All fields left in the targets_dict are unrecognized. + return cls(*common_args, targets, delegations, targets_dict) def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """