Skip to content

Use trust config everywhere #1363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2c9a4ff
cli: Get OIDC url from trust config
jku Apr 30, 2025
750547d
trust: Provide methods to load TrustConfig from tuf
jku Apr 30, 2025
05cc633
Use TrustConfig to initialize components
jku Apr 30, 2025
296f8b6
Remove tuf methods from TrustedRoot
jku Apr 30, 2025
91121ee
Update staging assets, refactor TUF asset lookup
jku May 2, 2025
ddc0b04
README: Update default oidc-issuer value
jku May 5, 2025
cc608e3
cli: Parse --offline carefully: not all commands have the flag
jku May 5, 2025
bb0126a
trust: TSA are not required in signing config
jku May 5, 2025
37195cc
CHANGELOG: Mention signing config from TUF
jku May 6, 2025
f94c3f0
Issuer: Remove from_trust_config()
jku May 6, 2025
a71f387
tests: Fake datetime.now() when using mock staging
jku May 9, 2025
51f54ed
Merge branch 'main' into use-trust-config-everywhere
jku May 12, 2025
96fc3b6
tests: Add explanation to oidc failure
jku May 13, 2025
1d45514
CHANGELOG: List all API changes WRT trust config
jku May 13, 2025
caa09d2
Merge branch 'main' into use-trust-config-everywhere
jku May 14, 2025
2236d97
ClientTrustConfig: Add comment with bug link
jku May 16, 2025
99b4f6d
Makefile: Update sigstore/_store/ paths
jku May 16, 2025
8321ada
cli: Refactor --offline parsing
jku May 16, 2025
6ee0ba4
CHANGELOG: Fix spelling
jku May 16, 2025
0e1aff7
Merge branch 'main' into use-trust-config-everywhere
jku May 16, 2025
6cae423
Merge branch 'main' into use-trust-config-everywhere
jku May 19, 2025
4551017
Merge branch 'main' into use-trust-config-everywhere
jku May 19, 2025
5e5e4af
Merge branch 'main' into use-trust-config-everywhere
jku May 20, 2025
023cc4b
Merge branch 'main' into use-trust-config-everywhere
jku May 21, 2025
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
23 changes: 20 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ All versions prior to 0.9.0 are untracked.
* TSA: Changed the Timestamp Authority requests to explicitly use sha256 for message digests.
[#1373](https://github.com/sigstore/sigstore-python/pull/1373)

* Fixed the certificate calidity period check for Timestamp Authorities (TSA).
Certificates need not have and end date, while still requiring a start date.
* Fixed the certificate validity period check for Timestamp Authorities (TSA).
Certificates need not have an end date, while still requiring a start date.
[#1368](https://github.com/sigstore/sigstore-python/pull/1368)

* API: Make Rekor APIs compatible with Rekor v2 by removing trailing slashes
* Made Rekor client more compatible with Rekor v2 by removing trailing slashes
from endpoints ([#1366](https://github.com/sigstore/sigstore-python/pull/1366))

* Verify: verify that all established times (timestamps or the log integration time)
Expand All @@ -38,8 +38,25 @@ All versions prior to 0.9.0 are untracked.

### Changed

* API:
* ClientTrustConfig now provides methods `production()`, `staging()`and `from_tuf()`
to get access to current client configuration (trusted keys & certificates,
URLs and their validity periods). [#1363](https://github.com/sigstore/sigstore-python/pull/1363)
* `--trust-config` now requires a file with SigningConfig v0.2, and is able to fully
configure the used Sigstore instance [#1358]/(https://github.com/sigstore/sigstore-python/pull/1358)
* By default (when `--trust-config` is not used) the whole trust configuration now
comes from the TUF repository [#1363](https://github.com/sigstore/sigstore-python/pull/1363)

### Removed
* API:
* `Issuer.production()` and `Issuer.staging()` have been removed: Use
`Issuer()` instead with relevant URL. The current public good production and
staging URLs are available via the `ClientTrustConfig` object.
[#1363](https://github.com/sigstore/sigstore-python/pull/1363)
* `SigningContext.production()` and `SigningContext.staging()` have been removed:
Use `SigningContext.from_trust_config()` instead.
[#1363](https://github.com/sigstore/sigstore-python/pull/1363)


## [3.6.2]

Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,6 @@ update-embedded-root: $(VENV)/pyvenv.cfg
. $(VENV_BIN)/activate && \
python -m sigstore plumbing update-trust-root
cp ~/.local/share/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/root.json \
sigstore/_store/prod/root.json
sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/root.json
cp ~/.cache/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/trusted_root.json \
sigstore/_store/prod/trusted_root.json
sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/trusted_root.json
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ OpenID Connect options:
--oidc-disable-ambient-providers
Disable ambient OpenID Connect credential detection
(e.g. on GitHub Actions) (default: False)
--oidc-issuer URL The OpenID Connect issuer to use (conflicts with
--staging) (default: https://oauth2.sigstore.dev/auth)
--oidc-issuer URL The OpenID Connect issuer to use (default: None)
--oauth-force-oob Force an out-of-band OAuth flow and do not
automatically start the default web browser (default:
False)
Expand Down Expand Up @@ -185,8 +184,7 @@ OpenID Connect options:
--oidc-disable-ambient-providers
Disable ambient OpenID Connect credential detection
(e.g. on GitHub Actions) (default: False)
--oidc-issuer URL The OpenID Connect issuer to use (conflicts with
--staging) (default: https://oauth2.sigstore.dev/auth)
--oidc-issuer URL The OpenID Connect issuer to use (default: None)
--oauth-force-oob Force an out-of-band OAuth flow and do not
automatically start the default web browser (default:
False)
Expand Down
89 changes: 48 additions & 41 deletions sigstore/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from sigstore._internal.fulcio.client import ExpiredCertificate
from sigstore._internal.rekor import _hashedrekord_from_parts
from sigstore._internal.rekor.client import RekorClient
from sigstore._internal.trust import ClientTrustConfig, TrustedRoot
from sigstore._internal.trust import ClientTrustConfig
from sigstore._utils import sha256_digest
from sigstore.dsse import StatementBuilder, Subject
from sigstore.dsse._predicate import (
Expand All @@ -51,7 +51,6 @@
from sigstore.hashes import Hashed
from sigstore.models import Bundle, InvalidBundle
from sigstore.oidc import (
DEFAULT_OAUTH_ISSUER_URL,
ExpiredIdentity,
IdentityToken,
Issuer,
Expand Down Expand Up @@ -229,8 +228,8 @@ def _add_shared_oidc_options(
"--oidc-issuer",
metavar="URL",
type=str,
default=os.getenv("SIGSTORE_OIDC_ISSUER", DEFAULT_OAUTH_ISSUER_URL),
help="The OpenID Connect issuer to use (conflicts with --staging)",
default=os.getenv("SIGSTORE_OIDC_ISSUER", None),
help="The OpenID Connect issuer to use",
)
group.add_argument(
"--oauth-force-oob",
Expand Down Expand Up @@ -614,11 +613,7 @@ def main(args: list[str] | None = None) -> None:
elif args.verify_subcommand == "github":
_verify_github(args)
elif args.subcommand == "get-identity-token":
identity = _get_identity(args)
if identity:
print(identity)
else:
_invalid_arguments(args, "No identity token supplied or detected!")
_get_identity_token(args)
elif args.subcommand == "plumbing":
if args.plumbing_subcommand == "fix-bundle":
_fix_bundle(args)
Expand All @@ -630,6 +625,17 @@ def main(args: list[str] | None = None) -> None:
e.log_and_exit(_logger, args.verbose >= 1)


def _get_identity_token(args: argparse.Namespace) -> None:
"""
Output the OIDC authentication token
"""
identity = _get_identity(args, _get_trust_config(args))
if identity:
print(identity)
else:
_invalid_arguments(args, "No identity token supplied or detected!")


def _sign_common(
args: argparse.Namespace, output_map: OutputMap, predicate: dict[str, Any] | None
) -> None:
Expand All @@ -643,17 +649,8 @@ def _sign_common(
not, it will use a hashedrekord.
"""
# Select the signing context to use.
if args.staging:
_logger.debug("sign: staging instances requested")
signing_ctx = SigningContext.staging()
elif args.trust_config:
trust_config = ClientTrustConfig.from_json(args.trust_config.read_text())
signing_ctx = SigningContext._from_trust_config(trust_config)
else:
# If the user didn't request the staging instance or pass in an
# explicit client trust config, we're using the public good (i.e.
# production) instance.
signing_ctx = SigningContext.production()
trust_config = _get_trust_config(args)
signing_ctx = SigningContext.from_trust_config(trust_config)

# The order of precedence for identities is as follows:
#
Expand All @@ -664,7 +661,7 @@ def _sign_common(
if args.identity_token:
identity = IdentityToken(args.identity_token)
else:
identity = _get_identity(args)
identity = _get_identity(args, trust_config)

if not identity:
_invalid_arguments(args, "No identity token supplied or detected!")
Expand Down Expand Up @@ -1009,14 +1006,8 @@ def _collect_verification_state(
f"Missing verification materials for {(hashed)}: {', '.join(missing)}",
)

if args.staging:
_logger.debug("verify: staging instances requested")
verifier = Verifier.staging(offline=args.offline)
elif args.trust_config:
trust_config = ClientTrustConfig.from_json(args.trust_config.read_text())
verifier = Verifier._from_trust_config(trust_config)
else:
verifier = Verifier.production(offline=args.offline)
trust_config = _get_trust_config(args)
verifier = Verifier(trusted_root=trust_config.trusted_root)

all_materials = []
for file_or_hashed, materials in input_map.items():
Expand Down Expand Up @@ -1167,7 +1158,27 @@ def _verify_common(
return None


def _get_identity(args: argparse.Namespace) -> Optional[IdentityToken]:
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.
"""
# Not all commands provide --offline
offline = getattr(args, "offline", False)

if args.trust_config:
return ClientTrustConfig.from_json(args.trust_config.read_text())
elif args.staging:
return ClientTrustConfig.staging(offline=offline)
else:
return ClientTrustConfig.production(offline=offline)


def _get_identity(
args: argparse.Namespace, trust_config: ClientTrustConfig
) -> Optional[IdentityToken]:
token = None
if not args.oidc_disable_ambient_providers:
token = detect_credential()
Expand All @@ -1176,12 +1187,10 @@ def _get_identity(args: argparse.Namespace) -> Optional[IdentityToken]:
if token:
return IdentityToken(token)

if args.staging:
issuer = Issuer.staging()
elif args.oidc_issuer == DEFAULT_OAUTH_ISSUER_URL:
issuer = Issuer.production()
else:
if args.oidc_issuer is not None:
issuer = Issuer(args.oidc_issuer)
else:
issuer = Issuer(trust_config.signing_config.get_oidc_url())

if args.oidc_client_secret is None:
args.oidc_client_secret = "" # nosec: B105
Expand All @@ -1198,6 +1207,7 @@ def _get_identity(args: argparse.Namespace) -> Optional[IdentityToken]:
def _fix_bundle(args: argparse.Namespace) -> None:
# NOTE: We could support `--trusted-root` here in the future,
# for custom Rekor instances.

rekor = RekorClient.staging() if args.staging else RekorClient.production()

raw_bundle = RawBundle.from_dict(json.loads(args.bundle.read_bytes()))
Expand Down Expand Up @@ -1234,13 +1244,10 @@ def _fix_bundle(args: argparse.Namespace) -> None:


def _update_trust_root(args: argparse.Namespace) -> None:
# Simply creating the TrustedRoot in online mode is enough to perform
# Simply creating the TrustConfig in online mode is enough to perform
# a metadata update.
if args.staging:
trusted_root = TrustedRoot.staging(offline=False)
else:
trusted_root = TrustedRoot.production(offline=False)

config = _get_trust_config(args)
_console.print(
f"Trust root updated: {len(trusted_root.get_fulcio_certs())} Fulcio certificates"
f"Trust root & signing config updated: {len(config.trusted_root.get_fulcio_certs())} Fulcio certificates"
)
18 changes: 1 addition & 17 deletions sigstore/_internal/fulcio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@

_logger = logging.getLogger(__name__)

DEFAULT_FULCIO_URL = "https://fulcio.sigstore.dev"
STAGING_FULCIO_URL = "https://fulcio.sigstage.dev"
SIGNING_CERT_ENDPOINT = "/api/v2/signingCert"
TRUST_BUNDLE_ENDPOINT = "/api/v2/trustBundle"

Expand Down Expand Up @@ -163,7 +161,7 @@ def get(self) -> FulcioTrustBundleResponse:
class FulcioClient:
"""The internal Fulcio client"""

def __init__(self, url: str = DEFAULT_FULCIO_URL) -> None:
def __init__(self, url: str) -> None:
"""Initialize the client"""
_logger.debug(f"Fulcio client using URL: {url}")
self.url = url
Expand All @@ -180,20 +178,6 @@ def __del__(self) -> None:
"""
self.session.close()

@classmethod
def production(cls) -> FulcioClient:
"""
Returns a `FulcioClient` for the Sigstore production instance of Fulcio.
"""
return cls(DEFAULT_FULCIO_URL)

@classmethod
def staging(cls) -> FulcioClient:
"""
Returns a `FulcioClient` for the Sigstore staging instance of Fulcio.
"""
return cls(STAGING_FULCIO_URL)

@property
def signing_cert(self) -> FulcioSigningCert:
"""
Expand Down
Loading