diff --git a/pyproject.toml b/pyproject.toml index 97918b73d..2028344f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ dependencies = [ "importlib_resources ~= 5.7; python_version < '3.11'", "pydantic >= 2,< 3", "pyjwt >= 2.1", - "pyOpenSSL >= 23.0.0", "requests", "rich >= 13,< 16", "rfc8785 ~= 0.1.2", @@ -66,7 +65,6 @@ dev = [ # NOTE(ww): ruff is under active development, so we pin conservatively here # and let Dependabot periodically perform this update. "ruff<0.15.23", - "types-pyOpenSSL", "mkdocs-material[imaging]", "mkdocstrings-python", "bump >= 1.3.2", diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index 13918366e..6d4f56d42 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -27,14 +27,21 @@ from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.x509 import Certificate, ExtendedKeyUsage, KeyUsage +from cryptography.x509 import ( + Certificate, + ExtendedKeyUsage, + KeyUsage, + UnsupportedGeneralNameType, +) from cryptography.x509.oid import ExtendedKeyUsageOID -from OpenSSL.crypto import ( - X509, - X509Store, - X509StoreContext, - X509StoreContextError, - X509StoreFlags, +from cryptography.x509.verification import ( + Criticality, + ExtensionPolicy, + PolicyBuilder, + Store, +) +from cryptography.x509.verification import ( + VerificationError as X509VerificationError, ) from pydantic import ValidationError from rfc3161_client import TimeStampResponse, VerifierBuilder @@ -80,10 +87,7 @@ def __init__(self, *, trusted_root: TrustedRoot): `trusted_root` is the `TrustedRoot` object containing the root of trust for the verification process. """ - self._fulcio_certificate_chain: list[X509] = [ - X509.from_cryptography(parent_cert) - for parent_cert in trusted_root.get_fulcio_certs() - ] + self._fulcio_certificate_chain = trusted_root.get_fulcio_certs() self._trusted_root = trusted_root # this is an ugly hack needed for verifying "detached" materials @@ -244,35 +248,34 @@ def _establish_time(self, bundle: Bundle) -> list[TimestampVerificationResult]: return verified_timestamps def _verify_chain_at_time( - self, certificate: X509, timestamp_result: TimestampVerificationResult - ) -> list[X509]: + self, certificate: Certificate, timestamp_result: TimestampVerificationResult + ) -> list[Certificate]: """ Verify the validity of the certificate chain at the given time. Raises a VerificationError if the chain can't be built or be verified. """ - # NOTE: The `X509Store` object cannot have its time reset once the `set_time` - # method been called on it. To get around this, we construct a new one in each - # 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) - - store.set_time(timestamp_result.time) - - store_ctx = X509StoreContext(store, certificate) + # Client verifiers normally require the client-auth EKU. Fulcio certificates + # instead use code-signing, which is checked separately below; overriding + # only the EKU validators preserves the remaining default extension policies. + ca_policy = ExtensionPolicy.webpki_defaults_ca().may_be_present( + ExtendedKeyUsage, Criticality.NON_CRITICAL, None + ) + ee_policy = ExtensionPolicy.webpki_defaults_ee().may_be_present( + ExtendedKeyUsage, Criticality.NON_CRITICAL, None + ) + verifier = ( + PolicyBuilder() + .store(Store(self._fulcio_certificate_chain)) + .time(timestamp_result.time) + .extension_policies(ca_policy=ca_policy, ee_policy=ee_policy) + .build_client_verifier() + ) try: - # get_verified_chain returns the full chain including the end-entity certificate - # and chain should contain only CA certificates - return store_ctx.get_verified_chain()[1:] - except X509StoreContextError as e: + # The verified chain includes the end-entity certificate, which callers omit. + return verifier.verify(certificate, []).chain[1:] + except (X509VerificationError, UnsupportedGeneralNameType) as e: raise CertValidationError( f"failed to build timestamp certificate chain: {e}" ) @@ -311,19 +314,6 @@ def _verify_common_signing_cert( 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. - 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) - # (0): Establishing a Time for the Signature # First, establish verified times for the signature. This is required to # validate the certificate chain, so this step comes first. @@ -336,16 +326,15 @@ def _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. - cert_ossl = X509.from_cryptography(cert) - chain: list[X509] = [] + chain: list[Certificate] = [] for vts in verified_timestamps: - chain = self._verify_chain_at_time(cert_ossl, vts) + chain = self._verify_chain_at_time(cert, vts) # (2): verify the signing certificate's SCT. try: verify_sct( cert, - [parent_cert.to_cryptography() for parent_cert in chain], + chain, self._trusted_root.ct_keyring(KeyringPurpose.VERIFY), ) except VerificationError as e: diff --git a/test/unit/verify/test_verifier.py b/test/unit/verify/test_verifier.py index 1a500ec93..81287e7f6 100644 --- a/test/unit/verify/test_verifier.py +++ b/test/unit/verify/test_verifier.py @@ -24,7 +24,7 @@ from sigstore._internal.trust import CertificateAuthority from sigstore.dsse import StatementBuilder, Subject -from sigstore.errors import VerificationError +from sigstore.errors import CertValidationError, VerificationError from sigstore.models import Bundle from sigstore.verify import policy from sigstore.verify.verifier import Verifier @@ -123,6 +123,18 @@ def test_verifier_bundle_offline(signing_bundle, null_policy, filename): verifier.verify_artifact(file.read_bytes(), bundle, null_policy) +def test_verifier_certificate_chain_rejects_invalid_time(signing_bundle): + _, bundle = signing_bundle("bundle.txt") + verifier = Verifier.staging(offline=True) + timestamp = verifier._establish_time(bundle)[0] + timestamp.time = datetime(2000, 1, 1, tzinfo=timezone.utc) + + with pytest.raises( + CertValidationError, match="failed to build timestamp certificate chain" + ): + verifier._verify_chain_at_time(bundle.signing_certificate, timestamp) + + @pytest.mark.staging def test_verifier_email_identity(signing_materials): verifier = Verifier.staging() diff --git a/uv.lock b/uv.lock index 69f55e093..fee7b29d4 100644 --- a/uv.lock +++ b/uv.lock @@ -1452,19 +1452,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, ] -[[package]] -name = "pyopenssl" -version = "26.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7", size = 182046, upload-time = "2026-08-01T19:50:50.512Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/ad/2cf6d3fa2fae5c79e1ed9960c0d42badd0f94d81dd12b50604cdc839e648/pyopenssl-26.4.0-py3-none-any.whl", hash = "sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c", size = 56026, upload-time = "2026-08-01T19:50:48.94Z" }, -] - [[package]] name = "pyproject-hooks" version = "1.2.0" @@ -1707,7 +1694,6 @@ dependencies = [ { name = "platformdirs" }, { name = "pydantic" }, { name = "pyjwt" }, - { name = "pyopenssl" }, { name = "requests" }, { name = "rfc3161-client" }, { name = "rfc8785" }, @@ -1730,7 +1716,6 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, - { name = "types-pyopenssl" }, ] [package.metadata] @@ -1741,7 +1726,6 @@ requires-dist = [ { name = "platformdirs", specifier = "~=4.2" }, { name = "pydantic", specifier = ">=2,<3" }, { name = "pyjwt", specifier = ">=2.1" }, - { name = "pyopenssl", specifier = ">=23.0.0" }, { name = "requests" }, { name = "rfc3161-client", specifier = ">=1.0.3,<1.1.0" }, { name = "rfc8785", specifier = "~=0.1.2" }, @@ -1764,7 +1748,6 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff", specifier = "<0.15.23" }, - { name = "types-pyopenssl" }, ] [[package]] @@ -1898,40 +1881,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/c7/301f699ad9427bb0e06935ed56850dd26eecb2b3e8e07b80311875eae676/tuf-7.0.0-py3-none-any.whl", hash = "sha256:572bdbdc9ff4a82278a0d4773e6100863b9b33023f27575e84ca65b486dd0d79", size = 55277, upload-time = "2026-05-18T08:28:56.123Z" }, ] -[[package]] -name = "types-cffi" -version = "2.0.0.20260506" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "types-setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/d0/b54c7338ae45580c56daed5f7d468e359a415b73637affed164f33d83a76/types_cffi-2.0.0.20260506.tar.gz", hash = "sha256:8cf63d7006bf0fec825cc5a70fa637ed783b25ef0d0980d09f27606600123f75", size = 17718, upload-time = "2026-05-06T05:17:56.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/cf/b562968181a5374c9bddaf5f543ddd857d1072c578e77f63744d8a8d3a17/types_cffi-2.0.0.20260506-py3-none-any.whl", hash = "sha256:43472f8e31f8dc7abbf0c4119828f79c3f4a048b5edcebd19538ee6cf3ca69ed", size = 20195, upload-time = "2026-05-06T05:17:54.925Z" }, -] - -[[package]] -name = "types-pyopenssl" -version = "24.1.0.20240722" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "types-cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458, upload-time = "2024-07-22T02:32:22.558Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499, upload-time = "2024-07-22T02:32:21.232Z" }, -] - -[[package]] -name = "types-setuptools" -version = "82.0.0.20260408" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/12/3464b410c50420dd4674fa5fe9d3880711c1dbe1a06f5fe4960ee9067b9e/types_setuptools-82.0.0.20260408.tar.gz", hash = "sha256:036c68caf7e672a699f5ebbf914708d40644c14e05298bc49f7272be91cf43d3", size = 44861, upload-time = "2026-04-08T04:29:33.292Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/e1/46a4fc3ef03aabf5d18bac9df5cf37c6b02c3bddf3e05c3533f4b4588331/types_setuptools-82.0.0.20260408-py3-none-any.whl", hash = "sha256:ece0a215cdfa6463a65fd6f68bd940f39e455729300ddfe61cab1147ed1d2462", size = 68428, upload-time = "2026-04-08T04:29:32.175Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0"