Skip to content

Commit 5c503a6

Browse files
committed
Merge remote-tracking branch 'origin/main' into rekov2-client
2 parents cba9507 + 5d9b210 commit 5c503a6

File tree

15 files changed

+307
-89
lines changed

15 files changed

+307
-89
lines changed

.github/workflows/scorecards-analysis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
persist-credentials: false
3030

3131
- name: "Run analysis"
32-
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
32+
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
3333
with:
3434
results_file: results.sarif
3535
results_format: sarif
@@ -52,6 +52,6 @@ jobs:
5252

5353
# Upload the results to GitHub's code scanning dashboard.
5454
- name: "Upload to code-scanning"
55-
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
55+
uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
5656
with:
5757
sarif_file: results.sarif

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ All versions prior to 0.9.0 are untracked.
1616

1717
* Added support for ed25519 keys.
1818
[#1377](https://github.com/sigstore/sigstore-python/pull/1377)
19+
* API: `IdentityToken` now supports `client_id` for audience claim validation.
20+
[#1402](https://github.com/sigstore/sigstore-python/pull/1402)
21+
1922

2023
* Added a `RekorV2Client` for posting new entries to a Rekor V2 instance.
2124
[#1400](https://github.com/sigstore/sigstore-python/pull/1400)
@@ -27,6 +30,10 @@ All versions prior to 0.9.0 are untracked.
2730
* TSA: Changed the Timestamp Authority requests to explicitly use sha256 for message digests.
2831
[#1373](https://github.com/sigstore/sigstore-python/pull/1373)
2932

33+
* TSA: Correctly verify timestamps with hashes other than SHA-256. Currently supported
34+
algorithms are SHA-256, SHA-384, SHA-512.
35+
[#1373](https://github.com/sigstore/sigstore-python/pull/1373)
36+
3037
* Fixed the certificate validity period check for Timestamp Authorities (TSA).
3138
Certificates need not have an end date, while still requiring a start date.
3239
[#1368](https://github.com/sigstore/sigstore-python/pull/1368)
@@ -39,6 +46,10 @@ All versions prior to 0.9.0 are untracked.
3946
still required.
4047
[#1381](https://github.com/sigstore/sigstore-python/pull/1381)
4148

49+
* Verify: Avoid hard failure if trusted root contains unsupported keytypes (as verification
50+
may succeed without that key).
51+
[#1424](https://github.com/sigstore/sigstore-python/pull/1424)
52+
4253
* CI: Timestamp Authority tests use latest release, not latest tag, of
4354
[sigstore/timestamp-authority](https://github.com/sigstore/timestamp-authority)
4455
[#1377](https://github.com/sigstore/sigstore-python/pull/1377)
@@ -53,6 +64,9 @@ All versions prior to 0.9.0 are untracked.
5364
* ClientTrustConfig now provides methods `production()`, `staging()`and `from_tuf()`
5465
to get access to current client configuration (trusted keys & certificates,
5566
URLs and their validity periods). [#1363](https://github.com/sigstore/sigstore-python/pull/1363)
67+
* SigningConfig now has methods that return actual clients (like `RekorClient`) instead of
68+
just URLs. The returned clients are also filtered according to SigningConfig contents.
69+
[#1407](https://github.com/sigstore/sigstore-python/pull/1407)
5670
* `--trust-config` now requires a file with SigningConfig v0.2, and is able to fully
5771
configure the used Sigstore instance [#1358]/(https://github.com/sigstore/sigstore-python/pull/1358)
5872
* By default (when `--trust-config` is not used) the whole trust configuration now

mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,4 @@ extra:
8080
- icon: fontawesome/brands/slack
8181
link: https://sigstore.slack.com
8282
- icon: fontawesome/brands/x-twitter
83-
link: https://twitter.com/projectsigstore
83+
link: https://twitter.com/projectsigstore

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ dependencies = [
3636
"requests",
3737
"rich >= 13,< 15",
3838
"rfc8785 ~= 0.1.2",
39-
"rfc3161-client >= 0.1.2,< 1.1.0",
39+
"rfc3161-client >= 1.0.2,< 1.1.0",
4040
# NOTE(ww): Both under active development, so strictly pinned.
4141
"sigstore-protobuf-specs == 0.4.2",
4242
"sigstore-rekor-types == 0.0.18",
@@ -62,7 +62,7 @@ lint = [
6262
"mypy ~= 1.1",
6363
# NOTE(ww): ruff is under active development, so we pin conservatively here
6464
# and let Dependabot periodically perform this update.
65-
"ruff < 0.11.12",
65+
"ruff < 0.11.13",
6666
"types-requests",
6767
"types-pyOpenSSL",
6868
]

sigstore/_cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -659,7 +659,7 @@ def _sign_common(
659659
# 3) Interactive OAuth flow
660660
identity: IdentityToken | None
661661
if args.identity_token:
662-
identity = IdentityToken(args.identity_token)
662+
identity = IdentityToken(args.identity_token, args.oidc_client_id)
663663
else:
664664
identity = _get_identity(args, trust_config)
665665

@@ -1181,11 +1181,11 @@ def _get_identity(
11811181
) -> Optional[IdentityToken]:
11821182
token = None
11831183
if not args.oidc_disable_ambient_providers:
1184-
token = detect_credential()
1184+
token = detect_credential(args.oidc_client_id)
11851185

11861186
# Happy path: we've detected an ambient credential, so we can return early.
11871187
if token:
1188-
return IdentityToken(token)
1188+
return IdentityToken(token, args.oidc_client_id)
11891189

11901190
if args.oidc_issuer is not None:
11911191
issuer = Issuer(args.oidc_issuer)

sigstore/_internal/trust.py

Lines changed: 86 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
from __future__ import annotations
2020

21+
import logging
22+
from collections import defaultdict
2123
from collections.abc import Iterable
2224
from dataclasses import dataclass
2325
from datetime import datetime, timezone
@@ -46,6 +48,7 @@
4648
)
4749
from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import (
4850
Service,
51+
ServiceConfiguration,
4952
ServiceSelector,
5053
TransparencyLogInstance,
5154
)
@@ -56,6 +59,9 @@
5659
TrustedRoot as _TrustedRoot,
5760
)
5861

62+
from sigstore._internal.fulcio.client import FulcioClient
63+
from sigstore._internal.rekor.client import RekorClient
64+
from sigstore._internal.timestamp import TimestampAuthorityClient
5965
from sigstore._internal.tuf import DEFAULT_TUF_URL, STAGING_TUF_URL, TrustUpdater
6066
from sigstore._utils import (
6167
KeyID,
@@ -66,6 +72,14 @@
6672
)
6773
from sigstore.errors import Error, MetadataError, TUFError, VerificationError
6874

75+
# Versions supported by this client
76+
REKOR_VERSIONS = [1]
77+
TSA_VERSIONS = [1]
78+
FULCIO_VERSIONS = [1]
79+
OIDC_VERSIONS = [1]
80+
81+
_logger = logging.getLogger(__name__)
82+
6983

7084
def _is_timerange_valid(period: TimeRange | None, *, allow_expired: bool) -> bool:
7185
"""
@@ -189,8 +203,11 @@ def __init__(self, public_keys: list[_PublicKey] = []):
189203
self._keyring: dict[KeyID, Key] = {}
190204

191205
for public_key in public_keys:
192-
key = Key(public_key)
193-
self._keyring[key.key_id] = key
206+
try:
207+
key = Key(public_key)
208+
self._keyring[key.key_id] = key
209+
except VerificationError as e:
210+
_logger.warning(f"Failed to load a trusted root key: {e}")
194211

195212
def verify(self, *, key_id: KeyID, signature: bytes, data: bytes) -> None:
196213
"""
@@ -323,28 +340,34 @@ def __init__(self, inner: _SigningConfig):
323340
@api private
324341
"""
325342
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-
"""
333343

334344
# must have a recognized media type.
335345
try:
336346
SigningConfig.SigningConfigType(self._inner.media_type)
337347
except ValueError:
338348
raise Error(f"unsupported signing config format: {self._inner.media_type}")
339349

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}")
350+
# Create lists of service protos that are valid, selected by the service
351+
# configuration & supported by this client
352+
self._tlogs = self._get_valid_services(
353+
self._inner.rekor_tlog_urls, REKOR_VERSIONS, self._inner.rekor_tlog_config
354+
)
355+
if not self._tlogs:
356+
raise Error("No valid Rekor transparency log found in signing config")
357+
358+
self._tsas = self._get_valid_services(
359+
self._inner.tsa_urls, TSA_VERSIONS, self._inner.tsa_config
360+
)
361+
362+
self._fulcios = self._get_valid_services(
363+
self._inner.ca_urls, FULCIO_VERSIONS, None
364+
)
365+
if not self._fulcios:
366+
raise Error("No valid Fulcio CA found in signing config")
367+
368+
self._oidcs = self._get_valid_services(
369+
self._inner.oidc_urls, OIDC_VERSIONS, None
370+
)
348371

349372
@classmethod
350373
def from_file(
@@ -356,54 +379,73 @@ def from_file(
356379
return cls(inner)
357380

358381
@staticmethod
359-
def _get_valid_service_url(services: list[Service]) -> str | None:
382+
def _get_valid_services(
383+
services: list[Service],
384+
supported_versions: list[int],
385+
config: ServiceConfiguration | None,
386+
) -> list[Service]:
387+
"""Return supported services, taking SigningConfig restrictions into account"""
388+
389+
# split services by operator, only include valid services
390+
services_by_operator: dict[str, list[Service]] = defaultdict(list)
360391
for service in services:
361-
if service.major_api_version != 1:
392+
if service.major_api_version not in supported_versions:
362393
continue
363394

364395
if not _is_timerange_valid(service.valid_for, allow_expired=False):
365396
continue
366-
return service.url
367-
return None
368397

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

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:
429+
def get_fulcio(self) -> FulcioClient:
381430
"""
382-
Returns url for the fulcio instance that client should use to get a
383-
signing certificate from
431+
Returns a Fulcio client to get a signing certificate from
384432
"""
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
433+
return FulcioClient(self._fulcios[0].url)
389434

390435
def get_oidc_url(self) -> str:
391436
"""
392437
Returns url for the OIDC provider that client should use to interactively
393438
authenticate.
394439
"""
395-
url = self._get_valid_service_url(self._inner.oidc_urls)
396-
if not url:
440+
if not self._oidcs:
397441
raise Error("No valid OIDC provider found in signing config")
398-
return url
442+
return self._oidcs[0].url
399443

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

408450

409451
class TrustedRoot:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@
3434
"selector": "ANY"
3535
},
3636
"tsaConfig": {
37-
"selector": "ANY"
37+
"selector": "ALL"
3838
}
3939
}

sigstore/oidc.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"https://oauth2.sigstage.dev/auth": "email",
4242
"https://token.actions.githubusercontent.com": "sub",
4343
}
44-
_DEFAULT_AUDIENCE = "sigstore"
44+
45+
_DEFAULT_CLIENT_ID = "sigstore"
4546

4647

4748
class _OpenIDConfiguration(BaseModel):
@@ -66,7 +67,7 @@ class IdentityToken:
6667
a sensible subject, issuer, and audience for Sigstore purposes.
6768
"""
6869

69-
def __init__(self, raw_token: str) -> None:
70+
def __init__(self, raw_token: str, client_id: str = _DEFAULT_CLIENT_ID) -> None:
7071
"""
7172
Create a new `IdentityToken` from the given OIDC token.
7273
"""
@@ -90,7 +91,7 @@ def __init__(self, raw_token: str) -> None:
9091
# See: https://openid.net/specs/openid-connect-basic-1_0.html#IDToken
9192
"require": ["aud", "sub", "iat", "exp", "iss"],
9293
},
93-
audience=_DEFAULT_AUDIENCE,
94+
audience=client_id,
9495
# NOTE: This leeway shouldn't be strictly necessary, but is
9596
# included to preempt any (small) skew between the host
9697
# and the originating IdP.
@@ -270,7 +271,7 @@ def __init__(self, base_url: str) -> None:
270271

271272
def identity_token( # nosec: B107
272273
self,
273-
client_id: str = "sigstore",
274+
client_id: str = _DEFAULT_CLIENT_ID,
274275
client_secret: str = "",
275276
force_oob: bool = False,
276277
) -> IdentityToken:
@@ -350,7 +351,7 @@ def identity_token( # nosec: B107
350351
if token_error is not None:
351352
raise IdentityError(f"Error response from token endpoint: {token_error}")
352353

353-
return IdentityToken(token_json["access_token"])
354+
return IdentityToken(token_json["access_token"], client_id)
354355

355356

356357
class IdentityError(Error):
@@ -402,9 +403,10 @@ def diagnostics(self) -> str:
402403
"""
403404

404405

405-
def detect_credential() -> Optional[str]:
406+
def detect_credential(client_id: str = _DEFAULT_CLIENT_ID) -> Optional[str]:
406407
"""Calls `id.detect_credential`, but wraps exceptions with our own exception type."""
408+
407409
try:
408-
return cast(Optional[str], id.detect_credential(_DEFAULT_AUDIENCE))
410+
return cast(Optional[str], id.detect_credential(client_id))
409411
except id.IdentityError as exc:
410412
IdentityError.raise_from_id(exc)

sigstore/sign.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,10 @@ def from_trust_config(cls, trust_config: ClientTrustConfig) -> SigningContext:
299299
"""
300300
signing_config = trust_config.signing_config
301301
return cls(
302-
fulcio=FulcioClient(signing_config.get_fulcio_url()),
303-
rekor=RekorClient(signing_config.get_tlog_urls()[0]),
302+
fulcio=signing_config.get_fulcio(),
303+
rekor=signing_config.get_tlogs()[0],
304304
trusted_root=trust_config.trusted_root,
305-
tsa_clients=[
306-
TimestampAuthorityClient(url) for url in signing_config.get_tsa_urls()
307-
],
305+
tsa_clients=signing_config.get_tsas(),
308306
)
309307

310308
@contextmanager

0 commit comments

Comments
 (0)