feat: add default client cert source util (#486)
feat: add default client cert source util
diff --git a/google/auth/exceptions.py b/google/auth/exceptions.py
index 9501386..5b61460 100644
--- a/google/auth/exceptions.py
+++ b/google/auth/exceptions.py
@@ -37,4 +37,5 @@
class MutualTLSChannelError(GoogleAuthError):
- """Used to indicate that mutual TLS channel creation is failed."""
+ """Used to indicate that mutual TLS channel creation is failed, or mutual
+ TLS channel credentials is missing or invalid."""
diff --git a/google/auth/transport/mtls.py b/google/auth/transport/mtls.py
new file mode 100644
index 0000000..063b265
--- /dev/null
+++ b/google/auth/transport/mtls.py
@@ -0,0 +1,60 @@
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilites for mutual TLS."""
+
+import six
+
+from google.auth import exceptions
+from google.auth.transport import _mtls_helper
+
+
+def has_default_client_cert_source():
+ """Check if default client SSL credentials exists on the device.
+
+ Returns:
+ bool: indicating if the default client cert source exists.
+ """
+ metadata_path = _mtls_helper._check_dca_metadata_path(
+ _mtls_helper.CONTEXT_AWARE_METADATA_PATH
+ )
+ return metadata_path is not None
+
+
+def default_client_cert_source():
+ """Get a callback which returns the default client SSL credentials.
+
+ Returns:
+ Callable[[], [bytes, bytes]]: A callback which returns the default
+ client certificate bytes and private key bytes, both in PEM format.
+
+ Raises:
+ google.auth.exceptions.DefaultClientCertSourceError: If the default
+ client SSL credentials don't exist or are malformed.
+ """
+ if not has_default_client_cert_source():
+ raise exceptions.MutualTLSChannelError(
+ "Default client cert source doesn't exist"
+ )
+
+ def callback():
+ try:
+ _, cert_bytes, key_bytes = _mtls_helper.get_client_cert_and_key()
+ except (OSError, RuntimeError, ValueError) as caught_exc:
+ new_exc = exceptions.MutualTLSChannelError(caught_exc)
+ six.raise_from(new_exc, caught_exc)
+
+ return cert_bytes, key_bytes
+
+ return callback
diff --git a/google/auth/transport/requests.py b/google/auth/transport/requests.py
index 26096e2..cc0e93b 100644
--- a/google/auth/transport/requests.py
+++ b/google/auth/transport/requests.py
@@ -249,8 +249,8 @@
credentials' headers to the request and refreshing credentials as needed.
This class also supports mutual TLS via :meth:`configure_mtls_channel`
- method. If client_cert_callabck is provided, client certificate and private
- key are loaded using the callback; if client_cert_callabck is None,
+ method. If client_cert_callback is provided, client certificate and private
+ key are loaded using the callback; if client_cert_callback is None,
application default SSL credentials will be used. Exceptions are raised if
there are problems with the certificate, private key, or the loading process,
so it should be called within a try/except block.
@@ -344,11 +344,11 @@
"""Configure the client certificate and key for SSL connection.
If client certificate and key are successfully obtained (from the given
- client_cert_callabck or from application default SSL credentials), a
+ client_cert_callback or from application default SSL credentials), a
:class:`_MutualTlsAdapter` instance will be mounted to "https://" prefix.
Args:
- client_cert_callabck (Optional[Callable[[], (bytes, bytes)]]):
+ client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
The optional callback returns the client certificate and private
key bytes both in PEM format.
If the callback is None, application default SSL credentials
diff --git a/google/auth/transport/urllib3.py b/google/auth/transport/urllib3.py
index c359f35..3771d84 100644
--- a/google/auth/transport/urllib3.py
+++ b/google/auth/transport/urllib3.py
@@ -202,8 +202,8 @@
credentials' headers to the request and refreshing credentials as needed.
This class also supports mutual TLS via :meth:`configure_mtls_channel`
- method. If client_cert_callabck is provided, client certificate and private
- key are loaded using the callback; if client_cert_callabck is None,
+ method. If client_cert_callback is provided, client certificate and private
+ key are loaded using the callback; if client_cert_callback is None,
application default SSL credentials will be used. Exceptions are raised if
there are problems with the certificate, private key, or the loading process,
so it should be called within a try/except block.
@@ -280,14 +280,14 @@
super(AuthorizedHttp, self).__init__()
- def configure_mtls_channel(self, client_cert_callabck=None):
- """Configures mutual TLS channel using the given client_cert_callabck or
+ def configure_mtls_channel(self, client_cert_callback=None):
+ """Configures mutual TLS channel using the given client_cert_callback or
application default SSL credentials. Returns True if the channel is
mutual TLS and False otherwise. Note that the `http` provided in the
constructor will be overwritten.
Args:
- client_cert_callabck (Optional[Callable[[], (bytes, bytes)]]):
+ client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
The optional callback returns the client certificate and private
key bytes both in PEM format.
If the callback is None, application default SSL credentials
@@ -308,7 +308,7 @@
try:
found_cert_key, cert, key = transport._mtls_helper.get_client_cert_and_key(
- client_cert_callabck
+ client_cert_callback
)
if found_cert_key:
diff --git a/system_tests/test_mtls_http.py b/system_tests/test_mtls_http.py
index 1fd8031..4a6a9c4 100644
--- a/system_tests/test_mtls_http.py
+++ b/system_tests/test_mtls_http.py
@@ -18,6 +18,7 @@
import google.auth
import google.auth.credentials
+from google.auth.transport import mtls
import google.auth.transport.requests
import google.auth.transport.urllib3
@@ -25,11 +26,6 @@
REGULAR_ENDPOINT = "https://pubsub.googleapis.com/v1/projects/{}/topics"
-def check_context_aware_metadata():
- metadata_path = path.expanduser("~/.secureConnect/context_aware_metadata.json")
- return path.exists(metadata_path)
-
-
def test_requests():
credentials, project_id = google.auth.default()
credentials = google.auth.credentials.with_scopes_if_required(
@@ -39,9 +35,9 @@
authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
authed_session.configure_mtls_channel()
- # If the devices has context aware metadata, then a mutual TLS channel is
- # supposed to be created.
- assert authed_session.is_mtls == check_context_aware_metadata()
+ # If the devices has default client cert source, then a mutual TLS channel
+ # is supposed to be created.
+ assert authed_session.is_mtls == mtls.has_default_client_cert_source()
# Sleep 1 second to avoid 503 error.
time.sleep(1)
@@ -63,9 +59,9 @@
authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
is_mtls = authed_http.configure_mtls_channel()
- # If the devices has context aware metadata, then a mutual TLS channel is
- # supposed to be created.
- assert is_mtls == check_context_aware_metadata()
+ # If the devices has default client cert source, then a mutual TLS channel
+ # is supposed to be created.
+ assert is_mtls == mtls.has_default_client_cert_source()
# Sleep 1 second to avoid 503 error.
time.sleep(1)
@@ -76,3 +72,45 @@
response = authed_http.request("GET", REGULAR_ENDPOINT.format(project_id))
assert response.status == 200
+
+
+def test_requests_with_default_client_cert_source():
+ credentials, project_id = google.auth.default()
+ credentials = google.auth.credentials.with_scopes_if_required(
+ credentials, ["https://www.googleapis.com/auth/pubsub"]
+ )
+
+ authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
+
+ if mtls.has_default_client_cert_source():
+ authed_session.configure_mtls_channel(
+ client_cert_callback=mtls.default_client_cert_source()
+ )
+
+ assert authed_session.is_mtls
+
+ # Sleep 1 second to avoid 503 error.
+ time.sleep(1)
+
+ response = authed_session.get(MTLS_ENDPOINT.format(project_id))
+ assert response.ok
+
+
+def test_urllib3_with_default_client_cert_source():
+ credentials, project_id = google.auth.default()
+ credentials = google.auth.credentials.with_scopes_if_required(
+ credentials, ["https://www.googleapis.com/auth/pubsub"]
+ )
+
+ authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
+
+ if mtls.has_default_client_cert_source():
+ assert authed_http.configure_mtls_channel(
+ client_cert_callback=mtls.default_client_cert_source()
+ )
+
+ # Sleep 1 second to avoid 503 error.
+ time.sleep(1)
+
+ response = authed_http.request("GET", MTLS_ENDPOINT.format(project_id))
+ assert response.status == 200
diff --git a/tests/transport/test_mtls.py b/tests/transport/test_mtls.py
new file mode 100644
index 0000000..d3bc391
--- /dev/null
+++ b/tests/transport/test_mtls.py
@@ -0,0 +1,55 @@
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mock
+import pytest
+
+from google.auth import exceptions
+from google.auth.transport import mtls
+
+
+@mock.patch(
+ "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True
+)
+def test_has_default_client_cert_source(check_dca_metadata_path):
+ check_dca_metadata_path.return_value = mock.Mock()
+ assert mtls.has_default_client_cert_source()
+
+ check_dca_metadata_path.return_value = None
+ assert not mtls.has_default_client_cert_source()
+
+
+@mock.patch("google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True)
+@mock.patch("google.auth.transport.mtls.has_default_client_cert_source", autospec=True)
+def test_default_client_cert_source(
+ has_default_client_cert_source, get_client_cert_and_key
+):
+ # Test default client cert source doesn't exist.
+ has_default_client_cert_source.return_value = False
+ with pytest.raises(exceptions.MutualTLSChannelError):
+ mtls.default_client_cert_source()
+
+ # The following tests will assume default client cert source exists.
+ has_default_client_cert_source.return_value = True
+
+ # Test good callback.
+ get_client_cert_and_key.return_value = (True, b"cert", b"key")
+ callback = mtls.default_client_cert_source()
+ assert callback() == (b"cert", b"key")
+
+ # Test bad callback which throws exception.
+ get_client_cert_and_key.side_effect = ValueError()
+ callback = mtls.default_client_cert_source()
+ with pytest.raises(exceptions.MutualTLSChannelError):
+ callback()