sigstore._internal.tuf

TUF functionality for sigstore-python.

  1# Copyright 2022 The Sigstore Authors
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#      http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""
 16TUF functionality for `sigstore-python`.
 17"""
 18
 19from __future__ import annotations
 20
 21import logging
 22from functools import lru_cache
 23from pathlib import Path
 24from urllib import parse
 25
 26import platformdirs
 27from tuf.api import exceptions as TUFExceptions
 28from tuf.ngclient import Updater, UpdaterConfig
 29
 30from sigstore import __version__
 31from sigstore._utils import read_embedded
 32from sigstore.errors import RootError, TUFError
 33
 34_logger = logging.getLogger(__name__)
 35
 36DEFAULT_TUF_URL = "https://tuf-repo-cdn.sigstore.dev"
 37STAGING_TUF_URL = "https://tuf-repo-cdn.sigstage.dev"
 38
 39
 40def _get_dirs(url: str) -> tuple[Path, Path]:
 41    """
 42    Given a TUF repository URL, return suitable local metadata and cache directories.
 43
 44    These directories are not guaranteed to already exist.
 45    """
 46
 47    app_name = "sigstore-python"
 48    app_author = "sigstore"
 49
 50    repo_base = parse.quote(url, safe="")
 51
 52    tuf_data_dir = Path(platformdirs.user_data_dir(app_name, app_author)) / "tuf"
 53    tuf_cache_dir = Path(platformdirs.user_cache_dir(app_name, app_author)) / "tuf"
 54
 55    return (tuf_data_dir / repo_base), (tuf_cache_dir / repo_base)
 56
 57
 58class TrustUpdater:
 59    """Internal trust root (certificates and keys) downloader.
 60
 61    TrustUpdater discovers the currently valid certificates and keys and
 62    securely downloads them from the remote TUF repository at 'url'.
 63
 64    TrustUpdater expects to find an initial root.json in either the local
 65    metadata directory for this URL, or (as special case for the sigstore.dev
 66    production and staging instances) in the application resources.
 67    """
 68
 69    def __init__(self, url: str, offline: bool = False) -> None:
 70        """
 71        Create a new `TrustUpdater`, pulling from the given `url`.
 72
 73        The URL is expected to match one of `sigstore-python`'s known TUF
 74        roots, i.e. for the production or staging Sigstore TUF repos.
 75
 76        If not `offline`, TrustUpdater will update the TUF metadata from
 77        the remote repository.
 78        """
 79        self._repo_url = url
 80        self._metadata_dir, self._targets_dir = _get_dirs(url)
 81
 82        rsrc_prefix: str
 83        if self._repo_url == DEFAULT_TUF_URL:
 84            rsrc_prefix = "prod"
 85        elif self._repo_url == STAGING_TUF_URL:
 86            rsrc_prefix = "staging"
 87        else:
 88            raise RootError
 89
 90        # Initialize metadata dir
 91        self._metadata_dir.mkdir(parents=True, exist_ok=True)
 92        tuf_root = self._metadata_dir / "root.json"
 93
 94        if not tuf_root.exists():
 95            try:
 96                root_json = read_embedded("root.json", rsrc_prefix)
 97            except FileNotFoundError as e:
 98                raise RootError from e
 99
100            tuf_root.write_bytes(root_json)
101
102        # Initialize targets cache dir
103        self._targets_dir.mkdir(parents=True, exist_ok=True)
104        trusted_root_target = self._targets_dir / "trusted_root.json"
105
106        if not trusted_root_target.exists():
107            try:
108                trusted_root_json = read_embedded("trusted_root.json", rsrc_prefix)
109            except FileNotFoundError as e:
110                raise RootError from e
111
112            trusted_root_target.write_bytes(trusted_root_json)
113
114        _logger.debug(f"TUF metadata: {self._metadata_dir}")
115        _logger.debug(f"TUF targets cache: {self._targets_dir}")
116
117        self._updater: None | Updater = None
118        if not offline:
119            # Initialize and update the toplevel TUF metadata
120            self._updater = Updater(
121                metadata_dir=str(self._metadata_dir),
122                metadata_base_url=self._repo_url,
123                target_base_url=parse.urljoin(f"{self._repo_url}/", "targets/"),
124                target_dir=str(self._targets_dir),
125                config=UpdaterConfig(app_user_agent=f"sigstore-python/{__version__}"),
126            )
127            try:
128                self._updater.refresh()
129            except Exception as e:
130                raise TUFError("Failed to refresh TUF metadata") from e
131
132    @lru_cache()
133    def get_trusted_root_path(self) -> str:
134        """Return local path to currently valid trusted root file"""
135        if not self._updater:
136            _logger.debug("Using unverified trusted root from cache")
137            return str(self._targets_dir / "trusted_root.json")
138
139        root_info = self._updater.get_targetinfo("trusted_root.json")
140        if root_info is None:
141            raise TUFError("Unsupported TUF configuration: no trusted root")
142        path = self._updater.find_cached_target(root_info)
143        if path is None:
144            try:
145                path = self._updater.download_target(root_info)
146            except (
147                TUFExceptions.DownloadError,
148                TUFExceptions.RepositoryError,
149            ) as e:
150                raise TUFError("Failed to download trusted key bundle") from e
151
152        _logger.debug("Found and verified trusted root")
153        return path
DEFAULT_TUF_URL = 'https://tuf-repo-cdn.sigstore.dev'
STAGING_TUF_URL = 'https://tuf-repo-cdn.sigstage.dev'
class TrustUpdater:
 59class TrustUpdater:
 60    """Internal trust root (certificates and keys) downloader.
 61
 62    TrustUpdater discovers the currently valid certificates and keys and
 63    securely downloads them from the remote TUF repository at 'url'.
 64
 65    TrustUpdater expects to find an initial root.json in either the local
 66    metadata directory for this URL, or (as special case for the sigstore.dev
 67    production and staging instances) in the application resources.
 68    """
 69
 70    def __init__(self, url: str, offline: bool = False) -> None:
 71        """
 72        Create a new `TrustUpdater`, pulling from the given `url`.
 73
 74        The URL is expected to match one of `sigstore-python`'s known TUF
 75        roots, i.e. for the production or staging Sigstore TUF repos.
 76
 77        If not `offline`, TrustUpdater will update the TUF metadata from
 78        the remote repository.
 79        """
 80        self._repo_url = url
 81        self._metadata_dir, self._targets_dir = _get_dirs(url)
 82
 83        rsrc_prefix: str
 84        if self._repo_url == DEFAULT_TUF_URL:
 85            rsrc_prefix = "prod"
 86        elif self._repo_url == STAGING_TUF_URL:
 87            rsrc_prefix = "staging"
 88        else:
 89            raise RootError
 90
 91        # Initialize metadata dir
 92        self._metadata_dir.mkdir(parents=True, exist_ok=True)
 93        tuf_root = self._metadata_dir / "root.json"
 94
 95        if not tuf_root.exists():
 96            try:
 97                root_json = read_embedded("root.json", rsrc_prefix)
 98            except FileNotFoundError as e:
 99                raise RootError from e
100
101            tuf_root.write_bytes(root_json)
102
103        # Initialize targets cache dir
104        self._targets_dir.mkdir(parents=True, exist_ok=True)
105        trusted_root_target = self._targets_dir / "trusted_root.json"
106
107        if not trusted_root_target.exists():
108            try:
109                trusted_root_json = read_embedded("trusted_root.json", rsrc_prefix)
110            except FileNotFoundError as e:
111                raise RootError from e
112
113            trusted_root_target.write_bytes(trusted_root_json)
114
115        _logger.debug(f"TUF metadata: {self._metadata_dir}")
116        _logger.debug(f"TUF targets cache: {self._targets_dir}")
117
118        self._updater: None | Updater = None
119        if not offline:
120            # Initialize and update the toplevel TUF metadata
121            self._updater = Updater(
122                metadata_dir=str(self._metadata_dir),
123                metadata_base_url=self._repo_url,
124                target_base_url=parse.urljoin(f"{self._repo_url}/", "targets/"),
125                target_dir=str(self._targets_dir),
126                config=UpdaterConfig(app_user_agent=f"sigstore-python/{__version__}"),
127            )
128            try:
129                self._updater.refresh()
130            except Exception as e:
131                raise TUFError("Failed to refresh TUF metadata") from e
132
133    @lru_cache()
134    def get_trusted_root_path(self) -> str:
135        """Return local path to currently valid trusted root file"""
136        if not self._updater:
137            _logger.debug("Using unverified trusted root from cache")
138            return str(self._targets_dir / "trusted_root.json")
139
140        root_info = self._updater.get_targetinfo("trusted_root.json")
141        if root_info is None:
142            raise TUFError("Unsupported TUF configuration: no trusted root")
143        path = self._updater.find_cached_target(root_info)
144        if path is None:
145            try:
146                path = self._updater.download_target(root_info)
147            except (
148                TUFExceptions.DownloadError,
149                TUFExceptions.RepositoryError,
150            ) as e:
151                raise TUFError("Failed to download trusted key bundle") from e
152
153        _logger.debug("Found and verified trusted root")
154        return path

Internal trust root (certificates and keys) downloader.

TrustUpdater discovers the currently valid certificates and keys and securely downloads them from the remote TUF repository at 'url'.

TrustUpdater expects to find an initial root.json in either the local metadata directory for this URL, or (as special case for the sigstore.dev production and staging instances) in the application resources.

TrustUpdater(url: str, offline: bool = False)
 70    def __init__(self, url: str, offline: bool = False) -> None:
 71        """
 72        Create a new `TrustUpdater`, pulling from the given `url`.
 73
 74        The URL is expected to match one of `sigstore-python`'s known TUF
 75        roots, i.e. for the production or staging Sigstore TUF repos.
 76
 77        If not `offline`, TrustUpdater will update the TUF metadata from
 78        the remote repository.
 79        """
 80        self._repo_url = url
 81        self._metadata_dir, self._targets_dir = _get_dirs(url)
 82
 83        rsrc_prefix: str
 84        if self._repo_url == DEFAULT_TUF_URL:
 85            rsrc_prefix = "prod"
 86        elif self._repo_url == STAGING_TUF_URL:
 87            rsrc_prefix = "staging"
 88        else:
 89            raise RootError
 90
 91        # Initialize metadata dir
 92        self._metadata_dir.mkdir(parents=True, exist_ok=True)
 93        tuf_root = self._metadata_dir / "root.json"
 94
 95        if not tuf_root.exists():
 96            try:
 97                root_json = read_embedded("root.json", rsrc_prefix)
 98            except FileNotFoundError as e:
 99                raise RootError from e
100
101            tuf_root.write_bytes(root_json)
102
103        # Initialize targets cache dir
104        self._targets_dir.mkdir(parents=True, exist_ok=True)
105        trusted_root_target = self._targets_dir / "trusted_root.json"
106
107        if not trusted_root_target.exists():
108            try:
109                trusted_root_json = read_embedded("trusted_root.json", rsrc_prefix)
110            except FileNotFoundError as e:
111                raise RootError from e
112
113            trusted_root_target.write_bytes(trusted_root_json)
114
115        _logger.debug(f"TUF metadata: {self._metadata_dir}")
116        _logger.debug(f"TUF targets cache: {self._targets_dir}")
117
118        self._updater: None | Updater = None
119        if not offline:
120            # Initialize and update the toplevel TUF metadata
121            self._updater = Updater(
122                metadata_dir=str(self._metadata_dir),
123                metadata_base_url=self._repo_url,
124                target_base_url=parse.urljoin(f"{self._repo_url}/", "targets/"),
125                target_dir=str(self._targets_dir),
126                config=UpdaterConfig(app_user_agent=f"sigstore-python/{__version__}"),
127            )
128            try:
129                self._updater.refresh()
130            except Exception as e:
131                raise TUFError("Failed to refresh TUF metadata") from e

Create a new TrustUpdater, pulling from the given url.

The URL is expected to match one of sigstore-python's known TUF roots, i.e. for the production or staging Sigstore TUF repos.

If not offline, TrustUpdater will update the TUF metadata from the remote repository.

@lru_cache()
def get_trusted_root_path(self) -> str:
133    @lru_cache()
134    def get_trusted_root_path(self) -> str:
135        """Return local path to currently valid trusted root file"""
136        if not self._updater:
137            _logger.debug("Using unverified trusted root from cache")
138            return str(self._targets_dir / "trusted_root.json")
139
140        root_info = self._updater.get_targetinfo("trusted_root.json")
141        if root_info is None:
142            raise TUFError("Unsupported TUF configuration: no trusted root")
143        path = self._updater.find_cached_target(root_info)
144        if path is None:
145            try:
146                path = self._updater.download_target(root_info)
147            except (
148                TUFExceptions.DownloadError,
149                TUFExceptions.RepositoryError,
150            ) as e:
151                raise TUFError("Failed to download trusted key bundle") from e
152
153        _logger.debug("Found and verified trusted root")
154        return path

Return local path to currently valid trusted root file