Skip to content

Commit f4296c0

Browse files
committed
trust: Support operator field, support multiple services
* Change the SigningConfig API so that it returns actual clients (like RekorClient): this may seem unusual but makes sense because SigningConfig must know which services this client supports, this means the code is simplest if it directly returns correct clients (e.g. when rekor has two separate clients for v1 and v2). This allows keeping version, operator, etc as SigningConfig imlementation details * There is one exception to previous point: get_oidc_url() returns string, not Issuer object. I could make this change as well to be consistent, it just requires a small refactor (because currently Issuer makes a http request on construction and that seems bad, especially for testing) * Support operator field: This is used to ensure we only return one service version per operator Signed-off-by: Jussi Kukkonen <jkukkonen@google.com>
1 parent 6ae464b commit f4296c0

File tree

3 files changed

+72
-39
lines changed

3 files changed

+72
-39
lines changed

sigstore/_internal/trust.py

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from __future__ import annotations
2020

21+
from collections import defaultdict
2122
from collections.abc import Iterable
2223
from dataclasses import dataclass
2324
from datetime import datetime, timezone
@@ -56,6 +57,9 @@
5657
TrustedRoot as _TrustedRoot,
5758
)
5859

60+
from sigstore._internal.fulcio.client import FulcioClient
61+
from sigstore._internal.rekor.client import RekorClient
62+
from sigstore._internal.timestamp import TimestampAuthorityClient
5963
from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater
6064
from sigstore._utils import (
6165
KeyID,
@@ -66,6 +70,12 @@
6670
)
6771
from sigstore.errors import Error, MetadataError, TUFError, VerificationError
6872

73+
# Versions supported by this client
74+
REKOR_VERSIONS = [1]
75+
TSA_VERSIONS = [1]
76+
FULCIO_VERSIONS = [1]
77+
OIDC_VERSIONS = [1]
78+
6979

7080
def _is_timerange_valid(period: TimeRange | None, *, allow_expired: bool) -> bool:
7181
"""
@@ -323,13 +333,6 @@ def __init__(self, inner: _SigningConfig):
323333
@api private
324334
"""
325335
self._inner = inner
326-
self._verify()
327-
328-
def _verify(self) -> None:
329-
"""
330-
Performs various feats of heroism to ensure that the signing config
331-
is well-formed.
332-
"""
333336

334337
# must have a recognized media type.
335338
try:
@@ -346,6 +349,14 @@ def _verify(self) -> None:
346349
if self._inner.tsa_config.selector != ServiceSelector.ANY:
347350
raise Error(f"unsupported TSA selector {self._inner.tsa_config.selector}")
348351

352+
# Create lists of service protos that are valid & supported by this client
353+
self._tlogs = self._get_valid_services(
354+
self._inner.rekor_tlog_urls, REKOR_VERSIONS
355+
)
356+
self._tsas = self._get_valid_services(self._inner.tsa_urls, TSA_VERSIONS)
357+
self._fulcios = self._get_valid_services(self._inner.ca_urls, FULCIO_VERSIONS)
358+
self._oidcs = self._get_valid_services(self._inner.oidc_urls, OIDC_VERSIONS)
359+
349360
@classmethod
350361
def from_file(
351362
cls,
@@ -355,55 +366,67 @@ def from_file(
355366
inner = _SigningConfig().from_json(Path(path).read_bytes())
356367
return cls(inner)
357368

358-
@staticmethod
359-
def _get_valid_service_url(services: list[Service]) -> str | None:
369+
def _get_valid_services(
370+
self, services: list[Service], valid_versions: list[int]
371+
) -> list[Service]:
372+
"""Return supported services, taking SigningConfig restrictions into account"""
373+
374+
# split logs by operator, only include valid services
375+
logs_by_operator: dict[str, list[Service]] = defaultdict(list)
360376
for service in services:
361-
if service.major_api_version != 1:
377+
if service.major_api_version not in valid_versions:
362378
continue
363379

364380
if not _is_timerange_valid(service.valid_for, allow_expired=False):
365381
continue
366-
return service.url
367-
return None
368382

369-
def get_tlog_urls(self) -> list[str]:
383+
logs_by_operator[service.operator].append(service)
384+
385+
# return a list of services but make sure we only return logs of one version per operator
386+
result: list[Service] = []
387+
for logs in logs_by_operator.values():
388+
logs.sort(key=lambda s: s.major_api_version)
389+
max_version = logs[-1].major_api_version
390+
391+
while logs and logs[-1].major_api_version == max_version:
392+
result.append(logs.pop())
393+
394+
return result
395+
396+
def get_tlogs(self) -> list[RekorClient]:
370397
"""
371398
Returns the rekor transparency logs that client should sign with.
372-
Currently only returns a single one but could in future return several
373399
"""
374400

375-
url = self._get_valid_service_url(self._inner.rekor_tlog_urls)
376-
if not url:
401+
if not self._tlogs:
377402
raise Error("No valid Rekor transparency log found in signing config")
378-
return [url]
403+
return [RekorClient(tlog.url) for tlog in self._tlogs]
379404

380-
def get_fulcio_url(self) -> str:
405+
def get_fulcio(self) -> FulcioClient:
381406
"""
382407
Returns url for the fulcio instance that client should use to get a
383408
signing certificate from
384409
"""
385-
url = self._get_valid_service_url(self._inner.ca_urls)
386-
if not url:
410+
if not self._fulcios:
387411
raise Error("No valid Fulcio CA found in signing config")
388-
return url
412+
return FulcioClient(self._fulcios[0].url)
389413

390414
def get_oidc_url(self) -> str:
391415
"""
392416
Returns url for the OIDC provider that client should use to interactively
393417
authenticate.
394418
"""
395-
url = self._get_valid_service_url(self._inner.oidc_urls)
396-
if not url:
419+
if not self._oidcs:
397420
raise Error("No valid OIDC provider found in signing config")
398-
return url
421+
return self._oidcs[0].url
399422

400-
def get_tsa_urls(self) -> list[str]:
423+
def get_tsas(self) -> list[TimestampAuthorityClient]:
401424
"""
402-
Returns timestamp authority API end points. Currently returns a single one
403-
but may return more in future.
425+
Returns timestamp authority API end points.
404426
"""
405-
url = self._get_valid_service_url(self._inner.tsa_urls)
406-
return [] if url is None else [url]
427+
if not self._tsas:
428+
raise Error("No valid Rekor transparency log found in signing config")
429+
return [TimestampAuthorityClient(s.url) for s in self._tsas]
407430

408431

409432
class TrustedRoot:

sigstore/sign.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,10 @@ def from_trust_config(cls, trust_config: ClientTrustConfig) -> SigningContext:
332332
"""
333333
signing_config = trust_config.signing_config
334334
return cls(
335-
fulcio=FulcioClient(signing_config.get_fulcio_url()),
336-
rekor=RekorClient(signing_config.get_tlog_urls()[0]),
335+
fulcio=signing_config.get_fulcio(),
336+
rekor=signing_config.get_tlogs()[0],
337337
trusted_root=trust_config.trusted_root,
338-
tsa_clients=[
339-
TimestampAuthorityClient(url) for url in signing_config.get_tsa_urls()
340-
],
338+
tsa_clients=signing_config.get_tsas(),
341339
)
342340

343341
@contextmanager

test/unit/internal/test_trust.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
from cryptography.x509 import load_pem_x509_certificate
2222
from sigstore_protobuf_specs.dev.sigstore.common.v1 import TimeRange
2323

24+
from sigstore._internal.fulcio.client import FulcioClient
25+
from sigstore._internal.rekor.client import RekorClient
26+
from sigstore._internal.timestamp import TimestampAuthorityClient
2427
from sigstore._internal.trust import (
2528
CertificateAuthority,
2629
ClientTrustConfig,
@@ -56,12 +59,21 @@ def test_good(self, asset):
5659
signing_config._inner.media_type
5760
== SigningConfig.SigningConfigType.SIGNING_CONFIG_0_2.value
5861
)
59-
assert signing_config.get_fulcio_url() == "https://fulcio.example.com"
62+
63+
fulcio = signing_config.get_fulcio()
64+
assert isinstance(fulcio, FulcioClient)
65+
assert fulcio.url == "https://fulcio.example.com"
6066
assert signing_config.get_oidc_url() == "https://oauth2.example.com/auth"
61-
assert signing_config.get_tlog_urls() == ["https://rekor.example.com"]
62-
assert signing_config.get_tsa_urls() == [
63-
"https://timestamp.example.com/api/v1/timestamp"
64-
]
67+
68+
tlogs = signing_config.get_tlogs()
69+
assert len(tlogs) == 1
70+
assert isinstance(tlogs[0], RekorClient)
71+
assert tlogs[0].url == "https://rekor.example.com/api/v1"
72+
73+
tsas = signing_config.get_tsas()
74+
assert len(tsas) == 1
75+
assert isinstance(tsas[0], TimestampAuthorityClient)
76+
assert tsas[0].url == "https://timestamp.example.com/api/v1/timestamp"
6577

6678

6779
class TestTrustedRoot:

0 commit comments

Comments
 (0)