diff --git a/.github/scripts/conformance-client.py b/.github/scripts/conformance-client.py index 0c44c7ff84..1f80ad6ce7 100755 --- a/.github/scripts/conformance-client.py +++ b/.github/scripts/conformance-client.py @@ -27,6 +27,7 @@ def refresh(metadata_url: str, metadata_dir: str) -> None: updater = Updater( metadata_dir, metadata_url, + bootstrap=None, ) updater.refresh() print(f"python-tuf test client: Refreshed metadata in {metadata_dir}") @@ -46,6 +47,7 @@ def download_target( metadata_url, download_dir, target_base_url, + bootstrap=None, ) target_info = updater.get_targetinfo(target_name) if not target_info: diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index a97bb0ce6c..029eba9dc2 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -11,14 +11,14 @@ jobs: steps: - name: Checkout TUF - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python (oldest supported version) - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.9" + python-version: "3.10" cache: 'pip' cache-dependency-path: | requirements/*.txt @@ -38,7 +38,7 @@ jobs: needs: lint-test strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] os: [ubuntu-latest] include: - python-version: "3.x" @@ -50,12 +50,12 @@ jobs: steps: - name: Checkout TUF - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -99,7 +99,7 @@ jobs: run: touch requirements.txt - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.x' cache: 'pip' diff --git a/.github/workflows/_test_sslib_main.yml b/.github/workflows/_test_sslib_main.yml index f3b89f25a1..8be70055b3 100644 --- a/.github/workflows/_test_sslib_main.yml +++ b/.github/workflows/_test_sslib_main.yml @@ -11,12 +11,12 @@ jobs: steps: - name: Checkout TUF - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.x' cache: 'pip' diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 198b14e2a3..b1820770d1 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -18,13 +18,13 @@ jobs: needs: test steps: - name: Checkout release tag - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false ref: ${{ github.event.workflow_run.head_branch }} - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.x' @@ -37,7 +37,7 @@ jobs: awk "/## $GITHUB_REF_NAME/{flag=1; next} /## v/{flag=0} flag" docs/CHANGELOG.md > changelog - name: Store build artifacts - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: build-artifacts path: | @@ -54,7 +54,7 @@ jobs: release_id: ${{ steps.gh-release.outputs.result }} steps: - name: Fetch build artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: build-artifacts @@ -96,7 +96,7 @@ jobs: id-token: write # to authenticate as Trusted Publisher to pypi.org steps: - name: Fetch build artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: build-artifacts diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 536e7dae12..9d4c54447e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index c17e3e13a9..3358297097 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -4,6 +4,8 @@ on: - develop pull_request: workflow_dispatch: + schedule: + - cron: '30 6 * * 3' permissions: contents: read @@ -14,11 +16,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout conformance client - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Run test suite - uses: theupdateframework/tuf-conformance@9bfc222a371e30ad5511eb17449f68f855fb9d8f # v2.3.0 + uses: theupdateframework/tuf-conformance@500c525c9ce287a472fd334fe8d885cace667d32 # v2.4.0 with: entrypoint: ".github/scripts/conformance-client.py" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index ac7f18c891..764d8e080c 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: 'Dependency Review' diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 5b80ecaf42..29a5b81ba9 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -22,7 +22,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false diff --git a/.github/workflows/specification-version-check.yml b/.github/workflows/specification-version-check.yml index 493275e969..9a78aa0d44 100644 --- a/.github/workflows/specification-version-check.yml +++ b/.github/workflows/specification-version-check.yml @@ -14,10 +14,10 @@ jobs: outputs: version: ${{ steps.get-version.outputs.version }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - id: get-version @@ -33,6 +33,6 @@ jobs: contents: read issues: write needs: get-supported-tuf-version - uses: theupdateframework/specification/.github/workflows/check-latest-spec-version.yml@master + uses: theupdateframework/specification/.github/workflows/check-latest-spec-version.yml@master # zizmor: ignore[unpinned-uses] with: tuf-version: ${{needs.get-supported-tuf-version.outputs.version}} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6beadca962..92f1bfc591 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### Changed + +* ngclient: `Updater()` now requires an explicit `bootstrap` argument + * This is a breaking change: callers must pass `bootstrap=` 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 diff --git a/docs/INSTALLATION.rst b/docs/INSTALLATION.rst index 8e23e927f8..012f0878da 100644 --- a/docs/INSTALLATION.rst +++ b/docs/INSTALLATION.rst @@ -53,6 +53,39 @@ from GitHub, change into the project root directory, and install with pip python3 -m pip install -r requirements/dev.txt +Application deployment +---------------------- + +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 ------------------------- diff --git a/examples/client/client b/examples/client/client index 883fd52cba..3a997a07d4 100755 --- a/examples/client/client +++ b/examples/client/client @@ -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() diff --git a/examples/uploader/_localrepo.py b/examples/uploader/_localrepo.py index c4d746a34d..7d8181b44e 100644 --- a/examples/uploader/_localrepo.py +++ b/examples/uploader/_localrepo.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml index 266b2188f5..e51626f22b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling==1.27.0"] +requires = ["hatchling==1.29.0"] build-backend = "hatchling.build" [project] @@ -8,7 +8,7 @@ description = "A secure updater framework for Python" readme = "README.md" license = "Apache-2.0 OR MIT" license-files = ["LICENSE", "LICENSE-MIT"] -requires-python = ">=3.8" +requires-python = ">=3.10" authors = [ { email = "theupdateframework@googlegroups.com" }, ] @@ -31,11 +31,11 @@ classifiers = [ "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Security", "Topic :: Software Development", diff --git a/requirements/build.txt b/requirements/build.txt index fc5bb56b8e..053a66b48f 100644 --- a/requirements/build.txt +++ b/requirements/build.txt @@ -1,4 +1,4 @@ # The build and tox versions specified here are also used as constraints # during CI and CD Github workflows -build==1.3.0 +build==1.4.0 tox==4.1.2 diff --git a/requirements/lint.txt b/requirements/lint.txt index b35db1f74e..d120b0dc5b 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -6,9 +6,9 @@ # Lint tools # (We are not so interested in the specific versions of the tools: the versions # are pinned to prevent unexpected linting failures when tools update) -ruff==0.14.2 -mypy==1.18.2 -zizmor==1.16.0 +ruff==0.15.4 +mypy==1.19.1 +zizmor==1.22.0 # Required for type stubs freezegun==1.5.5 diff --git a/requirements/pinned.txt b/requirements/pinned.txt index 7777d0cab0..5430cb54d1 100644 --- a/requirements/pinned.txt +++ b/requirements/pinned.txt @@ -6,11 +6,11 @@ # cffi==2.0.0 # via cryptography -cryptography==46.0.3 +cryptography==46.0.5 # via securesystemslib -pycparser==2.23 +pycparser==3.0 # via cffi securesystemslib==1.3.1 # via -r requirements/main.txt -urllib3==2.5.0 +urllib3==2.6.3 # via -r requirements/main.txt diff --git a/requirements/test.txt b/requirements/test.txt index 491d45a382..cea102cff1 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,5 +4,5 @@ -r pinned.txt # coverage measurement -coverage[toml]==7.10.7 +coverage[toml]==7.13.4 freezegun==1.5.5 diff --git a/tests/repository_simulator.py b/tests/repository_simulator.py index d0c50bc424..bd175e7244 100644 --- a/tests/repository_simulator.py +++ b/tests/repository_simulator.py @@ -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() """ diff --git a/tests/test_fetcher_ng.py b/tests/test_fetcher_ng.py index d04b09f427..7ef7c11b70 100644 --- a/tests/test_fetcher_ng.py +++ b/tests/test_fetcher_ng.py @@ -170,9 +170,10 @@ def test_download_file_upper_length(self) -> None: # Download a file bigger than expected def test_download_file_length_mismatch(self) -> None: - with self.assertRaises( - exceptions.DownloadLengthMismatchError - ), self.fetcher.download_file(self.url, self.file_length - 4): + with ( + self.assertRaises(exceptions.DownloadLengthMismatchError), + self.fetcher.download_file(self.url, self.file_length - 4), + ): pass # we never get here as download_file() raises diff --git a/tests/test_trusted_metadata_set.py b/tests/test_trusted_metadata_set.py index bd8113eb4a..fd59635ed8 100644 --- a/tests/test_trusted_metadata_set.py +++ b/tests/test_trusted_metadata_set.py @@ -7,7 +7,7 @@ import sys import unittest from datetime import datetime, timezone -from typing import Callable, ClassVar +from typing import TYPE_CHECKING, ClassVar from securesystemslib.signer import Signer @@ -30,6 +30,9 @@ ) from tuf.ngclient.config import EnvelopeType +if TYPE_CHECKING: + from collections.abc import Callable + logger = logging.getLogger(__name__) diff --git a/tests/test_updater_consistent_snapshot.py b/tests/test_updater_consistent_snapshot.py index 4ceb1fe7f9..abf6fb4a9b 100644 --- a/tests/test_updater_consistent_snapshot.py +++ b/tests/test_updater_consistent_snapshot.py @@ -74,10 +74,6 @@ def _init_repo( sim.publish_root() sim.prefix_targets_with_hash = prefix_targets - # Init trusted root with the latest consistent_snapshot - with open(os.path.join(self.metadata_dir, "root.json"), "bw") as f: - f.write(sim.signed_roots[-1]) - return sim def _init_updater(self) -> Updater: @@ -88,6 +84,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: diff --git a/tests/test_updater_delegation_graphs.py b/tests/test_updater_delegation_graphs.py index 770a1b3d71..536bb13a2d 100644 --- a/tests/test_updater_delegation_graphs.py +++ b/tests/test_updater_delegation_graphs.py @@ -120,16 +120,13 @@ def _init_repo(self, test_case: DelegationsTestCase) -> None: def _init_updater(self) -> Updater: """Create a new Updater instance""" - # Init trusted root for Updater - with open(os.path.join(self.metadata_dir, "root.json"), "bw") as f: - f.write(self.sim.signed_roots[0]) - return Updater( self.metadata_dir, "https://example.com/metadata/", self.targets_dir, "https://example.com/targets/", self.sim, + bootstrap=self.sim.signed_roots[0], ) def _assert_files_exist(self, roles: Iterable[str]) -> None: diff --git a/tests/test_updater_fetch_target.py b/tests/test_updater_fetch_target.py index 5ab8567032..ecf777c6f1 100644 --- a/tests/test_updater_fetch_target.py +++ b/tests/test_updater_fetch_target.py @@ -40,10 +40,8 @@ def setUp(self) -> None: os.mkdir(self.metadata_dir) os.mkdir(self.targets_dir) - # Setup the repository, bootstrap client root.json + # Setup the repository self.sim = RepositorySimulator() - with open(os.path.join(self.metadata_dir, "root.json"), "bw") as f: - f.write(self.sim.signed_roots[0]) if self.dump_dir is not None: # create test specific dump directory @@ -65,6 +63,7 @@ def _init_updater(self) -> Updater: self.targets_dir, "https://example.com/targets/", self.sim, + bootstrap=self.sim.signed_roots[0], ) targets = { diff --git a/tests/test_updater_key_rotations.py b/tests/test_updater_key_rotations.py index f79c3dd997..90dbd262f9 100644 --- a/tests/test_updater_key_rotations.py +++ b/tests/test_updater_key_rotations.py @@ -72,13 +72,12 @@ def _run_refresh(self) -> None: # bootstrap with initial root self.metadata_dir = tempfile.mkdtemp(dir=self.temp_dir.name) - with open(os.path.join(self.metadata_dir, "root.json"), "bw") as f: - f.write(self.sim.signed_roots[0]) updater = Updater( self.metadata_dir, "https://example.com/metadata/", fetcher=self.sim, + bootstrap=self.sim.signed_roots[0], ) updater.refresh() diff --git a/tests/test_updater_ng.py b/tests/test_updater_ng.py index 50ef5ee3be..5fc436ba97 100644 --- a/tests/test_updater_ng.py +++ b/tests/test_updater_ng.py @@ -12,7 +12,7 @@ import tempfile import unittest from collections.abc import Iterable -from typing import TYPE_CHECKING, Callable, ClassVar +from typing import TYPE_CHECKING, ClassVar from unittest.mock import MagicMock, patch from securesystemslib.signer import Signer @@ -30,7 +30,7 @@ from tuf.ngclient import Updater, UpdaterConfig if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Callable, Iterable logger = logging.getLogger(__name__) @@ -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,21 @@ 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 +352,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( diff --git a/tests/test_updater_validation.py b/tests/test_updater_validation.py index b9d6bb3cc7..7417b67c5d 100644 --- a/tests/test_updater_validation.py +++ b/tests/test_updater_validation.py @@ -23,10 +23,8 @@ def setUp(self) -> None: os.mkdir(self.metadata_dir) os.mkdir(self.targets_dir) - # Setup the repository, bootstrap client root.json + # Setup the repository self.sim = RepositorySimulator() - with open(os.path.join(self.metadata_dir, "root.json"), "bw") as f: - f.write(self.sim.signed_roots[0]) def tearDown(self) -> None: self.temp_dir.cleanup() @@ -38,8 +36,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, + ) # type: ignore[call-arg] + 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 +60,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, ) diff --git a/tests/utils.py b/tests/utils.py index bbfb07dbaa..cc35af0447 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -32,11 +32,11 @@ import time import warnings from contextlib import contextmanager -from typing import IO, TYPE_CHECKING, Any, Callable +from typing import IO, TYPE_CHECKING, Any if TYPE_CHECKING: import unittest - from collections.abc import Iterator + from collections.abc import Callable, Iterator logger = logging.getLogger(__name__) @@ -111,11 +111,11 @@ def wait_for_server( sock.settimeout(remaining_timeout) sock.connect((host, port)) succeeded = True - except socket.timeout: + except TimeoutError: pass except OSError as e: # ECONNREFUSED is expected while the server is not started - if e.errno not in [errno.ECONNREFUSED]: + if e.errno != errno.ECONNREFUSED: logger.warning( "Unexpected error while waiting for server: %s", str(e) ) diff --git a/tuf/api/_payload.py b/tuf/api/_payload.py index 8a8c40ffdb..c4a64bb565 100644 --- a/tuf/api/_payload.py +++ b/tuf/api/_payload.py @@ -396,19 +396,15 @@ def verified(self) -> bool: def signed(self) -> dict[str, Key]: """Dictionary of all signing keys that have signed, from both VerificationResults. - return a union of all signed (in python<3.9 this requires - dict unpacking) """ - return {**self.first.signed, **self.second.signed} + return self.first.signed | self.second.signed @property def unsigned(self) -> dict[str, Key]: """Dictionary of all signing keys that have not signed, from both VerificationResults. - return a union of all unsigned (in python<3.9 this requires - dict unpacking) """ - return {**self.first.unsigned, **self.second.unsigned} + return self.first.unsigned | self.second.unsigned class _DelegatorMixin(metaclass=abc.ABCMeta): @@ -1195,8 +1191,8 @@ def _is_target_in_pathpattern(targetpath: str, pathpattern: str) -> bool: # Every part in the pathpattern could include a glob pattern, that's why # each of the target and pathpattern parts should match. - for target_dir, pattern_dir in zip(target_parts, pattern_parts): - if not fnmatch.fnmatch(target_dir, pattern_dir): + for target, pattern in zip(target_parts, pattern_parts, strict=True): + if not fnmatch.fnmatch(target, pattern): return False return True diff --git a/tuf/ngclient/_internal/trusted_metadata_set.py b/tuf/ngclient/_internal/trusted_metadata_set.py index 179a65ed87..689eef01de 100644 --- a/tuf/ngclient/_internal/trusted_metadata_set.py +++ b/tuf/ngclient/_internal/trusted_metadata_set.py @@ -66,7 +66,7 @@ import datetime import logging from collections import abc -from typing import TYPE_CHECKING, Union, cast +from typing import TYPE_CHECKING, cast from tuf.api import exceptions from tuf.api.dsse import SimpleEnvelope @@ -88,7 +88,7 @@ logger = logging.getLogger(__name__) -Delegator = Union[Root, Targets] +Delegator = Root | Targets class TrustedMetadataSet(abc.Mapping): diff --git a/tuf/ngclient/updater.py b/tuf/ngclient/updater.py index a98e799ce4..a253b18d4c 100644 --- a/tuf/ngclient/updater.py +++ b/tuf/ngclient/updater.py @@ -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)