From c3533e99835cd31b331db1f9465ea7e90ab86f32 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 3 Apr 2024 19:12:30 -0400 Subject: [PATCH 01/14] verifier: hackety hack on DSSE support Signed-off-by: William Woodruff --- sigstore/_cli.py | 4 +- sigstore/dsse.py | 40 +++++++-- sigstore/errors.py | 6 ++ sigstore/verify/verifier.py | 135 ++++++++++++++++++++++++------ test/unit/verify/test_verifier.py | 16 ++-- 5 files changed, 161 insertions(+), 40 deletions(-) diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 48726421c..f845b8f1e 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -870,7 +870,7 @@ def _verify_identity(args: argparse.Namespace) -> None: issuer=args.cert_oidc_issuer, ) - result = verifier.verify( + result = verifier.verify_artifact( input_=hashed, bundle=bundle, policy=policy_, @@ -909,7 +909,7 @@ def _verify_github(args: argparse.Namespace) -> None: verifier, materials = _collect_verification_state(args) for file, hashed, bundle in materials: - result = verifier.verify(input_=hashed, bundle=bundle, policy=policy_) + result = verifier.verify_artifact(input_=hashed, bundle=bundle, policy=policy_) if result: print(f"OK: {file}") diff --git a/sigstore/dsse.py b/sigstore/dsse.py index 5dfd70b19..8632c442c 100644 --- a/sigstore/dsse.py +++ b/sigstore/dsse.py @@ -21,6 +21,7 @@ import logging from typing import Any, Dict, List, Literal, Optional, Union +from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from pydantic import BaseModel, ConfigDict, Field, RootModel, StrictStr, ValidationError @@ -103,12 +104,7 @@ def _pae(self) -> bytes: Construct the PAE encoding for this statement. """ - # See: - # https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md - # https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md - pae = f"DSSEv1 {len(Envelope._TYPE)} {Envelope._TYPE} ".encode() - pae += b" ".join([str(len(self._contents)).encode(), self._contents]) - return pae + return _pae(Envelope._TYPE, self._contents) class _StatementBuilder: @@ -193,6 +189,19 @@ def to_json(self) -> str: return self._inner.to_json() # type: ignore[no-any-return] +def _pae(type_: str, body: bytes) -> bytes: + """ + Compute the PAE encoding for the given `type_` and `body`. + """ + + # See: + # https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md + # https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md + pae = f"DSSEv1 {len(type_)} {type_} ".encode() + pae += b" ".join([str(len(body)).encode(), body]) + return pae + + def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope: """ Sign for the given in-toto `Statement`, and encapsulate the resulting @@ -209,3 +218,22 @@ def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope: signatures=[Signature(sig=signature, keyid=None)], ) ) + + +def _verify(key: ec.EllipticCurvePublicKey, sig: bytes, evp: Envelope) -> bytes: + """ + Verify the given in-toto `Envelope`, returning the verified inner payload. + + + This function does **not** check the envelope's payload type. The caller + is responsible for performing this check. + """ + + pae = _pae(evp._inner.payload_type, evp._inner.payload) + + try: + key.verify(sig, pae, ec.ECDSA(hashes.SHA256())) + except InvalidSignature: + raise ValueError("invalid signature") + + return evp._inner.payload diff --git a/sigstore/errors.py b/sigstore/errors.py index 5a044bc91..6b4aeffd6 100644 --- a/sigstore/errors.py +++ b/sigstore/errors.py @@ -117,3 +117,9 @@ def diagnostics(self) -> str: Unable to establish root of trust. This error may occur when the resources embedded in this distribution of sigstore-python are out of date.""" + + +class VerificationError(Error): + """ + Raised when a Sigstore verification flow fails. + """ diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 1f4d6d9eb..0529796cb 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -27,6 +27,7 @@ from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509 import ( + Certificate, ExtendedKeyUsage, KeyUsage, ) @@ -54,6 +55,7 @@ ) from sigstore._internal.trustroot import KeyringPurpose, TrustedRoot from sigstore._utils import B64Str, HexStr, sha256_digest +from sigstore.errors import Error, VerificationError from sigstore.hashes import Hashed from sigstore.transparency import InvalidLogEntry from sigstore.verify.models import ( @@ -146,7 +148,90 @@ def staging(cls) -> Verifier: trusted_root=trusted_root, ) - def verify( + def _verify_common_signing_cert( + self, cert: Certificate, policy: VerificationPolicy + ) -> None: + """ + Performs the signing certificate verification steps that are shared between + `verify_intoto` and `verify_artifact`. + + Raises `VerificationError` on all failures. + """ + + # NOTE: The `X509Store` object currently cannot have its time reset once the `set_time` + # method been called on it. To get around this, we construct a new one for every `verify` + # call. + store = X509Store() + # NOTE: By explicitly setting the flags here, we ensure that OpenSSL's + # PARTIAL_CHAIN default does not change on us. Enabling PARTIAL_CHAIN + # would be strictly more conformant of OpenSSL, but we currently + # *want* the "long" chain behavior of performing path validation + # down to a self-signed root. + store.set_flags(X509StoreFlags.X509_STRICT) + for parent_cert_ossl in self._fulcio_certificate_chain: + store.add_cert(parent_cert_ossl) + + # 1. Verify that the signing certificate is signed by the root certificate and that the + # signing certificate was valid at the time of signing. + sign_date = cert.not_valid_before_utc + cert_ossl = X509.from_cryptography(cert) + + store.set_time(sign_date) + store_ctx = X509StoreContext(store, cert_ossl) + try: + # get_verified_chain returns the full chain including the end-entity certificate + # and chain should contain only CA certificates + chain = store_ctx.get_verified_chain()[1:] + except X509StoreContextError as e: + raise VerificationError(f"failed to build chain: {e}") + + # 2. Check that the signing certificate has a valid SCT + sct = _get_precertificate_signed_certificate_timestamps(cert)[0] + try: + verify_sct( + sct, + cert, + [parent_cert.to_cryptography() for parent_cert in chain], + self._trusted_root.ct_keyring(), + ) + except Error as e: + raise VerificationError(f"failed to verify SCT on signing certificate: {e}") + + # 3. Check that the signing certificate has the expected KU/EKU and + # verifies against the given `VerificationPolicy`. + + usage_ext = cert.extensions.get_extension_for_class(KeyUsage) + if not usage_ext.value.digital_signature: + raise VerificationError("Key usage is not of type `digital signature`") + + extended_usage_ext = cert.extensions.get_extension_for_class(ExtendedKeyUsage) + if ExtendedKeyUsageOID.CODE_SIGNING not in extended_usage_ext.value: + raise VerificationError("Extended usage does not contain `code signing`") + + policy.verify(cert) + + _logger.debug("Successfully verified signing certificate validity...") + + def verify_intoto(self, bundle: Bundle, policy: VerificationPolicy) -> None: + """ + Verifies an bundle's in-toto statement (encapsulated within the bundle + as a DSSE envelope). + + This method is only for DSSE-enveloped in-toto statements. To verify + an arbitrary input against a bundle, use the `verify_artifact` + method. + + `bundle` is the Sigstore `Bundle` to both verify and verify against. + + `policy` is the `VerificationPolicy` to verify against. + + Returns the in-toto statement as a raw `bytes`, for subsequent + JSON decoding and policy evaluation. Callers **must** perform this decoding + and evaluation; mere signature verification by this API does not imply + that the in-toto statement is valid or trustworthy. + """ + + def verify_artifact( self, input_: bytes | Hashed, bundle: Bundle, @@ -182,23 +267,24 @@ def verify( # In order to verify an artifact, we need to achieve the following: # - # 1) Verify that the signing certificate is signed by the certificate - # chain and that the signing certificate was valid at the time - # of signing. - # 2) Verify the certificate sct. - # 3) Verify that the signing certificate belongs to the signer. - # 4) Verify that the artifact signature was signed by the public key in the - # signing certificate. - # 5) Verify that the log entry is consistent with the other verification + # 1. Verify that the signing certificate chains to the root of trust + # and is valid at the time of signing. + # 2. Verify the signing certificate's SCT. + # 3. Verify that the signing certificate conforms to the Sigstore + # X.509 profile as well as the passed-in `VerificationPolicy`. + # 4. Verify the signature and input against the signing certificate's + # public key. + # 5. Verify the transparency log entry's consistency against the other # materials, to prevent variants of CVE-2022-36056. - # 6) Verify the inclusion proof supplied by Rekor for this artifact, - # if we're doing online verification. - # 7) Verify the Signed Entry Timestamp (SET) supplied by Rekor for this - # artifact. - # 8) Verify that the signing certificate was valid at the time of - # signing by comparing the expiry against the integrated timestamp. - - # 1) Verify that the signing certificate is signed by the root certificate and that the + # 6. Verify the inclusion proof and signed checkpoint for the log + # entry. + # 7. Verify the inclusion promise for the log entry, if present. + # 8. Verify the timely insertion of the log entry against the validity + # period for the signing certificate. + + # (1) through (3) are performed by `_verify_common_signing_cert`. + + # 1. Verify that the signing certificate is signed by the root certificate and that the # signing certificate was valid at the time of signing. sign_date = bundle.signing_certificate.not_valid_before_utc cert_ossl = X509.from_cryptography(bundle.signing_certificate) @@ -214,7 +300,7 @@ def verify( exception=store_ctx_error, ) - # 2) Check that the signing certificate has a valid sct + # 2. Check that the signing certificate has a valid SCT # The SignedCertificateTimestamp should be acessed by the index 0 sct = _get_precertificate_signed_certificate_timestamps( @@ -227,7 +313,7 @@ def verify( self._trusted_root.ct_keyring(), ) - # 3) Check that the signing certificate contains the proof claim as the subject + # 3. Check that the signing certificate contains the proof claim as the subject # Check usage is "digital signature" usage_ext = bundle.signing_certificate.extensions.get_extension_for_class( KeyUsage @@ -254,7 +340,7 @@ def verify( _logger.debug("Successfully verified signing certificate validity...") - # 4) Verify that the signature was signed by the public key in the signing certificate + # 4. Verify that the signature was signed by the public key in the signing certificate try: signing_key = bundle.signing_certificate.public_key() signing_key = cast(ec.EllipticCurvePublicKey, signing_key) @@ -268,7 +354,7 @@ def verify( _logger.debug("Successfully verified signature...") - # 5) Verify the consistency of the log entry's body against + # 5. Verify the consistency of the log entry's body against # the other bundle materials (and input being verified). entry = bundle.log_entry @@ -285,8 +371,8 @@ def verify( reason="transparency log entry is inconsistent with other materials" ) - # 6) Verify the inclusion proof for this artifact, including its checkpoint. - # 7) Verify the optional inclusion promise (SET) for this artifact + # 6. Verify the inclusion proof for this artifact, including its checkpoint. + # 7. Verify the optional inclusion promise (SET) for this artifact try: entry._verify(self._trusted_root.rekor_keyring()) except InvalidInclusionProofError as exc: @@ -298,7 +384,8 @@ def verify( except InvalidLogEntry as exc: return VerificationFailure(reason=str(exc)) - # 7) Verify that the signing certificate was valid at the time of signing + # 8. Verify that log entry was integrated circa the signing certificate's + # validity period. integrated_time = datetime.fromtimestamp(entry.integrated_time, tz=timezone.utc) if not ( bundle.signing_certificate.not_valid_before_utc diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 8ac89c8e2..09ab54a94 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -42,14 +42,14 @@ def test_verifier_one_verification(signing_materials, null_policy): (file, bundle) = signing_materials("a.txt", verifier._rekor) - assert verifier.verify(file.read_bytes(), bundle, null_policy) + assert verifier.verify_artifact(file.read_bytes(), bundle, null_policy) def test_verifier_inconsistent_log_entry(signing_bundle, null_policy, mock_staging_tuf): (file, bundle) = signing_bundle("bundle_cve_2022_36056.txt") verifier = Verifier.staging() - result = verifier.verify(file.read_bytes(), bundle, null_policy) + result = verifier.verify_artifact(file.read_bytes(), bundle, null_policy) assert not result assert ( @@ -65,7 +65,7 @@ def test_verifier_multiple_verifications(signing_materials, null_policy): b = signing_materials("b.txt", verifier._rekor) for file, bundle in [a, b]: - assert verifier.verify(file.read_bytes(), bundle, null_policy) + assert verifier.verify_artifact(file.read_bytes(), bundle, null_policy) @pytest.mark.parametrize( @@ -75,7 +75,7 @@ def test_verifier_bundle(signing_bundle, null_policy, mock_staging_tuf, filename (file, bundle) = signing_bundle(filename) verifier = Verifier.staging() - assert verifier.verify(file.read_bytes(), bundle, null_policy) + assert verifier.verify_artifact(file.read_bytes(), bundle, null_policy) def test_verify_result_boolish(): @@ -94,7 +94,7 @@ def test_verifier_email_identity(signing_materials): issuer="https://github.com/login/oauth", ) - assert verifier.verify( + assert verifier.verify_artifact( file.read_bytes(), bundle, policy_, @@ -113,7 +113,7 @@ def test_verifier_uri_identity(signing_materials): issuer="https://token.actions.githubusercontent.com", ) - assert verifier.verify( + assert verifier.verify_artifact( file.read_bytes(), bundle, policy_, @@ -128,7 +128,7 @@ def test_verifier_policy_check(signing_materials): # policy that fails to verify for any given cert. policy_ = pretend.stub(verify=lambda cert: False) - assert not verifier.verify( + assert not verifier.verify_artifact( file.read_bytes(), bundle, policy_, @@ -152,4 +152,4 @@ def test_verifier_fail_expiry(signing_materials, null_policy, monkeypatch): entry = bundle._inner.verification_material.tlog_entries[0] entry.integrated_time = datetime.MINYEAR - assert not verifier.verify(file.read_bytes(), bundle, null_policy) + assert not verifier.verify_artifact(file.read_bytes(), bundle, null_policy) From d7464c7bf49a3304bd76b0e3c9915026fcbe16c3 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 3 Apr 2024 19:14:49 -0400 Subject: [PATCH 02/14] hackety hack Signed-off-by: William Woodruff --- sigstore/verify/verifier.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 0529796cb..40c222e00 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -231,6 +231,12 @@ def verify_intoto(self, bundle: Bundle, policy: VerificationPolicy) -> None: that the in-toto statement is valid or trustworthy. """ + # (1) through (3) are performed by `_verify_common_signing_cert`. + self._verify_common_signing_cert(bundle.signing_certificate, policy) + + # (4): verify the bundle's signature and DSSE envelope against the + # signing certificate's public key. + def verify_artifact( self, input_: bytes | Hashed, @@ -282,8 +288,6 @@ def verify_artifact( # 8. Verify the timely insertion of the log entry against the validity # period for the signing certificate. - # (1) through (3) are performed by `_verify_common_signing_cert`. - # 1. Verify that the signing certificate is signed by the root certificate and that the # signing certificate was valid at the time of signing. sign_date = bundle.signing_certificate.not_valid_before_utc From 40ec79caec37e0412157b734c7cbf562ec5822b7 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 5 Apr 2024 18:11:06 -0400 Subject: [PATCH 03/14] sigstore, test: initial DSSE verify APIs Signed-off-by: William Woodruff --- sigstore/_internal/rekor/__init__.py | 21 ++ sigstore/dsse.py | 14 +- sigstore/verify/models.py | 11 + sigstore/verify/verifier.py | 209 ++++++++---------- test/unit/assets/bundle_cve_2022_36056.txt | 3 +- .../assets/bundle_cve_2022_36056.txt.sigstore | 47 ++-- 6 files changed, 157 insertions(+), 148 deletions(-) diff --git a/sigstore/_internal/rekor/__init__.py b/sigstore/_internal/rekor/__init__.py index 281751f90..1890981e6 100644 --- a/sigstore/_internal/rekor/__init__.py +++ b/sigstore/_internal/rekor/__init__.py @@ -21,6 +21,7 @@ import rekor_types from cryptography.x509 import Certificate +from sigstore import dsse from sigstore._utils import base64_encode_pem_cert from sigstore.hashes import Hashed @@ -30,6 +31,26 @@ __all__ = ["RekorClient", "SignedCheckpoint"] +def _dsse_from_parts(cert: Certificate, evp: dsse.Envelope) -> rekor_types.Dsse: + signature = rekor_types.dsse.Signature( + signature=evp._inner.signatures[0].sig, + verifier=base64_encode_pem_cert(cert), + ) + return rekor_types.Dsse( + spec=rekor_types.dsse.DsseV001Schema( + signatures=[signature], + envelope_hash=rekor_types.dsse.EnvelopeHash( + algorithm=rekor_types.dsse.Algorithm.SHA256, + value=None, + ), + payload_hash=rekor_types.dsse.PayloadHash( + algorithm=rekor_types.dsse.Algorithm.SHA256, + value=None, + ), + ) + ) + + # TODO: This should probably live somewhere better. def _hashedrekord_from_parts( cert: Certificate, sig: bytes, hashed: Hashed diff --git a/sigstore/dsse.py b/sigstore/dsse.py index 8632c442c..eafef6b1b 100644 --- a/sigstore/dsse.py +++ b/sigstore/dsse.py @@ -28,6 +28,8 @@ from sigstore_protobuf_specs.io.intoto import Envelope as _Envelope from sigstore_protobuf_specs.io.intoto import Signature +from sigstore.errors import VerificationError + _logger = logging.getLogger(__name__) _Digest = Union[ @@ -220,20 +222,24 @@ def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope: ) -def _verify(key: ec.EllipticCurvePublicKey, sig: bytes, evp: Envelope) -> bytes: +def _verify(key: ec.EllipticCurvePublicKey, evp: Envelope) -> bytes: """ Verify the given in-toto `Envelope`, returning the verified inner payload. - This function does **not** check the envelope's payload type. The caller is responsible for performing this check. + + Assumes that the envelope only has a single signature. """ pae = _pae(evp._inner.payload_type, evp._inner.payload) try: - key.verify(sig, pae, ec.ECDSA(hashes.SHA256())) + # NB: Assumes only one signature in the DSSE envelope. + key.verify(evp._inner.signatures[0].sig, pae, ec.ECDSA(hashes.SHA256())) + except IndexError: + raise VerificationError("DSSE: no signature to verify") except InvalidSignature: - raise ValueError("invalid signature") + raise VerificationError("DSSE: invalid signature") return evp._inner.payload diff --git a/sigstore/verify/models.py b/sigstore/verify/models.py index 6d72fb4b8..4c09b46da 100644 --- a/sigstore/verify/models.py +++ b/sigstore/verify/models.py @@ -245,6 +245,17 @@ def log_entry(self) -> LogEntry: """ return self._log_entry + @property + def _dsse_envelope(self) -> dsse.Envelope | None: + """ + Returns the DSSE envelope within this Bundle as a `dsse.Envelope`. + + @private + """ + if self._inner.dsse_envelope: + return dsse.Envelope(self._inner.dsse_envelope) + return None + @classmethod def from_json(cls, raw: bytes | str) -> Bundle: """ diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 7ed27564f..af761f294 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -40,6 +40,7 @@ X509StoreFlags, ) +from sigstore import dsse from sigstore._internal.rekor import _hashedrekord_from_parts from sigstore._internal.rekor.client import RekorClient from sigstore._internal.sct import ( @@ -101,7 +102,7 @@ def staging(cls) -> Verifier: ) def _verify_common_signing_cert( - self, cert: Certificate, policy: VerificationPolicy + self, bundle: Bundle, policy: VerificationPolicy ) -> None: """ Performs the signing certificate verification steps that are shared between @@ -110,6 +111,29 @@ def _verify_common_signing_cert( Raises `VerificationError` on all failures. """ + # In order to verify an artifact, we need to achieve the following: + # + # 1. Verify that the signing certificate chains to the root of trust + # and is valid at the time of signing. + # 2. Verify the signing certificate's SCT. + # 3. Verify that the signing certificate conforms to the Sigstore + # X.509 profile as well as the passed-in `VerificationPolicy`. + # 4. Verify the inclusion proof and signed checkpoint for the log + # entry. + # 5. Verify the inclusion promise for the log entry, if present. + # 6. Verify the timely insertion of the log entry against the validity + # period for the signing certificate. + # 7. Verify the signature and input against the signing certificate's + # public key. + # 8. Verify the transparency log entry's consistency against the other + # materials, to prevent variants of CVE-2022-36056. + # + # This method performs steps (1) through (6) above. Its caller + # MUST perform steps (7) and (8) separately, since they vary based on + # the kind of verification being performed (i.e. hashedrekord, DSSE, etc.) + + cert = bundle.signing_certificate + # NOTE: The `X509Store` object currently cannot have its time reset once the `set_time` # method been called on it. To get around this, we construct a new one for every `verify` # call. @@ -123,8 +147,9 @@ def _verify_common_signing_cert( for parent_cert_ossl in self._fulcio_certificate_chain: store.add_cert(parent_cert_ossl) - # 1. Verify that the signing certificate is signed by the root certificate and that the - # signing certificate was valid at the time of signing. + # (1): verify that the signing certificate is signed by the root + # certificate and that the signing certificate was valid at the + # time of signing. sign_date = cert.not_valid_before_utc cert_ossl = X509.from_cryptography(cert) @@ -137,7 +162,7 @@ def _verify_common_signing_cert( except X509StoreContextError as e: raise VerificationError(f"failed to build chain: {e}") - # 2. Check that the signing certificate has a valid SCT + # (2): verify the signing certificate's SCT. sct = _get_precertificate_signed_certificate_timestamps(cert)[0] try: verify_sct( @@ -149,9 +174,8 @@ def _verify_common_signing_cert( except VerificationError as e: raise VerificationError(f"failed to verify SCT on signing certificate: {e}") - # 3. Check that the signing certificate has the expected KU/EKU and - # verifies against the given `VerificationPolicy`. - + # (3): verify the signing certificate against the Sigstore + # X.509 profile and verify against the given `VerificationPolicy`. usage_ext = cert.extensions.get_extension_for_class(KeyUsage) if not usage_ext.value.digital_signature: raise VerificationError("Key usage is not of type `digital signature`") @@ -164,12 +188,35 @@ def _verify_common_signing_cert( _logger.debug("Successfully verified signing certificate validity...") - def verify_intoto(self, bundle: Bundle, policy: VerificationPolicy) -> None: + # (4): verify the inclusion proof and signed checkpoint for the + # log entry. + # (5): verify the inclusion promise for the log entry, if present. + entry = bundle.log_entry + try: + entry._verify(self._trusted_root.rekor_keyring()) + except VerificationError as exc: + raise VerificationError(f"invalid log entry: {exc}") + + # (6): verify that log entry was integrated circa the signing certificate's + # validity period. + integrated_time = datetime.fromtimestamp(entry.integrated_time, tz=timezone.utc) + if not ( + bundle.signing_certificate.not_valid_before_utc + <= integrated_time + <= bundle.signing_certificate.not_valid_after_utc + ): + raise VerificationError( + "invalid signing cert: expired at time of Rekor entry" + ) + + def verify_dsse( + self, bundle: Bundle, policy: VerificationPolicy + ) -> tuple[str, bytes]: """ - Verifies an bundle's in-toto statement (encapsulated within the bundle - as a DSSE envelope). + Verifies an bundle's DSSE envelope, returning the encapsulated payload + and its content type. - This method is only for DSSE-enveloped in-toto statements. To verify + This method is only for DSSE-enveloped payloads. To verify an arbitrary input against a bundle, use the `verify_artifact` method. @@ -177,17 +224,34 @@ def verify_intoto(self, bundle: Bundle, policy: VerificationPolicy) -> None: `policy` is the `VerificationPolicy` to verify against. - Returns the in-toto statement as a raw `bytes`, for subsequent - JSON decoding and policy evaluation. Callers **must** perform this decoding - and evaluation; mere signature verification by this API does not imply - that the in-toto statement is valid or trustworthy. + Returns a tuple of `(type, payload)`, where `type` is the payload's + type as encoded in the DSSE envelope and `payload` is the raw `bytes` + of the payload. No validation of either `type` or `payload` is + performed; users of this API **must** assert that `type` is known + to them before proceeding to handle `payload` in an application-dependent + manner. """ - # (1) through (3) are performed by `_verify_common_signing_cert`. - self._verify_common_signing_cert(bundle.signing_certificate, policy) + # (1) through (6) are performed by `_verify_common_signing_cert`. + self._verify_common_signing_cert(bundle, policy) - # (4): verify the bundle's signature and DSSE envelope against the + # (7): verify the bundle's signature and DSSE envelope against the # signing certificate's public key. + envelope = bundle._dsse_envelope + if envelope is None: + raise VerificationError( + "cannot perform DSSE verification on a bundle without a DSSE envelope" + ) + + dsse._verify(bundle.signing_certificate.public_key(), envelope) + + # (8): verify the consistency of the log entry's body against + # the other bundle materials. + _ = bundle.log_entry + + # TODO + + return (envelope._inner.payload_type, envelope._inner.payload) def verify_artifact( self, @@ -208,89 +272,12 @@ def verify_artifact( On failure, this method raises `VerificationError`. """ - hashed_input = sha256_digest(input_) - - # NOTE: The `X509Store` object currently cannot have its time reset once the `set_time` - # method been called on it. To get around this, we construct a new one for every `verify` - # call. - store = X509Store() - # NOTE: By explicitly setting the flags here, we ensure that OpenSSL's - # PARTIAL_CHAIN default does not change on us. Enabling PARTIAL_CHAIN - # would be strictly more conformant of OpenSSL, but we currently - # *want* the "long" chain behavior of performing path validation - # down to a self-signed root. - store.set_flags(X509StoreFlags.X509_STRICT) - for parent_cert_ossl in self._fulcio_certificate_chain: - store.add_cert(parent_cert_ossl) - - # In order to verify an artifact, we need to achieve the following: - # - # 1. Verify that the signing certificate chains to the root of trust - # and is valid at the time of signing. - # 2. Verify the signing certificate's SCT. - # 3. Verify that the signing certificate conforms to the Sigstore - # X.509 profile as well as the passed-in `VerificationPolicy`. - # 4. Verify the signature and input against the signing certificate's - # public key. - # 5. Verify the transparency log entry's consistency against the other - # materials, to prevent variants of CVE-2022-36056. - # 6. Verify the inclusion proof and signed checkpoint for the log - # entry. - # 7. Verify the inclusion promise for the log entry, if present. - # 8. Verify the timely insertion of the log entry against the validity - # period for the signing certificate. - - # 1. Verify that the signing certificate is signed by the root certificate and that the - # signing certificate was valid at the time of signing. - sign_date = bundle.signing_certificate.not_valid_before_utc - cert_ossl = X509.from_cryptography(bundle.signing_certificate) - - store.set_time(sign_date) - store_ctx = X509StoreContext(store, cert_ossl) - try: - # get_verified_chain returns the full chain including the end-entity certificate - # and chain should contain only CA certificates - chain = store_ctx.get_verified_chain()[1:] - except X509StoreContextError as exc: - raise VerificationError( - f"failed to build chain to signing certificate: {exc}" - ) - - # 2. Check that the signing certificate has a valid SCT - - # The SignedCertificateTimestamp should be acessed by the index 0 - sct = _get_precertificate_signed_certificate_timestamps( - bundle.signing_certificate - )[0] - verify_sct( - sct, - bundle.signing_certificate, - [parent_cert.to_cryptography() for parent_cert in chain], - self._trusted_root.ct_keyring(), - ) - - # 3. Check that the signing certificate contains the proof claim as the subject - # Check usage is "digital signature" - usage_ext = bundle.signing_certificate.extensions.get_extension_for_class( - KeyUsage - ) - if not usage_ext.value.digital_signature: - raise VerificationError("Key usage is not of type `digital signature`") + # (1) through (6) are performed by `_verify_common_signing_cert`. + self._verify_common_signing_cert(bundle, policy) - # Check that extended usage contains "code signing" - extended_usage_ext = ( - bundle.signing_certificate.extensions.get_extension_for_class( - ExtendedKeyUsage - ) - ) - if ExtendedKeyUsageOID.CODE_SIGNING not in extended_usage_ext.value: - raise VerificationError("Extended usage does not contain `code signing`") - - policy.verify(bundle.signing_certificate) - - _logger.debug("Successfully verified signing certificate validity...") + hashed_input = sha256_digest(input_) - # 4. Verify that the signature was signed by the public key in the signing certificate + # (7): verify that the signature was signed by the public key in the signing certificate. try: signing_key = bundle.signing_certificate.public_key() signing_key = cast(ec.EllipticCurvePublicKey, signing_key) @@ -304,8 +291,8 @@ def verify_artifact( _logger.debug("Successfully verified signature...") - # 5. Verify the consistency of the log entry's body against - # the other bundle materials (and input being verified). + # (8): verify the consistency of the log entry's body against + # the other bundle materials (and input being verified). entry = bundle.log_entry expected_body = _hashedrekord_from_parts( @@ -320,23 +307,3 @@ def verify_artifact( raise VerificationError( "transparency log entry is inconsistent with other materials" ) - - # 6. Verify the inclusion proof for this artifact, including its checkpoint. - # 7. Verify the optional inclusion promise (SET) for this artifact - try: - entry._verify(self._trusted_root.rekor_keyring()) - except VerificationError as exc: - # NOTE: Re-raise with a prefix here for additional context. - raise VerificationError(f"invalid log entry: {exc}") - - # 8. Verify that log entry was integrated circa the signing certificate's - # validity period. - integrated_time = datetime.fromtimestamp(entry.integrated_time, tz=timezone.utc) - if not ( - bundle.signing_certificate.not_valid_before_utc - <= integrated_time - <= bundle.signing_certificate.not_valid_after_utc - ): - raise VerificationError( - "invalid signing cert: expired at time of Rekor entry" - ) diff --git a/test/unit/assets/bundle_cve_2022_36056.txt b/test/unit/assets/bundle_cve_2022_36056.txt index 35c9c862a..cb52646dc 100644 --- a/test/unit/assets/bundle_cve_2022_36056.txt +++ b/test/unit/assets/bundle_cve_2022_36056.txt @@ -3,6 +3,7 @@ DO NOT MODIFY ME! this is "bundle_cve_2022_36056.txt", a sample input for sigstore-python's unit tests. this has a corresponding bundle that is valid, *except* that the included log entry -is from a *valid but unrelated* bundle (specifically, `bundle.txt`'s bundle). +is from a *valid but unrelated* bundle (specifically, for an identical input +signed immediately after this one). DO NOT MODIFY ME! diff --git a/test/unit/assets/bundle_cve_2022_36056.txt.sigstore b/test/unit/assets/bundle_cve_2022_36056.txt.sigstore index 884ea4f7f..d5cfb644b 100644 --- a/test/unit/assets/bundle_cve_2022_36056.txt.sigstore +++ b/test/unit/assets/bundle_cve_2022_36056.txt.sigstore @@ -2,11 +2,11 @@ "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial": { "certificate": { - "rawBytes": "MIIC1TCCAlqgAwIBAgIUJr7mMojh1Oa9vVrbOBBwKRnPDhQwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwMzE1MjAxNzA2WhcNMjQwMzE1MjAyNzA2WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbhRwArjwQ6hh2GoqrM9QCmMBxeKPLA35VxEWtAM4LaHj0yJd8JV8GV0eaPEjnMiC/Y92GU39iPVWmKYqIa9yZ6OCAXkwggF1MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUEK6qVW+7VoizwygGTpYPKi9sAmAwHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYBBAHWeQIEAgR8BHoAeAB2ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjkPC13IAAAQDAEcwRQIhAKu8giO4JLjCB12g9bnqXNvLuRbcvFqTysjymJRXXGXPAiAhdLH8tY1S+gjUBew/be3YxndS/S88d/hSWbunUuANYjAKBggqhkjOPQQDAwNpADBmAjEAv1NAfS3UM3PVdMnom8CpFSgkTcWSt73md3+PVGpnEruaZSM4w/M932587R+McvL+AjEA1elUEuYaPTAkm1JHDNGSMRDNFkqXGtjAar6QCYkEJI20d2MIaOy8unvvIs54LxxJ" + "rawBytes": "MIIC1TCCAlugAwIBAgIUT8ug/4mjvLaDqXd4GKS6wmjq6MAwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwNDA1MjIwODEzWhcNMjQwNDA1MjIxODEzWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECiYUx1SVwX5EHulBZv0FOEJ9AYXmCMOS8QVJnU1jY6xY6t4DCfaGwRU2iRIx8l4MmRKw8dwK8iA4/28TZt1HFKOCAXowggF2MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUvL83tyuyhCcA6zBgQlsrD9b2z5owHwYDVR0jBBgwFoAUcYYwphR8Ym/599b0BRp/X//rb6wwIwYDVR0RAQH/BBkwF4EVd2lsbGlhbUB5b3NzYXJpYW4ubmV0MCwGCisGAQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEEAYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3ACswvNxoiMni4dgmKV50H0g5MZYC8pwzy15DQP6yrIZ6AAABjrBOHmkAAAQDAEgwRgIhAN/KC24XuwGgJRGpkvtzVVJSgEneKCV6PyM41Rul8gV0AiEA32ZU52ea/lCdPEzWTZxkdVbciAcsrATA+3D/o925g8owCgYIKoZIzj0EAwMDaAAwZQIxAO5FDiCQ79R69r6gyTgWhqADiisSZ7udiZGwRUWZcrBAYMKTw5Hy+1R/uKZcZ6jZKAIwFADtSVbmaXwC99hp++4aVyGo781VSiR5hIVRbFM+5l+psqG45/06bQy+Yj4EtrsY" }, "tlogEntries": [ { - "logIndex": "7390977", + "logIndex": "26084047", "logId": { "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" }, @@ -14,40 +14,43 @@ "kind": "hashedrekord", "version": "0.0.1" }, - "integratedTime": "1682468469", + "integratedTime": "1712354907", "inclusionPromise": { - "signedEntryTimestamp": "MEUCICSJs5PgN4W3Lku3ybrwfNLAKMWaOvffg2tnqm19VrWEAiEA16MVPsWDoaAljsxGefpQazpvYfs1pv8lzdgZQ0I4rH0=" + "signedEntryTimestamp": "MEUCIDMn04X1vVbjq+WBhC0jv9M3Py5KLujlCp9zaA5eUdNAAiEA3lalF4OHKNLmlKq06z2Zg9jtQYA7NxJ16zV6MglvHZI=" }, "inclusionProof": { - "logIndex": "7376158", - "rootHash": "LE67t2Zlc0g35az81xMg0cgM2DULj8fNsGGHTcRthcs=", - "treeSize": "7376159", + "logIndex": "26069228", + "rootHash": "Wr9rTCceIRRp9phvQmZTrPlNXo5b7i+9pIRkRSA9fG8=", + "treeSize": "26069230", "hashes": [ - "zgesNHwk09VvW4IDaPrJMtX59glNyyLPzeJO1Gw1hCI=", - "lJiFr9ZP5FO8BjqLAUQ16A/0/LoOOQ0gfeNhdxaxO2w=", - "sMImd51DBHQnH1tz4sGk8gXB+FjWyusVXbP0GmpFnB4=", - "cDU1nEpl0WCRlxLi/gNVzykDzobU4qG/7BQZxn0qDgU=", - "4CRqWzG3qpxKvlHuZg5O6QjQiwOzerbjwsAh30EVlA8=", - "Ru0p3GE/zB2zub2/xR5rY/aM4J+5VJmiIuIl2enF/ws=", - "2W+NG5yGR68lrLGcw4gn9CSCfeQF98d3LMfdo8tPyok=", - "bEs1eYxy9R6hR2veGEwYW4PEdrZKrdqZ7uDlmmNtlas=", - "sgQMnwcK7VxxAi+fygxq8iJ+zWqShjXm07/AWobWcXU=", - "y4BESazXFcefRzxpN1PfJHoqRaKnPJPM5H/jotx0QY8=", - "xiNEdLOpmGQERCR+DCEFVRK+Ns6G0BLV9M6sQQkRhik=" + "flCB8VB67ZGa6K2ZEtDTtgtm96F3EjjtFvnGXwPOYT8=", + "OzTdU4mq5jqXJ11gLmeEuCaLkxubkd4WVVwWUmZzgko=", + "JV1urrvYBsls45EY/TJOuoRH3ho9y0nY1RvEgj1LWAs=", + "VVpzU8MjvLgCT86Q0pSh57MzNiLGOphMU8kg9KAS9Lk=", + "Nre+FErsP3TpqQY1RK7/b0WAL8fQx1bSbAuKjFSYvWg=", + "jp/0CawpaDTbd+wM+aqjsO+AOVmIGunMId2ODziREU8=", + "hSeZIoNlyUSqlJ6UyVfZIv17plm/YOvzrYEukkUh3OM=", + "QdTMKazLZtCbvsCOn7U68L/vwKCJtgYyzRdxzbP3wcA=", + "1P/q3R3vArPmJE+OmmcIRlBnXa/F2drYwklLngyaNXU=", + "QyPS/J6veDqojEZv/v/8V1SpurFS22qOdFsQw1ZZH24=", + "zL40ndFRmx2oQWFRdGwPjCl5BubNud42vN+OfvM9z9g=", + "arvuzAipUJ14nDj14OBlvkMSicjdsE9Eus3hq9Jpqdk=", + "Edul4W41O3EfxKEEMlX2nW0+GTgCv00nGmcpwhALgVA=", + "rBWB37+HwkTZgDv0rMtGBUoDI0UZqcgDZp48M6CaUlA=" ], "checkpoint": { - "envelope": "rekor.sigstage.dev - 8050909264565447525\n7376159\nLE67t2Zlc0g35az81xMg0cgM2DULj8fNsGGHTcRthcs=\nTimestamp: 1682468469199678948\n\n\u2014 rekor.sigstage.dev 0y8wozBEAiBbAodz3dBqJjGMhnZEkbaTDVxc8+tBEPKbaWUZoqxFvwIgGtYzFgFaM3UXBRHmzgmcrCxA145dpQ2YD0yFqiPHO7U=\n" + "envelope": "rekor.sigstage.dev - 8050909264565447525\n26069230\nWr9rTCceIRRp9phvQmZTrPlNXo5b7i+9pIRkRSA9fG8=\n\n\u2014 rekor.sigstage.dev 0y8wozBFAiEAybn4EqPmEte82KeRUVEj5Kihrrm/72Bei84AF7CrPSwCIDANN3hLoyAiE5gN/3R2O4GRO+CvHZpsP2ZMB84X1Pa2\n" } }, - "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4MDJkZDYwZmY4ODMzMzgwMmYyNTg1ZTczMDQzYmQyMWMzNDEyODVlMTk5MmZlNWIzMTc1NWUxY2FkZWFlMzBlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1HVUNNUUNPT0pxVFk2WFdnQjY0aXpLMldWUDA3YjBTRzlNNVdQQ3dLaGZUUHdNdnRzZ1VpOEtlUkd3UWt2dkxZYktIZHFVQ01FYk9YRkcwTk1xRVF4V1ZiNnJtR25leGRBRHVHZjZKbDhxQUM4dG42N3AzUWZWb1h6TXZGQTYxUHp4d1Z3dmI4Zz09IiwicHVibGljS2V5Ijp7ImNvbnRlbnQiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VNMWVrTkRRVzE1WjBGM1NVSkJaMGxWU2pOMmNHVjNaR1kyWlRreGNtZHFjVU54WVdkemRFWTBjVzQ0ZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwTmQwNUVTVEpOUkVGNVRWUkJORmRvWTA1TmFrMTNUa1JKTWsxRVFYcE5WRUUwVjJwQlFVMUlXWGRGUVZsSUNrdHZXa2w2YWpCRFFWRlpSa3MwUlVWQlEwbEVXV2RCUlRKelpEWXJiRTlDWTI0MVRWaDBibUozWTJFM2VtTjNjSEJ5YkRkSFZWcHBTMVJQT1VsWGNFRUtWV1pXVkhSNEswSllSMGhSUTFKM2MwWjVMMlEzWkV4c1pqUm9kWEpKY1doNlRVUTFlV0ZETW10alZUa3ZPR001UnpVMVNubENXRVk0UkhnMVUxRnRPUXA1TW5KUVYwWkpaRzB5T1ZGc09VRXpTVE41ZVVWR2VWQnZORWxDWW1wRFEwRlhiM2RFWjFsRVZsSXdVRUZSU0M5Q1FWRkVRV2RsUVUxQ1RVZEJNVlZrQ2twUlVVMU5RVzlIUTBOelIwRlJWVVpDZDAxRVRVSXdSMEV4VldSRVoxRlhRa0pVYkdGVlptcHdhVmhIYUVKUU0yaFBRMWN3U2twYVJGTlFlR2Q2UVdZS1FtZE9Wa2hUVFVWSFJFRlhaMEpTZUdocVEyMUdTSGhwWWk5dU16RjJVVVpIYmpsbUx5dDBkbkpFUVZsQ1owNVdTRkpGUWtGbU9FVkVha0ZOWjFGd2FBcFJTRkoxWlZNMU1HSXpaSFZOUTNkSFEybHpSMEZSVVVKbk56aDNRVkZGUlVodGFEQmtTRUo2VDJrNGRsb3liREJoU0ZacFRHMU9kbUpUT1hOaU1tUndDbUpwT1haWldGWXdZVVJCZFVKbmIzSkNaMFZGUVZsUEwwMUJSVWxDUTBGTlNHMW9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWMySXlaSEFLWW1rNWRsbFlWakJoUkVOQ2FXZFpTMHQzV1VKQ1FVaFhaVkZKUlVGblVqaENTRzlCWlVGQ01rRkRjM2QyVG5odmFVMXVhVFJrWjIxTFZqVXdTREJuTlFwTldsbERPSEIzZW5reE5VUlJVRFo1Y2tsYU5rRkJRVUpvTjNKMlpVSnpRVUZCVVVSQlJXTjNVbEZKYUVGTFQxcFFUVTQ1VVRseFR6RklXR2xuU0VKUUNuUXJTV014Tm5sNU1scG5kakpMVVRJemFUVlhUR294TmtGcFFYcHlSbkIxWVhsSFdHUnZTeXRvV1dWUWJEbGtSV1ZZYWtjdmRrSXlha3N2UlROelJYTUtTWEpZZEVWVVFVdENaMmR4YUd0cVQxQlJVVVJCZDA1d1FVUkNiVUZxUlVGbmJXaG5PREJ0U1M5VFkzSXdhWE5DYmtRMVJsbFlXamhYZUVFNGRHNUNRZ3BRYldSbU5HRk9SMFp2Y2tkaGVrZFlZVVpSVmxCWVowSldVSFlyV1VkSkwwRnFSVUV3VVhwUVF6VmtTRVF2VjFkWVZ6SkhZa1ZETkdSd2QwWnJPRTlIQ2xKcmFVVjRUVTk1THl0RGNXRmlZbFpuS3k5c2VERk9PVlpIUWxSc1ZWUm1kRFExWkFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19" + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjODdiNWIyMTNhYjA4YjMyOTcwYzEwNmQwYzdlNDQyM2U2N2Y1NDQ5YzJmZDJkMWU5YjRlYTZjYzQzYjZlZTdjIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURacnZDMjVxNGRlbStIb0liZ3BLNHI2MHZyUjhyay9CaEgvbVp3enROYXBnSWhBTm5KK1JFNXFiZ0xFR2lOeXN1OFVvS0lFL1diSWJaT1ZCL3hCVXZMbFRPZCIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTXhha05EUVd4MVowRjNTVUpCWjBsVlF6Rk9hbmRVZUU5eU1FZHhWMGNyZVZoUWNuWlZLMDVZUldWbmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFJkMDVFUVRGTmFrbDNUMFJKTTFkb1kwNU5hbEYzVGtSQk1VMXFTWGhQUkVrelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZ0ZUVwQlVWQm5WWFpLTUZwWVJrdEVOVkV2WjJKSE0zRnZiMFFyYUdaVlRURTFWM1FLUm1aT1NUZHphemc0TVVNNFRUWXhSMDE2WWl0R2JsbEVhVkpUT0hCb1ZuQllWbEpyYUVOd1lWcEZZVloxSzJVeFVUWlBRMEZZYjNkblowWXlUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZyT0N0aUNtWXdMMFUzUkVwTGRVVlpNR3g0VjJwM1NUSlVZVUZyZDBoM1dVUldVakJxUWtKbmQwWnZRVlZqV1ZsM2NHaFNPRmx0THpVNU9XSXdRbEp3TDFndkwzSUtZalozZDBsM1dVUldVakJTUVZGSUwwSkNhM2RHTkVWV1pESnNjMkpIYkdoaVZVSTFZak5PZWxsWVNuQlpWelIxWW0xV01FMURkMGREYVhOSFFWRlJRZ3BuTnpoM1FWRkZSVWh0YURCa1NFSjZUMms0ZGxveWJEQmhTRlpwVEcxT2RtSlRPWE5pTW1Sd1ltazVkbGxZVmpCaFJFRjFRbWR2Y2tKblJVVkJXVTh2Q2sxQlJVbENRMEZOU0cxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YzJJeVpIQmlhVGwyV1ZoV01HRkVRMEpwZDFsTFMzZFpRa0pCU0ZjS1pWRkpSVUZuVWpsQ1NITkJaVkZDTTBGRGMzZDJUbmh2YVUxdWFUUmtaMjFMVmpVd1NEQm5OVTFhV1VNNGNIZDZlVEUxUkZGUU5ubHlTVm8yUVVGQlFncHFja0pQVmxCalFVRkJVVVJCUldkM1VtZEphRUZOYlZWSVVFOTRiWEE0VFdkblRtWndjbXRsUkdGbFdFbFZabnA1YlROVVEwdDVhV2xQUzNkd2VYQkdDa0ZwUlVFemRIWlVTbEJxV0dSMk1VMWtPSGd4WlRSWUt6RjFabWxWYUU5R1ZXdHFTVmxIVjA1NFpUQmFVeTlSZDBObldVbExiMXBKZW1vd1JVRjNUVVFLWVZGQmQxcG5TWGhCVFhNNVJGWmpNSHBEVjNSVmFFcEhPSEprVjJORGRHaHlNVmRZWkVGMllrODFSMFo2VEZsTVoyVlVhSFZJYzBZdk0yMXRkRmRpUWdwd1pVUkhZamtyZWpCblNYaEJVRmRRWVhFM1FqWllSamh5Y1ZwVFQzVTJVRWRwVFhOT2NXTTFRMmRPVUNzNFNWbFRVemt4V0U5aE1FRlJWamRoTUdzd0NtVkdVelZGZWtwNFRVMVVRMlJuUFQwS0xTMHRMUzFGVGtRZ1EwVlNWRWxHU1VOQlZFVXRMUzB0TFFvPSJ9fX19" } ] }, "messageSignature": { "messageDigest": { "algorithm": "SHA2_256", - "digest": "I7ce6VlZgnoxzARxnn8VcdF21SCB9EZ5gSLGbrLE8ZY=" + "digest": "yHtbITqwizKXDBBtDH5EI+Z/VEnC/S0em06mzEO27nw=" }, - "signature": "MEUCIQCXpEJg4djX++Z9PKcY38J7B5Mzcv4/SKhbdud5lacAVwIgNumnD1DtkszkyUIReufXkmNVTjrdEsrzGeVKlmzUm48=" + "signature": "MEUCIQCr7CB5uKBUYJQxVCiHA1kCZHusFBjKfI1G9cVcPfPDmAIgSzjMGvzMAI3/OvnDoVGWi2kVwXfuyCSqH/2EUjXA93o=" } } From 1c20552eaecbabe4c2ec2a7981246ea18e95f55e Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 5 Apr 2024 18:24:05 -0400 Subject: [PATCH 04/14] sigstore: lintage Signed-off-by: William Woodruff --- sigstore/dsse.py | 4 +++- sigstore/verify/verifier.py | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sigstore/dsse.py b/sigstore/dsse.py index eafef6b1b..aad6d1c3f 100644 --- a/sigstore/dsse.py +++ b/sigstore/dsse.py @@ -242,4 +242,6 @@ def _verify(key: ec.EllipticCurvePublicKey, evp: Envelope) -> bytes: except InvalidSignature: raise VerificationError("DSSE: invalid signature") - return evp._inner.payload + # TODO: Remove ignore when protobuf-specs contains a py.typed marker. + # See: + return evp._inner.payload # type: ignore[no-any-return] diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index af761f294..936dc022e 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -26,11 +26,7 @@ import rekor_types from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.x509 import ( - Certificate, - ExtendedKeyUsage, - KeyUsage, -) +from cryptography.x509 import ExtendedKeyUsage, KeyUsage from cryptography.x509.oid import ExtendedKeyUsageOID from OpenSSL.crypto import ( X509, @@ -243,7 +239,9 @@ def verify_dsse( "cannot perform DSSE verification on a bundle without a DSSE envelope" ) - dsse._verify(bundle.signing_certificate.public_key(), envelope) + signing_key = bundle.signing_certificate.public_key() + signing_key = cast(ec.EllipticCurvePublicKey, signing_key) + dsse._verify(signing_key, envelope) # (8): verify the consistency of the log entry's body against # the other bundle materials. From 4da441200fe1d752c3c966a88350dd5219bc740a Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 5 Apr 2024 19:11:03 -0400 Subject: [PATCH 05/14] test: fix test_sign_prehashed Signed-off-by: William Woodruff --- test/unit/test_sign.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/test_sign.py b/test/unit/test_sign.py index db755ef6d..947513709 100644 --- a/test/unit/test_sign.py +++ b/test/unit/test_sign.py @@ -141,9 +141,9 @@ def test_sign_prehashed(staging): assert bundle._inner.message_signature.message_digest.digest == hashed.digest # verifying against the original input works - verifier.verify(input_, bundle=bundle, policy=UnsafeNoOp()) + verifier.verify_artifact(input_, bundle=bundle, policy=UnsafeNoOp()) # verifying against the prehash also works - verifier.verify(hashed, bundle=bundle, policy=UnsafeNoOp()) + verifier.verify_artifact(hashed, bundle=bundle, policy=UnsafeNoOp()) @pytest.mark.online From 47dbf179e893ad1eda6c944bc3d143d72cc87f57 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 8 Apr 2024 11:58:44 -0400 Subject: [PATCH 06/14] sigstore: cap off DSSE verification Signed-off-by: William Woodruff --- sigstore/verify/verifier.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 936dc022e..a42c13fd9 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -35,6 +35,7 @@ X509StoreContextError, X509StoreFlags, ) +from pydantic import ValidationError from sigstore import dsse from sigstore._internal.rekor import _hashedrekord_from_parts @@ -44,7 +45,7 @@ verify_sct, ) from sigstore._internal.trustroot import KeyringPurpose, TrustedRoot -from sigstore._utils import sha256_digest +from sigstore._utils import base64_encode_pem_cert, sha256_digest from sigstore.errors import VerificationError from sigstore.hashes import Hashed from sigstore.verify.models import Bundle @@ -245,9 +246,35 @@ def verify_dsse( # (8): verify the consistency of the log entry's body against # the other bundle materials. - _ = bundle.log_entry + # NOTE: This is very slightly weaker than the consistency check + # for hashedrekord entries, due to how inclusion is recorded for DSSE: + # the included entry for DSSE includes an envelope hash that we + # *cannot* verify, since the envelope is uncanonicalized JSON. + # Instead, we manually pick apart the entry body below and verify + # the parts we can (namely the payload hash and signature list). + entry = bundle.log_entry + try: + entry_body = rekor_types.Dsse.model_validate_json( + base64.b64decode(entry.body) + ) + except ValidationError as exc: + raise VerificationError(f"invalid DSSE log entry: {exc}") - # TODO + payload_hash = sha256_digest(envelope._inner.payload).digest.hex() + if ( + entry_body.spec.root.payload_hash.algorithm + != rekor_types.dsse.Algorithm.SHA256 + ): + raise VerificationError("expected SHA256 payload hash in DSSE log entry") + if payload_hash != entry_body.spec.root.payload_hash.value: + raise VerificationError("log entry payload hash does not match bundle") + + signature = rekor_types.dsse.Signature( + signature=base64.b64encode(envelope._inner.signatures[0].sig).decode(), + verifier=base64_encode_pem_cert(bundle.signing_certificate), + ) + if [signature] != entry_body.spec.root.signatures: + raise VerificationError("log entry signatures do not match bundle") return (envelope._inner.payload_type, envelope._inner.payload) From a03520d699623de1668d59f631f1682f9b9e5411 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 8 Apr 2024 12:01:57 -0400 Subject: [PATCH 07/14] sigstore: judicious ignores Signed-off-by: William Woodruff --- sigstore/verify/verifier.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index a42c13fd9..55ef00293 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -262,11 +262,11 @@ def verify_dsse( payload_hash = sha256_digest(envelope._inner.payload).digest.hex() if ( - entry_body.spec.root.payload_hash.algorithm + entry_body.spec.root.payload_hash.algorithm # type: ignore[union-attr] != rekor_types.dsse.Algorithm.SHA256 ): raise VerificationError("expected SHA256 payload hash in DSSE log entry") - if payload_hash != entry_body.spec.root.payload_hash.value: + if payload_hash != entry_body.spec.root.payload_hash.value: # type: ignore[union-attr] raise VerificationError("log entry payload hash does not match bundle") signature = rekor_types.dsse.Signature( From d8663b012684f77c5b43367aa2e2ddd02a8c01ca Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 8 Apr 2024 16:53:17 -0400 Subject: [PATCH 08/14] test: add a DSSE roundtrip test Signed-off-by: William Woodruff --- test/unit/verify/test_verifier.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index be45ba743..5f5147327 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -13,9 +13,12 @@ # limitations under the License. +import hashlib + import pretend import pytest +from sigstore.dsse import _StatementBuilder, _Subject from sigstore.errors import VerificationError from sigstore.verify import policy from sigstore.verify.models import Bundle @@ -146,3 +149,31 @@ def test_verifier_fail_expiry(signing_materials, null_policy, monkeypatch): with pytest.raises(VerificationError): verifier.verify_artifact(file.read_bytes(), bundle, null_policy) + + +@pytest.mark.online +@pytest.mark.ambient_oidc +def test_verifier_dsse_roundtrip(staging): + sign_ctx, verifier, identity = staging + + ctx = sign_ctx + stmt = ( + _StatementBuilder() + .subjects( + [_Subject(name="null", digest={"sha256": hashlib.sha256(b"").hexdigest()})] + ) + .predicate_type("https://cosign.sigstore.dev/attestation/v1") + .predicate( + { + "Data": "", + "Timestamp": "2023-12-07T00:37:58Z", + } + ) + ).build() + + with ctx.signer(identity) as signer: + bundle = signer.sign(stmt) + + payload_type, payload = verifier.verify_dsse(bundle, policy.UnsafeNoOp()) + assert payload_type == "application/vnd.in-toto+json" + assert payload == stmt._contents From c49cb70d0541d490b99e364599820590f2f3cc6f Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 8 Apr 2024 16:54:53 -0400 Subject: [PATCH 09/14] missing call Signed-off-by: William Woodruff --- test/unit/verify/test_verifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 5f5147327..1ffc0a25c 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -156,7 +156,7 @@ def test_verifier_fail_expiry(signing_materials, null_policy, monkeypatch): def test_verifier_dsse_roundtrip(staging): sign_ctx, verifier, identity = staging - ctx = sign_ctx + ctx = sign_ctx() stmt = ( _StatementBuilder() .subjects( From 073ce524029dc06564c1ddea4d2f016828827cf3 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 8 Apr 2024 16:57:11 -0400 Subject: [PATCH 10/14] fix more types Signed-off-by: William Woodruff --- test/unit/verify/test_verifier.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 1ffc0a25c..f1bb497b1 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -154,9 +154,9 @@ def test_verifier_fail_expiry(signing_materials, null_policy, monkeypatch): @pytest.mark.online @pytest.mark.ambient_oidc def test_verifier_dsse_roundtrip(staging): - sign_ctx, verifier, identity = staging + signer_cls, verifier_cls, identity = staging - ctx = sign_ctx() + ctx = signer_cls() stmt = ( _StatementBuilder() .subjects( @@ -174,6 +174,7 @@ def test_verifier_dsse_roundtrip(staging): with ctx.signer(identity) as signer: bundle = signer.sign(stmt) + verifier = verifier_cls() payload_type, payload = verifier.verify_dsse(bundle, policy.UnsafeNoOp()) assert payload_type == "application/vnd.in-toto+json" assert payload == stmt._contents From 6d7d3af18feecd20d2ba4fd54d86d0f82df0eb9c Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 12 Apr 2024 12:10:21 -0400 Subject: [PATCH 11/14] fix test Signed-off-by: William Woodruff --- test/unit/verify/test_verifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index f1bb497b1..b5f96a273 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -172,7 +172,7 @@ def test_verifier_dsse_roundtrip(staging): ).build() with ctx.signer(identity) as signer: - bundle = signer.sign(stmt) + bundle = signer.sign_intoto(stmt) verifier = verifier_cls() payload_type, payload = verifier.verify_dsse(bundle, policy.UnsafeNoOp()) From a00b9db87c06c16eb599fc21ae28a57b31faf50d Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 12 Apr 2024 13:38:44 -0400 Subject: [PATCH 12/14] verifier: typo Signed-off-by: William Woodruff --- sigstore/verify/verifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 55ef00293..3a1916dba 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -103,7 +103,7 @@ def _verify_common_signing_cert( ) -> None: """ Performs the signing certificate verification steps that are shared between - `verify_intoto` and `verify_artifact`. + `verify_dsse` and `verify_artifact`. Raises `VerificationError` on all failures. """ From c44ca1273012433674d0a800d226a0611aeb219e Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 12 Apr 2024 13:43:42 -0400 Subject: [PATCH 13/14] rekor: remove _dsse_from_parts This was unused. Signed-off-by: William Woodruff --- sigstore/_internal/rekor/__init__.py | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/sigstore/_internal/rekor/__init__.py b/sigstore/_internal/rekor/__init__.py index 1890981e6..ab555a602 100644 --- a/sigstore/_internal/rekor/__init__.py +++ b/sigstore/_internal/rekor/__init__.py @@ -21,34 +21,17 @@ import rekor_types from cryptography.x509 import Certificate -from sigstore import dsse from sigstore._utils import base64_encode_pem_cert from sigstore.hashes import Hashed from .checkpoint import SignedCheckpoint from .client import RekorClient -__all__ = ["RekorClient", "SignedCheckpoint"] - - -def _dsse_from_parts(cert: Certificate, evp: dsse.Envelope) -> rekor_types.Dsse: - signature = rekor_types.dsse.Signature( - signature=evp._inner.signatures[0].sig, - verifier=base64_encode_pem_cert(cert), - ) - return rekor_types.Dsse( - spec=rekor_types.dsse.DsseV001Schema( - signatures=[signature], - envelope_hash=rekor_types.dsse.EnvelopeHash( - algorithm=rekor_types.dsse.Algorithm.SHA256, - value=None, - ), - payload_hash=rekor_types.dsse.PayloadHash( - algorithm=rekor_types.dsse.Algorithm.SHA256, - value=None, - ), - ) - ) +__all__ = [ + "RekorClient", + "SignedCheckpoint", + "_hashedrekord_from_parts", +] # TODO: This should probably live somewhere better. From e1753ffea3c3376068c13ded0375602f38c993e5 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 12 Apr 2024 15:12:10 -0400 Subject: [PATCH 14/14] dsse, verifier: handle multiple sigs gracefully Signed-off-by: William Woodruff --- sigstore/dsse.py | 20 +++++++++++--------- sigstore/verify/verifier.py | 16 +++++++++++----- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/sigstore/dsse.py b/sigstore/dsse.py index aad6d1c3f..2568b0ebd 100644 --- a/sigstore/dsse.py +++ b/sigstore/dsse.py @@ -228,19 +228,21 @@ def _verify(key: ec.EllipticCurvePublicKey, evp: Envelope) -> bytes: This function does **not** check the envelope's payload type. The caller is responsible for performing this check. - - Assumes that the envelope only has a single signature. """ pae = _pae(evp._inner.payload_type, evp._inner.payload) - try: - # NB: Assumes only one signature in the DSSE envelope. - key.verify(evp._inner.signatures[0].sig, pae, ec.ECDSA(hashes.SHA256())) - except IndexError: - raise VerificationError("DSSE: no signature to verify") - except InvalidSignature: - raise VerificationError("DSSE: invalid signature") + if not evp._inner.signatures: + raise VerificationError("DSSE: envelope contains no signatures") + + # In practice checking more than one signature here is frivolous, since + # they're all being checked against the same key. But there's no + # particular harm in checking them all either. + for signature in evp._inner.signatures: + try: + key.verify(signature.sig, pae, ec.ECDSA(hashes.SHA256())) + except InvalidSignature: + raise VerificationError("DSSE: invalid signature") # TODO: Remove ignore when protobuf-specs contains a py.typed marker. # See: diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 3a1916dba..6e0be2a0f 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -269,11 +269,17 @@ def verify_dsse( if payload_hash != entry_body.spec.root.payload_hash.value: # type: ignore[union-attr] raise VerificationError("log entry payload hash does not match bundle") - signature = rekor_types.dsse.Signature( - signature=base64.b64encode(envelope._inner.signatures[0].sig).decode(), - verifier=base64_encode_pem_cert(bundle.signing_certificate), - ) - if [signature] != entry_body.spec.root.signatures: + # NOTE: Like `dsse._verify`: multiple signatures would be frivolous here, + # but we handle them just in case the signer has somehow produced multiple + # signatures for their envelope with the same signing key. + signatures = [ + rekor_types.dsse.Signature( + signature=base64.b64encode(signature.sig).decode(), + verifier=base64_encode_pem_cert(bundle.signing_certificate), + ) + for signature in envelope._inner.signatures + ] + if signatures != entry_body.spec.root.signatures: raise VerificationError("log entry signatures do not match bundle") return (envelope._inner.payload_type, envelope._inner.payload)