Skip to content

Commit 4d8cbc7

Browse files
author
Jussi Kukkonen
authored
Merge pull request #1605 from MVrachev/snapshot-hashes-length-check
Introduce the idea of trusted/untrusted snapshot
2 parents cb94504 + 717eef9 commit 4d8cbc7

4 files changed

Lines changed: 78 additions & 16 deletions

File tree

tests/repository_simulator.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@
5050
import logging
5151
import os
5252
import tempfile
53-
from securesystemslib.hash import digest
53+
import securesystemslib.hash as sslib_hash
5454
from securesystemslib.keys import generate_ed25519_key
5555
from securesystemslib.signer import SSlibSigner
5656
from typing import Dict, Iterator, List, Optional, Tuple
5757
from urllib import parse
5858

5959
from tuf.api.serialization.json import JSONSerializer
60-
from tuf.exceptions import FetcherHTTPError, RepositoryError
60+
from tuf.exceptions import FetcherHTTPError
6161
from tuf.api.metadata import (
6262
Key,
6363
Metadata,
@@ -100,6 +100,9 @@ def __init__(self):
100100
# target downloads are served from this dict
101101
self.target_files: Dict[str, RepositoryTarget] = {}
102102

103+
# Whether to compute hashes and legth for meta in snapshot/timestamp
104+
self.compute_metafile_hashes_length = False
105+
103106
self.dump_dir = None
104107
self.dump_version = 0
105108

@@ -121,7 +124,8 @@ def snapshot(self) -> Snapshot:
121124
def targets(self) -> Targets:
122125
return self.md_targets.signed
123126

124-
def delegates(self) -> Iterator[Tuple[str, Targets]]:
127+
def all_targets(self) -> Iterator[Tuple[str, Targets]]:
128+
yield "targets", self.md_targets.signed
125129
for role, md in self.md_delegates.items():
126130
yield role, md.signed
127131

@@ -243,16 +247,34 @@ def _fetch_metadata(self, role: str, version: Optional[int] = None) -> bytes:
243247
)
244248
return md.to_bytes(JSONSerializer())
245249

250+
def _compute_hashes_and_length(
251+
self, role: str
252+
) -> Tuple[Dict[str, str], int]:
253+
data = self._fetch_metadata(role)
254+
digest_object = sslib_hash.digest(sslib_hash.DEFAULT_HASH_ALGORITHM)
255+
digest_object.update(data)
256+
hashes = {sslib_hash.DEFAULT_HASH_ALGORITHM: digest_object.hexdigest()}
257+
return hashes, len(data)
258+
246259
def update_timestamp(self):
247260
self.timestamp.snapshot_meta.version = self.snapshot.version
248261

262+
if self.compute_metafile_hashes_length:
263+
hashes, length = self._compute_hashes_and_length("snapshot")
264+
self.timestamp.snapshot_meta.hashes = hashes
265+
self.timestamp.snapshot_meta.length = length
266+
249267
self.timestamp.version += 1
250268

251269
def update_snapshot(self):
252-
self.snapshot.meta["targets.json"].version = self.targets.version
253-
for role, delegate in self.delegates():
270+
for role, delegate in self.all_targets():
254271
self.snapshot.meta[f"{role}.json"].version = delegate.version
255272

273+
if self.compute_metafile_hashes_length:
274+
hashes, length = self._compute_hashes_and_length(role)
275+
self.snapshot.meta[f"{role}.json"].hashes = hashes
276+
self.snapshot.meta[f"{role}.json"].length = length
277+
256278
self.snapshot.version += 1
257279
self.update_timestamp()
258280

tests/test_updater_with_simulator.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@
66
"""Test ngclient Updater using the repository simulator
77
"""
88

9-
import logging
109
import os
1110
import sys
1211
import tempfile
1312
from typing import Optional, Tuple
14-
from tuf.exceptions import UnsignedMetadataError
13+
from tuf.exceptions import UnsignedMetadataError, BadVersionNumberError
1514
import unittest
1615

1716
from tuf.ngclient import Updater
1817

1918
from tests import utils
2019
from tests.repository_simulator import RepositorySimulator
20+
from securesystemslib import hash as sslib_hash
2121

2222

2323
class TestUpdater(unittest.TestCase):
@@ -164,6 +164,37 @@ def test_keys_and_signatures(self):
164164

165165
self._run_refresh()
166166

167+
def test_snapshot_rollback_with_local_snapshot_hash_mismatch(self):
168+
# Test triggering snapshot rollback check on a newly downloaded snapshot
169+
# when the local snapshot is loaded even when there is a hash mismatch
170+
# with timestamp.snapshot_meta.
171+
172+
# By raising this flag on timestamp update the simulator would:
173+
# 1) compute the hash of the new modified version of snapshot
174+
# 2) assign the hash to timestamp.snapshot_meta
175+
# The purpose is to create a hash mismatch between timestamp.meta and
176+
# the local snapshot, but to have hash match between timestamp.meta and
177+
# the next snapshot version.
178+
self.sim.compute_metafile_hashes_length = True
179+
180+
# Initialize all metadata and assign targets version higher than 1.
181+
self.sim.targets.version = 2
182+
self.sim.update_snapshot()
183+
self._run_refresh()
184+
185+
# The new targets should have a lower version than the local trusted one.
186+
self.sim.targets.version = 1
187+
self.sim.update_snapshot()
188+
189+
# During the snapshot update, the local snapshot will be loaded even if
190+
# there is a hash mismatch with timestamp.snapshot_meta, because it will
191+
# be considered as trusted.
192+
# Should fail as a new version of snapshot will be fetched which lowers
193+
# the snapshot.meta["targets.json"] version by 1 and throws an error.
194+
with self.assertRaises(BadVersionNumberError):
195+
self._run_refresh()
196+
197+
167198
if __name__ == "__main__":
168199
if "--dump" in sys.argv:
169200
TestUpdater.dump_dir = tempfile.mkdtemp()

tuf/ngclient/_internal/trusted_metadata_set.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,9 @@ def _check_final_timestamp(self) -> None:
258258
if self.timestamp.signed.is_expired(self.reference_time):
259259
raise exceptions.ExpiredMetadataError("timestamp.json is expired")
260260

261-
def update_snapshot(self, data: bytes) -> None:
261+
def update_snapshot(
262+
self, data: bytes, trusted: Optional[bool] = False
263+
) -> None:
262264
"""Verifies and loads 'data' as new snapshot metadata.
263265
264266
Note that an intermediate snapshot is allowed to be expired and version
@@ -271,6 +273,11 @@ def update_snapshot(self, data: bytes) -> None:
271273
272274
Args:
273275
data: unverified new snapshot metadata as bytes
276+
trusted: whether data has at some point been verified by
277+
TrustedMetadataSet as a valid snapshot. Purpose of trusted is
278+
to allow loading of locally stored snapshot as intermediate
279+
snapshot even if hashes in current timestamp meta no longer
280+
match data. Default is False.
274281
275282
Raises:
276283
RepositoryError: data failed to load or verify as final snapshot.
@@ -288,13 +295,15 @@ def update_snapshot(self, data: bytes) -> None:
288295

289296
snapshot_meta = self.timestamp.signed.snapshot_meta
290297

291-
# Verify against the hashes in timestamp, if any
292-
try:
293-
snapshot_meta.verify_length_and_hashes(data)
294-
except exceptions.LengthOrHashMismatchError as e:
295-
raise exceptions.RepositoryError(
296-
"Snapshot length or hashes do not match"
297-
) from e
298+
# Verify non-trusted data against the hashes in timestamp, if any.
299+
# Trusted snapshot data has already been verified once.
300+
if not trusted:
301+
try:
302+
snapshot_meta.verify_length_and_hashes(data)
303+
except exceptions.LengthOrHashMismatchError as e:
304+
raise exceptions.RepositoryError(
305+
"Snapshot length or hashes do not match"
306+
) from e
298307

299308
try:
300309
new_snapshot = Metadata[Snapshot].from_bytes(data)

tuf/ngclient/updater.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def _load_snapshot(self) -> None:
346346
"""Load local (and if needed remote) snapshot metadata"""
347347
try:
348348
data = self._load_local_metadata("snapshot")
349-
self._trusted_set.update_snapshot(data)
349+
self._trusted_set.update_snapshot(data, trusted=True)
350350
logger.debug("Local snapshot is valid: not downloading new one")
351351
except (OSError, exceptions.RepositoryError) as e:
352352
# Local snapshot does not exist or is invalid: update from remote

0 commit comments

Comments
 (0)