Skip to content

Commit c49bdb9

Browse files
committed
feat(ngclient): require explicit bootstrap argument
make bootstrap required and explicit: callers must pass bootstrap=<root_bytes> or bootstrap=None. also tighten docs, examples, and tests to reflect the explicit trust anchor choice. Signed-off-by: 1seal <security@1seal.org>
1 parent 7b9d787 commit c49bdb9

12 files changed

+82
-14
lines changed

docs/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## Unreleased
44

5+
### Changed
6+
7+
* ngclient: `Updater()` now requires an explicit `bootstrap` argument
8+
* This is a breaking change: callers must pass `bootstrap=<root_bytes>` or `bootstrap=None`
9+
* `bootstrap=None` explicitly opts into using cached `root.json` as trust anchor
10+
511
## v6.0.0
612

713
This release is not strictly speaking an API break from 5.1 but it does contain some

docs/INSTALLATION.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,39 @@ from GitHub, change into the project root directory, and install with pip
5353
python3 -m pip install -r requirements/dev.txt
5454

5555

56+
Bootstrap root metadata
57+
-----------------------
58+
59+
The initial trusted root metadata (``root.json``) is the trust anchor for all
60+
subsequent metadata verification. Applications should deploy a trusted root
61+
with the application and provide it to :class:`tuf.ngclient.Updater`.
62+
63+
Recommended storage locations for bootstrap root metadata include:
64+
65+
* a system-wide read-only path (e.g. ``/usr/share/your-app/root.json``)
66+
* an application bundle with appropriate permissions
67+
* a read-only mounted volume in containerized deployments
68+
69+
Not recommended:
70+
71+
* ``metadata_dir`` (the metadata cache) since it is writable by design
72+
* user-writable install paths (e.g. a user site-packages directory)
73+
* any location writable by the account running the updater
74+
75+
Example::
76+
77+
from tuf.ngclient import Updater
78+
79+
with open("/usr/share/your-app/root.json", "rb") as f:
80+
bootstrap = f.read()
81+
82+
updater = Updater(
83+
metadata_dir="/var/lib/your-app/tuf/metadata",
84+
metadata_base_url="https://example.com/metadata/",
85+
bootstrap=bootstrap,
86+
)
87+
88+
5689
Verify release signatures
5790
-------------------------
5891

examples/client/client

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,15 @@ def download(base_url: str, target: str) -> bool:
7979
print(f"Using trusted root in {metadata_dir}")
8080

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

examples/uploader/_localrepo.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def __init__(self, metadata_dir: str, key_dir: str, base_url: str):
4747
self.updater = Updater(
4848
metadata_dir=metadata_dir,
4949
metadata_base_url=f"{base_url}/metadata/",
50+
bootstrap=None,
5051
)
5152
self.updater.refresh()
5253

tests/repository_simulator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@
3636
updater = Updater(
3737
dir,
3838
"https://example.com/metadata/",
39+
dir,
3940
"https://example.com/targets/",
40-
sim
41+
sim,
42+
bootstrap=sim.signed_roots[0],
4143
)
4244
updater.refresh()
4345
"""

tests/test_updater_consistent_snapshot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def _init_updater(self) -> Updater:
8888
self.targets_dir,
8989
"https://example.com/targets/",
9090
self.sim,
91+
bootstrap=self.sim.signed_roots[-1],
9192
)
9293

9394
def _assert_metadata_files_exist(self, roles: Iterable[str]) -> None:

tests/test_updater_delegation_graphs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def _init_updater(self) -> Updater:
130130
self.targets_dir,
131131
"https://example.com/targets/",
132132
self.sim,
133+
bootstrap=self.sim.signed_roots[0],
133134
)
134135

135136
def _assert_files_exist(self, roles: Iterable[str]) -> None:

tests/test_updater_fetch_target.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def _init_updater(self) -> Updater:
6565
self.targets_dir,
6666
"https://example.com/targets/",
6767
self.sim,
68+
bootstrap=self.sim.signed_roots[0],
6869
)
6970

7071
targets = {

tests/test_updater_key_rotations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def _run_refresh(self) -> None:
7979
self.metadata_dir,
8080
"https://example.com/metadata/",
8181
fetcher=self.sim,
82+
bootstrap=self.sim.signed_roots[0],
8283
)
8384
updater.refresh()
8485

tests/test_updater_ng.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def setUp(self) -> None:
115115
metadata_base_url=self.metadata_url,
116116
target_dir=self.dl_dir,
117117
target_base_url=self.targets_url,
118+
bootstrap=None,
118119
)
119120

120121
def tearDown(self) -> None:
@@ -247,14 +248,16 @@ def test_implicit_refresh_with_only_local_root(self) -> None:
247248

248249
def test_both_target_urls_not_set(self) -> None:
249250
# target_base_url = None and Updater._target_base_url = None
250-
updater = Updater(self.client_directory, self.metadata_url, self.dl_dir)
251+
updater = Updater(
252+
self.client_directory, self.metadata_url, self.dl_dir, bootstrap=None
253+
)
251254
info = TargetFile(1, {"sha256": ""}, "targetpath")
252255
with self.assertRaises(ValueError):
253256
updater.download_target(info)
254257

255258
def test_no_target_dir_no_filepath(self) -> None:
256259
# filepath = None and Updater.target_dir = None
257-
updater = Updater(self.client_directory, self.metadata_url)
260+
updater = Updater(self.client_directory, self.metadata_url, bootstrap=None)
258261
info = TargetFile(1, {"sha256": ""}, "targetpath")
259262
with self.assertRaises(ValueError):
260263
updater.find_cached_target(info)
@@ -344,6 +347,7 @@ def test_user_agent(self) -> None:
344347
self.dl_dir,
345348
self.targets_url,
346349
config=UpdaterConfig(app_user_agent="MyApp/1.2.3"),
350+
bootstrap=None,
347351
)
348352
updater.refresh()
349353
poolmgr = updater._fetcher._proxy_env.get_pool_manager(

0 commit comments

Comments
 (0)