Skip to content

Commit 3876ebe

Browse files
author
Jussi Kukkonen
committed
Merge branch 'develop' into experimental-client
2 parents 2b0324e + 878c8c6 commit 3876ebe

File tree

4 files changed

+93
-38
lines changed

4 files changed

+93
-38
lines changed

tests/test_api.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,37 @@ def test_metadata_targets(self):
349349
# Verify that data is updated
350350
self.assertEqual(targets.signed.targets[filename], fileinfo)
351351

352+
def setup_dict_with_unrecognized_field(self, file_path, field, value):
353+
json_dict = {}
354+
with open(file_path) as f:
355+
json_dict = json.loads(f.read())
356+
# We are changing the json dict without changing the signature.
357+
# This could be a problem if we want to do verification on this dict.
358+
json_dict["signed"][field] = value
359+
return json_dict
360+
361+
def test_support_for_unrecognized_fields(self):
362+
for metadata in ["root", "timestamp", "snapshot", "targets"]:
363+
path = os.path.join(self.repo_dir, "metadata", metadata + ".json")
364+
dict1 = self.setup_dict_with_unrecognized_field(path, "f", "b")
365+
# Test that the metadata classes store unrecognized fields when
366+
# initializing and passes them when casting the instance to a dict.
367+
368+
temp_copy = copy.deepcopy(dict1)
369+
metadata_obj = Metadata.from_dict(temp_copy)
370+
371+
self.assertEqual(dict1["signed"], metadata_obj.signed.to_dict())
372+
373+
# Test that two instances of the same class could have different
374+
# unrecognized fields.
375+
dict2 = self.setup_dict_with_unrecognized_field(path, "f2", "b2")
376+
temp_copy2 = copy.deepcopy(dict2)
377+
metadata_obj2 = Metadata.from_dict(temp_copy2)
378+
self.assertNotEqual(
379+
metadata_obj.signed.to_dict(), metadata_obj2.signed.to_dict()
380+
)
381+
382+
352383
# Run unit test.
353384
if __name__ == '__main__':
354385
utils.configure_test_logging(sys.argv)

tuf/api/metadata.py

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def from_bytes(
164164
return deserializer.deserialize(data)
165165

166166
def to_dict(self) -> Dict[str, Any]:
167-
"""Returns the dict representation of self. """
167+
"""Returns the dict representation of self."""
168168

169169
signatures = []
170170
for sig in self.signatures:
@@ -320,24 +320,31 @@ class Signed:
320320
spec_version: The TUF specification version number (semver) the
321321
metadata format adheres to.
322322
expires: The metadata expiration datetime object.
323+
unrecognized_fields: Dictionary of all unrecognized fields.
323324
324325
"""
325326

326327
# NOTE: Signed is a stupid name, because this might not be signed yet, but
327328
# we keep it to match spec terminology (I often refer to this as "payload",
328329
# or "inner metadata")
329330
def __init__(
330-
self, _type: str, version: int, spec_version: str, expires: datetime
331+
self,
332+
_type: str,
333+
version: int,
334+
spec_version: str,
335+
expires: datetime,
336+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
331337
) -> None:
332338

333339
self._type = _type
334340
self.spec_version = spec_version
335341
self.expires = expires
336342

337343
# TODO: Should we separate data validation from constructor?
338-
if version < 0:
339-
raise ValueError(f"version must be >= 0, got {version}")
344+
if version <= 0:
345+
raise ValueError(f"version must be > 0, got {version}")
340346
self.version = version
347+
self.unrecognized_fields = unrecognized_fields or {}
341348

342349
@staticmethod
343350
def _common_fields_from_dict(signed_dict: Mapping[str, Any]) -> list:
@@ -369,6 +376,7 @@ def _common_fields_to_dict(self) -> Dict[str, Any]:
369376
"version": self.version,
370377
"spec_version": self.spec_version,
371378
"expires": self.expires.isoformat() + "Z",
379+
**self.unrecognized_fields,
372380
}
373381

374382
def is_expired(self, reference_time: datetime = None) -> bool:
@@ -389,7 +397,7 @@ def is_expired(self, reference_time: datetime = None) -> bool:
389397

390398
# Modification.
391399
def bump_expiration(self, delta: timedelta = timedelta(days=1)) -> None:
392-
"""Increments the expires attribute by the passed timedelta. """
400+
"""Increments the expires attribute by the passed timedelta."""
393401
self.expires += delta
394402

395403
def bump_version(self) -> None:
@@ -445,24 +453,28 @@ def __init__(
445453
consistent_snapshot: bool,
446454
keys: Mapping[str, Any],
447455
roles: Mapping[str, Any],
456+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
448457
) -> None:
449-
super().__init__(_type, version, spec_version, expires)
458+
super().__init__(
459+
_type, version, spec_version, expires, unrecognized_fields
460+
)
450461
# TODO: Add classes for keys and roles
451462
self.consistent_snapshot = consistent_snapshot
452463
self.keys = keys
453464
self.roles = roles
454465

455466
@classmethod
456467
def from_dict(cls, root_dict: Mapping[str, Any]) -> "Root":
457-
"""Creates Root object from its dict representation. """
468+
"""Creates Root object from its dict representation."""
458469
common_args = cls._common_fields_from_dict(root_dict)
459470
consistent_snapshot = root_dict.pop("consistent_snapshot")
460471
keys = root_dict.pop("keys")
461472
roles = root_dict.pop("roles")
462-
return cls(*common_args, consistent_snapshot, keys, roles)
473+
# All fields left in the root_dict are unrecognized.
474+
return cls(*common_args, consistent_snapshot, keys, roles, root_dict)
463475

464476
def to_dict(self) -> Dict[str, Any]:
465-
"""Returns the dict representation of self. """
477+
"""Returns the dict representation of self."""
466478
root_dict = self._common_fields_to_dict()
467479
root_dict.update(
468480
{
@@ -477,14 +489,14 @@ def to_dict(self) -> Dict[str, Any]:
477489
def add_key(
478490
self, role: str, keyid: str, key_metadata: Mapping[str, Any]
479491
) -> None:
480-
"""Adds new key for 'role' and updates the key store. """
492+
"""Adds new key for 'role' and updates the key store."""
481493
if keyid not in self.roles[role]["keyids"]:
482494
self.roles[role]["keyids"].append(keyid)
483495
self.keys[keyid] = key_metadata
484496

485497
# Remove key for a role.
486498
def remove_key(self, role: str, keyid: str) -> None:
487-
"""Removes key for 'role' and updates the key store. """
499+
"""Removes key for 'role' and updates the key store."""
488500
if keyid in self.roles[role]["keyids"]:
489501
self.roles[role]["keyids"].remove(keyid)
490502
for keyinfo in self.roles.values():
@@ -521,20 +533,24 @@ def __init__(
521533
spec_version: str,
522534
expires: datetime,
523535
meta: Mapping[str, Any],
536+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
524537
) -> None:
525-
super().__init__(_type, version, spec_version, expires)
538+
super().__init__(
539+
_type, version, spec_version, expires, unrecognized_fields
540+
)
526541
# TODO: Add class for meta
527542
self.meta = meta
528543

529544
@classmethod
530545
def from_dict(cls, timestamp_dict: Mapping[str, Any]) -> "Timestamp":
531-
"""Creates Timestamp object from its dict representation. """
546+
"""Creates Timestamp object from its dict representation."""
532547
common_args = cls._common_fields_from_dict(timestamp_dict)
533548
meta = timestamp_dict.pop("meta")
534-
return cls(*common_args, meta)
549+
# All fields left in the timestamp_dict are unrecognized.
550+
return cls(*common_args, meta, timestamp_dict)
535551

536552
def to_dict(self) -> Dict[str, Any]:
537-
"""Returns the dict representation of self. """
553+
"""Returns the dict representation of self."""
538554
timestamp_dict = self._common_fields_to_dict()
539555
timestamp_dict.update({"meta": self.meta})
540556
return timestamp_dict
@@ -543,7 +559,7 @@ def to_dict(self) -> Dict[str, Any]:
543559
def update(
544560
self, version: int, length: int, hashes: Mapping[str, Any]
545561
) -> None:
546-
"""Assigns passed info about snapshot metadata to meta dict. """
562+
"""Assigns passed info about snapshot metadata to meta dict."""
547563
self.meta["snapshot.json"] = {
548564
"version": version,
549565
"length": length,
@@ -585,20 +601,24 @@ def __init__(
585601
spec_version: str,
586602
expires: datetime,
587603
meta: Mapping[str, Any],
604+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
588605
) -> None:
589-
super().__init__(_type, version, spec_version, expires)
606+
super().__init__(
607+
_type, version, spec_version, expires, unrecognized_fields
608+
)
590609
# TODO: Add class for meta
591610
self.meta = meta
592611

593612
@classmethod
594613
def from_dict(cls, snapshot_dict: Mapping[str, Any]) -> "Snapshot":
595-
"""Creates Snapshot object from its dict representation. """
614+
"""Creates Snapshot object from its dict representation."""
596615
common_args = cls._common_fields_from_dict(snapshot_dict)
597616
meta = snapshot_dict.pop("meta")
598-
return cls(*common_args, meta)
617+
# All fields left in the snapshot_dict are unrecognized.
618+
return cls(*common_args, meta, snapshot_dict)
599619

600620
def to_dict(self) -> Dict[str, Any]:
601-
"""Returns the dict representation of self. """
621+
"""Returns the dict representation of self."""
602622
snapshot_dict = self._common_fields_to_dict()
603623
snapshot_dict.update({"meta": self.meta})
604624
return snapshot_dict
@@ -611,7 +631,7 @@ def update(
611631
length: Optional[int] = None,
612632
hashes: Optional[Mapping[str, Any]] = None,
613633
) -> None:
614-
"""Assigns passed (delegated) targets role info to meta dict. """
634+
"""Assigns passed (delegated) targets role info to meta dict."""
615635
metadata_fn = f"{rolename}.json"
616636

617637
self.meta[metadata_fn] = {"version": version}
@@ -688,22 +708,26 @@ def __init__(
688708
expires: datetime,
689709
targets: Mapping[str, Any],
690710
delegations: Mapping[str, Any],
711+
unrecognized_fields: Optional[Mapping[str, Any]] = None,
691712
) -> None:
692-
super().__init__(_type, version, spec_version, expires)
713+
super().__init__(
714+
_type, version, spec_version, expires, unrecognized_fields
715+
)
693716
# TODO: Add class for meta
694717
self.targets = targets
695718
self.delegations = delegations
696719

697720
@classmethod
698721
def from_dict(cls, targets_dict: Mapping[str, Any]) -> "Targets":
699-
"""Creates Targets object from its dict representation. """
722+
"""Creates Targets object from its dict representation."""
700723
common_args = cls._common_fields_from_dict(targets_dict)
701724
targets = targets_dict.pop("targets")
702725
delegations = targets_dict.pop("delegations")
703-
return cls(*common_args, targets, delegations)
726+
# All fields left in the targets_dict are unrecognized.
727+
return cls(*common_args, targets, delegations, targets_dict)
704728

705729
def to_dict(self) -> Dict[str, Any]:
706-
"""Returns the dict representation of self. """
730+
"""Returns the dict representation of self."""
707731
targets_dict = self._common_fields_to_dict()
708732
targets_dict.update(
709733
{
@@ -715,5 +739,5 @@ def to_dict(self) -> Dict[str, Any]:
715739

716740
# Modification.
717741
def update(self, filename: str, fileinfo: Mapping[str, Any]) -> None:
718-
"""Assigns passed target file info to meta dict. """
742+
"""Assigns passed target file info to meta dict."""
719743
self.targets[filename] = fileinfo

tuf/api/serialization/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,35 +19,35 @@
1919

2020
# TODO: Should these be in tuf.exceptions or inherit from tuf.exceptions.Error?
2121
class SerializationError(Exception):
22-
"""Error during serialization. """
22+
"""Error during serialization."""
2323

2424

2525
class DeserializationError(Exception):
26-
"""Error during deserialization. """
26+
"""Error during deserialization."""
2727

2828

2929
class MetadataDeserializer(metaclass=abc.ABCMeta):
30-
"""Abstract base class for deserialization of Metadata objects. """
30+
"""Abstract base class for deserialization of Metadata objects."""
3131

3232
@abc.abstractmethod
3333
def deserialize(self, raw_data: bytes) -> "Metadata":
34-
"""Deserialize passed bytes to Metadata object. """
34+
"""Deserialize passed bytes to Metadata object."""
3535
raise NotImplementedError
3636

3737

3838
class MetadataSerializer(metaclass=abc.ABCMeta):
39-
"""Abstract base class for serialization of Metadata objects. """
39+
"""Abstract base class for serialization of Metadata objects."""
4040

4141
@abc.abstractmethod
4242
def serialize(self, metadata_obj: "Metadata") -> bytes:
43-
"""Serialize passed Metadata object to bytes. """
43+
"""Serialize passed Metadata object to bytes."""
4444
raise NotImplementedError
4545

4646

4747
class SignedSerializer(metaclass=abc.ABCMeta):
48-
"""Abstract base class for serialization of Signed objects. """
48+
"""Abstract base class for serialization of Signed objects."""
4949

5050
@abc.abstractmethod
5151
def serialize(self, signed_obj: "Signed") -> bytes:
52-
"""Serialize passed Signed object to bytes. """
52+
"""Serialize passed Signed object to bytes."""
5353
raise NotImplementedError

tuf/api/serialization/json.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@
2828

2929

3030
class JSONDeserializer(MetadataDeserializer):
31-
"""Provides JSON to Metadata deserialize method. """
31+
"""Provides JSON to Metadata deserialize method."""
3232

3333
def deserialize(self, raw_data: bytes) -> Metadata:
34-
"""Deserialize utf-8 encoded JSON bytes into Metadata object. """
34+
"""Deserialize utf-8 encoded JSON bytes into Metadata object."""
3535
try:
3636
json_dict = json.loads(raw_data.decode("utf-8"))
3737
metadata_obj = Metadata.from_dict(json_dict)
@@ -55,7 +55,7 @@ def __init__(self, compact: bool = False) -> None:
5555
self.compact = compact
5656

5757
def serialize(self, metadata_obj: Metadata) -> bytes:
58-
"""Serialize Metadata object into utf-8 encoded JSON bytes. """
58+
"""Serialize Metadata object into utf-8 encoded JSON bytes."""
5959
try:
6060
indent = None if self.compact else 1
6161
separators = (",", ":") if self.compact else (",", ": ")
@@ -73,7 +73,7 @@ def serialize(self, metadata_obj: Metadata) -> bytes:
7373

7474

7575
class CanonicalJSONSerializer(SignedSerializer):
76-
"""Provides Signed to OLPC Canonical JSON serialize method. """
76+
"""Provides Signed to OLPC Canonical JSON serialize method."""
7777

7878
def serialize(self, signed_obj: Signed) -> bytes:
7979
"""Serialize Signed object into utf-8 encoded OLPC Canonical JSON

0 commit comments

Comments
 (0)