-
Notifications
You must be signed in to change notification settings - Fork 62
trust: Support operator field, support multiple services #1407
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
Changes from all commits
f4296c0
e54457c
2543a7d
59bbb24
0dac9f4
9647148
f1c7cfd
b1344ef
9125aad
a69b590
10e37f9
eb657c3
6c24b67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ | |
|
||
from __future__ import annotations | ||
|
||
from collections import defaultdict | ||
from collections.abc import Iterable | ||
from dataclasses import dataclass | ||
from datetime import datetime, timezone | ||
|
@@ -46,6 +47,7 @@ | |
) | ||
from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import ( | ||
Service, | ||
ServiceConfiguration, | ||
ServiceSelector, | ||
TransparencyLogInstance, | ||
) | ||
|
@@ -56,6 +58,9 @@ | |
TrustedRoot as _TrustedRoot, | ||
) | ||
|
||
from sigstore._internal.fulcio.client import FulcioClient | ||
from sigstore._internal.rekor.client import RekorClient | ||
from sigstore._internal.timestamp import TimestampAuthorityClient | ||
from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater | ||
from sigstore._utils import ( | ||
KeyID, | ||
|
@@ -66,6 +71,12 @@ | |
) | ||
from sigstore.errors import Error, MetadataError, TUFError, VerificationError | ||
|
||
# Versions supported by this client | ||
REKOR_VERSIONS = [1] | ||
TSA_VERSIONS = [1] | ||
FULCIO_VERSIONS = [1] | ||
OIDC_VERSIONS = [1] | ||
|
||
|
||
def _is_timerange_valid(period: TimeRange | None, *, allow_expired: bool) -> bool: | ||
""" | ||
|
@@ -323,28 +334,34 @@ def __init__(self, inner: _SigningConfig): | |
@api private | ||
""" | ||
self._inner = inner | ||
self._verify() | ||
|
||
def _verify(self) -> None: | ||
""" | ||
Performs various feats of heroism to ensure that the signing config | ||
is well-formed. | ||
""" | ||
|
||
# must have a recognized media type. | ||
try: | ||
SigningConfig.SigningConfigType(self._inner.media_type) | ||
except ValueError: | ||
raise Error(f"unsupported signing config format: {self._inner.media_type}") | ||
|
||
# currently not supporting other select modes | ||
# TODO: Support other modes ensuring tsa_urls() and tlog_urls() work | ||
if self._inner.rekor_tlog_config.selector != ServiceSelector.ANY: | ||
raise Error( | ||
f"unsupported tlog selector {self._inner.rekor_tlog_config.selector}" | ||
) | ||
if self._inner.tsa_config.selector != ServiceSelector.ANY: | ||
raise Error(f"unsupported TSA selector {self._inner.tsa_config.selector}") | ||
# Create lists of service protos that are valid, selected by the service | ||
# configuration & supported by this client | ||
self._tlogs = self._get_valid_services( | ||
jku marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self._inner.rekor_tlog_urls, REKOR_VERSIONS, self._inner.rekor_tlog_config | ||
) | ||
if not self._tlogs: | ||
raise Error("No valid Rekor transparency log found in signing config") | ||
|
||
self._tsas = self._get_valid_services( | ||
self._inner.tsa_urls, TSA_VERSIONS, self._inner.tsa_config | ||
) | ||
|
||
self._fulcios = self._get_valid_services( | ||
self._inner.ca_urls, FULCIO_VERSIONS, None | ||
) | ||
if not self._fulcios: | ||
raise Error("No valid Fulcio CA found in signing config") | ||
|
||
self._oidcs = self._get_valid_services( | ||
self._inner.oidc_urls, OIDC_VERSIONS, None | ||
) | ||
woodruffw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@classmethod | ||
def from_file( | ||
|
@@ -356,54 +373,73 @@ def from_file( | |
return cls(inner) | ||
|
||
@staticmethod | ||
def _get_valid_service_url(services: list[Service]) -> str | None: | ||
def _get_valid_services( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jku Sorry about the late review. The only change I might suggest is to sort by newest to oldest rather than assuming it's already sorted. In the Sigstore TUF trusted root, we'll have them sorted already. This is something I am adding to sigstore-go's implementation now, sigstore-java already added this - sigstore/sigstore-go@main...haydentherapper:sigstore-go:operator#diff-35123d504dffe1b547e48e14acb7c7174416a48566d50e5a6b75e898c5667a4fR121-R123 |
||
services: list[Service], | ||
supported_versions: list[int], | ||
config: ServiceConfiguration | None, | ||
) -> list[Service]: | ||
"""Return supported services, taking SigningConfig restrictions into account""" | ||
|
||
# split services by operator, only include valid services | ||
services_by_operator: dict[str, list[Service]] = defaultdict(list) | ||
for service in services: | ||
if service.major_api_version != 1: | ||
if service.major_api_version not in supported_versions: | ||
continue | ||
|
||
if not _is_timerange_valid(service.valid_for, allow_expired=False): | ||
continue | ||
return service.url | ||
return None | ||
|
||
def get_tlog_urls(self) -> list[str]: | ||
services_by_operator[service.operator].append(service) | ||
|
||
# build a list of services but make sure we only include one service per operator | ||
# and use the highest version available for that operator | ||
result: list[Service] = [] | ||
for op_services in services_by_operator.values(): | ||
op_services.sort(key=lambda s: s.major_api_version) | ||
result.append(op_services[-1]) | ||
|
||
# Depending on ServiceSelector, prune the result list | ||
if not config or config.selector == ServiceSelector.ALL: | ||
return result | ||
|
||
if config.selector == ServiceSelector.UNDEFINED: | ||
raise ValueError("Undefined is not a valid signing config ServiceSelector") | ||
|
||
# handle EXACT and ANY selectors | ||
count = config.count if config.selector == ServiceSelector.EXACT else 1 | ||
jku marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if len(result) < count: | ||
raise ValueError( | ||
f"Expected {count} services in signing config, found {len(result)}" | ||
) | ||
|
||
return result[:count] | ||
|
||
def get_tlogs(self) -> list[RekorClient]: | ||
""" | ||
Returns the rekor transparency logs that client should sign with. | ||
Currently only returns a single one but could in future return several | ||
Returns the rekor transparency log clients to sign with. | ||
""" | ||
return [RekorClient(tlog.url) for tlog in self._tlogs] | ||
|
||
url = self._get_valid_service_url(self._inner.rekor_tlog_urls) | ||
if not url: | ||
raise Error("No valid Rekor transparency log found in signing config") | ||
return [url] | ||
|
||
def get_fulcio_url(self) -> str: | ||
def get_fulcio(self) -> FulcioClient: | ||
""" | ||
Returns url for the fulcio instance that client should use to get a | ||
signing certificate from | ||
Returns a Fulcio client to get a signing certificate from | ||
""" | ||
url = self._get_valid_service_url(self._inner.ca_urls) | ||
if not url: | ||
raise Error("No valid Fulcio CA found in signing config") | ||
return url | ||
return FulcioClient(self._fulcios[0].url) | ||
|
||
def get_oidc_url(self) -> str: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This method is now the odd one out: it could return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This design looks reasonable to me; doing it in a followup sounds good! |
||
""" | ||
Returns url for the OIDC provider that client should use to interactively | ||
authenticate. | ||
""" | ||
url = self._get_valid_service_url(self._inner.oidc_urls) | ||
if not url: | ||
if not self._oidcs: | ||
raise Error("No valid OIDC provider found in signing config") | ||
return url | ||
return self._oidcs[0].url | ||
|
||
def get_tsa_urls(self) -> list[str]: | ||
def get_tsas(self) -> list[TimestampAuthorityClient]: | ||
""" | ||
Returns timestamp authority API end points. Currently returns a single one | ||
but may return more in future. | ||
Returns timestamp authority clients for urls configured in signing config. | ||
""" | ||
url = self._get_valid_service_url(self._inner.tsa_urls) | ||
return [] if url is None else [url] | ||
return [TimestampAuthorityClient(s.url) for s in self._tsas] | ||
|
||
|
||
class TrustedRoot: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,6 @@ | |
"selector": "ANY" | ||
}, | ||
"tsaConfig": { | ||
"selector": "ANY" | ||
"selector": "ALL" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick:
SigningConfig
is a private API, so perhaps we should omit it from the CHANGELOG? Alternatively, if you think it's important to include from a developer/maintainer visibility perspective, maybe we should include "this is a private API" in some form in the CHANGELOG entry so users don't think we're committing to a public interface here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, I was thinking of this as well while adding the entry. In a way SigningConfig semi-public since the signing_config property in ClientTrustConfig is currently public -- and ClientTrustConfig is in practice exposed through the sign and verify modules...
I think the intent of the API now is that ClientTrustConfig is public (so users can e.g. call
from_production()
) but the internals of ClientTrustConfig are not... so in that way this is not an API change but it's debatable.I might leave it as is for now: We need to rewrite the changelog anyway for next release (combine related changes, try to make a coherent log).