Skip to content

Commit

Permalink
Trigger rollback check even without local snapshot
Browse files Browse the repository at this point in the history
If you do the following steps:
1. local root is loaded, root is updated until final
2. local timestamp is loaded
3. timestamp is updated (meta (version, hashes, length) for snapshot
changes here)
4. local snapshot is loaded (hashes and length do not match the data in
timestamp, preventing loading) remote snapshot is loaded
5. remote snapshot is loaded
then when executing step 5 the rollback checks will not be done because
the old snapshot was not loaded.

Similar issues for version numbers and expiry are being fixed by
delaying the checks until we know we have the "final" snapshot.
We don't want to do the exact same thing here as the hashes are meant
to prevent even parsing data we don't trust...
This seems to be a case where "trusted local metadata" and
"untrusted data from the network" is the deciding factor.

TrustedMetadataSet does not have this information currently so possibly
the best solution is to add a new argument "trusted" to update_snapshot.
Even though this is ugly as the rest of the update functions doesn't
have such an argument, it seems the best solution as it seems to work
in all cases:
- when loading a local snapshot, we know the data has at some point been
trusted (signatures have been checked): it doesn't need to match hashes
now
- if there is no local snapshot and we're updating from remote, the
remote data must match meta hashes in timestamp
- f there is a local snapshot and we're updating from remote, the remote
data must match meta hashes in timestamp

Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
  • Loading branch information
MVrachev committed Oct 5, 2021
1 parent bb1ed9f commit 81b0b5b
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 12 deletions.
31 changes: 29 additions & 2 deletions tests/test_updater_with_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@
"""Test ngclient Updater using the repository simulator
"""

import logging
import os
import sys
import tempfile
from typing import Optional
from tuf.exceptions import UnsignedMetadataError
from tuf.exceptions import UnsignedMetadataError, BadVersionNumberError
import unittest

from tuf.ngclient import Updater

from tests import utils
from tests.repository_simulator import RepositorySimulator
from securesystemslib import hash as sslib_hash

class TestUpdater(unittest.TestCase):
# set dump_dir to trigger repository state dumps
Expand Down Expand Up @@ -152,6 +152,33 @@ def test_keys_and_signatures(self):

self._run_refresh()

def test_snapshot_rollback_check_with_missing_local_snapshot(self):
# Test triggering snapshot rollback check on a newly downloaded snapshot
# when the local snapshot is not loaded due to hash mismatch.
# Initialize all metadata and assign targets version higher than 1.
self.sim.targets.version = 2
self.sim.update_snapshot()
self._run_refresh()

# The new targets should have a lower version than the local trusted one.
self.sim.targets.version = 1
self.sim.update_snapshot()

# Calculate the hash of the new modified version of snapshot.
digest_object = sslib_hash.digest("sha256")
digest_object.update(self.sim._fetch_metadata("snapshot"))
new_snapshot_hash = digest_object.hexdigest()
# Assign the new hash to meta in timestamp, so that loading the local
# snapshot will fail, but loading the next snapshot version will succeed.
self.sim.timestamp.snapshot_meta.hashes = {"sha256": new_snapshot_hash}
self.sim.update_timestamp()

# Should fail as a new version of snapshot will be fetched which lowers
# the snapshot.meta["targets.json"] version by 1 and throws an error.
with self.assertRaises(BadVersionNumberError):
self._run_refresh()


if __name__ == "__main__":
if "--dump" in sys.argv:
TestUpdater.dump_dir = tempfile.mkdtemp()
Expand Down
22 changes: 14 additions & 8 deletions tuf/ngclient/_internal/trusted_metadata_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,9 @@ def _check_final_timestamp(self) -> None:
if self.timestamp.signed.is_expired(self.reference_time):
raise exceptions.ExpiredMetadataError("timestamp.json is expired")

def update_snapshot(self, data: bytes) -> None:
def update_snapshot(
self, data: bytes, trusted: Optional[bool] = False
) -> None:
"""Verifies and loads 'data' as new snapshot metadata.
Note that an intermediate snapshot is allowed to be expired and version
Expand All @@ -271,6 +273,8 @@ def update_snapshot(self, data: bytes) -> None:
Args:
data: unverified new snapshot metadata as bytes
trusted: whether to consider the given data as trusted. Default is
False.
Raises:
RepositoryError: data failed to load or verify as final snapshot.
Expand All @@ -288,13 +292,15 @@ def update_snapshot(self, data: bytes) -> None:

snapshot_meta = self.timestamp.signed.snapshot_meta

# Verify against the hashes in timestamp, if any
try:
snapshot_meta.verify_length_and_hashes(data)
except exceptions.LengthOrHashMismatchError as e:
raise exceptions.RepositoryError(
"Snapshot length or hashes do not match"
) from e
# Verify non-trusted data against the hashes in timestamp, if any.
# Trusted snapshot data has already been verified once.
if not trusted:
try:
snapshot_meta.verify_length_and_hashes(data)
except exceptions.LengthOrHashMismatchError as e:
raise exceptions.RepositoryError(
"Snapshot length or hashes do not match"
) from e

try:
new_snapshot = Metadata[Snapshot].from_bytes(data)
Expand Down
4 changes: 2 additions & 2 deletions tuf/ngclient/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ def _load_snapshot(self) -> None:
"""Load local (and if needed remote) snapshot metadata"""
try:
data = self._load_local_metadata("snapshot")
self._trusted_set.update_snapshot(data)
self._trusted_set.update_snapshot(data, trusted=True)
logger.debug("Local snapshot is valid: not downloading new one")
except (OSError, exceptions.RepositoryError) as e:
# Local snapshot does not exist or is invalid: update from remote
Expand All @@ -359,7 +359,7 @@ def _load_snapshot(self) -> None:
version = snapshot_meta.version

data = self._download_metadata("snapshot", length, version)
self._trusted_set.update_snapshot(data)
self._trusted_set.update_snapshot(data, trusted=False)
self._persist_metadata("snapshot", data)

def _load_targets(self, role: str, parent_role: str) -> None:
Expand Down

0 comments on commit 81b0b5b

Please sign in to comment.