Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

### Changed

* ngclient: `Updater()` now requires an explicit `bootstrap` argument
* This is a breaking change: callers must pass `bootstrap=<root_bytes>` or `bootstrap=None`
* `bootstrap=None` explicitly opts into using cached `root.json` as trust anchor

## v6.0.0

This release is not strictly speaking an API break from 5.1 but it does contain some
Expand Down
33 changes: 33 additions & 0 deletions docs/INSTALLATION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,39 @@ from GitHub, change into the project root directory, and install with pip
python3 -m pip install -r requirements/dev.txt


Bootstrap root metadata
Comment thread
jku marked this conversation as resolved.
Outdated
-----------------------

The initial trusted root metadata (``root.json``) is the trust anchor for all
subsequent metadata verification. Applications should deploy a trusted root
with the application and provide it to :class:`tuf.ngclient.Updater`.

Recommended storage locations for bootstrap root metadata include:

* a system-wide read-only path (e.g. ``/usr/share/your-app/root.json``)
* an application bundle with appropriate permissions
* a read-only mounted volume in containerized deployments

Not recommended:

* ``metadata_dir`` (the metadata cache) since it is writable by design
* user-writable install paths (e.g. a user site-packages directory)
* any location writable by the account running the updater

Example::

from tuf.ngclient import Updater

with open("/usr/share/your-app/root.json", "rb") as f:
bootstrap = f.read()

updater = Updater(
metadata_dir="/var/lib/your-app/tuf/metadata",
metadata_base_url="https://example.com/metadata/",
bootstrap=bootstrap,
)


Verify release signatures
-------------------------

Expand Down
7 changes: 4 additions & 3 deletions examples/client/client
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,15 @@ def download(base_url: str, target: str) -> bool:
print(f"Using trusted root in {metadata_dir}")

try:
# NOTE: initial root should be provided with ``bootstrap`` argument:
# This examples uses unsafe Trust-On-First-Use initialization so it is
# not possible here.
# NOTE: production deployments should provide embedded root metadata
# bytes via the ``bootstrap`` argument. This example uses Trust-On-First-Use
# initialization, so it explicitly opts into using cached root.json.
updater = Updater(
metadata_dir=metadata_dir,
metadata_base_url=f"{base_url}/metadata/",
target_base_url=f"{base_url}/targets/",
target_dir=DOWNLOAD_DIR,
bootstrap=None,
)
updater.refresh()

Expand Down
1 change: 1 addition & 0 deletions examples/uploader/_localrepo.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(self, metadata_dir: str, key_dir: str, base_url: str):
self.updater = Updater(
metadata_dir=metadata_dir,
metadata_base_url=f"{base_url}/metadata/",
bootstrap=None,
)
self.updater.refresh()

Expand Down
4 changes: 3 additions & 1 deletion tests/repository_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
updater = Updater(
dir,
"https://example.com/metadata/",
dir,
"https://example.com/targets/",
sim
sim,
bootstrap=sim.signed_roots[0],
)
updater.refresh()
"""
Expand Down
1 change: 1 addition & 0 deletions tests/test_updater_consistent_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def _init_updater(self) -> Updater:
self.targets_dir,
"https://example.com/targets/",
self.sim,
bootstrap=self.sim.signed_roots[-1],
)

def _assert_metadata_files_exist(self, roles: Iterable[str]) -> None:
Expand Down
1 change: 1 addition & 0 deletions tests/test_updater_delegation_graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def _init_updater(self) -> Updater:
self.targets_dir,
"https://example.com/targets/",
self.sim,
bootstrap=self.sim.signed_roots[0],
)

def _assert_files_exist(self, roles: Iterable[str]) -> None:
Expand Down
1 change: 1 addition & 0 deletions tests/test_updater_fetch_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def _init_updater(self) -> Updater:
self.targets_dir,
"https://example.com/targets/",
self.sim,
bootstrap=self.sim.signed_roots[0],
)

targets = {
Expand Down
1 change: 1 addition & 0 deletions tests/test_updater_key_rotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def _run_refresh(self) -> None:
self.metadata_dir,
"https://example.com/metadata/",
fetcher=self.sim,
bootstrap=self.sim.signed_roots[0],
)
updater.refresh()

Expand Down
8 changes: 6 additions & 2 deletions tests/test_updater_ng.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def setUp(self) -> None:
metadata_base_url=self.metadata_url,
target_dir=self.dl_dir,
target_base_url=self.targets_url,
bootstrap=None,
)

def tearDown(self) -> None:
Expand Down Expand Up @@ -247,14 +248,16 @@ def test_implicit_refresh_with_only_local_root(self) -> None:

def test_both_target_urls_not_set(self) -> None:
# target_base_url = None and Updater._target_base_url = None
updater = Updater(self.client_directory, self.metadata_url, self.dl_dir)
updater = Updater(
self.client_directory, self.metadata_url, self.dl_dir, bootstrap=None
)
info = TargetFile(1, {"sha256": ""}, "targetpath")
with self.assertRaises(ValueError):
updater.download_target(info)

def test_no_target_dir_no_filepath(self) -> None:
# filepath = None and Updater.target_dir = None
updater = Updater(self.client_directory, self.metadata_url)
updater = Updater(self.client_directory, self.metadata_url, bootstrap=None)
info = TargetFile(1, {"sha256": ""}, "targetpath")
with self.assertRaises(ValueError):
updater.find_cached_target(info)
Expand Down Expand Up @@ -344,6 +347,7 @@ def test_user_agent(self) -> None:
self.dl_dir,
self.targets_url,
config=UpdaterConfig(app_user_agent="MyApp/1.2.3"),
bootstrap=None,
)
updater.refresh()
poolmgr = updater._fetcher._proxy_env.get_pool_manager(
Expand Down
14 changes: 13 additions & 1 deletion tests/test_updater_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,18 @@ def _new_updater(self) -> Updater:
self.targets_dir,
"https://example.com/targets/",
fetcher=self.sim,
bootstrap=self.sim.signed_roots[0],
)

def test_bootstrap_argument_required(self) -> None:
with self.assertRaises(TypeError) as ctx:
Updater(
self.metadata_dir,
"https://example.com/metadata/",
fetcher=self.sim,
)
self.assertIn("bootstrap", str(ctx.exception))

def test_local_target_storage_fail(self) -> None:
self.sim.add_target("targets", b"content", "targetpath")
self.sim.targets.version += 1
Expand All @@ -52,12 +62,14 @@ def test_local_target_storage_fail(self) -> None:
updater.download_target(target_info, filepath="")

def test_non_existing_metadata_dir(self) -> None:
non_existing_dir = os.path.join(self.temp_dir.name, "non-existing-dir")
with self.assertRaises(FileNotFoundError):
# Initialize Updater with non-existing metadata_dir
Updater(
"non_existing_metadata_dir",
non_existing_dir,
"https://example.com/metadata/",
fetcher=self.sim,
bootstrap=None,
)


Expand Down
19 changes: 12 additions & 7 deletions tuf/ngclient/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
* Initializing an ``Updater`` loads and validates the trusted local root
metadata: This root metadata is used as the source of trust for all other
metadata. Updater should always be initialized with the ``bootstrap``
argument: if this is not possible, it can be initialized from cache only.
argument: pass ``bootstrap=None`` only to explicitly opt into using the
cached root.json as the trust anchor.
* ``refresh()`` can optionally be called to update and load all top-level
metadata as described in the specification, using both locally cached
metadata and metadata downloaded from the remote repository. If refresh is
Expand Down Expand Up @@ -79,7 +80,8 @@ class Updater:

Args:
metadata_dir: Local metadata directory. Directory must be
writable and it must contain a trusted root.json file
writable. If ``bootstrap`` is ``None``, this directory must contain
a trusted root.json file.
metadata_base_url: Base URL for all remote metadata downloads
target_dir: Local targets directory. Directory must be writable. It
will be used as the default target download directory by
Expand All @@ -90,9 +92,11 @@ class Updater:
download both metadata and targets. Default is ``Urllib3Fetcher``
config: ``Optional``; ``UpdaterConfig`` could be used to setup common
configuration options.
bootstrap: ``Optional``; initial root metadata. A bootstrap root should
always be provided. If it is not, the current root.json in the
metadata cache is used as the initial root.
bootstrap: Initial root metadata bytes. This argument is required.
Pass the embedded root metadata bytes for secure initialization.
Pass ``None`` only if you explicitly want to use the cached
root.json as the trust anchor (not recommended for most
deployments).

Raises:
OSError: Local root.json cannot be read
Expand All @@ -107,7 +111,8 @@ def __init__(
target_base_url: str | None = None,
fetcher: FetcherInterface | None = None,
config: UpdaterConfig | None = None,
bootstrap: bytes | None = None,
*,
bootstrap: bytes | None,
):
self._dir = metadata_dir
self._metadata_base_url = _ensure_trailing_slash(metadata_base_url)
Expand All @@ -131,7 +136,7 @@ def __init__(
f"got '{self.config.envelope_type}'"
)

if not bootstrap:
if bootstrap is None:
# if no root was provided, use the cached non-versioned root.json
bootstrap = self._load_local_metadata(Root.type)

Expand Down