Skip to content

Commit 7c891db

Browse files
committed
Merge branch 'main' into ww/refactor-trust
Signed-off-by: William Woodruff <william@trailofbits.com>
2 parents f7f3307 + dbab104 commit 7c891db

File tree

6 files changed

+141
-50
lines changed

6 files changed

+141
-50
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ All versions prior to 0.9.0 are untracked.
3232
enabling consistent "BYO PKI" uses of `sigstore` with a single flag
3333
([#1010](https://github.com/sigstore/sigstore-python/pull/1010))
3434

35+
* CLI: The `sigstore verify` subcommands can now verify bundles containing
36+
DSSE entries, such as those produced by
37+
[GitHub Artifact Attestations](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)
38+
([#1015](https://github.com/sigstore/sigstore-python/pull/1015))
39+
3540
### Removed
3641

3742
* **BREAKING API CHANGE**: `SigningResult` has been removed.
@@ -96,6 +101,13 @@ All versions prior to 0.9.0 are untracked.
96101
contains. No functional changes have been made to it
97102
([#1016](https://github.com/sigstore/sigstore-python/pull/1016))
98103

104+
* API: `policy.Identity` now takes an **optional** OIDC issuer, rather than a
105+
required one ([#1015](https://github.com/sigstore/sigstore-python/pull/1015))
106+
107+
* CLI: `sigstore verify github` now requires `--cert-identity` **or**
108+
`--repository`, not just `--cert-identity`
109+
([#1015](https://github.com/sigstore/sigstore-python/pull/1015))
110+
99111
## [2.1.5]
100112

101113
## Fixed

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ to by a particular OIDC provider (like `https://github.com/login/oauth`).
190190
<!-- @begin-sigstore-verify-identity-help@ -->
191191
```
192192
usage: sigstore verify identity [-h] [-v] [--certificate FILE]
193-
[--signature FILE] [--bundle FILE]
194-
--cert-identity IDENTITY [--offline]
195-
--cert-oidc-issuer URL
193+
[--signature FILE] [--bundle FILE] [--offline]
194+
--cert-identity IDENTITY --cert-oidc-issuer
195+
URL
196196
FILE [FILE ...]
197197

198198
optional arguments:
@@ -211,11 +211,11 @@ Verification inputs:
211211
FILE The file to verify
212212

213213
Verification options:
214+
--offline Perform offline verification; requires a Sigstore
215+
bundle (default: False)
214216
--cert-identity IDENTITY
215217
The identity to check for in the certificate's Subject
216218
Alternative Name (default: None)
217-
--offline Perform offline verification; requires a Sigstore
218-
bundle (default: False)
219219
--cert-oidc-issuer URL
220220
The OIDC issuer URL to check for in the certificate's
221221
OIDC issuer extension (default: None)
@@ -232,13 +232,13 @@ claims more precisely than `sigstore verify identity` allows:
232232
<!-- @begin-sigstore-verify-github-help@ -->
233233
```
234234
usage: sigstore verify github [-h] [-v] [--certificate FILE]
235-
[--signature FILE] [--bundle FILE]
236-
--cert-identity IDENTITY [--offline]
237-
[--trigger EVENT] [--sha SHA] [--name NAME]
238-
[--repository REPO] [--ref REF]
235+
[--signature FILE] [--bundle FILE] [--offline]
236+
[--cert-identity IDENTITY] [--trigger EVENT]
237+
[--sha SHA] [--name NAME] [--repository REPO]
238+
[--ref REF]
239239
FILE [FILE ...]
240240

241-
optional arguments:
241+
options:
242242
-h, --help show this help message and exit
243243
-v, --verbose run with additional debug logging; supply multiple
244244
times to increase verbosity (default: 0)
@@ -254,11 +254,11 @@ Verification inputs:
254254
FILE The file to verify
255255

256256
Verification options:
257+
--offline Perform offline verification; requires a Sigstore
258+
bundle (default: False)
257259
--cert-identity IDENTITY
258260
The identity to check for in the certificate's Subject
259261
Alternative Name (default: None)
260-
--offline Perform offline verification; requires a Sigstore
261-
bundle (default: False)
262262
--trigger EVENT The GitHub Actions event name that triggered the
263263
workflow (default: None)
264264
--sha SHA The `git` commit SHA that the workflow run was invoked

sigstore/_cli.py

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from cryptography.x509 import load_pem_x509_certificate
2727
from rich.logging import RichHandler
2828

29-
from sigstore import __version__
29+
from sigstore import __version__, dsse
3030
from sigstore._internal.fulcio.client import ExpiredCertificate
3131
from sigstore._internal.rekor import _hashedrekord_from_parts
3232
from sigstore._internal.trust import ClientTrustConfig
@@ -122,14 +122,6 @@ def _add_shared_verify_input_options(group: argparse._ArgumentGroup) -> None:
122122

123123

124124
def _add_shared_verification_options(group: argparse._ArgumentGroup) -> None:
125-
group.add_argument(
126-
"--cert-identity",
127-
metavar="IDENTITY",
128-
type=str,
129-
default=os.getenv("SIGSTORE_CERT_IDENTITY"),
130-
help="The identity to check for in the certificate's Subject Alternative Name",
131-
required=True,
132-
)
133125
group.add_argument(
134126
"--offline",
135127
action="store_true",
@@ -326,6 +318,14 @@ def _parser() -> argparse.ArgumentParser:
326318

327319
verification_options = verify_identity.add_argument_group("Verification options")
328320
_add_shared_verification_options(verification_options)
321+
verification_options.add_argument(
322+
"--cert-identity",
323+
metavar="IDENTITY",
324+
type=str,
325+
default=os.getenv("SIGSTORE_CERT_IDENTITY"),
326+
help="The identity to check for in the certificate's Subject Alternative Name",
327+
required=True,
328+
)
329329
verification_options.add_argument(
330330
"--cert-oidc-issuer",
331331
metavar="URL",
@@ -348,6 +348,13 @@ def _parser() -> argparse.ArgumentParser:
348348

349349
verification_options = verify_github.add_argument_group("Verification options")
350350
_add_shared_verification_options(verification_options)
351+
verification_options.add_argument(
352+
"--cert-identity",
353+
metavar="IDENTITY",
354+
type=str,
355+
default=os.getenv("SIGSTORE_CERT_IDENTITY"),
356+
help="The identity to check for in the certificate's Subject Alternative Name",
357+
)
351358
verification_options.add_argument(
352359
"--trigger",
353360
dest="workflow_trigger",
@@ -732,28 +739,36 @@ def _verify_identity(args: argparse.Namespace) -> None:
732739
)
733740

734741
try:
735-
verifier.verify_artifact(
736-
input_=hashed,
737-
bundle=bundle,
738-
policy=policy_,
739-
)
742+
_verify_common(verifier, hashed, bundle, policy_)
740743
print(f"OK: {file}")
741-
except VerificationError as exc:
744+
except Error as exc:
742745
_logger.error(f"FAIL: {file}")
743746
exc.log_and_exit(_logger, args.verbose >= 1)
744747

745748

746749
def _verify_github(args: argparse.Namespace) -> None:
747-
# Every GitHub verification begins with an identity policy,
748-
# for which we know the issuer URL ahead of time.
749-
# We then add more policies, as configured by the user's passed-in options.
750-
inner_policies: list[policy.VerificationPolicy] = [
751-
policy.Identity(
752-
identity=args.cert_identity,
753-
issuer="https://token.actions.githubusercontent.com",
750+
inner_policies: list[policy.VerificationPolicy] = []
751+
752+
# We require at least one of `--cert-identity` or `--repository`,
753+
# to minimize the risk of user confusion about what's being verified.
754+
if not (args.cert_identity or args.workflow_repository):
755+
_die(args, "--cert-identity or --repository is required")
756+
757+
# No matter what the user configures above, we require the OIDC issuer to
758+
# be GitHub Actions.
759+
inner_policies.append(
760+
policy.OIDCIssuer("https://token.actions.githubusercontent.com")
761+
)
762+
763+
if args.cert_identity:
764+
inner_policies.append(
765+
policy.Identity(
766+
identity=args.cert_identity,
767+
# We always explicitly check the issuer below, so configuring
768+
# it here is unnecessary.
769+
issuer=None,
770+
)
754771
)
755-
]
756-
757772
if args.workflow_trigger:
758773
inner_policies.append(policy.GitHubWorkflowTrigger(args.workflow_trigger))
759774
if args.workflow_sha:
@@ -770,13 +785,47 @@ def _verify_github(args: argparse.Namespace) -> None:
770785
verifier, materials = _collect_verification_state(args)
771786
for file, hashed, bundle in materials:
772787
try:
773-
verifier.verify_artifact(input_=hashed, bundle=bundle, policy=policy_)
788+
_verify_common(verifier, hashed, bundle, policy_)
774789
print(f"OK: {file}")
775-
except VerificationError as exc:
790+
except Error as exc:
776791
_logger.error(f"FAIL: {file}")
777792
exc.log_and_exit(_logger, args.verbose >= 1)
778793

779794

795+
def _verify_common(
796+
verifier: Verifier,
797+
hashed: Hashed,
798+
bundle: Bundle,
799+
policy_: policy.VerificationPolicy,
800+
) -> None:
801+
"""
802+
Common verification handling.
803+
804+
This dispatches to either artifact or DSSE verification, depending on
805+
`bundle`'s inner type.
806+
"""
807+
808+
# If the bundle specifies a DSSE envelope, perform DSSE verification
809+
# and assert that the inner payload is an in-toto statement bound
810+
# to a subject matching the input's digest.
811+
if bundle._dsse_envelope:
812+
type_, payload = verifier.verify_dsse(bundle=bundle, policy=policy_)
813+
if type_ != dsse.Envelope._TYPE:
814+
raise VerificationError(f"expected JSON payload for DSSE, got {type_}")
815+
816+
stmt = dsse.Statement(payload)
817+
if not stmt._matches_digest(hashed):
818+
raise VerificationError(
819+
f"in-toto statement has no subject for digest {hashed.digest.hex()}"
820+
)
821+
else:
822+
verifier.verify_artifact(
823+
input_=hashed,
824+
bundle=bundle,
825+
policy=policy_,
826+
)
827+
828+
780829
def _get_identity(args: argparse.Namespace) -> Optional[IdentityToken]:
781830
token = None
782831
if not args.oidc_disable_ambient_providers:

sigstore/dsse.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
from cryptography.hazmat.primitives import hashes
2626
from cryptography.hazmat.primitives.asymmetric import ec
2727
from pydantic import BaseModel, ConfigDict, Field, RootModel, StrictStr, ValidationError
28+
from sigstore_protobuf_specs.dev.sigstore.common.v1 import HashAlgorithm
2829
from sigstore_protobuf_specs.io.intoto import Envelope as _Envelope
2930
from sigstore_protobuf_specs.io.intoto import Signature
3031

31-
from sigstore.errors import VerificationError
32+
from sigstore.errors import Error, VerificationError
33+
from sigstore.hashes import Hashed
3234

3335
_logger = logging.getLogger(__name__)
3436

@@ -97,9 +99,28 @@ def __init__(self, contents: bytes) -> None:
9799
"""
98100
self._contents = contents
99101
try:
100-
self._statement = _Statement.model_validate_json(contents)
102+
self._inner = _Statement.model_validate_json(contents)
101103
except ValidationError:
102-
raise ValueError("malformed in-toto statement")
104+
raise Error("malformed in-toto statement")
105+
106+
def _matches_digest(self, digest: Hashed) -> bool:
107+
"""
108+
Returns a boolean indicating whether this in-toto Statement contains a subject
109+
matching the given digest. The subject's name is **not** checked.
110+
111+
No digests other than SHA256 are currently supported.
112+
"""
113+
if digest.algorithm != HashAlgorithm.SHA2_256:
114+
raise VerificationError(f"unexpected digest algorithm: {digest.algorithm}")
115+
116+
for sub in self._inner.subjects:
117+
sub_digest = sub.digest.root.get("sha256")
118+
if sub_digest is None:
119+
continue
120+
if sub_digest == digest.digest.hex():
121+
return True
122+
123+
return False
103124

104125
def _pae(self) -> bytes:
105126
"""
@@ -160,7 +181,7 @@ def build(self) -> Statement:
160181
predicate=self._predicate,
161182
)
162183
except ValidationError as e:
163-
raise ValueError(f"invalid statement: {e}")
184+
raise Error(f"invalid statement: {e}")
164185

165186
return Statement(stmt.model_dump_json(by_alias=True).encode())
166187

sigstore/hashes.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from pydantic import BaseModel
2323
from sigstore_protobuf_specs.dev.sigstore.common.v1 import HashAlgorithm
2424

25+
from sigstore.errors import Error
26+
2527

2628
class Hashed(BaseModel):
2729
"""
@@ -44,12 +46,12 @@ def _as_hashedrekord_algorithm(self) -> rekor_types.hashedrekord.Algorithm:
4446
"""
4547
if self.algorithm == HashAlgorithm.SHA2_256:
4648
return rekor_types.hashedrekord.Algorithm.SHA256
47-
raise ValueError(f"unknown hash algorithm: {self.algorithm}")
49+
raise Error(f"unknown hash algorithm: {self.algorithm}")
4850

4951
def _as_prehashed(self) -> Prehashed:
5052
"""
5153
Returns an appropriate Cryptography `Prehashed` for this `Hashed`.
5254
"""
5355
if self.algorithm == HashAlgorithm.SHA2_256:
5456
return Prehashed(hashes.SHA256())
55-
raise ValueError(f"unknown hash algorithm: {self.algorithm}")
57+
raise Error(f"unknown hash algorithm: {self.algorithm}")

sigstore/verify/policy.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def verify(self, cert: Certificate) -> None:
107107
raise VerificationError(
108108
(
109109
f"Certificate's {self.__class__.__name__} does not match "
110-
f"(got {ext_value}, expected {self._value})"
110+
f"(got '{ext_value}', expected '{self._value}')"
111111
)
112112
)
113113

@@ -441,26 +441,33 @@ def verify(self, cert: Certificate) -> None:
441441
class Identity:
442442
"""
443443
Verifies the certificate's "identity", corresponding to the X.509v3 SAN.
444-
Identities are verified modulo an OIDC issuer, so the issuer's URI
445-
is also required.
444+
445+
Identities can be verified modulo an OIDC issuer, to prevent an unexpected
446+
issuer from offering a particular identity.
446447
447448
Supported SAN types include emails, URIs, and Sigstore-specific "other names".
448449
"""
449450

450-
def __init__(self, *, identity: str, issuer: str):
451+
_issuer: OIDCIssuer | None
452+
453+
def __init__(self, *, identity: str, issuer: str | None = None):
451454
"""
452455
Create a new `Identity`, with the given expected identity and issuer values.
453456
"""
454457

455458
self._identity = identity
456-
self._issuer = OIDCIssuer(issuer)
459+
if issuer:
460+
self._issuer = OIDCIssuer(issuer)
461+
else:
462+
self._issuer = None
457463

458464
def verify(self, cert: Certificate) -> None:
459465
"""
460466
Verify `cert` against the policy.
461467
"""
462468

463-
self._issuer.verify(cert)
469+
if self._issuer:
470+
self._issuer.verify(cert)
464471

465472
# Build a set of all valid identities.
466473
san_ext = cert.extensions.get_extension_for_class(SubjectAlternativeName).value

0 commit comments

Comments
 (0)