Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ Top-level:

<!-- @begin-sigstore-help@ -->
```
usage: sigstore [-h] [-v] [-V] [--staging | --trust-config FILE] COMMAND ...
usage: sigstore [-h] [-v] [-V]
[--staging | --instance URL | --trust-config FILE]
COMMAND ...

a tool for signing and verifying Python package distributions

Expand All @@ -76,16 +78,24 @@ positional arguments:
get-identity-token
retrieve and return a Sigstore-compatible OpenID
Connect token
trust-instance Initialize trust for a Sigstore instance
plumbing developer-only plumbing operations

optional arguments:
-h, --help show this help message and exit
-v, --verbose run with additional debug logging; supply multiple
times to increase verbosity (default: 0)
-V, --version show program's version number and exit
--staging Use sigstore's staging instances, instead of the
default production instances (default: False)
--trust-config FILE The client trust configuration to use (default: None)
--staging Use sigstore's staging instance, instead of the default
production instance. Mutually exclusive with other
instance configuration arguments. (default: False)
--instance URL Use a given Sigstore instance URL, instead of the
default production instance. Mutually exclusive with
other instance configuration arguments. (default: None)
--trust-config FILE Use given client trust configuration instead of using
the default production instance. Mutually exclusive
with other instance configuration arguments. (default:
None)
```
<!-- @end-sigstore-help@ -->

Expand Down
55 changes: 52 additions & 3 deletions sigstore/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,28 @@ def _parser() -> argparse.ArgumentParser:
"--staging",
action="store_true",
default=_boolify_env("SIGSTORE_STAGING"),
help="Use sigstore's staging instances, instead of the default production instances",
help=(
"Use sigstore's staging instance, instead of the default production instance."
" Mutually exclusive with other instance configuration arguments."
),
)
global_instance_options.add_argument(
"--instance",
metavar="URL",
type=str,
help=(
"Use a given Sigstore instance URL, instead of the default production instance."
" Mutually exclusive with other instance configuration arguments."
),
)
global_instance_options.add_argument(
"--trust-config",
metavar="FILE",
type=Path,
help="The client trust configuration to use",
help=(
"Use given client trust configuration instead of using the default production"
" instance. Mutually exclusive with other instance configuration arguments."
),
)
subcommands = parser.add_subparsers(
required=True,
Expand Down Expand Up @@ -545,6 +560,20 @@ def _parser() -> argparse.ArgumentParser:
)
_add_shared_oidc_options(get_identity_token)

# `sigstore trust-instance`
trust_instance = subcommands.add_parser(
"trust-instance",
help="Initialize trust for a Sigstore instance",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
parents=[parent_parser],
)
trust_instance.add_argument(
"root",
metavar="ROOT",
type=Path,
help="The TUF root metadata for the instance",
)

# `sigstore plumbing`
plumbing = subcommands.add_parser(
"plumbing",
Expand Down Expand Up @@ -626,6 +655,8 @@ def main(args: list[str] | None = None) -> None:
_verify_github(args)
elif args.subcommand == "get-identity-token":
_get_identity_token(args)
elif args.subcommand == "trust-instance":
_trust_instance(args)
elif args.subcommand == "plumbing":
if args.plumbing_subcommand == "fix-bundle":
_fix_bundle(args)
Expand All @@ -637,6 +668,22 @@ def main(args: list[str] | None = None) -> None:
e.log_and_exit(_logger, args.verbose >= 1)


def _trust_instance(args: argparse.Namespace) -> None:
"""
Initialize trust for a Sigstore instance
"""
root: Path = args.root
instance: str | None = args.instance
if not root.is_file():
_invalid_arguments(args, f"Input must be a file: {root}")
if instance is None:
_invalid_arguments(args, "trust-instance requires '--instance URL'")

# ClientTrustConfig construction verifies the root is valid, and
# stores it in the local metadata store for future use
_ = ClientTrustConfig.from_tuf(instance, bootstrap_root=root)


def _get_identity_token(args: argparse.Namespace) -> None:
"""
Output the OIDC authentication token
Expand Down Expand Up @@ -1210,13 +1257,15 @@ def _get_trust_config(args: argparse.Namespace) -> ClientTrustConfig:
Return the client trust configuration (Sigstore service URLs, key material and lifetimes)

The configuration may come from explicit argument (--trust-config) or from the TUF
repository of the used Sigstore instance.
repository of the used Sigstore instance (--staging or --instance).
"""
# Not all commands provide --offline
offline = getattr(args, "offline", False)

if args.trust_config:
trust_config = ClientTrustConfig.from_json(args.trust_config.read_text())
elif args.instance:
trust_config = ClientTrustConfig.from_tuf(args.instance, offline=offline)
elif args.staging:
trust_config = ClientTrustConfig.staging(offline=offline)
else:
Expand Down
41 changes: 23 additions & 18 deletions sigstore/_internal/tuf.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,21 @@ class TrustUpdater:
production and staging instances) in the application resources.
"""

def __init__(self, url: str, offline: bool = False) -> None:
def __init__(
self, url: str, offline: bool = False, bootstrap_root: Path | None = None
) -> None:
"""
Create a new `TrustUpdater`, pulling from the given `url`.

TrustUpdater expects that either embedded data contains
a root.json for this url or that local data has been initialized
already.
a root.json for this url or that `bootstrap_root` is provided as argument.

If not `offline`, TrustUpdater will update the TUF metadata from
the remote repository.
"""
self._repo_url = url
# not canonicalization, just handling trailing slash as common mistake:
url = url.rstrip("/")

self._metadata_dir, self._targets_dir = _get_dirs(url)

# Populate targets cache so we don't have to download these versions
Expand All @@ -90,7 +93,7 @@ def __init__(self, url: str, offline: bool = False) -> None:
data = read_embedded(artifact, url)
artifact_path.write_bytes(data)
except FileNotFoundError:
pass # this is ok: e.g. signing_config is not in prod repository yet
pass # this is ok: we only have embedded data for specific repos

_logger.debug(f"TUF metadata: {self._metadata_dir}")
_logger.debug(f"TUF targets cache: {self._targets_dir}")
Expand All @@ -103,22 +106,24 @@ def __init__(self, url: str, offline: bool = False) -> None:
else:
# Initialize and update the toplevel TUF metadata
try:
root_json = read_embedded("root.json", url)
root_json: bytes | None = read_embedded("root.json", url)
except FileNotFoundError:
# embedded root not found: we can still initialize _if_ the local metadata
# exists already
root_json = None

self._updater = Updater(
metadata_dir=str(self._metadata_dir),
metadata_base_url=self._repo_url,
target_base_url=parse.urljoin(f"{self._repo_url}/", "targets/"),
target_dir=str(self._targets_dir),
config=UpdaterConfig(app_user_agent=f"sigstore-python/{__version__}"),
bootstrap=root_json,
)
# We do not have embedded root metadata for this URL: we can still
# initialize _if_ given bootstrap root (i.e. during "sigstore trust-instance")
# or local metadata exists already (after "sigstore trust-instance")
root_json = bootstrap_root.read_bytes() if bootstrap_root else None

try:
self._updater = Updater(
metadata_dir=str(self._metadata_dir),
metadata_base_url=url,
target_base_url=parse.urljoin(f"{url}/", "targets/"),
target_dir=str(self._targets_dir),
config=UpdaterConfig(
app_user_agent=f"sigstore-python/{__version__}"
),
bootstrap=root_json,
)
self._updater.refresh()
except Exception as e:
raise TUFError("Failed to refresh TUF metadata") from e
Expand Down
3 changes: 2 additions & 1 deletion sigstore/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ def diagnostics(self) -> str:
"""Returns diagnostics specialized to the wrapped TUF error."""
details = TUFError._details.get(
type(self.__context__),
"Please report this issue at <https://github.com/sigstore/sigstore-python/issues/new>.",
"Please check any Sigstore instance related arguments and consider "
"reporting the issue at <https://github.com/sigstore/sigstore-python/issues/new>.",
)

return f"""\
Expand Down
3 changes: 2 additions & 1 deletion sigstore/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -929,13 +929,14 @@ def from_tuf(
cls,
url: str,
offline: bool = False,
bootstrap_root: Path | None = None,
) -> ClientTrustConfig:
"""Create a new trust config from a TUF repository.

If `offline`, will use data in local TUF cache. Otherwise will
update the trust config from remote TUF repository.
"""
updater = TrustUpdater(url, offline)
updater = TrustUpdater(url, offline, bootstrap_root)

tr_path = updater.get_trusted_root_path()
inner_tr = trustroot_v1.TrustedRoot.from_json(Path(tr_path).read_bytes())
Expand Down
7 changes: 3 additions & 4 deletions test/unit/internal/test_trust.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
KeyringPurpose,
)
from sigstore._utils import is_timerange_valid
from sigstore.errors import Error
from sigstore.errors import Error, TUFError
from sigstore.models import (
ClientTrustConfig,
SigningConfig,
Expand Down Expand Up @@ -294,9 +294,8 @@ def range_from(offset_lower=0, offset_upper=0):


def test_trust_root_tuf_instance_error():
# Expect file not found since embedded root.json is not found and
# no local metadata is found
with pytest.raises(FileNotFoundError):
# embedded root.json is not found and no local metadata is found
with pytest.raises(TUFError):
ClientTrustConfig.from_tuf("foo.bar")


Expand Down