Skip to content

Commit

Permalink
feat: add default client cert source util (#486)
Browse files Browse the repository at this point in the history
feat: add default client cert source util
  • Loading branch information
arithmetic1728 committed Apr 13, 2020
1 parent cc614a6 commit ed41b49
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 22 deletions.
3 changes: 2 additions & 1 deletion google/auth/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ class DefaultCredentialsError(GoogleAuthError):


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."""
60 changes: 60 additions & 0 deletions google/auth/transport/mtls.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 4 additions & 4 deletions google/auth/transport/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,8 @@ class AuthorizedSession(requests.Session):
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.
Expand Down Expand Up @@ -344,11 +344,11 @@ def configure_mtls_channel(self, client_cert_callback=None):
"""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
Expand Down
12 changes: 6 additions & 6 deletions google/auth/transport/urllib3.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ class AuthorizedHttp(urllib3.request.RequestMethods):
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.
Expand Down Expand Up @@ -280,14 +280,14 @@ def __init__(

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
Expand All @@ -308,7 +308,7 @@ def configure_mtls_channel(self, client_cert_callabck=None):

try:
found_cert_key, cert, key = transport._mtls_helper.get_client_cert_and_key(
client_cert_callabck
client_cert_callback
)

if found_cert_key:
Expand Down
60 changes: 49 additions & 11 deletions system_tests/test_mtls_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,14 @@

import google.auth
import google.auth.credentials
from google.auth.transport import mtls
import google.auth.transport.requests
import google.auth.transport.urllib3

MTLS_ENDPOINT = "https://pubsub.mtls.googleapis.com/v1/projects/{}/topics"
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(
Expand All @@ -39,9 +35,9 @@ def test_requests():
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)
Expand All @@ -63,9 +59,9 @@ def test_urllib3():
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)
Expand All @@ -76,3 +72,45 @@ def test_urllib3():
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
55 changes: 55 additions & 0 deletions tests/transport/test_mtls.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit ed41b49

Please sign in to comment.