mirror of
https://github.com/theupdateframework/python-tuf
synced 2026-05-24 10:08:28 +00:00
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>
This commit is contained in:
parent
7b9d787971
commit
c49bdb9322
12 changed files with 82 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-----------------------
|
||||
|
||||
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
|
||||
-------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -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)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue