Skip to content

Commit ad9a001

Browse files
jkuwoodruffw
andauthored
More proto updates (#1358)
* deps: bump protobuf-specs Signed-off-by: William Woodruff <william@trailofbits.com> * more protobuf-specs hackery Signed-off-by: William Woodruff <william@trailofbits.com> * ignore another mypy finding Signed-off-by: William Woodruff <william@trailofbits.com> * cherry-pick 7028110 Signed-off-by: William Woodruff <william@trailofbits.com> * _cli: fix proto mess Signed-off-by: William Woodruff <william@trailofbits.com> * verifier: Remove unused argument Verifier only needs a Rekor client for the detached materials hack in _cli... and it should not be using SigningConfig to get it. * This is an API change for Verifier: I believe the proposed API should be stable now * RekorClient definitely needs more work: I'm just punting the can down the road here. Signed-off-by: Jussi Kukkonen <jkukkonen@google.com> * _internal/trust: Upgrade to SigningConfig 0.2 * Let's forget that v0.1 ever existed (it was not really used): We could try to support both but since 0.1 does not really work, I won't bother * Support signing config v0.2 in a minimal way (see note on selectors below) Things that could be improved: * Rekor client is still a bit of a hack: that area likely needs a redesign * The "service selectors" in SigningConfig are not all yet supported: Only the ANY selector works (this is the one staging will use soon) * The CLI does not yet use the OIDC provider specified in SigningConfig (this should be a small refactor) Signed-off-by: Jussi Kukkonen <jkukkonen@google.com> * CHANGELOG: Mention --trust-config changes Signed-off-by: Jussi Kukkonen <jkukkonen@google.com> --------- Signed-off-by: William Woodruff <william@trailofbits.com> Signed-off-by: Jussi Kukkonen <jkukkonen@google.com> Co-authored-by: William Woodruff <william@trailofbits.com>
1 parent e80427f commit ad9a001

File tree

12 files changed

+372
-70
lines changed

12 files changed

+372
-70
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ All versions prior to 0.9.0 are untracked.
1717
* API: Make Rekor APIs compatible with Rekor v2 by removing trailing slashes
1818
from endpoints ([#1366](https://github.com/sigstore/sigstore-python/pull/1366))
1919

20+
### Changed
21+
22+
* `--trust-config` now requires a file with SigningConfig v0.2, and is able to fully
23+
configure the used Sigstore instance [#1358]/(https://github.com/sigstore/sigstore-python/pull/1358)
24+
2025
## [3.6.2]
2126

2227
### Fixed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ dependencies = [
3838
"rfc8785 ~= 0.1.2",
3939
"rfc3161-client >= 0.1.2,< 1.1.0",
4040
# NOTE(ww): Both under active development, so strictly pinned.
41-
"sigstore-protobuf-specs == 0.3.2",
41+
"sigstore-protobuf-specs == 0.4.1",
4242
"sigstore-rekor-types == 0.0.18",
4343
"tuf ~= 6.0",
4444
"platformdirs ~= 4.2",

sigstore/_cli.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -713,9 +713,7 @@ def _sign_common(
713713
else:
714714
sig_output = sys.stdout
715715

716-
signature = base64.b64encode(
717-
result._inner.message_signature.signature
718-
).decode()
716+
signature = base64.b64encode(result.signature).decode()
719717
print(signature, file=sig_output)
720718
if outputs.signature is not None:
721719
print(f"Signature written to {outputs.signature}")
@@ -1202,7 +1200,7 @@ def _fix_bundle(args: argparse.Namespace) -> None:
12021200
# for custom Rekor instances.
12031201
rekor = RekorClient.staging() if args.staging else RekorClient.production()
12041202

1205-
raw_bundle = RawBundle().from_json(args.bundle.read_text())
1203+
raw_bundle = RawBundle.from_dict(json.loads(args.bundle.read_bytes()))
12061204

12071205
if len(raw_bundle.verification_material.tlog_entries) != 1:
12081206
_fatal("unfixable bundle: must have exactly one log entry")

sigstore/_internal/trust.py

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,13 @@
4545
ClientTrustConfig as _ClientTrustConfig,
4646
)
4747
from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import (
48+
Service,
49+
ServiceSelector,
4850
TransparencyLogInstance,
4951
)
52+
from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import (
53+
SigningConfig as _SigningConfig,
54+
)
5055
from sigstore_protobuf_specs.dev.sigstore.trustroot.v1 import (
5156
TrustedRoot as _TrustedRoot,
5257
)
@@ -93,14 +98,14 @@ class Key:
9398
key: PublicKey
9499
key_id: KeyID
95100

96-
_RSA_SHA_256_DETAILS: ClassVar[set[_PublicKeyDetails]] = {
101+
_RSA_SHA_256_DETAILS: ClassVar = {
97102
_PublicKeyDetails.PKCS1_RSA_PKCS1V5,
98103
_PublicKeyDetails.PKIX_RSA_PKCS1V15_2048_SHA256,
99104
_PublicKeyDetails.PKIX_RSA_PKCS1V15_3072_SHA256,
100105
_PublicKeyDetails.PKIX_RSA_PKCS1V15_4096_SHA256,
101106
}
102107

103-
_EC_DETAILS_TO_HASH: ClassVar[dict[_PublicKeyDetails, hashes.HashAlgorithm]] = {
108+
_EC_DETAILS_TO_HASH: ClassVar = {
104109
_PublicKeyDetails.PKIX_ECDSA_P256_SHA_256: hashes.SHA256(),
105110
_PublicKeyDetails.PKIX_ECDSA_P384_SHA_384: hashes.SHA384(),
106111
_PublicKeyDetails.PKIX_ECDSA_P521_SHA_512: hashes.SHA512(),
@@ -278,6 +283,114 @@ def certificates(self, *, allow_expired: bool) -> list[Certificate]:
278283
return self._certificates
279284

280285

286+
class SigningConfig:
287+
"""
288+
Signing configuration for a Sigstore instance.
289+
"""
290+
291+
class SigningConfigType(str, Enum):
292+
"""
293+
Known Sigstore signing config media types.
294+
"""
295+
296+
SIGNING_CONFIG_0_2 = "application/vnd.dev.sigstore.signingconfig.v0.2+json"
297+
298+
def __str__(self) -> str:
299+
"""Returns the variant's string value."""
300+
return self.value
301+
302+
def __init__(self, inner: _SigningConfig):
303+
"""
304+
Construct a new `SigningConfig`.
305+
306+
@api private
307+
"""
308+
self._inner = inner
309+
self._verify()
310+
311+
def _verify(self) -> None:
312+
"""
313+
Performs various feats of heroism to ensure that the signing config
314+
is well-formed.
315+
"""
316+
317+
# must have a recognized media type.
318+
try:
319+
SigningConfig.SigningConfigType(self._inner.media_type)
320+
except ValueError:
321+
raise Error(f"unsupported signing config format: {self._inner.media_type}")
322+
323+
# currently not supporting other select modes
324+
# TODO: Support other modes ensuring tsa_urls() and tlog_urls() work
325+
if self._inner.rekor_tlog_config.selector != ServiceSelector.ANY:
326+
raise Error(
327+
f"unsupported tlog selector {self._inner.rekor_tlog_config.selector}"
328+
)
329+
if self._inner.tsa_config.selector != ServiceSelector.ANY:
330+
raise Error(f"unsupported TSA selector {self._inner.tsa_config.selector}")
331+
332+
@classmethod
333+
def from_file(
334+
cls,
335+
path: str,
336+
) -> SigningConfig:
337+
"""Create a new signing config from file"""
338+
inner = _SigningConfig().from_json(Path(path).read_bytes())
339+
return cls(inner)
340+
341+
@staticmethod
342+
def _get_valid_service_url(services: list[Service]) -> str | None:
343+
for service in services:
344+
if service.major_api_version != 1:
345+
continue
346+
347+
if not _is_timerange_valid(service.valid_for, allow_expired=False):
348+
continue
349+
return service.url
350+
return None
351+
352+
def get_tlog_urls(self) -> list[str]:
353+
"""
354+
Returns the rekor transparency logs that client should sign with.
355+
Currently only returns a single one but could in future return several
356+
"""
357+
358+
url = self._get_valid_service_url(self._inner.rekor_tlog_urls)
359+
if not url:
360+
raise Error("No valid Rekor transparency log found in signing config")
361+
return [url]
362+
363+
def get_fulcio_url(self) -> str:
364+
"""
365+
Returns url for the fulcio instance that client should use to get a
366+
signing certificate from
367+
"""
368+
url = self._get_valid_service_url(self._inner.ca_urls)
369+
if not url:
370+
raise Error("No valid Fulcio CA found in signing config")
371+
return url
372+
373+
def get_oidc_url(self) -> str:
374+
"""
375+
Returns url for the OIDC provider that client should use to interactively
376+
authenticate.
377+
"""
378+
url = self._get_valid_service_url(self._inner.oidc_urls)
379+
if not url:
380+
raise Error("No valid OIDC provider found in signing config")
381+
return url
382+
383+
def get_tsa_urls(self) -> list[str]:
384+
"""
385+
Returns timestamp authority API end points. Currently returns a single one
386+
but may return more in future.
387+
"""
388+
url = self._get_valid_service_url(self._inner.tsa_urls)
389+
if not url:
390+
raise Error("No valid Timestamp Authority found in signing config")
391+
return [url]
392+
393+
281394
class TrustedRoot:
282395
"""
283396
The cryptographic root(s) of trust for a Sigstore instance.
@@ -473,3 +586,10 @@ def trusted_root(self) -> TrustedRoot:
473586
Return the interior root of trust, as a `TrustedRoot`.
474587
"""
475588
return TrustedRoot(self._inner.trusted_root)
589+
590+
@property
591+
def signing_config(self) -> SigningConfig:
592+
"""
593+
Return the interior root of trust, as a `SigningConfig`.
594+
"""
595+
return SigningConfig(self._inner.signing_config)

sigstore/hashes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,4 @@ def __str__(self) -> str:
6060
"""
6161
Returns a str representation of this `Hashed`.
6262
"""
63-
return f"{self.algorithm.name}:{self.digest.hex()}"
63+
return f"{HashAlgorithm(self.algorithm)}:{self.digest.hex()}"

sigstore/models.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from __future__ import annotations
2020

2121
import base64
22+
import json
2223
import logging
2324
import typing
2425
from enum import Enum
@@ -466,18 +467,18 @@ def _verify(self) -> None:
466467
Bundle.BundleType.BUNDLE_0_3_ALT,
467468
):
468469
# For "v3" bundles, the signing certificate is the only one present.
470+
if not self._inner.verification_material.certificate:
471+
raise InvalidBundle("expected certificate in bundle")
472+
469473
leaf_cert = load_der_x509_certificate(
470474
self._inner.verification_material.certificate.raw_bytes
471475
)
472476
else:
473477
# In older bundles, there is an entire pool (misleadingly called
474478
# a chain) of certificates, the first of which is the signing
475479
# certificate.
476-
certs = (
477-
self._inner.verification_material.x509_certificate_chain.certificates
478-
)
479-
480-
if len(certs) == 0:
480+
chain = self._inner.verification_material.x509_certificate_chain
481+
if not chain or not chain.certificates:
481482
raise InvalidBundle("expected non-empty certificate chain in bundle")
482483

483484
# Per client policy in protobuf-specs: the first entry in the chain
@@ -489,7 +490,7 @@ def _verify(self) -> None:
489490
# and intermediate CAs, so we issue warnings and not hard errors
490491
# in those cases.
491492
leaf_cert, *chain_certs = (
492-
load_der_x509_certificate(cert.raw_bytes) for cert in certs
493+
load_der_x509_certificate(cert.raw_bytes) for cert in chain.certificates
493494
)
494495
if not cert_is_leaf(leaf_cert):
495496
raise InvalidBundle(
@@ -576,8 +577,8 @@ def _dsse_envelope(self) -> dsse.Envelope | None:
576577
577578
@private
578579
"""
579-
if self._inner.dsse_envelope:
580-
return dsse.Envelope(self._inner.dsse_envelope)
580+
if self._inner.is_set("dsse_envelope"):
581+
return dsse.Envelope(self._inner.dsse_envelope) # type: ignore[arg-type]
581582
return None
582583

583584
@property
@@ -589,7 +590,7 @@ def signature(self) -> bytes:
589590
return (
590591
self._dsse_envelope.signature
591592
if self._dsse_envelope
592-
else self._inner.message_signature.signature
593+
else self._inner.message_signature.signature # type: ignore[union-attr]
593594
)
594595

595596
@property
@@ -604,7 +605,7 @@ def from_json(cls, raw: bytes | str) -> Bundle:
604605
"""
605606
Deserialize the given Sigstore bundle.
606607
"""
607-
inner = _Bundle().from_json(raw)
608+
inner = _Bundle.from_dict(json.loads(raw))
608609
return cls(inner)
609610

610611
def to_json(self) -> str:
@@ -623,7 +624,10 @@ def _to_parts(
623624
"""
624625

625626
content: common_v1.MessageSignature | dsse.Envelope
626-
content = self._dsse_envelope or self._inner.message_signature
627+
if self._dsse_envelope:
628+
content = self._dsse_envelope
629+
else:
630+
content = self._inner.message_signature # type: ignore[assignment]
627631

628632
return (self.signing_certificate, content, self.log_entry)
629633

@@ -650,30 +654,32 @@ def _from_parts(
650654
@private
651655
"""
652656

653-
inner = _Bundle(
654-
media_type=Bundle.BundleType.BUNDLE_0_3.value,
655-
verification_material=bundle_v1.VerificationMaterial(
656-
certificate=common_v1.X509Certificate(cert.public_bytes(Encoding.DER)),
657-
),
657+
timestamp_verifcation_data = bundle_v1.TimestampVerificationData(
658+
rfc3161_timestamps=[]
658659
)
660+
if signed_timestamp is not None:
661+
timestamp_verifcation_data.rfc3161_timestamps.extend(
662+
[
663+
Rfc3161SignedTimestamp(signed_timestamp=response.as_bytes())
664+
for response in signed_timestamp
665+
]
666+
)
659667

660668
# Fill in the appropriate variants.
661669
if isinstance(content, common_v1.MessageSignature):
662-
inner.message_signature = content
670+
# mypy will be mystified if types are specified here
671+
content_dict: dict[str, Any] = {"message_signature": content}
663672
else:
664-
inner.dsse_envelope = content._inner
673+
content_dict = {"dsse_envelope": content._inner}
665674

666-
tlog_entry = log_entry._to_rekor()
667-
inner.verification_material.tlog_entries = [tlog_entry]
668-
669-
if signed_timestamp is not None:
670-
inner.verification_material.timestamp_verification_data = (
671-
bundle_v1.TimestampVerificationData(
672-
rfc3161_timestamps=[
673-
Rfc3161SignedTimestamp(signed_timestamp=response.as_bytes())
674-
for response in signed_timestamp
675-
]
676-
)
677-
)
675+
inner = _Bundle(
676+
media_type=Bundle.BundleType.BUNDLE_0_3.value,
677+
verification_material=bundle_v1.VerificationMaterial(
678+
certificate=common_v1.X509Certificate(cert.public_bytes(Encoding.DER)),
679+
tlog_entries=[log_entry._to_rekor()],
680+
timestamp_verification_data=timestamp_verifcation_data,
681+
),
682+
**content_dict,
683+
)
678684

679685
return cls(inner)

sigstore/sign.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,13 +352,13 @@ def _from_trust_config(cls, trust_config: ClientTrustConfig) -> SigningContext:
352352
353353
@api private
354354
"""
355+
signing_config = trust_config.signing_config
355356
return cls(
356-
fulcio=FulcioClient(trust_config._inner.signing_config.ca_url),
357-
rekor=RekorClient(trust_config._inner.signing_config.tlog_urls[0]),
357+
fulcio=FulcioClient(signing_config.get_fulcio_url()),
358+
rekor=RekorClient(signing_config.get_tlog_urls()[0]),
358359
trusted_root=trust_config.trusted_root,
359360
tsa_clients=[
360-
TimestampAuthorityClient(tsa_url)
361-
for tsa_url in trust_config._inner.signing_config.tsa_urls
361+
TimestampAuthorityClient(url) for url in signing_config.get_tsa_urls()
362362
],
363363
)
364364

0 commit comments

Comments
 (0)