8000 Merge branch 'main' into use-correct-hash-algo · jku/sigstore-python@c6eae36 · GitHub
[go: up one dir, main page]

Skip to content

Commit c6eae36

Browse files
authored
Merge branch 'main' into use-correct-hash-algo
2 parents 9c0828a + 06e0ae2 commit c6eae36

File tree

5 files changed

+224
-53
lines changed

5 files changed

+224
-53
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ All versions prior to 0.9.0 are untracked.
5454
* ClientTrustConfig now provides methods `production()`, `staging()`and `from_tuf()`
5555
to get access to current client configuration (trusted keys & certificates,
5656
URLs and their validity periods). [#1363](https://github.com/sigstore/sigstore-python/pull/1363)
57+
* SigningConfig now has methods that return actual clients (like `RekorClient`) instead of
58+
just URLs. The returned clients are also filtered according to SigningConfig contents.
59+
[#1407](https://github.com/sigstore/sigstore-python/pull/1407)
5760
* `--trust-config` now requires a file with SigningConfig v0.2, and is able to fully
5861
configure the used Sigstore instance [#1358]/(https://github.com/sigstore/sigstore-python/pull/1358)
5962
* By default (when `--trust-config` is not used) the whole trust configuration now

sigstore/_internal/trust.py

Lines changed: 78 additions & 42 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
@@ -46,6 +47,7 @@
4647
)
4748
from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import (
4849
Service,
50+
ServiceConfiguration,
4951
ServiceSelector,
5052
TransparencyLogInstance,
5153
)
@@ -56,6 +58,9 @@
5658
TrustedRoot as _TrustedRoot,
5759
)
5860

61+
from sigstore._internal.fulcio.client import FulcioClient
62+
from sigstore._internal.rekor.client import RekorClient
63+
from sigstore._internal.timestamp import TimestampAuthorityClient
5964
from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater
6065
from sigstore._utils import (
6166
KeyID,
@@ -66,6 +71,12 @@
6671
)
6772
from sigstore.errors import Error, MetadataError, TUFError, VerificationError
6873

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

70 A3E2 81
def _is_timerange_valid(period: TimeRange | None, *, allow_expired: bool) -> bool:
7182
"""
@@ -323,28 +334,34 @@ def __init__(self, inner: _SigningConfig):
323334
@api private
324335
"""
325336
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-
"""
333337

334338
# must have a recognized media type.
335339
try:
336340
SigningConfig.SigningConfigType(self._inner.media_type)
337341
except ValueError:
338342
raise Error(f"unsupported signing config format: {self._inner.media_type}")
339343

340-
# currently not supporting other select modes
341-
# TODO: Support other modes ensuring tsa_urls() and tlog_urls() work
342-
if self._inner.rekor_tlog_config.selector != ServiceSelector.ANY:
343-
raise Error(
344-
f"unsupported tlog selector {self._inner.rekor_tlog_config.selector}"
345-
)
346-
if self._inner.tsa_config.selector != ServiceSelector.ANY:
347-
raise Error(f"unsupported TSA selector {self._inner.tsa_config.selector}")
344+
# Create lists of service protos that are valid, selected by the service
345+
# configuration & supported by this client
346+
self._tlogs = self._get_valid_services(
347+
self._inner.rekor_tlog_urls, REKOR_VERSIONS, self._inner.rekor_tlog_config
348+
)
349+
if not self._tlogs:
350+
raise Error("No valid Rekor transparency log found in signing config")
351+
352+
self._tsas = self._get_valid_services(
353+
self._inner.tsa_urls, TSA_VERSIONS, self._inner.tsa_config
354+
)
355+
356+
self._fulcios = self._get_valid_services(
357+
self._inner.ca_urls, FULCIO_VERSIONS, None
358+
)
359+
if not self._fulcios:
360+
raise Error("No valid Fulcio CA found in signing config")
361+
362+
self._oidcs = self._get_valid_services(
363+
self._inner.oidc_urls, OIDC_VERSIONS, None
364+
)
348365

349366
@classmethod
350367
def from_file(
@@ -356,54 +373,73 @@ def from_file(
356373
return cls(inner)
357374

358375
@staticmethod
359-
def _get_valid_service_url(services: list[Service]) -> str | None:
376+
def _get_valid_services(
377+
services: list[Service],
378+
supported_versions: list[int],
379+
config: ServiceConfiguration | None,
380+
) -> list[Service]:
381+
"""Return supported services, taking SigningConfig restrictions into account"""
382+
383+
# split services by operator, only include valid services
384+
services_by_operator: dict[str, list[Service]] = defaultdict(list)
360385
for service in services:
361-
if service.major_api_version != 1:
386+
if service.major_api_version not in supported_versions:
362387
continue
363388

364389
if not _is_timerange_valid(service.valid_for, allow_expired=False):
365390
continue
366-
return service.url
367-
return None
368391

369-
def get_tlog_urls(self) -> list[str]:
392+
services_by_operator[service.operator].append(service)
393+
394+
# build a list of services but make sure we only include one service per operator
395+
# and use the highest version available for that operator
396+
result: list[Service] = []
397+
for op_services in services_by_operator.values():
398+
op_services.sort(key=lambda s: s.major_api_version)
399+
result.append(op_services[-1])
400+
401+
# Depending on ServiceSelector, prune the result list
402+
if not config or config.selector == ServiceSelector.ALL:
403+
return result
404+
405+
if config.selector == ServiceSelector.UNDEFINED:
406+
raise ValueError("Undefined is not a valid signing config ServiceSelector")
407+
408+
# handle EXACT and ANY selectors
409+
count = config.count if config.selector == ServiceSelector.EXACT else 1
410+
if len(result) < count:
411+
raise ValueError(
412+
f"Expected {count} services in signing config, found {len(result)}"
413+
)
414+
415+
return result[:count]
416+
417+
def get_tlogs(self) -> list[RekorClient]:
370418
"""
371-
Returns the rekor transparency logs that client should sign with.
372-
Currently only returns a single one but could in future return several
419+
Returns the rekor transparency log clients to sign with.
373420
"""
421+
return [RekorClient(tlog.url) for tlog in self._tlogs]
374422

375-
url = self._get_valid_service_url(self._inner.rekor_tlog_urls)
376-
if not url:
377-
raise Error("No valid Rekor transparency log found in signing config")
378-
return [url]
379-
380-
def get_fulcio_url(self) -> str:
423+
def get_fulcio(self) -> FulcioClient:
381424
"""
382-
Returns url for the fulcio instance that client should use to get a
383-
signing certificate from
425+
Returns a Fulcio client to get a signing certificate from
384426
"""
385-
url = self._get_valid_service_url(self._inner.ca_urls)
386-
if not url:
387-
raise Error("No valid Fulcio CA found in signing config")
388-
return url
427+
return FulcioClient(self._fulcios[0].url)
389428

390429
def get_oidc_url(self) -> str:
391430
"""
392431
Returns url for the OIDC provider that client should use to interactively
393432
authenticate.
394433
"""
395-
url = self._get_valid_service_url(self._inner.oidc_urls)
396-
if not url:
434+
if not self._oidcs:
397435
raise Error("No valid OIDC provider found in signing config")
398-
return url
436+
return self._oidcs[0].url
399437

400-
def get_tsa_urls(self) -> list[str]:
438+
def get_tsas(self) -> list[TimestampAuthorityClient]:
401439
"""
402-
Returns timestamp authority API end points. Currently returns a single one
403-
but may return more in future.
440+
Returns timestamp authority clients for urls configured in signing config.
404441
"""
405-
url = self._get_valid_service_url(self._inner.tsa_urls)
406-
return [] if url is None else [url]
442+
return [TimestampAuthorityClient(s.url) for s in self._tsas]
407443

408444

409445
class TrustedRoot:

sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/signing_config.v0.2.json

Lines changed: 1 addition & 1 deletion
< 179B div class="border position-relative rounded-bottom-2">
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@
3434
"selector": "ANY"
3535
},
3636
"tsa F438 Config": {
37-
"selector": "ANY"
37+
"selector": "ALL"
3838
}
3939
}

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: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@
2020
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
2121
from cryptography.x509 import load_pem_x509_certificate
2222
from sigstore_protobuf_specs.dev.sigstore.common.v1 import TimeRange
23+
from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import (
24+
Service,
25+
ServiceConfiguration,
26+
ServiceSelector,
27+
)
2328

29+
from sigstore._internal.fulcio.client import FulcioClient
30+
from sigstore._internal.rekor.client import RekorClient
31+
from sigstore._internal.timestamp import TimestampAuthorityClient
2432
from sigstore._internal.trust import (
2533
CertificateAuthority,
2634
ClientTrustConfig,
@@ -32,6 +40,19 @@
3240
from sigstore._utils import load_pem_public_key
3341
from sigstore.errors import Error
3442

43+
# Test data for TestSigningcconfig
44+
_service_v1_op1 = Service("url1", major_api_version=1, operator="op1")
45+
_service2_v1_op1 = Service("url2", major_api_version=1, operator="op1")
46+
_service_v2_op1 = Service("url3", major_api_version=2, operator="op1")
47+
_service_v1_op2 = Service("url4", major_api_version=1, operator="op2")
48+
_service_v1_op3 = Service("url5", major_api_version=1, operator="op3")
49+
_service_v1_op4 = Service(
50+
"url6",
51+
major_api_version=1,
52+
operator="op4",
53+
valid_for=TimeRange(datetime(3000, 1, 1, tzinfo=timezone.utc)),
54+
)
55+
3556

3657
class TestCertificateAuthority:
3758
def test_good(self, asset):
@@ -56,12 +77,125 @@ def test_good(self, asset):
5677
signing_config._inner.media_type
5778
== SigningConfig.SigningConfigType.SIGNING_CONFIG_0_2.value
5879
)
59-
assert signing_config.get_fulcio_url() == "https://fulcio.example.com"
80+
81+
fulcio = signing_config.get_fulcio()
82+
assert isinstance(fulcio, FulcioClient)
83+
assert fulcio.url == "https://fulcio.example.com"
6084
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-
]
85+
86+
tlogs = signing_config.get_tlogs()
87+
assert len(tlogs) == 1
88+
assert isinstance(tlogs[0], RekorClient)
89+
assert tlogs[0].url == "https://rekor.example.com/api/v1"
90+
91+
tsas = signing_config.get_tsas()
92+
assert len(tsas) == 1
93+
assert isinstance(tsas[0], TimestampAuthorityClient)
94+
assert tsas[0].url == "https://timestamp.example.com/api/v1/timestamp"
95+
96+
@pytest.mark.parametrize(
97+
"services, versions, config, expected_result",
98+
[
99+
pytest.param(
100+
[_service_v1_op1],
101+
[1],
102+
ServiceConfiguration(ServiceSelector.ALL),
103+
[_service_v1_op1],
104+
id="base case",
105+
),
106+
pytest.param(
107+
[_service_v1_op1, _service2_v1_op1],
108+
[1],
109+
ServiceConfiguration(ServiceSelector.ALL),
110+
[_service2_v1_op1],
111+
id="multiple services, same operator: expect 1 service in result",
112+
),
113+
pytest.param(
114+
[_service_v1_op1, _service_v1_op2],
115+
[1],
116+
ServiceConfiguration(ServiceSelector.ALL),
117+
[_service_v1_op1, _service_v1_op2],
118+
id="2 services, different operator: expect 2 services in result",
119+
),
120+
pytest.param(
121+
[_service_v1_op1, _service_v1_op2, _service_v1_op4],
122+
[1],
123+
ServiceConfiguration(ServiceSelector.ALL),
124+
[_service_v1_op1, _service_v1_op2],
125+
id="3 services, one is not yet valid: expect 2 services in result",
126+
),
127+
pytest.param(
128+
[_service_v1_op1, _service_v1_op2],
129+
[1],
130+
ServiceConfiguration(ServiceSelector.ANY),
131+
[_service_v1_op1],
132+
id="ANY selector: expect 1 service only in result",
133+
),
134+
pytest.param(
135+
[_service_v1_op1, _service_v1_op2, _service_v1_op3],
136+
[1],
137+
ServiceConfiguration(ServiceSelector.EXACT, 2),
138+
[_service_v1_op1, _service_v1_op2],
139+
id="EXACT selector: expect configured number of services in result",
140+
),
141+
pytest.param(
142+
[_service_v1_op1, _service_v2_op1],
143+
[1, 2],
144+
ServiceConfiguration(ServiceSelector.ALL),
145+
[_service_v2_op1],
146+
id="services with different version: expect highest version",
147+
),
148+
pytest.param(
149+
[_service_v1_op1, _service_v2_op1],
150+
[1],
151+
ServiceConfiguration(ServiceSelector.ALL),
152+
[_service_v1_op1],
153+
id="services with different version: expect the supported version",
154+
),
155+
pytest.param(
156+
[_service_v1_op1, _service_v1_op2],
157+
[2],
158+
ServiceConfiguration(ServiceSelector.ALL),
159+
[],
160+
id="No supported versions: expect no results",
161+
),
162+
pytest.param(
163+
[_service_v1_op1, _service_v2_op1, _service_v1_op2],
164+
[1],
165+
None,
166+
[_service_v1_op1, _service_v1_op2],
167+
id="services without ServiceConfiguration: expect all supported",
168+
),
169+
],
170+
)
171+
def test_get_valid_services(self, services, versions, config, expected_result):
172+
result = SigningConfig._get_valid_services(services, versions, config)
173+
174+
assert result == expected_result
175+
176+
@pytest.mark.parametrize(
177+
"services, versions, config",
178+
[
179+
( # ANY selector without services
180+
[],
181+
[1],
182+
ServiceConfiguration(ServiceSelector.ANY),
183+
),
184+
( # EXACT selector without enough services
185+
[_service_v1_op1],
186+
[1],
187+
ServiceConfiguration(ServiceSelector.EXACT, 2),
188+
),
189+
( # UNDEFINED selector
190+
[_service_v1_op1],
191+
[1],
192+
ServiceConfiguration(ServiceSelector.UNDEFINED, 1),
193+
),
194+
],
195+
)
196+
def test_get_valid_services_fail(self, services, versions, config):
197+
with pytest.raises(ValueError):
198+
SigningConfig._get_valid_services(services, versions, config)
65199

66200

67201
class TestTrustedRoot:

0 commit comments

Comments
 (0)
0