Skip to content

Commit 9f34e0f

Browse files
authored
Merge pull request #2903 from 1seal/hardening/require-explicit-bootstrap
feat(ngclient): require explicit bootstrap argument
2 parents d68ea59 + d5fa0b0 commit 9f34e0f

13 files changed

+91
-30
lines changed

.github/scripts/conformance-client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def refresh(metadata_url: str, metadata_dir: str) -> None:
2727
updater = Updater(
2828
metadata_dir,
2929
metadata_url,
30+
bootstrap=None,
3031
)
3132
updater.refresh()
3233
print(f"python-tuf test client: Refreshed metadata in {metadata_dir}")
@@ -46,6 +47,7 @@ def download_target(
4647
metadata_url,
4748
download_dir,
4849
target_base_url,
50+
bootstrap=None,
4951
)
5052
target_info = updater.get_targetinfo(target_name)
5153
if not target_info:

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+
Application deployment
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 & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@ def _init_repo(
7474
sim.publish_root()
7575
sim.prefix_targets_with_hash = prefix_targets
7676

77-
# Init trusted root with the latest consistent_snapshot
78-
with open(os.path.join(self.metadata_dir, "root.json"), "bw") as f:
79-
f.write(sim.signed_roots[-1])
80-
8177
return sim
8278

8379
def _init_updater(self) -> Updater:
@@ -88,6 +84,7 @@ def _init_updater(self) -> Updater:
8884
self.targets_dir,
8985
"https://example.com/targets/",
9086
self.sim,
87+
bootstrap=self.sim.signed_roots[-1],
9188
)
9289

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

tests/test_updater_delegation_graphs.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,13 @@ def _init_repo(self, test_case: DelegationsTestCase) -> None:
120120

121121
def _init_updater(self) -> Updater:
122122
"""Create a new Updater instance"""
123-
# Init trusted root for Updater
124-
with open(os.path.join(self.metadata_dir, "root.json"), "bw") as f:
125-
f.write(self.sim.signed_roots[0])
126-
127123
return Updater(
128124
self.metadata_dir,
129125
"https://example.com/metadata/",
130126
self.targets_dir,
131127
"https://example.com/targets/",
132128
self.sim,
129+
bootstrap=self.sim.signed_roots[0],
133130
)
134131

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

tests/test_updater_fetch_target.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,8 @@ def setUp(self) -> None:
4040
os.mkdir(self.metadata_dir)
4141
os.mkdir(self.targets_dir)
4242

43-
# Setup the repository, bootstrap client root.json
43+
# Setup the repository
4444
self.sim = RepositorySimulator()
45-
with open(os.path.join(self.metadata_dir, "root.json"), "bw") as f:
46-
f.write(self.sim.signed_roots[0])
4745

4846
if self.dump_dir is not None:
4947
# create test specific dump directory
@@ -65,6 +63,7 @@ def _init_updater(self) -> Updater:
6563
self.targets_dir,
6664
"https://example.com/targets/",
6765
self.sim,
66+
bootstrap=self.sim.signed_roots[0],
6867
)
6968

7069
targets = {

tests/test_updater_key_rotations.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,12 @@ def _run_refresh(self) -> None:
7272

7373
# bootstrap with initial root
7474
self.metadata_dir = tempfile.mkdtemp(dir=self.temp_dir.name)
75-
with open(os.path.join(self.metadata_dir, "root.json"), "bw") as f:
76-
f.write(self.sim.signed_roots[0])
7775

7876
updater = Updater(
7977
self.metadata_dir,
8078
"https://example.com/metadata/",
8179
fetcher=self.sim,
80+
bootstrap=self.sim.signed_roots[0],
8281
)
8382
updater.refresh()
8483

0 commit comments

Comments
 (0)