Skip to content

Commit

Permalink
Metadata API: change meta type in Timestamp
Browse files Browse the repository at this point in the history
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_meta" 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 <mvrachev@vmware.com>
  • Loading branch information
MVrachev committed Sep 17, 2021
1 parent 6a5b642 commit af5090e
Show file tree
Hide file tree
Showing 6 changed files with 35 additions and 41 deletions.
6 changes: 3 additions & 3 deletions tests/repository_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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_meta.version = self.snapshot.version

self.timestamp.version += 1

Expand Down
6 changes: 3 additions & 3 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_meta.to_dict(), fileinfo.to_dict()
)
timestamp.signed.update(fileinfo)
self.assertEqual(
timestamp.signed.meta['snapshot.json'].to_dict(), fileinfo.to_dict()
timestamp.signed.snapshot_meta.to_dict(), fileinfo.to_dict()
)


Expand Down Expand Up @@ -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_meta

snapshot_path = os.path.join(
self.repo_dir, 'metadata', 'snapshot.json')
Expand Down
14 changes: 7 additions & 7 deletions tests/test_trusted_metadata_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_meta.hashes = None
timestamp.snapshot_meta.length = None

cls.metadata["timestamp"] = cls.modify_metadata(
cls, "timestamp", hashes_length_modifier
Expand Down Expand Up @@ -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_meta.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"])

Expand All @@ -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_meta.length = 1

# set known snapshot.json length to 1
timestamp = self.modify_metadata("timestamp", modify_snapshot_length)
Expand All @@ -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_meta.version = 2

timestamp = self.modify_metadata("timestamp", timestamp_version_modifier)
self.trusted_set.update_timestamp(timestamp)
Expand Down Expand Up @@ -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_meta.version += 1

def version_bump(snapshot: Snapshot) -> None:
snapshot.version += 1
Expand Down
22 changes: 10 additions & 12 deletions tuf/api/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_meta 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_meta: MetaFile instance with the snapshot meta information.
"""

_signed_type = "timestamp"
Expand All @@ -918,33 +918,31 @@ def __init__(
version: int,
spec_version: str,
expires: datetime,
meta: Dict[str, MetaFile],
snapshot_meta: MetaFile,
unrecognized_fields: Optional[Mapping[str, Any]] = None,
) -> None:
super().__init__(version, spec_version, expires, unrecognized_fields)
self.meta = meta
self.snapshot_meta = snapshot_meta

@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_meta.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_meta = snapshot_meta


class Snapshot(Signed):
Expand Down
22 changes: 9 additions & 13 deletions tuf/ngclient/_internal/trusted_metadata_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_meta.version
< self.timestamp.signed.snapshot_meta.version
):
raise exceptions.ReplayedMetadataError(
"snapshot",
new_timestamp.signed.meta["snapshot.json"].version,
self.timestamp.signed.meta["snapshot.json"].version,
new_timestamp.signed.snapshot_meta.version,
self.timestamp.signed.snapshot_meta.version,
)

# expiry not checked to allow old timestamp to be used for rollback
Expand Down Expand Up @@ -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_meta

# 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"
Expand Down Expand Up @@ -345,14 +345,10 @@ 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_meta
if self.snapshot.signed.version != snapshot_meta.version:
raise exceptions.BadVersionNumberError(
f"Expected snapshot version "
f"{self.timestamp.signed.meta['snapshot.json'].version}, "
f"Expected snapshot version {snapshot_meta.version}, "
f"got {self.snapshot.signed.version}"
)

Expand Down
6 changes: 3 additions & 3 deletions tuf/ngclient/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_meta
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)
Expand Down

0 comments on commit af5090e

Please sign in to comment.