diff --git a/sigstore/_cli.py b/sigstore/_cli.py index 6cc4298a4..25008bc43 100644 --- a/sigstore/_cli.py +++ b/sigstore/_cli.py @@ -813,7 +813,7 @@ def _verify_identity(args: argparse.Namespace) -> None: ) try: - verifier.verify( + verifier.verify_artifact( input_=hashed, bundle=bundle, policy=policy_, @@ -851,7 +851,7 @@ def _verify_github(args: argparse.Namespace) -> None: verifier, materials = _collect_verification_state(args) for file, hashed, bundle in materials: try: - verifier.verify(input_=hashed, bundle=bundle, policy=policy_) + verifier.verify_artifact(input_=hashed, bundle=bundle, policy=policy_) print(f"OK: {file}") except VerificationError as exc: _logger.error(f"FAIL: {file}") diff --git a/sigstore/_internal/rekor/__init__.py b/sigstore/_internal/rekor/__init__.py index 281751f90..ab555a602 100644 --- a/sigstore/_internal/rekor/__init__.py +++ b/sigstore/_internal/rekor/__init__.py @@ -27,7 +27,11 @@ from .checkpoint import SignedCheckpoint from .client import RekorClient -__all__ = ["RekorClient", "SignedCheckpoint"] +__all__ = [ + "RekorClient", + "SignedCheckpoint", + "_hashedrekord_from_parts", +] # TODO: This should probably live somewhere better. diff --git a/sigstore/dsse.py b/sigstore/dsse.py index 5dfd70b19..2568b0ebd 100644 --- a/sigstore/dsse.py +++ b/sigstore/dsse.py @@ -21,12 +21,15 @@ 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 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[ @@ -103,12 +106,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 +191,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 +220,30 @@ def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope: signatures=[Signature(sig=signature, keyid=None)], ) ) + + +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. + """ + + pae = _pae(evp._inner.payload_type, evp._inner.payload) + + 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: + return evp._inner.payload # type: ignore[no-any-return] 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 03ea68194..6e0be2a0f 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -26,10 +26,7 @@ import rekor_types from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.x509 import ( - ExtendedKeyUsage, - KeyUsage, -) +from cryptography.x509 import ExtendedKeyUsage, KeyUsage from cryptography.x509.oid import ExtendedKeyUsageOID from OpenSSL.crypto import ( X509, @@ -38,7 +35,9 @@ X509StoreContextError, X509StoreFlags, ) +from pydantic import ValidationError +from sigstore import dsse from sigstore._internal.rekor import _hashedrekord_from_parts from sigstore._internal.rekor.client import RekorClient from sigstore._internal.sct import ( @@ -46,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 @@ -99,26 +98,38 @@ def staging(cls) -> Verifier: trusted_root=trusted_root, ) - def verify( - self, - input_: bytes | Hashed, - bundle: Bundle, - policy: VerificationPolicy, + def _verify_common_signing_cert( + self, bundle: Bundle, policy: VerificationPolicy ) -> None: """ - Public API for verifying. - - `input_` is the input to verify, either as a buffer of contents or as - a prehashed `Hashed` object. - - `bundle` is the Sigstore `Bundle` to verify against. + Performs the signing certificate verification steps that are shared between + `verify_dsse` and `verify_artifact`. - `policy` is the `VerificationPolicy` to verify against. - - On failure, this method raises `VerificationError`. + Raises `VerificationError` on all failures. """ - hashed_input = sha256_digest(input_) + # 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` @@ -133,28 +144,11 @@ def verify( 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 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 - # 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 - # 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) + # (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) @@ -162,46 +156,159 @@ def verify( # 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 + except X509StoreContextError as e: + raise VerificationError(f"failed to build chain: {e}") - # 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(), - ) + # (2): verify the signing certificate's 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 VerificationError as e: + raise VerificationError(f"failed to verify SCT on signing certificate: {e}") - # 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 - ) + # (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`") - # Check that extended usage contains "code signing" - extended_usage_ext = ( - bundle.signing_certificate.extensions.get_extension_for_class( - ExtendedKeyUsage - ) - ) + 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(bundle.signing_certificate) + policy.verify(cert) _logger.debug("Successfully verified signing certificate validity...") - # 4) Verify that the signature was signed by the public key in the signing certificate + # (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 DSSE envelope, returning the encapsulated payload + and its content type. + + This method is only for DSSE-enveloped payloads. 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 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 (6) are performed by `_verify_common_signing_cert`. + self._verify_common_signing_cert(bundle, policy) + + # (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" + ) + + 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. + # 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}") + + payload_hash = sha256_digest(envelope._inner.payload).digest.hex() + if ( + 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: # type: ignore[union-attr] + raise VerificationError("log entry payload hash does not match bundle") + + # 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) + + def verify_artifact( + self, + input_: bytes | Hashed, + bundle: Bundle, + policy: VerificationPolicy, + ) -> None: + """ + Public API for verifying. + + `input_` is the input to verify, either as a buffer of contents or as + a prehashed `Hashed` object. + + `bundle` is the Sigstore `Bundle` to verify against. + + `policy` is the `VerificationPolicy` to verify against. + + On failure, this method raises `VerificationError`. + """ + + # (1) through (6) are performed by `_verify_common_signing_cert`. + self._verify_common_signing_cert(bundle, policy) + + hashed_input = sha256_digest(input_) + + # (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) @@ -215,8 +322,8 @@ def verify( _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( @@ -231,22 +338,3 @@ def verify( 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}") - - # 7) Verify that the signing certificate was valid at the time of signing - 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=" } } diff --git a/test/unit/test_sign.py b/test/unit/test_sign.py index fe7b69a9b..79597cdaa 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 diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 0ec94076f..b5f96a273 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 @@ -39,7 +42,7 @@ def test_verifier_one_verification(signing_materials, null_policy): (file, bundle) = signing_materials("a.txt", verifier._rekor) - verifier.verify(file.read_bytes(), bundle, null_policy) + verifier.verify_artifact(file.read_bytes(), bundle, null_policy) def test_verifier_inconsistent_log_entry(signing_bundle, null_policy, mock_staging_tuf): @@ -51,7 +54,7 @@ def test_verifier_inconsistent_log_entry(signing_bundle, null_policy, mock_stagi VerificationError, match="transparency log entry is inconsistent with other materials", ): - verifier.verify(file.read_bytes(), bundle, null_policy) + verifier.verify_artifact(file.read_bytes(), bundle, null_policy) @pytest.mark.online @@ -62,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]: - verifier.verify(file.read_bytes(), bundle, null_policy) + verifier.verify_artifact(file.read_bytes(), bundle, null_policy) @pytest.mark.parametrize( @@ -72,7 +75,7 @@ def test_verifier_bundle(signing_bundle, null_policy, mock_staging_tuf, filename (file, bundle) = signing_bundle(filename) verifier = Verifier.staging() - verifier.verify(file.read_bytes(), bundle, null_policy) + verifier.verify_artifact(file.read_bytes(), bundle, null_policy) @pytest.mark.online @@ -85,7 +88,7 @@ def test_verifier_email_identity(signing_materials): issuer="https://github.com/login/oauth", ) - verifier.verify( + verifier.verify_artifact( file.read_bytes(), bundle, policy_, @@ -104,7 +107,7 @@ def test_verifier_uri_identity(signing_materials): issuer="https://token.actions.githubusercontent.com", ) - verifier.verify( + verifier.verify_artifact( file.read_bytes(), bundle, policy_, @@ -120,7 +123,7 @@ def test_verifier_policy_check(signing_materials): policy_ = pretend.stub(verify=pretend.raiser(VerificationError("policy failed"))) with pytest.raises(VerificationError, match="policy failed"): - verifier.verify( + verifier.verify_artifact( file.read_bytes(), bundle, policy_, @@ -145,4 +148,33 @@ def test_verifier_fail_expiry(signing_materials, null_policy, monkeypatch): entry.integrated_time = datetime.MINYEAR with pytest.raises(VerificationError): - verifier.verify(file.read_bytes(), bundle, null_policy) + verifier.verify_artifact(file.read_bytes(), bundle, null_policy) + + +@pytest.mark.online +@pytest.mark.ambient_oidc +def test_verifier_dsse_roundtrip(staging): + signer_cls, verifier_cls, identity = staging + + ctx = signer_cls() + 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_intoto(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