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..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 @@ -278,7 +284,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 +296,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) @@ -537,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/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..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() @@ -1061,6 +1079,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 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"