From c332a848303feb8060b23ab373d00cfc40567b33 Mon Sep 17 00:00:00 2001 From: Martin Vrachev Date: Mon, 14 Jun 2021 14:13:15 +0300 Subject: [PATCH] Metadata API: change meta type in Timestamp In Timestamp, the only valid "meta" value is the dictionary representing meta information for the snapshot file. This makes the API unnecessarily complicated and requires validation that only information about snapshot is available inside "meta". Together with the python-tuf maintainers, we decided that snapshot meta information will not be represented by a "meta" dictionary but instead by a MetaFile instance and with this it will diverge from the specification. Additionally, to prevent confusion, I will rename the "meta" attribute to "snapshot" as this attribute will be related only to meta information about snapshot. This decision is coherent with ADR9 and the rationale behind it is to provide easier, safer, and direct access to the snapshot meta information. Signed-off-by: Martin Vrachev --- tests/repository_simulator.py | 6 ++--- tests/test_api.py | 6 ++--- tests/test_trusted_metadata_set.py | 14 ++++++------ tuf/api/metadata.py | 22 +++++++++---------- .../_internal/trusted_metadata_set.py | 21 ++++++++---------- tuf/ngclient/updater.py | 6 ++--- 6 files changed, 35 insertions(+), 40 deletions(-) diff --git a/tests/repository_simulator.py b/tests/repository_simulator.py index 67329bde72..81a3e60adb 100644 --- a/tests/repository_simulator.py +++ b/tests/repository_simulator.py @@ -100,8 +100,8 @@ def _initialize(self): snapshot = Snapshot(1, SPEC_VER, expiry, meta) self.md_snapshot = Metadata(snapshot, OrderedDict()) - meta = {"snapshot.json": MetaFile(snapshot.version)} - timestamp = Timestamp(1, SPEC_VER, expiry, meta) + snapshot_meta = MetaFile(snapshot.version) + timestamp = Timestamp(1, SPEC_VER, expiry, snapshot_meta) self.md_timestamp = Metadata(timestamp, OrderedDict()) root = Root(1, SPEC_VER, expiry, {}, {}, True) @@ -175,7 +175,7 @@ def _fetch_metadata(self, role: str, version: Optional[int] = None) -> bytes: return md.to_bytes(JSONSerializer()) def update_timestamp(self): - self.timestamp.meta["snapshot.json"].version = self.snapshot.version + self.timestamp.snapshot.version = self.snapshot.version self.timestamp.version += 1 diff --git a/tests/test_api.py b/tests/test_api.py index 76b8ba45fe..758dedc811 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -350,11 +350,11 @@ def test_metadata_timestamp(self): fileinfo = MetaFile(2, 520, hashes) self.assertNotEqual( - timestamp.signed.meta['snapshot.json'].to_dict(), fileinfo.to_dict() + timestamp.signed.snapshot.to_dict(), fileinfo.to_dict() ) timestamp.signed.update(fileinfo) self.assertEqual( - timestamp.signed.meta['snapshot.json'].to_dict(), fileinfo.to_dict() + timestamp.signed.snapshot.to_dict(), fileinfo.to_dict() ) @@ -563,7 +563,7 @@ def test_length_and_hash_validation(self): timestamp_path = os.path.join( self.repo_dir, 'metadata', 'timestamp.json') timestamp = Metadata[Timestamp].from_file(timestamp_path) - snapshot_metafile = timestamp.signed.meta["snapshot.json"] + snapshot_metafile = timestamp.signed.snapshot snapshot_path = os.path.join( self.repo_dir, 'metadata', 'snapshot.json') diff --git a/tests/test_trusted_metadata_set.py b/tests/test_trusted_metadata_set.py index b097e350a1..f0fbf85304 100644 --- a/tests/test_trusted_metadata_set.py +++ b/tests/test_trusted_metadata_set.py @@ -70,8 +70,8 @@ def setUpClass(cls): cls.keystore[role] = SSlibSigner(key_dict) def hashes_length_modifier(timestamp: Timestamp) -> None: - timestamp.meta["snapshot.json"].hashes = None - timestamp.meta["snapshot.json"].length = None + timestamp.snapshot.hashes = None + timestamp.snapshot.length = None cls.metadata["timestamp"] = cls.modify_metadata( cls, "timestamp", hashes_length_modifier @@ -245,13 +245,13 @@ def version_modifier(timestamp: Timestamp) -> None: def test_update_timestamp_snapshot_ver_below_current(self): def bump_snapshot_version(timestamp: Timestamp) -> None: - timestamp.meta["snapshot.json"].version = 2 + timestamp.snapshot.version = 2 # set current known snapshot.json version to 2 timestamp = self.modify_metadata("timestamp", bump_snapshot_version) self.trusted_set.update_timestamp(timestamp) - # newtimestamp.meta["snapshot.json"].version < trusted_timestamp.meta["snapshot.json"].version + # newtimestamp.meta.version < trusted_timestamp.meta.version with self.assertRaises(exceptions.ReplayedMetadataError): self.trusted_set.update_timestamp(self.metadata["timestamp"]) @@ -271,7 +271,7 @@ def timestamp_expired_modifier(timestamp: Timestamp) -> None: def test_update_snapshot_length_or_hash_mismatch(self): def modify_snapshot_length(timestamp: Timestamp) -> None: - timestamp.meta["snapshot.json"].length = 1 + timestamp.snapshot.length = 1 # set known snapshot.json length to 1 timestamp = self.modify_metadata("timestamp", modify_snapshot_length) @@ -289,7 +289,7 @@ def test_update_snapshot_cannot_verify_snapshot_with_threshold(self): def test_update_snapshot_version_different_timestamp_snapshot_version(self): def timestamp_version_modifier(timestamp: Timestamp) -> None: - timestamp.meta["snapshot.json"].version = 2 + timestamp.snapshot.version = 2 timestamp = self.modify_metadata("timestamp", timestamp_version_modifier) self.trusted_set.update_timestamp(timestamp) @@ -341,7 +341,7 @@ def snapshot_expired_modifier(snapshot: Snapshot) -> None: def test_update_snapshot_successful_rollback_checks(self): def meta_version_bump(timestamp: Timestamp) -> None: - timestamp.meta["snapshot.json"].version += 1 + timestamp.snapshot.version += 1 def version_bump(snapshot: Snapshot) -> None: snapshot.version += 1 diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index cca8247147..ff3704d87a 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -904,11 +904,11 @@ def verify_length_and_hashes(self, data: Union[bytes, IO[bytes]]) -> None: class Timestamp(Signed): """A container for the signed part of timestamp metadata. - Timestamp contains information about the snapshot Metadata file. + TUF file format uses a dictionary to contain the snapshot information: + this is not the case with Timestamp.snapshot which is a MetaFile. Attributes: - meta: A dictionary of filenames to MetaFiles. The only valid key value - is the snapshot filename, as defined by the specification. + snapshot: MetaFile instance with the snapshot meta information. """ _signed_type = "timestamp" @@ -918,33 +918,31 @@ def __init__( version: int, spec_version: str, expires: datetime, - meta: Dict[str, MetaFile], + snapshot: MetaFile, unrecognized_fields: Optional[Mapping[str, Any]] = None, ) -> None: super().__init__(version, spec_version, expires, unrecognized_fields) - self.meta = meta + self.snapshot = snapshot @classmethod def from_dict(cls, signed_dict: Dict[str, Any]) -> "Timestamp": """Creates Timestamp object from its dict representation.""" common_args = cls._common_fields_from_dict(signed_dict) meta_dict = signed_dict.pop("meta") - meta = {"snapshot.json": MetaFile.from_dict(meta_dict["snapshot.json"])} + snapshot_meta = MetaFile.from_dict(meta_dict["snapshot.json"]) # All fields left in the timestamp_dict are unrecognized. - return cls(*common_args, meta, signed_dict) + return cls(*common_args, snapshot_meta, signed_dict) def to_dict(self) -> Dict[str, Any]: """Returns the dict representation of self.""" res_dict = self._common_fields_to_dict() - res_dict["meta"] = { - "snapshot.json": self.meta["snapshot.json"].to_dict() - } + res_dict["meta"] = {"snapshot.json": self.snapshot.to_dict()} return res_dict # Modification. def update(self, snapshot_meta: MetaFile) -> None: - """Assigns passed info about snapshot metadata to meta dict.""" - self.meta["snapshot.json"] = snapshot_meta + """Assigns passed info about snapshot metadata.""" + self.snapshot = snapshot_meta class Snapshot(Signed): diff --git a/tuf/ngclient/_internal/trusted_metadata_set.py b/tuf/ngclient/_internal/trusted_metadata_set.py index cef3d6fa1e..1c89f1fd47 100644 --- a/tuf/ngclient/_internal/trusted_metadata_set.py +++ b/tuf/ngclient/_internal/trusted_metadata_set.py @@ -233,13 +233,13 @@ def update_timestamp(self, data: bytes) -> None: ) # Prevent rolling back snapshot version if ( - new_timestamp.signed.meta["snapshot.json"].version - < self.timestamp.signed.meta["snapshot.json"].version + new_timestamp.signed.snapshot.version + < self.timestamp.signed.snapshot.version ): raise exceptions.ReplayedMetadataError( "snapshot", - new_timestamp.signed.meta["snapshot.json"].version, - self.timestamp.signed.meta["snapshot.json"].version, + new_timestamp.signed.snapshot.version, + self.timestamp.signed.snapshot.version, ) # expiry not checked to allow old timestamp to be used for rollback @@ -286,11 +286,11 @@ def update_snapshot(self, data: bytes) -> None: # Snapshot cannot be loaded if final timestamp is expired self._check_final_timestamp() - meta = self.timestamp.signed.meta["snapshot.json"] + snapshot_meta = self.timestamp.signed.snapshot # Verify against the hashes in timestamp, if any try: - meta.verify_length_and_hashes(data) + snapshot_meta.verify_length_and_hashes(data) except exceptions.LengthOrHashMismatchError as e: raise exceptions.RepositoryError( "Snapshot length or hashes do not match" @@ -345,14 +345,11 @@ def _check_final_snapshot(self) -> None: assert self.timestamp is not None # nosec if self.snapshot.signed.is_expired(self.reference_time): raise exceptions.ExpiredMetadataError("snapshot.json is expired") - - if ( - self.snapshot.signed.version - != self.timestamp.signed.meta["snapshot.json"].version - ): + snapshot_meta = self.timestamp.signed.snapshot + if self.snapshot.signed.version != snapshot_meta.version: raise exceptions.BadVersionNumberError( f"Expected snapshot version " - f"{self.timestamp.signed.meta['snapshot.json'].version}, " + f"{snapshot_meta.version}, " f"got {self.snapshot.signed.version}" ) diff --git a/tuf/ngclient/updater.py b/tuf/ngclient/updater.py index 68db18874a..4ea7fdb838 100644 --- a/tuf/ngclient/updater.py +++ b/tuf/ngclient/updater.py @@ -352,11 +352,11 @@ def _load_snapshot(self) -> None: logger.debug("Local snapshot not valid as final: %s", e) assert self._trusted_set.timestamp is not None # nosec - metainfo = self._trusted_set.timestamp.signed.meta["snapshot.json"] - length = metainfo.length or self.config.snapshot_max_length + snapshot_meta = self._trusted_set.timestamp.signed.snapshot + length = snapshot_meta.length or self.config.snapshot_max_length version = None if self._trusted_set.root.signed.consistent_snapshot: - version = metainfo.version + version = snapshot_meta.version data = self._download_metadata("snapshot", length, version) self._trusted_set.update_snapshot(data)