From 4935250afa476607fbe3214278202f3d7dc6d473 Mon Sep 17 00:00:00 2001 From: nbayati <99771966+nbayati@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:29:55 -0700 Subject: [PATCH 1/3] fix(auth): restore pyOpenSSL for ECP offload flow Resolves an issue where Enterprise Certificate Proxy (ECP) offload failed with "failed to configure ECP Offload SSL context" after pyOpenSSL was removed in #16976. The `tls_offload` C library requires a PyOpenSSL context pointer and does not support standard library CPython `ssl.SSLContext`. This change: - Restores `_cast_ssl_ctx_to_void_p_pyopenssl` in `_custom_tls_signer.py` for the offload branch. - Conditionally calls `urllib3.contrib.pyopenssl.inject_into_urllib3()` in `_MutualTlsOffloadAdapter` only when the offload flow is used, leaving the ECP Provider flow on standard library `ssl.SSLContext`. - Restores `pyopenssl` and `cffi` in `enterprise_cert` extra requirements in `setup.py`. - Updates unit tests Fixes #17791 --- .../google/auth/transport/_custom_tls_signer.py | 14 +++++++++++++- .../google/auth/transport/requests.py | 7 ++++++- packages/google-auth/setup.py | 3 ++- .../tests/transport/test__custom_tls_signer.py | 16 +++++++++++++++- .../google-auth/tests/transport/test_requests.py | 1 + 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/google-auth/google/auth/transport/_custom_tls_signer.py b/packages/google-auth/google/auth/transport/_custom_tls_signer.py index 90143101ab07..6f4c5d2d959c 100644 --- a/packages/google-auth/google/auth/transport/_custom_tls_signer.py +++ b/packages/google-auth/google/auth/transport/_custom_tls_signer.py @@ -45,6 +45,18 @@ ) +# Cast SSL_CTX* to void* +def _cast_ssl_ctx_to_void_p_pyopenssl(ssl_ctx): + try: + import cffi + except ImportError as caught_exc: + raise exceptions.MutualTLSChannelError( + "cffi is required for pyOpenSSL ECP support." + ) from caught_exc + + return ctypes.cast(int(cffi.FFI().cast("intptr_t", ssl_ctx)), ctypes.c_void_p) + + # Cast SSL_CTX* to void* def _cast_ssl_ctx_to_void_p_stdlib(context): if not issubclass(type(context), ssl.SSLContext): @@ -281,7 +293,7 @@ def attach_to_ssl_context(self, ctx): if not self._offload_lib.ConfigureSslContext( self._sign_callback, ctypes.c_char_p(self._cert), - _cast_ssl_ctx_to_void_p_stdlib(ctx), + _cast_ssl_ctx_to_void_p_pyopenssl(ctx._ctx._context), ): raise exceptions.MutualTLSChannelError( "failed to configure ECP Offload SSL context" diff --git a/packages/google-auth/google/auth/transport/requests.py b/packages/google-auth/google/auth/transport/requests.py index 822cf687f5d0..0cb7342105b5 100644 --- a/packages/google-auth/google/auth/transport/requests.py +++ b/packages/google-auth/google/auth/transport/requests.py @@ -278,7 +278,7 @@ class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter): } Raises: - ImportError: if certifi is not installed + ImportError: if certifi or pyOpenSSL is not installed google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel creation failed for any reason. """ @@ -290,6 +290,11 @@ def __init__(self, enterprise_cert_file_path): self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path) self.signer.load_libraries() + if not self.signer.should_use_provider(): + import urllib3.contrib.pyopenssl + + urllib3.contrib.pyopenssl.inject_into_urllib3() + poolmanager = create_urllib3_context() poolmanager.load_verify_locations(cafile=certifi.where()) self.signer.attach_to_ssl_context(poolmanager) diff --git a/packages/google-auth/setup.py b/packages/google-auth/setup.py index cff8a7bdbda9..29ab4b9590b6 100644 --- a/packages/google-auth/setup.py +++ b/packages/google-auth/setup.py @@ -39,7 +39,7 @@ reauth_extra_require = ["pyu2f>=0.1.5"] -enterprise_cert_extra_require = cryptography_base_require +enterprise_cert_extra_require = ["pyopenssl>=20.0.0", "cffi>=1.0.0"] urllib3_extra_require = [ "urllib3 >= 1.26.15, < 3.0.0", @@ -65,6 +65,7 @@ *reauth_extra_require, "responses", *urllib3_extra_require, + *enterprise_cert_extra_require, # Async Dependencies *aiohttp_extra_require, "aioresponses", diff --git a/packages/google-auth/tests/transport/test__custom_tls_signer.py b/packages/google-auth/tests/transport/test__custom_tls_signer.py index fa210ee0b8d7..395dbfbb2245 100644 --- a/packages/google-auth/tests/transport/test__custom_tls_signer.py +++ b/packages/google-auth/tests/transport/test__custom_tls_signer.py @@ -168,6 +168,9 @@ def test_get_cert(): def test_custom_tls_signer(): + urllib3_pyopenssl = pytest.importorskip("urllib3.contrib.pyopenssl") + urllib3_pyopenssl.inject_into_urllib3() + offload_lib = mock.MagicMock() signer_lib = mock.MagicMock() @@ -238,7 +241,9 @@ def test_custom_tls_signer_failed_to_attach(): signer_object._sign_callback = mock.MagicMock() signer_object._cert = b"mock cert" signer_object._offload_lib.ConfigureSslContext.return_value = False - signer_object.attach_to_ssl_context(ssl.SSLContext()) + ctx = mock.Mock() + ctx._ctx._context = 123456 + signer_object.attach_to_ssl_context(ctx) assert excinfo.match("failed to configure ECP Offload SSL context") @@ -366,3 +371,12 @@ def test_cast_ssl_ctx_to_void_p_stdlib_mock_error(): TypeError, match="context must be an instance of ssl.SSLContext, not a mock" ): _custom_tls_signer._cast_ssl_ctx_to_void_p_stdlib(context) + + +def test_cast_ssl_ctx_to_void_p_pyopenssl(): + urllib3_pyopenssl = pytest.importorskip("urllib3.contrib.pyopenssl") + urllib3_pyopenssl.inject_into_urllib3() + + context = create_urllib3_context() + res = _custom_tls_signer._cast_ssl_ctx_to_void_p_pyopenssl(context._ctx._context) + assert isinstance(res, ctypes.c_void_p) diff --git a/packages/google-auth/tests/transport/test_requests.py b/packages/google-auth/tests/transport/test_requests.py index 2ca1922494ef..70eca665c16e 100644 --- a/packages/google-auth/tests/transport/test_requests.py +++ b/packages/google-auth/tests/transport/test_requests.py @@ -1061,6 +1061,7 @@ def test_success( mock_proxy_manager_for, mock_init_poolmanager, ): + pytest.importorskip("urllib3.contrib.pyopenssl") enterprise_cert_file_path = "/path/to/enterprise/cert/json" adapter = google.auth.transport.requests._MutualTlsOffloadAdapter( enterprise_cert_file_path From 0f0d97b68f7dd4d1b6e1edf7f1b89622a4d48229 Mon Sep 17 00:00:00 2001 From: Negar Bayati Date: Thu, 13 Aug 2026 20:22:25 +0000 Subject: [PATCH 2/3] fix(auth): catch OpenSSL.SSL.Error during mTLS channel setup in requests and urllib3 --- .../google/auth/transport/requests.py | 12 ++++++++--- .../google/auth/transport/urllib3.py | 18 ++++++++++++++-- packages/google-auth/noxfile.py | 1 + .../tests/transport/test_requests.py | 18 ++++++++++++++++ .../tests/transport/test_urllib3.py | 21 +++++++++++++++++++ 5 files changed, 65 insertions(+), 5 deletions(-) diff --git a/packages/google-auth/google/auth/transport/requests.py b/packages/google-auth/google/auth/transport/requests.py index 0cb7342105b5..d367c3958c5e 100644 --- a/packages/google-auth/google/auth/transport/requests.py +++ b/packages/google-auth/google/auth/transport/requests.py @@ -39,9 +39,15 @@ from google.auth import exceptions from google.auth import transport from google.auth.transport import _mtls_helper -import google.auth.transport._mtls_helper from google.oauth2 import service_account +try: + import OpenSSL.SSL # type: ignore + + _OPENSSL_SSL_ERROR = (OpenSSL.SSL.Error,) +except ImportError: + _OPENSSL_SSL_ERROR = () # type: ignore + _LOGGER = logging.getLogger(__name__) _DEFAULT_TIMEOUT = 120 # in seconds @@ -242,7 +248,7 @@ def __init__(self, cert, key, **kwargs): ValueError, RuntimeError, TypeError, - ) as exc: + ) + _OPENSSL_SSL_ERROR as exc: raise exceptions.MutualTLSChannelError( "Failed to configure client certificate and key for mTLS." ) from exc @@ -542,7 +548,7 @@ def configure_mtls_channel(self, client_cert_callback=None): ImportError, OSError, ValueError, - ) as caught_exc: + ) + _OPENSSL_SSL_ERROR as caught_exc: new_exc = exceptions.MutualTLSChannelError(caught_exc) raise new_exc from caught_exc diff --git a/packages/google-auth/google/auth/transport/urllib3.py b/packages/google-auth/google/auth/transport/urllib3.py index 18e6128e03bd..9e8265dfbc52 100644 --- a/packages/google-auth/google/auth/transport/urllib3.py +++ b/packages/google-auth/google/auth/transport/urllib3.py @@ -56,6 +56,13 @@ from google.auth.transport import _mtls_helper from google.oauth2 import service_account +try: + import OpenSSL.SSL # type: ignore + + _OPENSSL_SSL_ERROR = (OpenSSL.SSL.Error,) +except ImportError: + _OPENSSL_SSL_ERROR = () # type: ignore + if version.parse(urllib3.__version__) >= version.parse("2.0.0"): # pragma: NO COVER RequestMethods = urllib3._request_methods.RequestMethods # type: ignore else: # pragma: NO COVER @@ -194,7 +201,14 @@ def _make_mutual_tls_http(cert, key): keyfile=key_path, password=password, ) - except (ssl.SSLError, OSError, IOError, ValueError, RuntimeError, TypeError) as exc: + except ( + ssl.SSLError, + OSError, + IOError, + ValueError, + RuntimeError, + TypeError, + ) + _OPENSSL_SSL_ERROR as exc: raise exceptions.MutualTLSChannelError( "Failed to configure client certificate and key for mTLS." ) from exc @@ -368,7 +382,7 @@ def configure_mtls_channel(self, client_cert_callback=None): ImportError, OSError, ValueError, - ) as caught_exc: + ) + _OPENSSL_SSL_ERROR as caught_exc: new_exc = exceptions.MutualTLSChannelError(caught_exc) raise new_exc from caught_exc diff --git a/packages/google-auth/noxfile.py b/packages/google-auth/noxfile.py index 86a31f3b4660..c6da87bcbaad 100644 --- a/packages/google-auth/noxfile.py +++ b/packages/google-auth/noxfile.py @@ -158,6 +158,7 @@ def mypy(session): session.install( "mypy", "types-certifi", + "types-cffi", "types-freezegun", "types-requests", "types-setuptools", diff --git a/packages/google-auth/tests/transport/test_requests.py b/packages/google-auth/tests/transport/test_requests.py index 70eca665c16e..b6b199bc3a14 100644 --- a/packages/google-auth/tests/transport/test_requests.py +++ b/packages/google-auth/tests/transport/test_requests.py @@ -203,6 +203,24 @@ def test_setup_error_raises_mutual_tls_channel_error(self, mock_secure_paths): assert "Failed to configure client certificate" in str(exc_info.value) assert isinstance(exc_info.value.__cause__, OSError) + @mock.patch("google.auth.transport.requests.create_urllib3_context") + def test_pyopenssl_error_raises_mtls_error(self, mock_create_context): + try: + import OpenSSL.SSL # type: ignore + except ImportError: + pytest.skip("pyOpenSSL not installed") + + mock_context = mock.MagicMock() + mock_context.load_cert_chain.side_effect = OpenSSL.SSL.Error( + "OpenSSL cert load failure" + ) + mock_create_context.return_value = mock_context + + with pytest.raises(exceptions.MutualTLSChannelError) as exc_info: + google.auth.transport.requests._MutualTlsAdapter(b"cert", b"key") + assert "Failed to configure client certificate" in str(exc_info.value) + assert isinstance(exc_info.value.__cause__, OpenSSL.SSL.Error) + def make_response(status=http_client.OK, data=None): response = requests.Response() diff --git a/packages/google-auth/tests/transport/test_urllib3.py b/packages/google-auth/tests/transport/test_urllib3.py index e1c92dbebc2c..2e973c0a5be5 100644 --- a/packages/google-auth/tests/transport/test_urllib3.py +++ b/packages/google-auth/tests/transport/test_urllib3.py @@ -114,6 +114,27 @@ def test_setup_error_raises_mutual_tls_channel_error(self, mock_secure_paths): assert "Failed to configure client certificate" in str(exc_info.value) assert isinstance(exc_info.value.__cause__, OSError) + @mock.patch( + "google.auth.transport.urllib3.urllib3.util.ssl_.create_urllib3_context", + autospec=True, + ) + def test_pyopenssl_error_raises_mtls_error(self, mock_create_context): + try: + import OpenSSL.SSL # type: ignore + except ImportError: + pytest.skip("pyOpenSSL not installed") + + mock_context = mock.MagicMock() + mock_context.load_cert_chain.side_effect = OpenSSL.SSL.Error( + "OpenSSL cert load failure" + ) + mock_create_context.return_value = mock_context + + with pytest.raises(exceptions.MutualTLSChannelError) as exc_info: + google.auth.transport.urllib3._make_mutual_tls_http(b"cert", b"key") + assert "Failed to configure client certificate" in str(exc_info.value) + assert isinstance(exc_info.value.__cause__, OpenSSL.SSL.Error) + class TestAuthorizedHttp(object): TEST_URL = "http://example.com" From e0c06a0ff4a337b799747e9c3d72c62a4b275578 Mon Sep 17 00:00:00 2001 From: Negar Bayati Date: Thu, 13 Aug 2026 22:51:40 +0000 Subject: [PATCH 3/3] fix(test): teardown pyopenssl injection and isolate gcloud config in transport tests --- .../google-auth/google/auth/transport/requests.py | 6 ++---- .../tests/transport/test__custom_tls_signer.py | 12 +++++++----- .../google-auth/tests/transport/test_requests.py | 15 ++++++++++++++- .../google-auth/tests/transport/test_urllib3.py | 5 ++++- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/google-auth/google/auth/transport/requests.py b/packages/google-auth/google/auth/transport/requests.py index d367c3958c5e..af0fedb595f5 100644 --- a/packages/google-auth/google/auth/transport/requests.py +++ b/packages/google-auth/google/auth/transport/requests.py @@ -478,7 +478,7 @@ def configure_mtls_channel(self, client_cert_callback=None): creation failed for any reason. The existing session state (such as adapter mounts) remains unmodified if this error is raised. """ - use_client_cert = google.auth.transport._mtls_helper.check_use_client_cert() + use_client_cert = _mtls_helper.check_use_client_cert() if not use_client_cert: return @@ -487,9 +487,7 @@ def configure_mtls_channel(self, client_cert_callback=None): is_mtls, cert, key, - ) = google.auth.transport._mtls_helper.get_client_cert_and_key( - client_cert_callback - ) + ) = _mtls_helper.get_client_cert_and_key(client_cert_callback) old_adapter = self.adapters.get("https://") diff --git a/packages/google-auth/tests/transport/test__custom_tls_signer.py b/packages/google-auth/tests/transport/test__custom_tls_signer.py index 395dbfbb2245..f12e247bb701 100644 --- a/packages/google-auth/tests/transport/test__custom_tls_signer.py +++ b/packages/google-auth/tests/transport/test__custom_tls_signer.py @@ -167,10 +167,15 @@ def test_get_cert(): assert len(mock_cert) == mock_cert_len -def test_custom_tls_signer(): +@pytest.fixture +def inject_pyopenssl(): urllib3_pyopenssl = pytest.importorskip("urllib3.contrib.pyopenssl") urllib3_pyopenssl.inject_into_urllib3() + yield + urllib3_pyopenssl.extract_from_urllib3() + +def test_custom_tls_signer(inject_pyopenssl): offload_lib = mock.MagicMock() signer_lib = mock.MagicMock() @@ -373,10 +378,7 @@ def test_cast_ssl_ctx_to_void_p_stdlib_mock_error(): _custom_tls_signer._cast_ssl_ctx_to_void_p_stdlib(context) -def test_cast_ssl_ctx_to_void_p_pyopenssl(): - urllib3_pyopenssl = pytest.importorskip("urllib3.contrib.pyopenssl") - urllib3_pyopenssl.inject_into_urllib3() - +def test_cast_ssl_ctx_to_void_p_pyopenssl(inject_pyopenssl): context = create_urllib3_context() res = _custom_tls_signer._cast_ssl_ctx_to_void_p_pyopenssl(context._ctx._context) assert isinstance(res, ctypes.c_void_p) diff --git a/packages/google-auth/tests/transport/test_requests.py b/packages/google-auth/tests/transport/test_requests.py index b6b199bc3a14..982b6febd790 100644 --- a/packages/google-auth/tests/transport/test_requests.py +++ b/packages/google-auth/tests/transport/test_requests.py @@ -694,8 +694,11 @@ def test_configure_mtls_channel_cert_loading_exceptions( "CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH": "", }, ) + @mock.patch( + "google.auth.transport._mtls_helper._get_cert_config_path", return_value=None + ) def test_configure_mtls_channel_without_client_cert_env( - self, get_client_cert_and_key + self, mock_get_cert_config_path, get_client_cert_and_key ): env_to_patch = { environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "", @@ -1063,6 +1066,16 @@ def test_configure_mtls_channel_subsequent_disabled(self): class TestMutualTlsOffloadAdapter(object): + @pytest.fixture(autouse=True) + def teardown_pyopenssl(self): + yield + try: + from urllib3.contrib.pyopenssl import extract_from_urllib3 + + extract_from_urllib3() + except ImportError: + pass + @mock.patch.object(requests.adapters.HTTPAdapter, "init_poolmanager") @mock.patch.object(requests.adapters.HTTPAdapter, "proxy_manager_for") @mock.patch.object( diff --git a/packages/google-auth/tests/transport/test_urllib3.py b/packages/google-auth/tests/transport/test_urllib3.py index 2e973c0a5be5..0777f02267ce 100644 --- a/packages/google-auth/tests/transport/test_urllib3.py +++ b/packages/google-auth/tests/transport/test_urllib3.py @@ -418,8 +418,11 @@ def test_configure_mtls_channel_cert_loading_exceptions( "CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH": "", }, ) + @mock.patch( + "google.auth.transport._mtls_helper._get_cert_config_path", return_value=None + ) def test_configure_mtls_channel_without_client_cert_env( - self, get_client_cert_and_key + self, mock_get_cert_config_path, get_client_cert_and_key ): callback = mock.Mock()