From 3d8cade4714b2496fd60ae1da61d47e7b743a83b Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 8 Feb 2021 17:15:27 +0100 Subject: [PATCH 01/18] Add metadata serialization sub-package Add sub-package with 3 abstract base classes to: - serialize Metadata objects to bytes (transport) - deserialize Metadata objects from bytes (transport) - serialize Signed objects to bytes (signatures) pylint notes: - configure tox to use api/pylintrc - configure api/pylintrc to allow classes without public methods (default was 2) Design considerations --------------------- - Why not implement de/serialization on metadata classes? -> See ADR0006. - Why use separate classes for serialization and deserialization? -> Some users might only need either one, e.g. client only needs Deserializer. Maybe there are use cases where different implementations are used to serialize and deserialize. - Why use separate classes for Metadata- and Signed-Serialization? -> They require different concrete types, i.e. Metadata and Signed as parameters, and using these specific types seems to make the interface stronger. - Why are de/serialize methods not class/staticmethods? -> In reality we only use classes to namespace and define a type annotated interface, thus it would be enough to make the methods classmethods. However, to keep the de/serialize interface minimal, we move any custom format configuration to the constructor. (See e.g. "compact" for JSONSerializer in subsequent commit). Naming considerations --------------------- - Why de/serialize? -> Implies byte stream as input or output to the function, which is what our interface needs. - Why not marshaling? -> Synonym for serialize but implies transport, would be okay. - Why not encoding? -> Too abstract and too many connotations (character, a/v). - Why not parse? -> Too abstract and no good opposite terms (unparse, write, dump?) Signed-off-by: Lukas Puehringer --- tox.ini | 5 +++- tuf/api/pylintrc | 3 +++ tuf/api/serialization/__init__.py | 43 +++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tuf/api/serialization/__init__.py diff --git a/tox.ini b/tox.ini index 9ab6dee135..b54a221cc0 100644 --- a/tox.ini +++ b/tox.ini @@ -42,6 +42,9 @@ commands = [testenv:lint] commands = - pylint {toxinidir}/tuf --ignore={toxinidir}/tuf/api + # Use different pylint configs for legacy and new (tuf/api) code + # NOTE: Contrary to what the pylint docs suggest, ignoring full paths does + # work, unfortunately each subdirectory has to be ignored explicitly. + pylint {toxinidir}/tuf --ignore={toxinidir}/tuf/api,{toxinidir}/tuf/api/serialization pylint {toxinidir}/tuf/api --rcfile={toxinidir}/tuf/api/pylintrc bandit -r {toxinidir}/tuf diff --git a/tuf/api/pylintrc b/tuf/api/pylintrc index a75347f446..ad338b0564 100644 --- a/tuf/api/pylintrc +++ b/tuf/api/pylintrc @@ -4,3 +4,6 @@ disable=fixme [FORMAT] indent-string=" " max-line-length=79 + +[DESIGN] +min-public-methods=0 diff --git a/tuf/api/serialization/__init__.py b/tuf/api/serialization/__init__.py new file mode 100644 index 0000000000..a0f05b909a --- /dev/null +++ b/tuf/api/serialization/__init__.py @@ -0,0 +1,43 @@ +"""TUF role metadata de/serialization. + +This sub-package provides abstract base classes and concrete implementations to +serialize and deserialize TUF role metadata and metadata parts. + +Any custom de/serialization implementations should inherit from the abstract +base classes defined in this __init__.py module. + +- Metadata de/serializers are used to convert to and from wireline formats. +- Signed serializers are used to canonicalize data for cryptographic signatures + generation and verification. + +""" +import abc + +class MetadataDeserializer(): + """Abstract base class for deserialization of Metadata objects. """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def deserialize(self, raw_data: bytes) -> "Metadata": + """Deserialize passed bytes to Metadata object. """ + raise NotImplementedError + + +class MetadataSerializer(): + """Abstract base class for serialization of Metadata objects. """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def serialize(self, metadata_obj: "Metadata") -> bytes: + """Serialize passed Metadata object to bytes. """ + raise NotImplementedError + + +class SignedSerializer(): + """Abstract base class for serialization of Signed objects. """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def serialize(self, signed_obj: "Signed") -> bytes: + """Serialize passed Signed object to bytes. """ + raise NotImplementedError From 4a22b4a578d968a1ff2b8da9c05e4a9093f060ad Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 8 Feb 2021 17:32:06 +0100 Subject: [PATCH 02/18] Add concrete de/serializer implementations Add serializer.json module with implementations to serialize and deserialize TUF role metadata to and from the JSON wireline format for transportation, and to serialize the 'signed' part of TUF role metadata to the OLPC Canonical JSON format for signature generation and verification. Signed-off-by: Lukas Puehringer --- tuf/api/serialization/json.py | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tuf/api/serialization/json.py diff --git a/tuf/api/serialization/json.py b/tuf/api/serialization/json.py new file mode 100644 index 0000000000..28152e18d0 --- /dev/null +++ b/tuf/api/serialization/json.py @@ -0,0 +1,55 @@ +"""TUF role metadata JSON serialization and deserialization. + +This module provides concrete implementations to serialize and deserialize TUF +role metadata to and from the JSON wireline format for transportation, and +to serialize the 'signed' part of TUF role metadata to the OLPC Canonical JSON +format for signature generation and verification. + +""" +import json + +from securesystemslib.formats import encode_canonical + +from tuf.api.metadata import Metadata, Signed +from tuf.api.serialization import (MetadataSerializer, + MetadataDeserializer, + SignedSerializer) + + +class JSONDeserializer(MetadataDeserializer): + """Provides JSON-to-Metadata deserialize method. """ + + def deserialize(self, raw_data: bytes) -> Metadata: + """Deserialize utf-8 encoded JSON bytes into Metadata object. """ + _dict = json.loads(raw_data.decode("utf-8")) + return Metadata.from_dict(_dict) + + +class JSONSerializer(MetadataSerializer): + """A Metadata-to-JSON serialize method. + + Attributes: + compact: A boolean indicating if the JSON bytes generated in + 'serialize' should be compact by excluding whitespace. + + """ + def __init__(self, compact: bool = False) -> None: + self.compact = compact + + def serialize(self, metadata_obj: Metadata) -> bytes: + """Serialize Metadata object into utf-8 encoded JSON bytes. """ + indent = (None if self.compact else 1) + separators=((',', ':') if self.compact else (',', ': ')) + return json.dumps(metadata_obj.to_dict(), + indent=indent, + separators=separators, + sort_keys=True).encode("utf-8") + + +class CanonicalJSONSerializer(SignedSerializer): + """A Signed-to-Canonical JSON 'serialize' method. """ + + def serialize(self, signed_obj: Signed) -> bytes: + """Serialize Signed object into utf-8 encoded Canonical JSON bytes. """ + signed_dict = signed_obj.to_dict() + return encode_canonical(signed_dict).encode("utf-8") From 499f1c858ea315be578087bbb189b46e9504ecfc Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 4 Mar 2021 10:51:45 +0100 Subject: [PATCH 03/18] Adopt serialization sub-package in metadata API - Rename Metadata methods: - to_json_file -> to_file - from_json_file -> from_file - Remove Metadata.from_json/to_json - Remove Signed.to_canonical_bytes - Accept optional de/serializer arguments: - from_file (default: JSONDeserializer) - to_file (default: JSONSerializer) - sign, verify (default: CanonicalJSONSerializer) - inline disable pylint cyclic-import checks Signed-off-by: Lukas Puehringer --- tests/test_api.py | 36 ++++++---- tuf/api/metadata.py | 127 ++++++++++++++++++---------------- tuf/api/serialization/json.py | 3 + 3 files changed, 91 insertions(+), 75 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index ec7d182b79..c3f443e0f8 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -28,6 +28,12 @@ Targets ) +from tuf.api.serialization.json import ( + JSONSerializer, + JSONDeserializer, + CanonicalJSONSerializer +) + from securesystemslib.interface import ( import_ed25519_publickey_from_file, import_ed25519_privatekey_from_file @@ -89,10 +95,10 @@ def test_generic_read(self): # Load JSON-formatted metdata of each supported type from file # and from out-of-band read JSON string path = os.path.join(self.repo_dir, 'metadata', metadata + '.json') - metadata_obj = Metadata.from_json_file(path) + metadata_obj = Metadata.from_file(path) with open(path, 'rb') as f: metadata_str = f.read() - metadata_obj2 = Metadata.from_json(metadata_str) + metadata_obj2 = JSONDeserializer().deserialize(metadata_str) # Assert that both methods instantiate the right inner class for # each metadata type and ... @@ -113,27 +119,27 @@ def test_generic_read(self): f.write(json.dumps(bad_metadata).encode('utf-8')) with self.assertRaises(ValueError): - Metadata.from_json_file(bad_metadata_path) + Metadata.from_file(bad_metadata_path) os.remove(bad_metadata_path) def test_compact_json(self): path = os.path.join(self.repo_dir, 'metadata', 'targets.json') - metadata_obj = Metadata.from_json_file(path) + metadata_obj = Metadata.from_file(path) self.assertTrue( - len(metadata_obj.to_json(compact=True)) < - len(metadata_obj.to_json())) + len(JSONSerializer(compact=True).serialize(metadata_obj)) < + len(JSONSerializer().serialize(metadata_obj))) def test_read_write_read_compare(self): for metadata in ['snapshot', 'timestamp', 'targets']: path = os.path.join(self.repo_dir, 'metadata', metadata + '.json') - metadata_obj = Metadata.from_json_file(path) + metadata_obj = Metadata.from_file(path) path_2 = path + '.tmp' - metadata_obj.to_json_file(path_2) - metadata_obj_2 = Metadata.from_json_file(path_2) + metadata_obj.to_file(path_2) + metadata_obj_2 = Metadata.from_file(path_2) self.assertDictEqual( metadata_obj.to_dict(), @@ -145,7 +151,7 @@ def test_read_write_read_compare(self): def test_sign_verify(self): # Load sample metadata (targets) and assert ... path = os.path.join(self.repo_dir, 'metadata', 'targets.json') - metadata_obj = Metadata.from_json_file(path) + metadata_obj = Metadata.from_file(path) # ... it has a single existing signature, self.assertTrue(len(metadata_obj.signatures) == 1) @@ -192,7 +198,7 @@ def test_metadata_base(self): # with real data snapshot_path = os.path.join( self.repo_dir, 'metadata', 'snapshot.json') - md = Metadata.from_json_file(snapshot_path) + md = Metadata.from_file(snapshot_path) self.assertEqual(md.signed.version, 1) md.signed.bump_version() @@ -207,7 +213,7 @@ def test_metadata_base(self): def test_metadata_snapshot(self): snapshot_path = os.path.join( self.repo_dir, 'metadata', 'snapshot.json') - snapshot = Metadata.from_json_file(snapshot_path) + snapshot = Metadata.from_file(snapshot_path) # Create a dict representing what we expect the updated data to be fileinfo = copy.deepcopy(snapshot.signed.meta) @@ -225,7 +231,7 @@ def test_metadata_snapshot(self): def test_metadata_timestamp(self): timestamp_path = os.path.join( self.repo_dir, 'metadata', 'timestamp.json') - timestamp = Metadata.from_json_file(timestamp_path) + timestamp = Metadata.from_file(timestamp_path) self.assertEqual(timestamp.signed.version, 1) timestamp.signed.bump_version() @@ -260,7 +266,7 @@ def test_metadata_timestamp(self): def test_metadata_root(self): root_path = os.path.join( self.repo_dir, 'metadata', 'root.json') - root = Metadata.from_json_file(root_path) + root = Metadata.from_file(root_path) # Add a second key to root role root_key2 = import_ed25519_publickey_from_file( @@ -293,7 +299,7 @@ def test_metadata_root(self): def test_metadata_targets(self): targets_path = os.path.join( self.repo_dir, 'metadata', 'targets.json') - targets = Metadata.from_json_file(targets_path) + targets = Metadata.from_file(targets_path) # Create a fileinfo dict representing what we expect the updated data to be filename = 'file2.txt' diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index a747be6d13..393a2a793b 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -1,7 +1,7 @@ """TUF role metadata model. This module provides container classes for TUF role metadata, including methods -to read/serialize/write from and to JSON, perform TUF-compliant metadata +to read/serialize/write from and to file, perform TUF-compliant metadata updates, and create and verify signatures. """ @@ -9,22 +9,21 @@ from datetime import datetime, timedelta from typing import Any, Dict, Optional -import json import tempfile -from securesystemslib.formats import encode_canonical -from securesystemslib.util import ( - load_json_file, - load_json_string, - persist_temp_file -) -from securesystemslib.storage import StorageBackendInterface +from securesystemslib.util import persist_temp_file +from securesystemslib.storage import (StorageBackendInterface, + FilesystemBackend) from securesystemslib.keys import create_signature, verify_signature +from tuf.api.serialization import (MetadataSerializer, MetadataDeserializer, + SignedSerializer) + import tuf.formats import tuf.exceptions + # Types JsonDict = Dict[str, Any] @@ -101,32 +100,17 @@ class also that has a 'from_dict' factory method. (Currently this is @classmethod - def from_json(cls, metadata_json: str) -> 'Metadata': - """Loads JSON-formatted TUF metadata from a string. - - Arguments: - metadata_json: TUF metadata in JSON-string representation. - - Raises: - securesystemslib.exceptions.Error, ValueError, KeyError: The - metadata cannot be parsed. - - Returns: - A TUF Metadata object. - - """ - return cls.from_dict(load_json_string(metadata_json)) - - - @classmethod - def from_json_file( - cls, filename: str, - storage_backend: Optional[StorageBackendInterface] = None - ) -> 'Metadata': - """Loads JSON-formatted TUF metadata from file storage. + def from_file( + cls, filename: str, deserializer: MetadataDeserializer = None, + storage_backend: Optional[StorageBackendInterface] = None + ) -> 'Metadata': + """Loads TUF metadata from file storage. Arguments: filename: The path to read the file from. + deserializer: A MetadataDeserializer subclass instance that + implements the desired wireline format deserialization. Per + default a JSONDeserializer is used. storage_backend: An object that implements securesystemslib.storage.StorageBackendInterface. Per default a (local) FilesystemBackend is used. @@ -140,7 +124,19 @@ def from_json_file( A TUF Metadata object. """ - return cls.from_dict(load_json_file(filename, storage_backend)) + if deserializer is None: + # Function-scope import to avoid circular dependency. Yucky!!! + # TODO: At least move to _get_default_metadata_deserializer helper. + from tuf.api.serialization.json import JSONDeserializer # pylint: disable=import-outside-toplevel + deserializer = JSONDeserializer() + + if storage_backend is None: + storage_backend = FilesystemBackend() + + with storage_backend.get(filename) as file_obj: + raw_data = file_obj.read() + + return deserializer.deserialize(raw_data) # Serialization. @@ -151,40 +147,38 @@ def to_dict(self) -> JsonDict: 'signed': self.signed.to_dict() } - - def to_json(self, compact: bool = False) -> None: - """Returns the optionally compacted JSON representation of self. """ - return json.dumps( - self.to_dict(), - indent=(None if compact else 1), - separators=((',', ':') if compact else (',', ': ')), - sort_keys=True) - - - def to_json_file( - self, filename: str, compact: bool = False, - storage_backend: StorageBackendInterface = None) -> None: - """Writes the JSON representation of self to file storage. + def to_file(self, filename: str, serializer: MetadataSerializer = None, + storage_backend: StorageBackendInterface = None) -> None: + """Writes TUF metadata to file storage. Arguments: filename: The path to write the file to. - compact: A boolean indicating if the JSON string should be compact - by excluding whitespace. + serializer: A MetadataSerializer subclass instance that implements + the desired wireline format serialization. Per default a + JSONSerializer is used. storage_backend: An object that implements securesystemslib.storage.StorageBackendInterface. Per default a (local) FilesystemBackend is used. + Raises: securesystemslib.exceptions.StorageError: The file cannot be written. """ + if serializer is None: + # Function-scope import to avoid circular dependency. Yucky!!! + # TODO: At least move to a _get_default_metadata_serializer helper. + from tuf.api.serialization.json import JSONSerializer # pylint: disable=import-outside-toplevel + serializer = JSONSerializer(True) # Pass True to compact JSON + with tempfile.TemporaryFile() as temp_file: - temp_file.write(self.to_json(compact).encode('utf-8')) + temp_file.write(serializer.serialize(self)) persist_temp_file(temp_file, filename, storage_backend) # Signatures. - def sign(self, key: JsonDict, append: bool = False) -> JsonDict: + def sign(self, key: JsonDict, append: bool = False, + serializer: SignedSerializer = None) -> JsonDict: """Creates signature over 'signed' and assigns it to 'signatures'. Arguments: @@ -192,6 +186,9 @@ def sign(self, key: JsonDict, append: bool = False) -> JsonDict: append: A boolean indicating if the signature should be appended to the list of signatures or replace any existing signatures. The default behavior is to replace signatures. + serializer: A SignedSerializer subclass instance that implements + the desired canonicalization format. Per default a + CanonicalJSONSerializer is used. Raises: securesystemslib.exceptions.FormatError: Key argument is malformed. @@ -203,7 +200,13 @@ def sign(self, key: JsonDict, append: bool = False) -> JsonDict: A securesystemslib-style signature object. """ - signature = create_signature(key, self.signed.to_canonical_bytes()) + if serializer is None: + # Function-scope import to avoid circular dependency. Yucky!!! + # TODO: At least move to a _get_default_signed_serializer helper. + from tuf.api.serialization.json import CanonicalJSONSerializer # pylint: disable=import-outside-toplevel + serializer = CanonicalJSONSerializer() + + signature = create_signature(key, serializer.serialize(self.signed)) if append: self.signatures.append(signature) @@ -213,11 +216,15 @@ def sign(self, key: JsonDict, append: bool = False) -> JsonDict: return signature - def verify(self, key: JsonDict) -> bool: + def verify(self, key: JsonDict, + serializer: SignedSerializer = None) -> bool: """Verifies 'signatures' over 'signed' that match the passed key by id. Arguments: key: A securesystemslib-style public key object. + serializer: A SignedSerializer subclass instance that implements + the desired canonicalization format. Per default a + CanonicalJSONSerializer is used. Raises: # TODO: Revise exception taxonomy @@ -243,9 +250,15 @@ def verify(self, key: JsonDict) -> bool: f'{len(signatures_for_keyid)} signatures for key ' f'{key["keyid"]}, not sure which one to verify.') + if serializer is None: + # Function-scope import to avoid circular dependency. Yucky!!! + # TODO: At least move to a _get_default_signed_serializer helper. + from tuf.api.serialization.json import CanonicalJSONSerializer # pylint: disable=import-outside-toplevel + serializer = CanonicalJSONSerializer() + return verify_signature( key, signatures_for_keyid[0], - self.signed.to_canonical_bytes()) + serializer.serialize(self.signed)) @@ -307,12 +320,6 @@ def from_dict(cls, signed_dict: JsonDict) -> 'Signed': return cls(**signed_dict) - # Serialization. - def to_canonical_bytes(self) -> bytes: - """Returns the UTF-8 encoded canonical JSON representation of self. """ - return encode_canonical(self.to_dict()).encode('UTF-8') - - def to_dict(self) -> JsonDict: """Returns the JSON-serializable dictionary representation of self. """ return { diff --git a/tuf/api/serialization/json.py b/tuf/api/serialization/json.py index 28152e18d0..3f25085dc7 100644 --- a/tuf/api/serialization/json.py +++ b/tuf/api/serialization/json.py @@ -10,6 +10,9 @@ from securesystemslib.formats import encode_canonical +# pylint: disable=cyclic-import +# ... to allow de/serializing the correct metadata class here, while also +# creating default de/serializers there (see metadata function scope imports). from tuf.api.metadata import Metadata, Signed from tuf.api.serialization import (MetadataSerializer, MetadataDeserializer, From 240fb547af388c5b66946ad50261a2caf261c54e Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 9 Feb 2021 15:36:49 +0100 Subject: [PATCH 04/18] Use custom errors in serializer.json sub-package Re-raise all errors that happen during de/serialization as custom De/SerializationError. Whilelist 'e', which is idiomatic for error, in api/pylintrc, and inline exempt broad-except, which are okay if re-raised. Signed-off-by: Lukas Puehringer --- tests/test_api.py | 6 ++++- tuf/api/metadata.py | 11 ++++++--- tuf/api/pylintrc | 3 +++ tuf/api/serialization/__init__.py | 8 +++++++ tuf/api/serialization/json.py | 37 ++++++++++++++++++++++--------- 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index c3f443e0f8..e13c704242 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -28,6 +28,10 @@ Targets ) +from tuf.api.serialization import ( + DeserializationError +) + from tuf.api.serialization.json import ( JSONSerializer, JSONDeserializer, @@ -118,7 +122,7 @@ def test_generic_read(self): with open(bad_metadata_path, 'wb') as f: f.write(json.dumps(bad_metadata).encode('utf-8')) - with self.assertRaises(ValueError): + with self.assertRaises(DeserializationError): Metadata.from_file(bad_metadata_path) os.remove(bad_metadata_path) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 393a2a793b..cdbe0ad9eb 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -117,8 +117,8 @@ def from_file( Raises: securesystemslib.exceptions.StorageError: The file cannot be read. - securesystemslib.exceptions.Error, ValueError, KeyError: The - metadata cannot be parsed. + tuf.api.serialization.DeserializationError: + The file cannot be deserialized. Returns: A TUF Metadata object. @@ -161,6 +161,8 @@ def to_file(self, filename: str, serializer: MetadataSerializer = None, a (local) FilesystemBackend is used. Raises: + tuf.api.serialization.SerializationError: + The metadata object cannot be serialized. securesystemslib.exceptions.StorageError: The file cannot be written. @@ -191,7 +193,8 @@ def sign(self, key: JsonDict, append: bool = False, CanonicalJSONSerializer is used. Raises: - securesystemslib.exceptions.FormatError: Key argument is malformed. + tuf.api.serialization.SerializationError: + 'signed' cannot be serialized. securesystemslib.exceptions.CryptoError, \ securesystemslib.exceptions.UnsupportedAlgorithmError: Signing errors. @@ -230,6 +233,8 @@ def verify(self, key: JsonDict, # TODO: Revise exception taxonomy tuf.exceptions.Error: None or multiple signatures found for key. securesystemslib.exceptions.FormatError: Key argument is malformed. + tuf.api.serialization.SerializationError: + 'signed' cannot be serialized. securesystemslib.exceptions.CryptoError, \ securesystemslib.exceptions.UnsupportedAlgorithmError: Signing errors. diff --git a/tuf/api/pylintrc b/tuf/api/pylintrc index ad338b0564..badef7613d 100644 --- a/tuf/api/pylintrc +++ b/tuf/api/pylintrc @@ -1,6 +1,9 @@ [MESSAGE_CONTROL] disable=fixme +[BASIC] +good-names=e + [FORMAT] indent-string=" " max-line-length=79 diff --git a/tuf/api/serialization/__init__.py b/tuf/api/serialization/__init__.py index a0f05b909a..0866214633 100644 --- a/tuf/api/serialization/__init__.py +++ b/tuf/api/serialization/__init__.py @@ -13,6 +13,14 @@ """ import abc +# TODO: Should these be in tuf.exceptions or inherit from tuf.exceptions.Error? +class SerializationError(Exception): + """Error during serialization. """ + +class DeserializationError(Exception): + """Error during deserialization. """ + + class MetadataDeserializer(): """Abstract base class for deserialization of Metadata objects. """ __metaclass__ = abc.ABCMeta diff --git a/tuf/api/serialization/json.py b/tuf/api/serialization/json.py index 3f25085dc7..602fbf59c0 100644 --- a/tuf/api/serialization/json.py +++ b/tuf/api/serialization/json.py @@ -7,6 +7,7 @@ """ import json +import six from securesystemslib.formats import encode_canonical @@ -16,7 +17,9 @@ from tuf.api.metadata import Metadata, Signed from tuf.api.serialization import (MetadataSerializer, MetadataDeserializer, - SignedSerializer) + SignedSerializer, + SerializationError, + DeserializationError) class JSONDeserializer(MetadataDeserializer): @@ -24,8 +27,12 @@ class JSONDeserializer(MetadataDeserializer): def deserialize(self, raw_data: bytes) -> Metadata: """Deserialize utf-8 encoded JSON bytes into Metadata object. """ - _dict = json.loads(raw_data.decode("utf-8")) - return Metadata.from_dict(_dict) + try: + _dict = json.loads(raw_data.decode("utf-8")) + return Metadata.from_dict(_dict) + + except Exception as e: # pylint: disable=broad-except + six.raise_from(DeserializationError, e) class JSONSerializer(MetadataSerializer): @@ -41,12 +48,16 @@ def __init__(self, compact: bool = False) -> None: def serialize(self, metadata_obj: Metadata) -> bytes: """Serialize Metadata object into utf-8 encoded JSON bytes. """ - indent = (None if self.compact else 1) - separators=((',', ':') if self.compact else (',', ': ')) - return json.dumps(metadata_obj.to_dict(), - indent=indent, - separators=separators, - sort_keys=True).encode("utf-8") + try: + indent = (None if self.compact else 1) + separators=((',', ':') if self.compact else (',', ': ')) + return json.dumps(metadata_obj.to_dict(), + indent=indent, + separators=separators, + sort_keys=True).encode("utf-8") + + except Exception as e: # pylint: disable=broad-except + six.raise_from(SerializationError, e) class CanonicalJSONSerializer(SignedSerializer): @@ -54,5 +65,9 @@ class CanonicalJSONSerializer(SignedSerializer): def serialize(self, signed_obj: Signed) -> bytes: """Serialize Signed object into utf-8 encoded Canonical JSON bytes. """ - signed_dict = signed_obj.to_dict() - return encode_canonical(signed_dict).encode("utf-8") + try: + signed_dict = signed_obj.to_dict() + return encode_canonical(signed_dict).encode("utf-8") + + except Exception as e: # pylint: disable=broad-except + six.raise_from(SerializationError, e) From e1be085c3cfc626e9021c3bc16c7b1047a0a55a2 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 8 Feb 2021 17:46:20 +0100 Subject: [PATCH 05/18] Move to/from_dict metadata API methods to util Add tuf.api.serialization.util module with functions to convert between TUF metadata class model and the corresponding dictionary representation. These functions replace the corresponding to/from_dict classmethods. Configure api/pylintrc to exempt '_type' from protected member access warning, because the underscore prefix here is only used to avoid name shadowing. Signed-off-by: Lukas Puehringer --- tests/test_api.py | 14 ++-- tuf/api/metadata.py | 130 +---------------------------- tuf/api/pylintrc | 3 + tuf/api/serialization/json.py | 9 +- tuf/api/serialization/util.py | 149 ++++++++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 137 deletions(-) create mode 100644 tuf/api/serialization/util.py diff --git a/tests/test_api.py b/tests/test_api.py index e13c704242..dcc6f0f2ee 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -38,6 +38,8 @@ CanonicalJSONSerializer ) +from tuf.api.serialization.util import metadata_to_dict + from securesystemslib.interface import ( import_ed25519_publickey_from_file, import_ed25519_privatekey_from_file @@ -47,6 +49,7 @@ format_keyval_to_metadata ) + logger = logging.getLogger(__name__) @@ -111,9 +114,9 @@ def test_generic_read(self): self.assertTrue( isinstance(metadata_obj2.signed, inner_metadata_cls)) - # ... and return the same object (compared by dict representation) - self.assertDictEqual( - metadata_obj.to_dict(), metadata_obj2.to_dict()) + # ... and are equal (compared by dict representation) + self.assertDictEqual(metadata_to_dict(metadata_obj), + metadata_to_dict(metadata_obj2)) # Assert that it chokes correctly on an unknown metadata type @@ -145,9 +148,8 @@ def test_read_write_read_compare(self): metadata_obj.to_file(path_2) metadata_obj_2 = Metadata.from_file(path_2) - self.assertDictEqual( - metadata_obj.to_dict(), - metadata_obj_2.to_dict()) + self.assertDictEqual(metadata_to_dict(metadata_obj), + metadata_to_dict(metadata_obj_2)) os.remove(path_2) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index cdbe0ad9eb..eca2bf337b 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -23,7 +23,6 @@ import tuf.exceptions - # Types JsonDict = Dict[str, Any] @@ -56,49 +55,6 @@ def __init__(self, signed: 'Signed', signatures: list) -> None: self.signatures = signatures - # Deserialization (factories). - @classmethod - def from_dict(cls, metadata: JsonDict) -> 'Metadata': - """Creates Metadata object from its JSON/dict representation. - - Calls 'from_dict' for any complex metadata attribute represented by a - class also that has a 'from_dict' factory method. (Currently this is - only the signed attribute.) - - Arguments: - metadata: TUF metadata in JSON/dict representation, as e.g. - returned by 'json.loads'. - - Raises: - KeyError: The metadata dict format is invalid. - ValueError: The metadata has an unrecognized signed._type field. - - Returns: - A TUF Metadata object. - - """ - # Dispatch to contained metadata class on metadata _type field. - _type = metadata['signed']['_type'] - - if _type == 'targets': - inner_cls = Targets - elif _type == 'snapshot': - inner_cls = Snapshot - elif _type == 'timestamp': - inner_cls = Timestamp - elif _type == 'root': - inner_cls = Root - else: - raise ValueError(f'unrecognized metadata type "{_type}"') - - # NOTE: If Signature becomes a class, we should iterate over - # metadata['signatures'], call Signature.from_dict for each item, and - # pass a list of Signature objects to the Metadata constructor intead. - return cls( - signed=inner_cls.from_dict(metadata['signed']), - signatures=metadata['signatures']) - - @classmethod def from_file( cls, filename: str, deserializer: MetadataDeserializer = None, @@ -139,14 +95,6 @@ def from_file( return deserializer.deserialize(raw_data) - # Serialization. - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ - return { - 'signatures': self.signatures, - 'signed': self.signed.to_dict() - } - def to_file(self, filename: str, serializer: MetadataSerializer = None, storage_backend: StorageBackendInterface = None) -> None: """Writes TUF metadata to file storage. @@ -302,39 +250,6 @@ def __init__( self.version = version - # Deserialization (factories). - @classmethod - def from_dict(cls, signed_dict: JsonDict) -> 'Signed': - """Creates Signed object from its JSON/dict representation. """ - - # Convert 'expires' TUF metadata string to a datetime object, which is - # what the constructor expects and what we store. The inverse operation - # is implemented in 'to_dict'. - signed_dict['expires'] = tuf.formats.expiry_string_to_datetime( - signed_dict['expires']) - # NOTE: We write the converted 'expires' back into 'signed_dict' above - # so that we can pass it to the constructor as '**signed_dict' below, - # along with other fields that belong to Signed subclasses. - # Any 'from_dict'(-like) conversions of fields that correspond to a - # subclass should be performed in the 'from_dict' method of that - # subclass and also be written back into 'signed_dict' before calling - # super().from_dict. - - # NOTE: cls might be a subclass of Signed, if 'from_dict' was called on - # that subclass (see e.g. Metadata.from_dict). - return cls(**signed_dict) - - - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ - return { - '_type': self._type, - 'version': self.version, - 'spec_version': self.spec_version, - 'expires': self.expires.isoformat() + 'Z' - } - - # Modification. def bump_expiration(self, delta: timedelta = timedelta(days=1)) -> None: """Increments the expires attribute by the passed timedelta. """ @@ -346,6 +261,7 @@ def bump_version(self) -> None: self.version += 1 + class Root(Signed): """A container for the signed part of root metadata. @@ -395,18 +311,6 @@ def __init__( self.roles = roles - # Serialization. - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ - json_dict = super().to_dict() - json_dict.update({ - 'consistent_snapshot': self.consistent_snapshot, - 'keys': self.keys, - 'roles': self.roles - }) - return json_dict - - # Update key for a role. def add_key(self, role: str, keyid: str, key_metadata: JsonDict) -> None: """Adds new key for 'role' and updates the key store. """ @@ -428,7 +332,6 @@ def remove_key(self, role: str, keyid: str) -> None: - class Timestamp(Signed): """A container for the signed part of timestamp metadata. @@ -456,16 +359,6 @@ def __init__( self.meta = meta - # Serialization. - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ - json_dict = super().to_dict() - json_dict.update({ - 'meta': self.meta - }) - return json_dict - - # Modification. def update(self, version: int, length: int, hashes: JsonDict) -> None: """Assigns passed info about snapshot metadata to meta dict. """ @@ -476,6 +369,7 @@ def update(self, version: int, length: int, hashes: JsonDict) -> None: } + class Snapshot(Signed): """A container for the signed part of snapshot metadata. @@ -509,15 +403,6 @@ def __init__( # TODO: Add class for meta self.meta = meta - # Serialization. - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ - json_dict = super().to_dict() - json_dict.update({ - 'meta': self.meta - }) - return json_dict - # Modification. def update( @@ -534,6 +419,7 @@ def update( self.meta[metadata_fn]['hashes'] = hashes + class Targets(Signed): """A container for the signed part of targets metadata. @@ -601,16 +487,6 @@ def __init__( self.delegations = delegations - # Serialization. - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ - json_dict = super().to_dict() - json_dict.update({ - 'targets': self.targets, - 'delegations': self.delegations, - }) - return json_dict - # Modification. def update(self, filename: str, fileinfo: JsonDict) -> None: """Assigns passed target file info to meta dict. """ diff --git a/tuf/api/pylintrc b/tuf/api/pylintrc index badef7613d..38562e62ea 100644 --- a/tuf/api/pylintrc +++ b/tuf/api/pylintrc @@ -8,5 +8,8 @@ good-names=e indent-string=" " max-line-length=79 +[CLASSES] +exclude-protected:_type + [DESIGN] min-public-methods=0 diff --git a/tuf/api/serialization/json.py b/tuf/api/serialization/json.py index 602fbf59c0..fd73511455 100644 --- a/tuf/api/serialization/json.py +++ b/tuf/api/serialization/json.py @@ -19,7 +19,8 @@ MetadataDeserializer, SignedSerializer, SerializationError, - DeserializationError) + DeserializationError, + util) class JSONDeserializer(MetadataDeserializer): @@ -29,7 +30,7 @@ def deserialize(self, raw_data: bytes) -> Metadata: """Deserialize utf-8 encoded JSON bytes into Metadata object. """ try: _dict = json.loads(raw_data.decode("utf-8")) - return Metadata.from_dict(_dict) + return util.metadata_from_dict(_dict) except Exception as e: # pylint: disable=broad-except six.raise_from(DeserializationError, e) @@ -51,7 +52,7 @@ def serialize(self, metadata_obj: Metadata) -> bytes: try: indent = (None if self.compact else 1) separators=((',', ':') if self.compact else (',', ': ')) - return json.dumps(metadata_obj.to_dict(), + return json.dumps(util.metadata_to_dict(metadata_obj), indent=indent, separators=separators, sort_keys=True).encode("utf-8") @@ -66,7 +67,7 @@ class CanonicalJSONSerializer(SignedSerializer): def serialize(self, signed_obj: Signed) -> bytes: """Serialize Signed object into utf-8 encoded Canonical JSON bytes. """ try: - signed_dict = signed_obj.to_dict() + signed_dict = util.signed_to_dict(signed_obj) return encode_canonical(signed_dict).encode("utf-8") except Exception as e: # pylint: disable=broad-except diff --git a/tuf/api/serialization/util.py b/tuf/api/serialization/util.py new file mode 100644 index 0000000000..db06e92530 --- /dev/null +++ b/tuf/api/serialization/util.py @@ -0,0 +1,149 @@ +"""Utility functions to facilitate TUF metadata de/serialization. + +Currently, this module contains functions to convert between the TUF metadata +class model and a corresponding dictionary representation. + +""" + + +from typing import Any, Dict, Mapping + +from tuf import formats +from tuf.api.metadata import (Signed, Metadata, Root, Timestamp, Snapshot, + Targets) + + +def _get_signed_common_args_from_dict(_dict: Mapping[str, Any]) -> list: + """Returns ordered positional arguments for 'Signed' subclass constructors. + + See '{root, timestamp, snapshot, targets}_from_dict' functions for usage. + + """ + _type = _dict.pop("_type") + version = _dict.pop("version") + spec_version = _dict.pop("spec_version") + expires_str = _dict.pop("expires") + expires = formats.expiry_string_to_datetime(expires_str) + return [_type, version, spec_version, expires] + + +def root_from_dict(_dict: Mapping[str, Any]) -> Root: + """Returns 'Root' object based on its dict representation. """ + common_args = _get_signed_common_args_from_dict(_dict) + consistent_snapshot = _dict.pop("consistent_snapshot") + keys = _dict.pop("keys") + roles = _dict.pop("roles") + return Root(*common_args, consistent_snapshot, keys, roles) + + +def timestamp_from_dict(_dict: Mapping[str, Any]) -> Timestamp: + """Returns 'Timestamp' object based on its dict representation. """ + common_args = _get_signed_common_args_from_dict(_dict) + meta = _dict.pop("meta") + return Timestamp(*common_args, meta) + + +def snapshot_from_dict(_dict: Mapping[str, Any]) -> Snapshot: + """Returns 'Snapshot' object based on its dict representation. """ + common_args = _get_signed_common_args_from_dict(_dict) + meta = _dict.pop("meta") + return Snapshot(*common_args, meta) + + +def targets_from_dict(_dict: Mapping[str, Any]) -> Targets: + """Returns 'Targets' object based on its dict representation. """ + common_args = _get_signed_common_args_from_dict(_dict) + targets = _dict.pop("targets") + delegations = _dict.pop("delegations") + return Targets(*common_args, targets, delegations) + +def signed_from_dict(_dict) -> Signed: + """Returns 'Signed'-subclass object based on its dict representation. """ + # Dispatch to '*_from_dict'-function based on '_type' field. + # TODO: Use if/else cascade, if easier to read! + # TODO: Use constants for types! (e.g. Root._type, Targets._type, etc.) + return { + "root": root_from_dict, + "timestamp": timestamp_from_dict, + "snapshot": snapshot_from_dict, + "targets": targets_from_dict + }[_dict["_type"]](_dict) + + +def metadata_from_dict(_dict: Mapping[str, Any]) -> Metadata: + """Returns 'Metadata' object based on its dict representation. """ + signed_dict = _dict.pop("signed") + signatures = _dict.pop("signatures") + return Metadata(signatures=signatures, + signed=signed_from_dict(signed_dict)) + + +def _get_signed_common_fields_as_dict(obj: Signed) -> Dict[str, Any]: + """Returns dict representation of 'Signed' object. + + See '{root, timestamp, snapshot, targets}_to_dict' functions for usage. + + """ + return { + "_type": obj._type, + "version": obj.version, + "spec_version": obj.spec_version, + "expires": obj.expires.isoformat() + "Z" + } + + +def root_to_dict(obj: Root) -> Dict[str, Any]: + """Returns dict representation of 'Root' object. """ + _dict = _get_signed_common_fields_as_dict(obj) + _dict.update({ + "consistent_snapshot": obj.consistent_snapshot, + "keys": obj.keys, + "roles": obj.roles + }) + return _dict + + +def timestamp_to_dict(obj: Timestamp) -> Dict[str, Any]: + """Returns dict representation of 'Timestamp' object. """ + _dict = _get_signed_common_fields_as_dict(obj) + _dict.update({ + "meta": obj.meta + }) + return _dict + + +def snapshot_to_dict(obj: Snapshot) -> Dict[str, Any]: + """Returns dict representation of 'Snapshot' object. """ + _dict = _get_signed_common_fields_as_dict(obj) + _dict.update({ + "meta": obj.meta + }) + return _dict + + +def targets_to_dict(obj: Targets) -> Dict[str, Any]: + """Returns dict representation of 'Targets' object. """ + _dict = _get_signed_common_fields_as_dict(obj) + _dict.update({ + "targets": obj.targets, + "delegations": obj.delegations, + }) + return _dict + +def signed_to_dict(obj: Signed) -> Dict[str, Any]: + """Returns dict representation of 'Signed'-subclass object. """ + # Dispatch to '*_to_dict'-function based on 'Signed' subclass type. + # TODO: Use if/else cascade, if easier to read! + return { + Root: root_to_dict, + Timestamp: timestamp_to_dict, + Snapshot: snapshot_to_dict, + Targets: targets_to_dict + }[obj.__class__](obj) + +def metadata_to_dict(obj: Metadata) -> Dict[str, Any]: + """Returns dict representation of 'Metadata' object. """ + return { + "signatures": obj.signatures, + "signed": signed_to_dict(obj.signed) + } From 8e9afc96f90cfee91493f887f208cf03aea59b44 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 4 Mar 2021 12:46:16 +0100 Subject: [PATCH 06/18] Revert "Move to/from_dict metadata API methods..." Revert an earlier commit that moved to/from_dict metadata class model methods to a util module of the serialization sub-package. We keep to/from_dict methods on the metadata classes because: - It seems **idiomatic** (see e.g. 3rd-party libaries such as attrs, pydantic, marshmallow, or built-ins that provide default or customizable dict representation for higher-level objects). The idiomatic choice should make usage more intuitive. - It feels better **structured** when each method is encapsulated within the corresponding class, which in turn should make maintaining/modifying/extending the class model easier. - It allows us to remove function-scope imports (see subsequent commit). Caveat: Now that "the meat" of the sub-packaged JSON serializer is implemented on the class, it might make it harder to create a non-dict based serializer by copy-paste-amending the JSON serializer. However, the benefits from above seem to outweigh the disadvantage. See option 5 of ADR0006 for further details (#1270). Signed-off-by: Lukas Puehringer --- tests/test_api.py | 14 ++-- tuf/api/metadata.py | 130 ++++++++++++++++++++++++++++- tuf/api/pylintrc | 3 - tuf/api/serialization/json.py | 9 +- tuf/api/serialization/util.py | 149 ---------------------------------- 5 files changed, 137 insertions(+), 168 deletions(-) delete mode 100644 tuf/api/serialization/util.py diff --git a/tests/test_api.py b/tests/test_api.py index dcc6f0f2ee..e13c704242 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -38,8 +38,6 @@ CanonicalJSONSerializer ) -from tuf.api.serialization.util import metadata_to_dict - from securesystemslib.interface import ( import_ed25519_publickey_from_file, import_ed25519_privatekey_from_file @@ -49,7 +47,6 @@ format_keyval_to_metadata ) - logger = logging.getLogger(__name__) @@ -114,9 +111,9 @@ def test_generic_read(self): self.assertTrue( isinstance(metadata_obj2.signed, inner_metadata_cls)) - # ... and are equal (compared by dict representation) - self.assertDictEqual(metadata_to_dict(metadata_obj), - metadata_to_dict(metadata_obj2)) + # ... and return the same object (compared by dict representation) + self.assertDictEqual( + metadata_obj.to_dict(), metadata_obj2.to_dict()) # Assert that it chokes correctly on an unknown metadata type @@ -148,8 +145,9 @@ def test_read_write_read_compare(self): metadata_obj.to_file(path_2) metadata_obj_2 = Metadata.from_file(path_2) - self.assertDictEqual(metadata_to_dict(metadata_obj), - metadata_to_dict(metadata_obj_2)) + self.assertDictEqual( + metadata_obj.to_dict(), + metadata_obj_2.to_dict()) os.remove(path_2) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index eca2bf337b..cdbe0ad9eb 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -23,6 +23,7 @@ import tuf.exceptions + # Types JsonDict = Dict[str, Any] @@ -55,6 +56,49 @@ def __init__(self, signed: 'Signed', signatures: list) -> None: self.signatures = signatures + # Deserialization (factories). + @classmethod + def from_dict(cls, metadata: JsonDict) -> 'Metadata': + """Creates Metadata object from its JSON/dict representation. + + Calls 'from_dict' for any complex metadata attribute represented by a + class also that has a 'from_dict' factory method. (Currently this is + only the signed attribute.) + + Arguments: + metadata: TUF metadata in JSON/dict representation, as e.g. + returned by 'json.loads'. + + Raises: + KeyError: The metadata dict format is invalid. + ValueError: The metadata has an unrecognized signed._type field. + + Returns: + A TUF Metadata object. + + """ + # Dispatch to contained metadata class on metadata _type field. + _type = metadata['signed']['_type'] + + if _type == 'targets': + inner_cls = Targets + elif _type == 'snapshot': + inner_cls = Snapshot + elif _type == 'timestamp': + inner_cls = Timestamp + elif _type == 'root': + inner_cls = Root + else: + raise ValueError(f'unrecognized metadata type "{_type}"') + + # NOTE: If Signature becomes a class, we should iterate over + # metadata['signatures'], call Signature.from_dict for each item, and + # pass a list of Signature objects to the Metadata constructor intead. + return cls( + signed=inner_cls.from_dict(metadata['signed']), + signatures=metadata['signatures']) + + @classmethod def from_file( cls, filename: str, deserializer: MetadataDeserializer = None, @@ -95,6 +139,14 @@ def from_file( return deserializer.deserialize(raw_data) + # Serialization. + def to_dict(self) -> JsonDict: + """Returns the JSON-serializable dictionary representation of self. """ + return { + 'signatures': self.signatures, + 'signed': self.signed.to_dict() + } + def to_file(self, filename: str, serializer: MetadataSerializer = None, storage_backend: StorageBackendInterface = None) -> None: """Writes TUF metadata to file storage. @@ -250,6 +302,39 @@ def __init__( self.version = version + # Deserialization (factories). + @classmethod + def from_dict(cls, signed_dict: JsonDict) -> 'Signed': + """Creates Signed object from its JSON/dict representation. """ + + # Convert 'expires' TUF metadata string to a datetime object, which is + # what the constructor expects and what we store. The inverse operation + # is implemented in 'to_dict'. + signed_dict['expires'] = tuf.formats.expiry_string_to_datetime( + signed_dict['expires']) + # NOTE: We write the converted 'expires' back into 'signed_dict' above + # so that we can pass it to the constructor as '**signed_dict' below, + # along with other fields that belong to Signed subclasses. + # Any 'from_dict'(-like) conversions of fields that correspond to a + # subclass should be performed in the 'from_dict' method of that + # subclass and also be written back into 'signed_dict' before calling + # super().from_dict. + + # NOTE: cls might be a subclass of Signed, if 'from_dict' was called on + # that subclass (see e.g. Metadata.from_dict). + return cls(**signed_dict) + + + def to_dict(self) -> JsonDict: + """Returns the JSON-serializable dictionary representation of self. """ + return { + '_type': self._type, + 'version': self.version, + 'spec_version': self.spec_version, + 'expires': self.expires.isoformat() + 'Z' + } + + # Modification. def bump_expiration(self, delta: timedelta = timedelta(days=1)) -> None: """Increments the expires attribute by the passed timedelta. """ @@ -261,7 +346,6 @@ def bump_version(self) -> None: self.version += 1 - class Root(Signed): """A container for the signed part of root metadata. @@ -311,6 +395,18 @@ def __init__( self.roles = roles + # Serialization. + def to_dict(self) -> JsonDict: + """Returns the JSON-serializable dictionary representation of self. """ + json_dict = super().to_dict() + json_dict.update({ + 'consistent_snapshot': self.consistent_snapshot, + 'keys': self.keys, + 'roles': self.roles + }) + return json_dict + + # Update key for a role. def add_key(self, role: str, keyid: str, key_metadata: JsonDict) -> None: """Adds new key for 'role' and updates the key store. """ @@ -332,6 +428,7 @@ def remove_key(self, role: str, keyid: str) -> None: + class Timestamp(Signed): """A container for the signed part of timestamp metadata. @@ -359,6 +456,16 @@ def __init__( self.meta = meta + # Serialization. + def to_dict(self) -> JsonDict: + """Returns the JSON-serializable dictionary representation of self. """ + json_dict = super().to_dict() + json_dict.update({ + 'meta': self.meta + }) + return json_dict + + # Modification. def update(self, version: int, length: int, hashes: JsonDict) -> None: """Assigns passed info about snapshot metadata to meta dict. """ @@ -369,7 +476,6 @@ def update(self, version: int, length: int, hashes: JsonDict) -> None: } - class Snapshot(Signed): """A container for the signed part of snapshot metadata. @@ -403,6 +509,15 @@ def __init__( # TODO: Add class for meta self.meta = meta + # Serialization. + def to_dict(self) -> JsonDict: + """Returns the JSON-serializable dictionary representation of self. """ + json_dict = super().to_dict() + json_dict.update({ + 'meta': self.meta + }) + return json_dict + # Modification. def update( @@ -419,7 +534,6 @@ def update( self.meta[metadata_fn]['hashes'] = hashes - class Targets(Signed): """A container for the signed part of targets metadata. @@ -487,6 +601,16 @@ def __init__( self.delegations = delegations + # Serialization. + def to_dict(self) -> JsonDict: + """Returns the JSON-serializable dictionary representation of self. """ + json_dict = super().to_dict() + json_dict.update({ + 'targets': self.targets, + 'delegations': self.delegations, + }) + return json_dict + # Modification. def update(self, filename: str, fileinfo: JsonDict) -> None: """Assigns passed target file info to meta dict. """ diff --git a/tuf/api/pylintrc b/tuf/api/pylintrc index 38562e62ea..badef7613d 100644 --- a/tuf/api/pylintrc +++ b/tuf/api/pylintrc @@ -8,8 +8,5 @@ good-names=e indent-string=" " max-line-length=79 -[CLASSES] -exclude-protected:_type - [DESIGN] min-public-methods=0 diff --git a/tuf/api/serialization/json.py b/tuf/api/serialization/json.py index fd73511455..602fbf59c0 100644 --- a/tuf/api/serialization/json.py +++ b/tuf/api/serialization/json.py @@ -19,8 +19,7 @@ MetadataDeserializer, SignedSerializer, SerializationError, - DeserializationError, - util) + DeserializationError) class JSONDeserializer(MetadataDeserializer): @@ -30,7 +29,7 @@ def deserialize(self, raw_data: bytes) -> Metadata: """Deserialize utf-8 encoded JSON bytes into Metadata object. """ try: _dict = json.loads(raw_data.decode("utf-8")) - return util.metadata_from_dict(_dict) + return Metadata.from_dict(_dict) except Exception as e: # pylint: disable=broad-except six.raise_from(DeserializationError, e) @@ -52,7 +51,7 @@ def serialize(self, metadata_obj: Metadata) -> bytes: try: indent = (None if self.compact else 1) separators=((',', ':') if self.compact else (',', ': ')) - return json.dumps(util.metadata_to_dict(metadata_obj), + return json.dumps(metadata_obj.to_dict(), indent=indent, separators=separators, sort_keys=True).encode("utf-8") @@ -67,7 +66,7 @@ class CanonicalJSONSerializer(SignedSerializer): def serialize(self, signed_obj: Signed) -> bytes: """Serialize Signed object into utf-8 encoded Canonical JSON bytes. """ try: - signed_dict = util.signed_to_dict(signed_obj) + signed_dict = signed_obj.to_dict() return encode_canonical(signed_dict).encode("utf-8") except Exception as e: # pylint: disable=broad-except diff --git a/tuf/api/serialization/util.py b/tuf/api/serialization/util.py deleted file mode 100644 index db06e92530..0000000000 --- a/tuf/api/serialization/util.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Utility functions to facilitate TUF metadata de/serialization. - -Currently, this module contains functions to convert between the TUF metadata -class model and a corresponding dictionary representation. - -""" - - -from typing import Any, Dict, Mapping - -from tuf import formats -from tuf.api.metadata import (Signed, Metadata, Root, Timestamp, Snapshot, - Targets) - - -def _get_signed_common_args_from_dict(_dict: Mapping[str, Any]) -> list: - """Returns ordered positional arguments for 'Signed' subclass constructors. - - See '{root, timestamp, snapshot, targets}_from_dict' functions for usage. - - """ - _type = _dict.pop("_type") - version = _dict.pop("version") - spec_version = _dict.pop("spec_version") - expires_str = _dict.pop("expires") - expires = formats.expiry_string_to_datetime(expires_str) - return [_type, version, spec_version, expires] - - -def root_from_dict(_dict: Mapping[str, Any]) -> Root: - """Returns 'Root' object based on its dict representation. """ - common_args = _get_signed_common_args_from_dict(_dict) - consistent_snapshot = _dict.pop("consistent_snapshot") - keys = _dict.pop("keys") - roles = _dict.pop("roles") - return Root(*common_args, consistent_snapshot, keys, roles) - - -def timestamp_from_dict(_dict: Mapping[str, Any]) -> Timestamp: - """Returns 'Timestamp' object based on its dict representation. """ - common_args = _get_signed_common_args_from_dict(_dict) - meta = _dict.pop("meta") - return Timestamp(*common_args, meta) - - -def snapshot_from_dict(_dict: Mapping[str, Any]) -> Snapshot: - """Returns 'Snapshot' object based on its dict representation. """ - common_args = _get_signed_common_args_from_dict(_dict) - meta = _dict.pop("meta") - return Snapshot(*common_args, meta) - - -def targets_from_dict(_dict: Mapping[str, Any]) -> Targets: - """Returns 'Targets' object based on its dict representation. """ - common_args = _get_signed_common_args_from_dict(_dict) - targets = _dict.pop("targets") - delegations = _dict.pop("delegations") - return Targets(*common_args, targets, delegations) - -def signed_from_dict(_dict) -> Signed: - """Returns 'Signed'-subclass object based on its dict representation. """ - # Dispatch to '*_from_dict'-function based on '_type' field. - # TODO: Use if/else cascade, if easier to read! - # TODO: Use constants for types! (e.g. Root._type, Targets._type, etc.) - return { - "root": root_from_dict, - "timestamp": timestamp_from_dict, - "snapshot": snapshot_from_dict, - "targets": targets_from_dict - }[_dict["_type"]](_dict) - - -def metadata_from_dict(_dict: Mapping[str, Any]) -> Metadata: - """Returns 'Metadata' object based on its dict representation. """ - signed_dict = _dict.pop("signed") - signatures = _dict.pop("signatures") - return Metadata(signatures=signatures, - signed=signed_from_dict(signed_dict)) - - -def _get_signed_common_fields_as_dict(obj: Signed) -> Dict[str, Any]: - """Returns dict representation of 'Signed' object. - - See '{root, timestamp, snapshot, targets}_to_dict' functions for usage. - - """ - return { - "_type": obj._type, - "version": obj.version, - "spec_version": obj.spec_version, - "expires": obj.expires.isoformat() + "Z" - } - - -def root_to_dict(obj: Root) -> Dict[str, Any]: - """Returns dict representation of 'Root' object. """ - _dict = _get_signed_common_fields_as_dict(obj) - _dict.update({ - "consistent_snapshot": obj.consistent_snapshot, - "keys": obj.keys, - "roles": obj.roles - }) - return _dict - - -def timestamp_to_dict(obj: Timestamp) -> Dict[str, Any]: - """Returns dict representation of 'Timestamp' object. """ - _dict = _get_signed_common_fields_as_dict(obj) - _dict.update({ - "meta": obj.meta - }) - return _dict - - -def snapshot_to_dict(obj: Snapshot) -> Dict[str, Any]: - """Returns dict representation of 'Snapshot' object. """ - _dict = _get_signed_common_fields_as_dict(obj) - _dict.update({ - "meta": obj.meta - }) - return _dict - - -def targets_to_dict(obj: Targets) -> Dict[str, Any]: - """Returns dict representation of 'Targets' object. """ - _dict = _get_signed_common_fields_as_dict(obj) - _dict.update({ - "targets": obj.targets, - "delegations": obj.delegations, - }) - return _dict - -def signed_to_dict(obj: Signed) -> Dict[str, Any]: - """Returns dict representation of 'Signed'-subclass object. """ - # Dispatch to '*_to_dict'-function based on 'Signed' subclass type. - # TODO: Use if/else cascade, if easier to read! - return { - Root: root_to_dict, - Timestamp: timestamp_to_dict, - Snapshot: snapshot_to_dict, - Targets: targets_to_dict - }[obj.__class__](obj) - -def metadata_to_dict(obj: Metadata) -> Dict[str, Any]: - """Returns dict representation of 'Metadata' object. """ - return { - "signatures": obj.signatures, - "signed": signed_to_dict(obj.signed) - } From 2f57eb8ed7e5ea8c09bd2aa7725daad38cd3f93f Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 4 Mar 2021 16:25:36 +0100 Subject: [PATCH 07/18] Add SPDX style license and copyright boilerplate Signed-off-by: Lukas Puehringer --- tuf/api/metadata.py | 3 +++ tuf/api/serialization/__init__.py | 3 +++ tuf/api/serialization/json.py | 3 +++ 3 files changed, 9 insertions(+) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index cdbe0ad9eb..3f7d89b173 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -1,3 +1,6 @@ +# Copyright New York University and the TUF contributors +# SPDX-License-Identifier: MIT OR Apache-2.0 + """TUF role metadata model. This module provides container classes for TUF role metadata, including methods diff --git a/tuf/api/serialization/__init__.py b/tuf/api/serialization/__init__.py index 0866214633..6089add4ac 100644 --- a/tuf/api/serialization/__init__.py +++ b/tuf/api/serialization/__init__.py @@ -1,3 +1,6 @@ +# Copyright New York University and the TUF contributors +# SPDX-License-Identifier: MIT OR Apache-2.0 + """TUF role metadata de/serialization. This sub-package provides abstract base classes and concrete implementations to diff --git a/tuf/api/serialization/json.py b/tuf/api/serialization/json.py index 602fbf59c0..1faaf6afd8 100644 --- a/tuf/api/serialization/json.py +++ b/tuf/api/serialization/json.py @@ -1,3 +1,6 @@ +# Copyright New York University and the TUF contributors +# SPDX-License-Identifier: MIT OR Apache-2.0 + """TUF role metadata JSON serialization and deserialization. This module provides concrete implementations to serialize and deserialize TUF From 2b4085718bfc662f46dad13bd6fc214074f1b1ad Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 4 Mar 2021 16:31:36 +0100 Subject: [PATCH 08/18] Re-word serialization cyclic import code comments - Try to clarify purpose and remove unimportant TODO note - Use pylint block-level control for shorter lines, see http://pylint.pycqa.org/en/latest/user_guide/message-control.html#block-disables Signed-off-by: Lukas Puehringer --- tuf/api/metadata.py | 24 ++++++++++++------------ tuf/api/serialization/json.py | 5 +++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 3f7d89b173..83d5a79a12 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -128,9 +128,9 @@ def from_file( """ if deserializer is None: - # Function-scope import to avoid circular dependency. Yucky!!! - # TODO: At least move to _get_default_metadata_deserializer helper. - from tuf.api.serialization.json import JSONDeserializer # pylint: disable=import-outside-toplevel + # Use local scope import to avoid circular import errors + # pylint: disable=import-outside-toplevel + from tuf.api.serialization.json import JSONDeserializer deserializer = JSONDeserializer() if storage_backend is None: @@ -171,9 +171,9 @@ def to_file(self, filename: str, serializer: MetadataSerializer = None, """ if serializer is None: - # Function-scope import to avoid circular dependency. Yucky!!! - # TODO: At least move to a _get_default_metadata_serializer helper. - from tuf.api.serialization.json import JSONSerializer # pylint: disable=import-outside-toplevel + # Use local scope import to avoid circular import errors + # pylint: disable=import-outside-toplevel + from tuf.api.serialization.json import JSONSerializer serializer = JSONSerializer(True) # Pass True to compact JSON with tempfile.TemporaryFile() as temp_file: @@ -207,9 +207,9 @@ def sign(self, key: JsonDict, append: bool = False, """ if serializer is None: - # Function-scope import to avoid circular dependency. Yucky!!! - # TODO: At least move to a _get_default_signed_serializer helper. - from tuf.api.serialization.json import CanonicalJSONSerializer # pylint: disable=import-outside-toplevel + # Use local scope import to avoid circular import errors + # pylint: disable=import-outside-toplevel + from tuf.api.serialization.json import CanonicalJSONSerializer serializer = CanonicalJSONSerializer() signature = create_signature(key, serializer.serialize(self.signed)) @@ -259,9 +259,9 @@ def verify(self, key: JsonDict, f'{key["keyid"]}, not sure which one to verify.') if serializer is None: - # Function-scope import to avoid circular dependency. Yucky!!! - # TODO: At least move to a _get_default_signed_serializer helper. - from tuf.api.serialization.json import CanonicalJSONSerializer # pylint: disable=import-outside-toplevel + # Use local scope import to avoid circular import errors + # pylint: disable=import-outside-toplevel + from tuf.api.serialization.json import CanonicalJSONSerializer serializer = CanonicalJSONSerializer() return verify_signature( diff --git a/tuf/api/serialization/json.py b/tuf/api/serialization/json.py index 1faaf6afd8..86a804dc60 100644 --- a/tuf/api/serialization/json.py +++ b/tuf/api/serialization/json.py @@ -15,8 +15,9 @@ from securesystemslib.formats import encode_canonical # pylint: disable=cyclic-import -# ... to allow de/serializing the correct metadata class here, while also -# creating default de/serializers there (see metadata function scope imports). +# ... to allow de/serializing Metadata and Signed objects here, while also +# creating default de/serializers there (see metadata local scope imports). +# NOTE: A less desirable alternative would be to add more abstraction layers. from tuf.api.metadata import Metadata, Signed from tuf.api.serialization import (MetadataSerializer, MetadataDeserializer, From aa8225cb079536c09c497696c8d2fb1321a8f1df Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 5 Mar 2021 11:27:33 +0100 Subject: [PATCH 09/18] Mark kwargs in metadata API methods as Optional Use typing.Optional for optional kwargs that default to None. Signed-off-by: Lukas Puehringer --- tuf/api/metadata.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 83d5a79a12..8af694ce3d 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -104,7 +104,8 @@ class also that has a 'from_dict' factory method. (Currently this is @classmethod def from_file( - cls, filename: str, deserializer: MetadataDeserializer = None, + cls, filename: str, + deserializer: Optional[MetadataDeserializer] = None, storage_backend: Optional[StorageBackendInterface] = None ) -> 'Metadata': """Loads TUF metadata from file storage. @@ -150,8 +151,10 @@ def to_dict(self) -> JsonDict: 'signed': self.signed.to_dict() } - def to_file(self, filename: str, serializer: MetadataSerializer = None, - storage_backend: StorageBackendInterface = None) -> None: + def to_file( + self, filename: str, serializer: Optional[MetadataSerializer] = None, + storage_backend: Optional[StorageBackendInterface] = None + ) -> None: """Writes TUF metadata to file storage. Arguments: @@ -183,7 +186,7 @@ def to_file(self, filename: str, serializer: MetadataSerializer = None, # Signatures. def sign(self, key: JsonDict, append: bool = False, - serializer: SignedSerializer = None) -> JsonDict: + serializer: Optional[SignedSerializer] = None) -> JsonDict: """Creates signature over 'signed' and assigns it to 'signatures'. Arguments: @@ -223,7 +226,7 @@ def sign(self, key: JsonDict, append: bool = False, def verify(self, key: JsonDict, - serializer: SignedSerializer = None) -> bool: + serializer: Optional[SignedSerializer] = None) -> bool: """Verifies 'signatures' over 'signed' that match the passed key by id. Arguments: From d823c8fc01c3e31364ecbf858ef9063198191e99 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 5 Mar 2021 11:46:00 +0100 Subject: [PATCH 10/18] Rename a few variables in tuf.api - Rename _dict to json_dict to avoid wrong semantics of leading underscore. (leading underscore was initially chosen to avoid name shadowing) - Rename 'serializer' argument of type 'SignedSerializer' to 'signed_serializer', to distinguish from 'serializer' argument of type 'MetadataSerializer'. Signed-off-by: Lukas Puehringer --- tuf/api/metadata.py | 25 +++++++++++++------------ tuf/api/serialization/json.py | 4 ++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 8af694ce3d..26ad595565 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -186,7 +186,7 @@ def to_file( # Signatures. def sign(self, key: JsonDict, append: bool = False, - serializer: Optional[SignedSerializer] = None) -> JsonDict: + signed_serializer: Optional[SignedSerializer] = None) -> JsonDict: """Creates signature over 'signed' and assigns it to 'signatures'. Arguments: @@ -194,8 +194,8 @@ def sign(self, key: JsonDict, append: bool = False, append: A boolean indicating if the signature should be appended to the list of signatures or replace any existing signatures. The default behavior is to replace signatures. - serializer: A SignedSerializer subclass instance that implements - the desired canonicalization format. Per default a + signed_serializer: A SignedSerializer subclass instance that + implements the desired canonicalization format. Per default a CanonicalJSONSerializer is used. Raises: @@ -209,13 +209,14 @@ def sign(self, key: JsonDict, append: bool = False, A securesystemslib-style signature object. """ - if serializer is None: + if signed_serializer is None: # Use local scope import to avoid circular import errors # pylint: disable=import-outside-toplevel from tuf.api.serialization.json import CanonicalJSONSerializer - serializer = CanonicalJSONSerializer() + signed_serializer = CanonicalJSONSerializer() - signature = create_signature(key, serializer.serialize(self.signed)) + signature = create_signature(key, + signed_serializer.serialize(self.signed)) if append: self.signatures.append(signature) @@ -226,13 +227,13 @@ def sign(self, key: JsonDict, append: bool = False, def verify(self, key: JsonDict, - serializer: Optional[SignedSerializer] = None) -> bool: + signed_serializer: Optional[SignedSerializer] = None) -> bool: """Verifies 'signatures' over 'signed' that match the passed key by id. Arguments: key: A securesystemslib-style public key object. - serializer: A SignedSerializer subclass instance that implements - the desired canonicalization format. Per default a + signed_serializer: A SignedSerializer subclass instance that + implements the desired canonicalization format. Per default a CanonicalJSONSerializer is used. Raises: @@ -261,15 +262,15 @@ def verify(self, key: JsonDict, f'{len(signatures_for_keyid)} signatures for key ' f'{key["keyid"]}, not sure which one to verify.') - if serializer is None: + if signed_serializer is None: # Use local scope import to avoid circular import errors # pylint: disable=import-outside-toplevel from tuf.api.serialization.json import CanonicalJSONSerializer - serializer = CanonicalJSONSerializer() + signed_serializer = CanonicalJSONSerializer() return verify_signature( key, signatures_for_keyid[0], - serializer.serialize(self.signed)) + signed_serializer.serialize(self.signed)) diff --git a/tuf/api/serialization/json.py b/tuf/api/serialization/json.py index 86a804dc60..52dbeb100b 100644 --- a/tuf/api/serialization/json.py +++ b/tuf/api/serialization/json.py @@ -32,8 +32,8 @@ class JSONDeserializer(MetadataDeserializer): def deserialize(self, raw_data: bytes) -> Metadata: """Deserialize utf-8 encoded JSON bytes into Metadata object. """ try: - _dict = json.loads(raw_data.decode("utf-8")) - return Metadata.from_dict(_dict) + json_dict = json.loads(raw_data.decode("utf-8")) + return Metadata.from_dict(json_dict) except Exception as e: # pylint: disable=broad-except six.raise_from(DeserializationError, e) From aba6ba3f3077ce1a438372a156c23954d7aa87e9 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 5 Mar 2021 12:25:50 +0100 Subject: [PATCH 11/18] Use named argument instead of clarifying comment Signed-off-by: Lukas Puehringer --- tuf/api/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 26ad595565..0673011a5c 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -177,7 +177,7 @@ def to_file( # Use local scope import to avoid circular import errors # pylint: disable=import-outside-toplevel from tuf.api.serialization.json import JSONSerializer - serializer = JSONSerializer(True) # Pass True to compact JSON + serializer = JSONSerializer(compact=True) with tempfile.TemporaryFile() as temp_file: temp_file.write(serializer.serialize(self)) From f8fc5e263b90da4e628dc140dc437c0e2f1f623d Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 5 Mar 2021 12:29:57 +0100 Subject: [PATCH 12/18] Reduce JSON-bias in metadata class model Clarify that the TUF metadata class model is not bound to a JSON wireline format by: - re-wording module, class and method docstrings and code comments to add details about custom and default serialization and the purpose of from/to_dict methods, and - removing the 'JsonDict' type annotation -- instead we use generic Mapping[str, Any] for method arguments and strict Dict[str, Any] as return value as suggested in https://docs.python.org/3/library/typing.html#typing.Dict Signed-off-by: Lukas Puehringer --- tuf/api/metadata.py | 148 ++++++++++++++++++++------------------------ 1 file changed, 67 insertions(+), 81 deletions(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 0673011a5c..1f7998ddef 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -4,13 +4,19 @@ """TUF role metadata model. This module provides container classes for TUF role metadata, including methods -to read/serialize/write from and to file, perform TUF-compliant metadata -updates, and create and verify signatures. +to read and write from and to file, perform TUF-compliant metadata updates, and +create and verify signatures. + +The metadata model supports any custom serialization format, defaulting to JSON +as wireline format and Canonical JSON for reproducible signature creation and +verification. +Custom serializers must implement the abstract serialization interface defined +in 'tuf.api.serialization', and may use the [to|from]_dict convenience methods +available in the class model. """ -# Imports from datetime import datetime, timedelta -from typing import Any, Dict, Optional +from typing import Any, Dict, Mapping, Optional import tempfile @@ -26,24 +32,18 @@ import tuf.exceptions - -# Types -JsonDict = Dict[str, Any] - - -# Classes. class Metadata(): """A container for signed TUF metadata. - Provides methods to (de-)serialize JSON metadata from and to file - storage, and to create and verify signatures. + Provides methods to convert to and from dictionary, read and write to and + from file and to create and verify metadata signatures. Attributes: signed: A subclass of Signed, which has the actual metadata payload, i.e. one of Targets, Snapshot, Timestamp or Root. - signatures: A list of signatures over the canonical JSON representation - of the value of the signed attribute:: + signatures: A list of signatures over the canonical representation of + the value of the signed attribute:: [ { @@ -58,24 +58,20 @@ def __init__(self, signed: 'Signed', signatures: list) -> None: self.signed = signed self.signatures = signatures - - # Deserialization (factories). @classmethod - def from_dict(cls, metadata: JsonDict) -> 'Metadata': - """Creates Metadata object from its JSON/dict representation. - - Calls 'from_dict' for any complex metadata attribute represented by a - class also that has a 'from_dict' factory method. (Currently this is - only the signed attribute.) + def from_dict(cls, metadata: Mapping[str, Any]) -> 'Metadata': + """Creates Metadata object from its dict representation. Arguments: - metadata: TUF metadata in JSON/dict representation, as e.g. - returned by 'json.loads'. + metadata: TUF metadata in dict representation. Raises: KeyError: The metadata dict format is invalid. ValueError: The metadata has an unrecognized signed._type field. + Side Effect: + Destroys the metadata Mapping passed by reference. + Returns: A TUF Metadata object. @@ -96,11 +92,10 @@ class also that has a 'from_dict' factory method. (Currently this is # NOTE: If Signature becomes a class, we should iterate over # metadata['signatures'], call Signature.from_dict for each item, and - # pass a list of Signature objects to the Metadata constructor intead. + # pass a list of Signature objects to the Metadata constructor instead. return cls( - signed=inner_cls.from_dict(metadata['signed']), - signatures=metadata['signatures']) - + signed=inner_cls.from_dict(metadata.pop('signed')), + signatures=metadata.pop('signatures')) @classmethod def from_file( @@ -142,10 +137,8 @@ def from_file( return deserializer.deserialize(raw_data) - - # Serialization. - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ + def to_dict(self) -> Dict[str, Any]: + """Returns the dict representation of self. """ return { 'signatures': self.signatures, 'signed': self.signed.to_dict() @@ -183,10 +176,11 @@ def to_file( temp_file.write(serializer.serialize(self)) persist_temp_file(temp_file, filename, storage_backend) - # Signatures. - def sign(self, key: JsonDict, append: bool = False, - signed_serializer: Optional[SignedSerializer] = None) -> JsonDict: + def sign( + self, key: Mapping[str, Any], append: bool = False, + signed_serializer: Optional[SignedSerializer] = None + ) -> Dict[str, Any]: """Creates signature over 'signed' and assigns it to 'signatures'. Arguments: @@ -225,8 +219,7 @@ def sign(self, key: JsonDict, append: bool = False, return signature - - def verify(self, key: JsonDict, + def verify(self, key: Mapping[str, Any], signed_serializer: Optional[SignedSerializer] = None) -> bool: """Verifies 'signatures' over 'signed' that match the passed key by id. @@ -311,9 +304,8 @@ def __init__( # Deserialization (factories). @classmethod - def from_dict(cls, signed_dict: JsonDict) -> 'Signed': - """Creates Signed object from its JSON/dict representation. """ - + def from_dict(cls, signed_dict: Mapping[str, Any]) -> 'Signed': + """Creates Signed object from its dict representation. """ # Convert 'expires' TUF metadata string to a datetime object, which is # what the constructor expects and what we store. The inverse operation # is implemented in 'to_dict'. @@ -332,8 +324,8 @@ def from_dict(cls, signed_dict: JsonDict) -> 'Signed': return cls(**signed_dict) - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ + def to_dict(self) -> Dict[str, Any]: + """Returns the dict representation of self. """ return { '_type': self._type, 'version': self.version, @@ -394,7 +386,7 @@ class Root(Signed): def __init__( self, _type: str, version: int, spec_version: str, expires: datetime, consistent_snapshot: bool, - keys: JsonDict, roles: JsonDict) -> None: + keys: Mapping[str, Any], roles: Mapping[str, Any]) -> None: super().__init__(_type, version, spec_version, expires) # TODO: Add classes for keys and roles self.consistent_snapshot = consistent_snapshot @@ -402,20 +394,19 @@ def __init__( self.roles = roles - # Serialization. - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ - json_dict = super().to_dict() - json_dict.update({ + def to_dict(self) -> Dict[str, Any]: + """Returns the dict representation of self. """ + root_dict = super().to_dict() + root_dict.update({ 'consistent_snapshot': self.consistent_snapshot, 'keys': self.keys, 'roles': self.roles }) - return json_dict - + return root_dict # Update key for a role. - def add_key(self, role: str, keyid: str, key_metadata: JsonDict) -> None: + def add_key(self, role: str, keyid: str, + key_metadata: Mapping[str, Any]) -> None: """Adds new key for 'role' and updates the key store. """ if keyid not in self.roles[role]['keyids']: self.roles[role]['keyids'].append(keyid) @@ -457,24 +448,22 @@ class Timestamp(Signed): """ def __init__( self, _type: str, version: int, spec_version: str, - expires: datetime, meta: JsonDict) -> None: + expires: datetime, meta: Mapping[str, Any]) -> None: super().__init__(_type, version, spec_version, expires) # TODO: Add class for meta self.meta = meta - - # Serialization. - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ - json_dict = super().to_dict() - json_dict.update({ + def to_dict(self) -> Dict[str, Any]: + """Returns the dict representation of self. """ + timestamp_dict = super().to_dict() + timestamp_dict.update({ 'meta': self.meta }) - return json_dict - + return timestamp_dict # Modification. - def update(self, version: int, length: int, hashes: JsonDict) -> None: + def update(self, version: int, length: int, + hashes: Mapping[str, Any]) -> None: """Assigns passed info about snapshot metadata to meta dict. """ self.meta['snapshot.json'] = { 'version': version, @@ -511,25 +500,23 @@ class Snapshot(Signed): """ def __init__( self, _type: str, version: int, spec_version: str, - expires: datetime, meta: JsonDict) -> None: + expires: datetime, meta: Mapping[str, Any]) -> None: super().__init__(_type, version, spec_version, expires) # TODO: Add class for meta self.meta = meta - # Serialization. - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ - json_dict = super().to_dict() - json_dict.update({ + def to_dict(self) -> Dict[str, Any]: + """Returns the dict representation of self. """ + snapshot_dict = super().to_dict() + snapshot_dict.update({ 'meta': self.meta }) - return json_dict - + return snapshot_dict # Modification. def update( self, rolename: str, version: int, length: Optional[int] = None, - hashes: Optional[JsonDict] = None) -> None: + hashes: Optional[Mapping[str, Any]] = None) -> None: """Assigns passed (delegated) targets role info to meta dict. """ metadata_fn = f'{rolename}.json' @@ -599,26 +586,25 @@ class Targets(Signed): # default max-args value for pylint is 5 # pylint: disable=too-many-arguments def __init__( - self, _type: str, version: int, spec_version: str, - expires: datetime, targets: JsonDict, delegations: JsonDict - ) -> None: + self, _type: str, version: int, spec_version: str, + expires: datetime, targets: Mapping[str, Any], + delegations: Mapping[str, Any] + ) -> None: super().__init__(_type, version, spec_version, expires) # TODO: Add class for meta self.targets = targets self.delegations = delegations - - # Serialization. - def to_dict(self) -> JsonDict: - """Returns the JSON-serializable dictionary representation of self. """ - json_dict = super().to_dict() - json_dict.update({ + def to_dict(self) -> Dict[str, Any]: + """Returns the dict representation of self. """ + targets_dict = super().to_dict() + targets_dict.update({ 'targets': self.targets, 'delegations': self.delegations, }) - return json_dict + return targets_dict # Modification. - def update(self, filename: str, fileinfo: JsonDict) -> None: + def update(self, filename: str, fileinfo: Mapping[str, Any]) -> None: """Assigns passed target file info to meta dict. """ self.targets[filename] = fileinfo From ace25e4ad3d731a66171f7b9e6978384e4629efb Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 5 Mar 2021 12:46:28 +0100 Subject: [PATCH 13/18] Demystify from/to_dict methods in Signed baseclass Prior to this commit the (abstract) 'Signed' base class implemented from/to_dict methods, to be used by any subclass in addition to or instead of a custom from/to_dict method. The design led to some confusion, especially in 'Signed.from_dict' factories, which instantiated subclass objects when called on a subclass, which didn't implement its own 'from_dict' method. This commit demystifies the design, by implementing from/to_dict on all 'Signed' subclasses, and moving common from/to_dict tasks to helper functions in the 'Signed' class. The newly gained clarity and explicitness comes at the cost of slightly more lines of code. Signed-off-by: Lukas Puehringer --- tuf/api/metadata.py | 78 +++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 1f7998ddef..2b183ed32e 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -301,31 +301,31 @@ def __init__( raise ValueError(f'version must be < 0, got {version}') self.version = version + @staticmethod + def _common_fields_from_dict(signed_dict: Mapping[str, Any]) -> list: + """Returns common fields of 'Signed' instances from the passed dict + representation, and returns an ordered list to be passed as leading + positional arguments to a subclass constructor. - # Deserialization (factories). - @classmethod - def from_dict(cls, signed_dict: Mapping[str, Any]) -> 'Signed': - """Creates Signed object from its dict representation. """ + See '{Root, Timestamp, Snapshot, Targets}.from_dict' methods for usage. + + """ + _type = signed_dict.pop('_type') + version = signed_dict.pop('version') + spec_version = signed_dict.pop('spec_version') + expires_str = signed_dict.pop('expires') # Convert 'expires' TUF metadata string to a datetime object, which is # what the constructor expects and what we store. The inverse operation - # is implemented in 'to_dict'. - signed_dict['expires'] = tuf.formats.expiry_string_to_datetime( - signed_dict['expires']) - # NOTE: We write the converted 'expires' back into 'signed_dict' above - # so that we can pass it to the constructor as '**signed_dict' below, - # along with other fields that belong to Signed subclasses. - # Any 'from_dict'(-like) conversions of fields that correspond to a - # subclass should be performed in the 'from_dict' method of that - # subclass and also be written back into 'signed_dict' before calling - # super().from_dict. - - # NOTE: cls might be a subclass of Signed, if 'from_dict' was called on - # that subclass (see e.g. Metadata.from_dict). - return cls(**signed_dict) + # is implemented in '_common_fields_to_dict'. + expires = tuf.formats.expiry_string_to_datetime(expires_str) + return [_type, version, spec_version, expires] + def _common_fields_to_dict(self) -> Dict[str, Any]: + """Returns dict representation of common fields of 'Signed' instances. - def to_dict(self) -> Dict[str, Any]: - """Returns the dict representation of self. """ + See '{Root, Timestamp, Snapshot, Targets}.to_dict' methods for usage. + + """ return { '_type': self._type, 'version': self.version, @@ -393,10 +393,18 @@ def __init__( self.keys = keys self.roles = roles + @classmethod + def from_dict(cls, root_dict: Mapping[str, Any]) -> 'Root': + """Creates Root object from its dict representation. """ + common_args = super()._common_fields_from_dict(root_dict) + 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) def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """ - root_dict = super().to_dict() + root_dict = super()._common_fields_to_dict() root_dict.update({ 'consistent_snapshot': self.consistent_snapshot, 'keys': self.keys, @@ -453,9 +461,16 @@ def __init__( # TODO: Add class for meta self.meta = meta + @classmethod + def from_dict(cls, timestamp_dict: Mapping[str, Any]) -> 'Timestamp': + """Creates Timestamp object from its dict representation. """ + common_args = super()._common_fields_from_dict(timestamp_dict) + meta = timestamp_dict.pop('meta') + return cls(*common_args, meta) + def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """ - timestamp_dict = super().to_dict() + timestamp_dict = super()._common_fields_to_dict() timestamp_dict.update({ 'meta': self.meta }) @@ -505,9 +520,16 @@ def __init__( # TODO: Add class for meta self.meta = meta + @classmethod + def from_dict(cls, snapshot_dict: Mapping[str, Any]) -> 'Snapshot': + """Creates Snapshot object from its dict representation. """ + common_args = super()._common_fields_from_dict(snapshot_dict) + meta = snapshot_dict.pop('meta') + return cls(*common_args, meta) + def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """ - snapshot_dict = super().to_dict() + snapshot_dict = super()._common_fields_to_dict() snapshot_dict.update({ 'meta': self.meta }) @@ -595,9 +617,17 @@ def __init__( self.targets = targets self.delegations = delegations + @classmethod + def from_dict(cls, targets_dict: Mapping[str, Any]) -> 'Targets': + """Creates Targets object from its dict representation. """ + common_args = super()._common_fields_from_dict(targets_dict) + targets = targets_dict.pop('targets') + delegations = targets_dict.pop('delegations') + return cls(*common_args, targets, delegations) + def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """ - targets_dict = super().to_dict() + targets_dict = super()._common_fields_to_dict() targets_dict.update({ 'targets': self.targets, 'delegations': self.delegations, From 326d2af7c40a83c2cc3731a60a33893078547ed4 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 5 Mar 2021 12:33:24 +0100 Subject: [PATCH 14/18] Fix blank lines in tuf.api as per styleguide https://github.com/google/styleguide/blob/gh-pages/pyguide.md#35-blank-lines Signed-off-by: Lukas Puehringer --- tuf/api/metadata.py | 8 -------- tuf/api/serialization/__init__.py | 2 ++ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 2b183ed32e..1bd2535487 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -266,7 +266,6 @@ def verify(self, key: Mapping[str, Any], signed_serializer.serialize(self.signed)) - class Signed: """A base class for the signed part of TUF metadata. @@ -281,12 +280,10 @@ class Signed: metadata format adheres to. expires: The metadata expiration datetime object. - """ # NOTE: Signed is a stupid name, because this might not be signed yet, but # 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) -> None: @@ -333,13 +330,11 @@ def _common_fields_to_dict(self) -> Dict[str, Any]: 'expires': self.expires.isoformat() + 'Z' } - # Modification. def bump_expiration(self, delta: timedelta = timedelta(days=1)) -> None: """Increments the expires attribute by the passed timedelta. """ self.expires += delta - def bump_version(self) -> None: """Increments the metadata version number by 1.""" self.version += 1 @@ -420,7 +415,6 @@ def add_key(self, role: str, keyid: str, self.roles[role]['keyids'].append(keyid) self.keys[keyid] = key_metadata - # Remove key for a role. def remove_key(self, role: str, keyid: str) -> None: """Removes key for 'role' and updates the key store. """ @@ -433,8 +427,6 @@ def remove_key(self, role: str, keyid: str) -> None: del self.keys[keyid] - - class Timestamp(Signed): """A container for the signed part of timestamp metadata. diff --git a/tuf/api/serialization/__init__.py b/tuf/api/serialization/__init__.py index 6089add4ac..d7b473b2c4 100644 --- a/tuf/api/serialization/__init__.py +++ b/tuf/api/serialization/__init__.py @@ -16,10 +16,12 @@ """ import abc + # TODO: Should these be in tuf.exceptions or inherit from tuf.exceptions.Error? class SerializationError(Exception): """Error during serialization. """ + class DeserializationError(Exception): """Error during deserialization. """ From ab92ba257f466159b1becbc06effabb14d867553 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 5 Mar 2021 12:54:31 +0100 Subject: [PATCH 15/18] Fix inconsistent returns in json serializers Signed-off-by: Lukas Puehringer --- tuf/api/serialization/json.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tuf/api/serialization/json.py b/tuf/api/serialization/json.py index 52dbeb100b..600b73d2a5 100644 --- a/tuf/api/serialization/json.py +++ b/tuf/api/serialization/json.py @@ -33,11 +33,13 @@ def deserialize(self, raw_data: bytes) -> Metadata: """Deserialize utf-8 encoded JSON bytes into Metadata object. """ try: json_dict = json.loads(raw_data.decode("utf-8")) - return Metadata.from_dict(json_dict) + metadata_obj = Metadata.from_dict(json_dict) except Exception as e: # pylint: disable=broad-except six.raise_from(DeserializationError, e) + return metadata_obj + class JSONSerializer(MetadataSerializer): """A Metadata-to-JSON serialize method. @@ -54,15 +56,17 @@ def serialize(self, metadata_obj: Metadata) -> bytes: """Serialize Metadata object into utf-8 encoded JSON bytes. """ try: indent = (None if self.compact else 1) - separators=((',', ':') if self.compact else (',', ': ')) - return json.dumps(metadata_obj.to_dict(), - indent=indent, - separators=separators, - sort_keys=True).encode("utf-8") + separators = ((',', ':') if self.compact else (',', ': ')) + json_bytes = json.dumps(metadata_obj.to_dict(), + indent=indent, + separators=separators, + sort_keys=True).encode("utf-8") except Exception as e: # pylint: disable=broad-except six.raise_from(SerializationError, e) + return json_bytes + class CanonicalJSONSerializer(SignedSerializer): """A Signed-to-Canonical JSON 'serialize' method. """ @@ -71,7 +75,9 @@ def serialize(self, signed_obj: Signed) -> bytes: """Serialize Signed object into utf-8 encoded Canonical JSON bytes. """ try: signed_dict = signed_obj.to_dict() - return encode_canonical(signed_dict).encode("utf-8") + canonical_bytes = encode_canonical(signed_dict).encode("utf-8") except Exception as e: # pylint: disable=broad-except six.raise_from(SerializationError, e) + + return canonical_bytes From bd94f6d8d140f117f7e5f79af5c892423d70e18e Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 9 Mar 2021 17:27:39 +0100 Subject: [PATCH 16/18] Remove py2 compat from api.serialization package tuf.api is not designed for Python 2 compatibility. This commit removes the following stray compatibility constructs in its serialization subpackage: - '__metaclass__ = abc.ABCMeta' - six.raise_from Signed-off-by: Lukas Puehringer --- tuf/api/serialization/__init__.py | 9 +++------ tuf/api/serialization/json.py | 7 +++---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/tuf/api/serialization/__init__.py b/tuf/api/serialization/__init__.py index d7b473b2c4..ed3191a103 100644 --- a/tuf/api/serialization/__init__.py +++ b/tuf/api/serialization/__init__.py @@ -26,9 +26,8 @@ class DeserializationError(Exception): """Error during deserialization. """ -class MetadataDeserializer(): +class MetadataDeserializer(metaclass=abc.ABCMeta): """Abstract base class for deserialization of Metadata objects. """ - __metaclass__ = abc.ABCMeta @abc.abstractmethod def deserialize(self, raw_data: bytes) -> "Metadata": @@ -36,9 +35,8 @@ def deserialize(self, raw_data: bytes) -> "Metadata": raise NotImplementedError -class MetadataSerializer(): +class MetadataSerializer(metaclass=abc.ABCMeta): """Abstract base class for serialization of Metadata objects. """ - __metaclass__ = abc.ABCMeta @abc.abstractmethod def serialize(self, metadata_obj: "Metadata") -> bytes: @@ -46,9 +44,8 @@ def serialize(self, metadata_obj: "Metadata") -> bytes: raise NotImplementedError -class SignedSerializer(): +class SignedSerializer(metaclass=abc.ABCMeta): """Abstract base class for serialization of Signed objects. """ - __metaclass__ = abc.ABCMeta @abc.abstractmethod def serialize(self, signed_obj: "Signed") -> bytes: diff --git a/tuf/api/serialization/json.py b/tuf/api/serialization/json.py index 600b73d2a5..85d3c63ff1 100644 --- a/tuf/api/serialization/json.py +++ b/tuf/api/serialization/json.py @@ -10,7 +10,6 @@ """ import json -import six from securesystemslib.formats import encode_canonical @@ -36,7 +35,7 @@ def deserialize(self, raw_data: bytes) -> Metadata: metadata_obj = Metadata.from_dict(json_dict) except Exception as e: # pylint: disable=broad-except - six.raise_from(DeserializationError, e) + raise DeserializationError from e return metadata_obj @@ -63,7 +62,7 @@ def serialize(self, metadata_obj: Metadata) -> bytes: sort_keys=True).encode("utf-8") except Exception as e: # pylint: disable=broad-except - six.raise_from(SerializationError, e) + raise SerializationError from e return json_bytes @@ -78,6 +77,6 @@ def serialize(self, signed_obj: Signed) -> bytes: canonical_bytes = encode_canonical(signed_dict).encode("utf-8") except Exception as e: # pylint: disable=broad-except - six.raise_from(SerializationError, e) + raise SerializationError from e return canonical_bytes From a53d68b91d54cae3cfaa099e18a46503d76a3814 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 9 Mar 2021 17:35:08 +0100 Subject: [PATCH 17/18] Re-word api.serializer.json docstrings - Make class docstrings wording consistent. - Emphasize that we use the OLPC Canonical JSON specification. Signed-off-by: Lukas Puehringer --- tuf/api/serialization/json.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tuf/api/serialization/json.py b/tuf/api/serialization/json.py index 85d3c63ff1..215a0ad790 100644 --- a/tuf/api/serialization/json.py +++ b/tuf/api/serialization/json.py @@ -26,7 +26,7 @@ class JSONDeserializer(MetadataDeserializer): - """Provides JSON-to-Metadata deserialize method. """ + """Provides JSON to Metadata deserialize method. """ def deserialize(self, raw_data: bytes) -> Metadata: """Deserialize utf-8 encoded JSON bytes into Metadata object. """ @@ -41,7 +41,7 @@ def deserialize(self, raw_data: bytes) -> Metadata: class JSONSerializer(MetadataSerializer): - """A Metadata-to-JSON serialize method. + """Provides Metadata to JSON serialize method. Attributes: compact: A boolean indicating if the JSON bytes generated in @@ -68,10 +68,12 @@ def serialize(self, metadata_obj: Metadata) -> bytes: class CanonicalJSONSerializer(SignedSerializer): - """A Signed-to-Canonical JSON 'serialize' method. """ + """Provides Signed to OLPC Canonical JSON serialize method. """ def serialize(self, signed_obj: Signed) -> bytes: - """Serialize Signed object into utf-8 encoded Canonical JSON bytes. """ + """Serialize Signed object into utf-8 encoded OLPC Canonical JSON + bytes. + """ try: signed_dict = signed_obj.to_dict() canonical_bytes = encode_canonical(signed_dict).encode("utf-8") From ef91964db0260238a77d3a9cf89ab73ee40e53a0 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Tue, 9 Mar 2021 17:47:45 +0100 Subject: [PATCH 18/18] Call mixin-style parent methods on cls/self Call an instance method and a static method that are only defined in a parent class from child instances using self (instance) and cls (static) instead of super(). While this doesn't make a practical difference, the new syntax is probably less confusing to the reader. Signed-off-by: Lukas Puehringer --- tuf/api/metadata.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 1bd2535487..1209577457 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -391,7 +391,7 @@ def __init__( @classmethod def from_dict(cls, root_dict: Mapping[str, Any]) -> 'Root': """Creates Root object from its dict representation. """ - common_args = super()._common_fields_from_dict(root_dict) + common_args = cls._common_fields_from_dict(root_dict) consistent_snapshot = root_dict.pop('consistent_snapshot') keys = root_dict.pop('keys') roles = root_dict.pop('roles') @@ -399,7 +399,7 @@ def from_dict(cls, root_dict: Mapping[str, Any]) -> 'Root': def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """ - root_dict = super()._common_fields_to_dict() + root_dict = self._common_fields_to_dict() root_dict.update({ 'consistent_snapshot': self.consistent_snapshot, 'keys': self.keys, @@ -456,13 +456,13 @@ def __init__( @classmethod def from_dict(cls, timestamp_dict: Mapping[str, Any]) -> 'Timestamp': """Creates Timestamp object from its dict representation. """ - common_args = super()._common_fields_from_dict(timestamp_dict) + common_args = cls._common_fields_from_dict(timestamp_dict) meta = timestamp_dict.pop('meta') return cls(*common_args, meta) def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """ - timestamp_dict = super()._common_fields_to_dict() + timestamp_dict = self._common_fields_to_dict() timestamp_dict.update({ 'meta': self.meta }) @@ -515,13 +515,13 @@ def __init__( @classmethod def from_dict(cls, snapshot_dict: Mapping[str, Any]) -> 'Snapshot': """Creates Snapshot object from its dict representation. """ - common_args = super()._common_fields_from_dict(snapshot_dict) + common_args = cls._common_fields_from_dict(snapshot_dict) meta = snapshot_dict.pop('meta') return cls(*common_args, meta) def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """ - snapshot_dict = super()._common_fields_to_dict() + snapshot_dict = self._common_fields_to_dict() snapshot_dict.update({ 'meta': self.meta }) @@ -612,14 +612,14 @@ def __init__( @classmethod def from_dict(cls, targets_dict: Mapping[str, Any]) -> 'Targets': """Creates Targets object from its dict representation. """ - common_args = super()._common_fields_from_dict(targets_dict) + 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) def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self. """ - targets_dict = super()._common_fields_to_dict() + targets_dict = self._common_fields_to_dict() targets_dict.update({ 'targets': self.targets, 'delegations': self.delegations,