Skip to content

Commit 8dcbda4

Browse files
committed
cli: Support using other Sigstore instances
example: $ sigstore --instance https://sigstore.github.io/root-signing \ init-instance ~/src/root-signing/metadata/root.json $ sigstore --instance https://sigstore.github.io/root-signing \ sign README.md Signed-off-by: Jussi Kukkonen <jkukkonen@google.com>
1 parent 1432127 commit 8dcbda4

File tree

2 files changed

+69
-3
lines changed

2 files changed

+69
-3
lines changed

sigstore/_cli.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from sigstore._internal.fulcio.client import ExpiredCertificate
3939
from sigstore._internal.rekor import _hashedrekord_from_parts
4040
from sigstore._internal.rekor.client import RekorClient
41+
from sigstore._internal.tuf import TrustUpdater
4142
from sigstore._utils import sha256_digest
4243
from sigstore.dsse import StatementBuilder, Subject
4344
from sigstore.dsse._predicate import (
@@ -263,7 +264,13 @@ def _parser() -> argparse.ArgumentParser:
263264
"--staging",
264265
action="store_true",
265266
default=_boolify_env("SIGSTORE_STAGING"),
266-
help="Use sigstore's staging instances, instead of the default production instances",
267+
help="Use sigstore's staging instance, instead of the default production instance",
268+
)
269+
global_instance_options.add_argument(
270+
"--instance",
271+
metavar="URL",
272+
type=str,
273+
help="Use a given Sigstore instance URL, instead of the default production instance",
267274
)
268275
global_instance_options.add_argument(
269276
"--trust-config",
@@ -545,6 +552,20 @@ def _parser() -> argparse.ArgumentParser:
545552
)
546553
_add_shared_oidc_options(get_identity_token)
547554

555+
# `sigstore init-instance`
556+
init_instance = subcommands.add_parser(
557+
"init-instance",
558+
help="Initialize trust for a Sigstore instance",
559+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
560+
parents=[parent_parser],
561+
)
562+
init_instance.add_argument(
563+
"root",
564+
metavar="ROOT",
565+
type=Path,
566+
help="The TUF root metadata for the instance",
567+
)
568+
548569
# `sigstore plumbing`
549570
plumbing = subcommands.add_parser(
550571
"plumbing",
@@ -626,6 +647,8 @@ def main(args: list[str] | None = None) -> None:
626647
_verify_github(args)
627648
elif args.subcommand == "get-identity-token":
628649
_get_identity_token(args)
650+
elif args.subcommand == "init-instance":
651+
_init_instance(args)
629652
elif args.subcommand == "plumbing":
630653
if args.plumbing_subcommand == "fix-bundle":
631654
_fix_bundle(args)
@@ -636,6 +659,24 @@ def main(args: list[str] | None = None) -> None:
636659
except Error as e:
637660
e.log_and_exit(_logger, args.verbose >= 1)
638661

662+
def _init_instance(args: argparse.Namespace) -> None:
663+
"""
664+
Initialize trust for a Sigstore instance
665+
"""
666+
root: Path = args.root
667+
instance: str | None = args.instance
668+
if not root.is_file():
669+
_invalid_arguments(args, f"Input must be a file: {root}")
670+
if instance is None:
671+
_invalid_arguments(args, "init-instance requires '--instance URL'")
672+
673+
try:
674+
TrustUpdater.trust_instance(instance, root)
675+
except ValueError as e:
676+
# instance is already configured
677+
_logger.warning(e)
678+
679+
639680

640681
def _get_identity_token(args: argparse.Namespace) -> None:
641682
"""
@@ -1210,13 +1251,18 @@ def _get_trust_config(args: argparse.Namespace) -> ClientTrustConfig:
12101251
Return the client trust configuration (Sigstore service URLs, key material and lifetimes)
12111252
12121253
The configuration may come from explicit argument (--trust-config) or from the TUF
1213-
repository of the used Sigstore instance.
1254+
repository of the used Sigstore instance (--staging or --instance).
12141255
"""
12151256
# Not all commands provide --offline
12161257
offline = getattr(args, "offline", False)
12171258

12181259
if args.trust_config:
12191260
trust_config = ClientTrustConfig.from_json(args.trust_config.read_text())
1261+
elif args.instance:
1262+
try:
1263+
trust_config = ClientTrustConfig.from_tuf(args.instance, offline=offline)
1264+
except FileNotFoundError:
1265+
raise ValueError(f"Instance {args.instance} trust seems to not be initialized.")
12201266
elif args.staging:
12211267
trust_config = ClientTrustConfig.staging(offline=offline)
12221268
else:

sigstore/_internal/tuf.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import logging
2222
from functools import lru_cache
2323
from pathlib import Path
24+
import shutil
2425
from urllib import parse
2526

2627
import platformdirs
@@ -47,7 +48,10 @@ def _get_dirs(url: str) -> tuple[Path, Path]:
4748
app_name = "sigstore-python"
4849
app_author = "sigstore"
4950

50-
repo_base = parse.quote(url, safe="")
51+
# not canonicalization, just handling trailing slash as common mistake:
52+
_url = url.rstrip("/")
53+
54+
repo_base = parse.quote(_url, safe="")
5155

5256
tuf_data_dir = Path(platformdirs.user_data_dir(app_name, app_author)) / "tuf"
5357
tuf_cache_dir = Path(platformdirs.user_cache_dir(app_name, app_author)) / "tuf"
@@ -123,6 +127,22 @@ def __init__(self, url: str, offline: bool = False) -> None:
123127
except Exception as e:
124128
raise TUFError("Failed to refresh TUF metadata") from e
125129

130+
@classmethod
131+
def trust_instance(self, url: str, root: Path) -> None:
132+
"""Trust a new Sigstore instance.
133+
134+
No checks are made for the validity of the root metadata.
135+
"""
136+
metadata_dir, _ = _get_dirs(url)
137+
metadata_dir.mkdir(parents=True, exist_ok=True)
138+
139+
dst = metadata_dir / "root.json"
140+
if dst.is_file():
141+
raise ValueError(f"Instance {url} has already been initialized in {dst}")
142+
143+
shutil.copyfile(root, dst)
144+
145+
126146
@lru_cache()
127147
def get_trusted_root_path(self) -> str:
128148
"""Return local path to currently valid trusted root file"""

0 commit comments

Comments
 (0)