Skip to content

Commit d0d907d

Browse files
committed
New API: accept metadata with unrecognized fields
In order to support ADR 0008 we would want to accept unrecognized fields in all metadata classes. Input that contains unknown fields in the 'signed' dictionary should successfully deserialize into a Metadata object, and that object should successfully serialize with the unknown fields intact. Also, we should test that we support unrecognized fields when adding new classes or modifying existing ones to make sure we support ADR 0008. Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
1 parent 08f48d5 commit d0d907d

File tree

2 files changed

+74
-9
lines changed

2 files changed

+74
-9
lines changed

tests/test_api.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,39 @@ def test_metadata_targets(self):
330330
# Verify that data is updated
331331
self.assertEqual(targets.signed.targets[filename], fileinfo)
332332

333+
def setup_dict_with_unrecognized_field(self, file_path, field, value):
334+
json_dict = {}
335+
with open(file_path) as f:
336+
json_dict = json.loads(f.read())
337+
# We are changing the json dict without changing the signature.
338+
# This could be a problem if we want to do verification on this dict.
339+
json_dict["signed"][field] = value
340+
return json_dict
341+
342+
def test_support_for_unrecognized_fields(self):
343+
for metadata in ["root", "timestamp", "snapshot", "targets"]:
344+
path = os.path.join(self.repo_dir, "metadata", metadata + ".json")
345+
dict1 = self.setup_dict_with_unrecognized_field(path, "f", "b")
346+
# Test that the metadata classes store unrecognized fields when
347+
# initializing and passes them when casting the instance to a dict.
348+
349+
# TODO: Remove the deepcopy when Metadata.from_dict() doesn't have
350+
# the side effect to destroy the passed dictionary.
351+
temp_copy = copy.deepcopy(dict1)
352+
metadata_obj = Metadata.from_dict(temp_copy)
353+
354+
self.assertEqual(dict1["signed"], metadata_obj.signed.to_dict())
355+
356+
# Test that two instances of the same class could have different
357+
# unrecognized fields.
358+
dict2 = self.setup_dict_with_unrecognized_field(path, "f2", "b2")
359+
temp_copy2 = copy.deepcopy(dict2)
360+
metadata_obj2 = Metadata.from_dict(temp_copy2)
361+
self.assertNotEqual(
362+
metadata_obj.signed.to_dict(), metadata_obj2.signed.to_dict()
363+
)
364+
365+
333366
# Run unit test.
334367
if __name__ == '__main__':
335368
utils.configure_test_logging(sys.argv)

tuf/api/metadata.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -300,14 +300,20 @@ class Signed:
300300
spec_version: The TUF specification version number (semver) the
301301
metadata format adheres to.
302302
expires: The metadata expiration datetime object.
303+
unrecognized_fields: Dictionary of all unrecognized fields.
303304
304305
"""
305306

306307
# NOTE: Signed is a stupid name, because this might not be signed yet, but
307308
# we keep it to match spec terminology (I often refer to this as "payload",
308309
# or "inner metadata")
309310
def __init__(
310-
self, _type: str, version: int, spec_version: str, expires: datetime
311+
self,
312+
_type: str,
313+
version: int,
314+
spec_version: str,
315+
expires: datetime,
316+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
311317
) -> None:
312318

313319
self._type = _type
@@ -318,6 +324,9 @@ def __init__(
318324
if version < 0:
319325
raise ValueError(f"version must be >= 0, got {version}")
320326
self.version = version
327+
if unrecognized_fields is None:
328+
unrecognized_fields = {}
329+
self.unrecognized_fields = unrecognized_fields
321330

322331
@staticmethod
323332
def _common_fields_from_dict(signed_dict: Mapping[str, Any]) -> list:
@@ -349,6 +358,7 @@ def _common_fields_to_dict(self) -> Dict[str, Any]:
349358
"version": self.version,
350359
"spec_version": self.spec_version,
351360
"expires": self.expires.isoformat() + "Z",
361+
**self.unrecognized_fields,
352362
}
353363

354364
# Modification.
@@ -409,8 +419,11 @@ def __init__(
409419
consistent_snapshot: bool,
410420
keys: Mapping[str, Any],
411421
roles: Mapping[str, Any],
422+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
412423
) -> None:
413-
super().__init__(_type, version, spec_version, expires)
424+
super().__init__(
425+
_type, version, spec_version, expires, unrecognized_fields
426+
)
414427
# TODO: Add classes for keys and roles
415428
self.consistent_snapshot = consistent_snapshot
416429
self.keys = keys
@@ -423,7 +436,11 @@ def from_dict(cls, root_dict: Mapping[str, Any]) -> "Root":
423436
consistent_snapshot = root_dict.pop("consistent_snapshot")
424437
keys = root_dict.pop("keys")
425438
roles = root_dict.pop("roles")
426-
return cls(*common_args, consistent_snapshot, keys, roles)
439+
# All fields left in the root_dict are unrecognized.
440+
unrecognized_fields = root_dict
441+
return cls(
442+
*common_args, consistent_snapshot, keys, roles, unrecognized_fields
443+
)
427444

428445
def to_dict(self) -> Dict[str, Any]:
429446
"""Returns the dict representation of self. """
@@ -485,8 +502,11 @@ def __init__(
485502
spec_version: str,
486503
expires: datetime,
487504
meta: Mapping[str, Any],
505+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
488506
) -> None:
489-
super().__init__(_type, version, spec_version, expires)
507+
super().__init__(
508+
_type, version, spec_version, expires, unrecognized_fields
509+
)
490510
# TODO: Add class for meta
491511
self.meta = meta
492512

@@ -495,7 +515,9 @@ def from_dict(cls, timestamp_dict: Mapping[str, Any]) -> "Timestamp":
495515
"""Creates Timestamp object from its dict representation. """
496516
common_args = cls._common_fields_from_dict(timestamp_dict)
497517
meta = timestamp_dict.pop("meta")
498-
return cls(*common_args, meta)
518+
# All fields left in the timestamp_dict are unrecognized.
519+
unrecognized_fields = timestamp_dict
520+
return cls(*common_args, meta, unrecognized_fields)
499521

500522
def to_dict(self) -> Dict[str, Any]:
501523
"""Returns the dict representation of self. """
@@ -549,8 +571,11 @@ def __init__(
549571
spec_version: str,
550572
expires: datetime,
551573
meta: Mapping[str, Any],
574+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
552575
) -> None:
553-
super().__init__(_type, version, spec_version, expires)
576+
super().__init__(
577+
_type, version, spec_version, expires, unrecognized_fields
578+
)
554579
# TODO: Add class for meta
555580
self.meta = meta
556581

@@ -559,7 +584,9 @@ def from_dict(cls, snapshot_dict: Mapping[str, Any]) -> "Snapshot":
559584
"""Creates Snapshot object from its dict representation. """
560585
common_args = cls._common_fields_from_dict(snapshot_dict)
561586
meta = snapshot_dict.pop("meta")
562-
return cls(*common_args, meta)
587+
# All fields left in the snapshot_dict are unrecognized.
588+
unrecognized_fields = snapshot_dict
589+
return cls(*common_args, meta, unrecognized_fields)
563590

564591
def to_dict(self) -> Dict[str, Any]:
565592
"""Returns the dict representation of self. """
@@ -652,8 +679,11 @@ def __init__(
652679
expires: datetime,
653680
targets: Mapping[str, Any],
654681
delegations: Mapping[str, Any],
682+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
655683
) -> None:
656-
super().__init__(_type, version, spec_version, expires)
684+
super().__init__(
685+
_type, version, spec_version, expires, unrecognized_fields
686+
)
657687
# TODO: Add class for meta
658688
self.targets = targets
659689
self.delegations = delegations
@@ -664,7 +694,9 @@ def from_dict(cls, targets_dict: Mapping[str, Any]) -> "Targets":
664694
common_args = cls._common_fields_from_dict(targets_dict)
665695
targets = targets_dict.pop("targets")
666696
delegations = targets_dict.pop("delegations")
667-
return cls(*common_args, targets, delegations)
697+
# All fields left in the targets_dict are unrecognized.
698+
unrecognized_fields = targets_dict
699+
return cls(*common_args, targets, delegations, unrecognized_fields)
668700

669701
def to_dict(self) -> Dict[str, Any]:
670702
"""Returns the dict representation of self. """

0 commit comments

Comments
 (0)