Skip to content

Commit

Permalink
feat: add GOOGLE_API_USE_CLIENT_CERTIFICATE support (#592)
Browse files Browse the repository at this point in the history
  • Loading branch information
arithmetic1728 committed Aug 27, 2020
1 parent ae27b49 commit c0c995f
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 54 deletions.
7 changes: 7 additions & 0 deletions docs/reference/google.auth.transport.mtls.rst
@@ -0,0 +1,7 @@
google.auth.transport.mtls module
=================================

.. automodule:: google.auth.transport.mtls
:members:
:inherited-members:
:show-inheritance:
6 changes: 6 additions & 0 deletions google/auth/environment_vars.py
Expand Up @@ -53,3 +53,9 @@
GCE_METADATA_IP = "GCE_METADATA_IP"
"""Environment variable providing an alternate ip:port to be used for ip-only
GCE metadata requests."""

GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
"""Environment variable controlling whether to use client certificate or not.
The default value is false. Users have to explicitly set this value to true
in order to use client certificate to establish a mutual TLS channel."""
57 changes: 43 additions & 14 deletions google/auth/transport/grpc.py
Expand Up @@ -17,9 +17,11 @@
from __future__ import absolute_import

import logging
import os

import six

from google.auth import environment_vars
from google.auth import exceptions
from google.auth.transport import _mtls_helper

Expand Down Expand Up @@ -96,6 +98,9 @@ def secure_authorized_channel(
This creates a channel with SSL and :class:`AuthMetadataPlugin`. This
channel can be used to create a stub that can make authorized requests.
Users can configure client certificate or rely on device certificates to
establish a mutual TLS channel, if the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
variable is explicitly set to `true`.
Example::
Expand Down Expand Up @@ -138,7 +143,9 @@ def secure_authorized_channel(
ssl_credentials=regular_ssl_credentials)
Option 2: create a mutual TLS channel by calling a callback which returns
the client side certificate and the key::
the client side certificate and the key (Note that
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
set to `true`)::
def my_client_cert_callback():
code_to_load_client_cert_and_key()
Expand All @@ -155,7 +162,9 @@ def my_client_cert_callback():
Option 3: use application default SSL credentials. It searches and uses
the command in a context aware metadata file, which is available on devices
with endpoint verification support.
with endpoint verification support (Note that
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
set to `true`).
See https://cloud.google.com/endpoint-verification/docs/overview::
try:
Expand All @@ -174,7 +183,8 @@ def my_client_cert_callback():
ssl_credentials=default_ssl_credentials)
Option 4: not setting ssl_credentials and client_cert_callback. For devices
without endpoint verification support, a regular TLS channel is created;
without endpoint verification support or `GOOGLE_API_USE_CLIENT_CERTIFICATE`
environment variable is not `true`, a regular TLS channel is created;
otherwise, a mutual TLS channel is created, however, the call should be
wrapped in a try/except block in case of malformed context aware metadata.
Expand Down Expand Up @@ -205,13 +215,15 @@ def my_client_cert_callback():
This argument is mutually exclusive with client_cert_callback;
providing both will raise an exception.
If ssl_credentials and client_cert_callback are None, application
default SSL credentials will be used.
default SSL credentials are used if `GOOGLE_API_USE_CLIENT_CERTIFICATE`
environment variable is explicitly set to `true`, otherwise one way TLS
SSL credentials are used.
client_cert_callback (Callable[[], (bytes, bytes)]): Optional
callback function to obtain client certicate and key for mutual TLS
connection. This argument is mutually exclusive with
ssl_credentials; providing both will raise an exception.
If ssl_credentials and client_cert_callback are None, application
default SSL credentials will be used.
This argument does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE`
environment variable is explicitly set to `true`.
kwargs: Additional arguments to pass to :func:`grpc.secure_channel`.
Returns:
Expand All @@ -235,16 +247,21 @@ def my_client_cert_callback():

# If SSL credentials are not explicitly set, try client_cert_callback and ADC.
if not ssl_credentials:
if client_cert_callback:
use_client_cert = os.getenv(
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
)
if use_client_cert == "true" and client_cert_callback:
# Use the callback if provided.
cert, key = client_cert_callback()
ssl_credentials = grpc.ssl_channel_credentials(
certificate_chain=cert, private_key=key
)
else:
elif use_client_cert == "true":
# Use application default SSL credentials.
adc_ssl_credentils = SslCredentials()
ssl_credentials = adc_ssl_credentils.ssl_credentials
else:
ssl_credentials = grpc.ssl_channel_credentials()

# Combine the ssl credentials and the authorization credentials.
composite_credentials = grpc.composite_channel_credentials(
Expand All @@ -257,17 +274,29 @@ def my_client_cert_callback():
class SslCredentials:
"""Class for application default SSL credentials.
For devices with endpoint verification support, a device certificate will be
automatically loaded and mutual TLS will be established.
The behavior is controlled by `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment
variable whose default value is `false`. Client certificate will not be used
unless the environment variable is explicitly set to `true`. See
https://google.aip.dev/auth/4114
If the environment variable is `true`, then for devices with endpoint verification
support, a device certificate will be automatically loaded and mutual TLS will
be established.
See https://cloud.google.com/endpoint-verification/docs/overview.
"""

def __init__(self):
# Load client SSL credentials.
metadata_path = _mtls_helper._check_dca_metadata_path(
_mtls_helper.CONTEXT_AWARE_METADATA_PATH
use_client_cert = os.getenv(
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
)
self._is_mtls = metadata_path is not None
if use_client_cert != "true":
self._is_mtls = False
else:
# Load client SSL credentials.
metadata_path = _mtls_helper._check_dca_metadata_path(
_mtls_helper.CONTEXT_AWARE_METADATA_PATH
)
self._is_mtls = metadata_path is not None

@property
def ssl_credentials(self):
Expand Down
26 changes: 21 additions & 5 deletions google/auth/transport/requests.py
Expand Up @@ -19,6 +19,7 @@
import functools
import logging
import numbers
import os
import time

try:
Expand All @@ -40,6 +41,7 @@
) # pylint: disable=ungrouped-imports
import six # pylint: disable=ungrouped-imports

from google.auth import environment_vars
from google.auth import exceptions
from google.auth import transport
import google.auth.transport._mtls_helper
Expand Down Expand Up @@ -249,13 +251,18 @@ 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_callback is provided, client certificate and private
method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
environment variable must be explicitly set to `true`, otherwise it does
nothing. Assume the environment is set to `true`, the method behaves in the
following manner:
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.
First we create an :class:`AuthorizedSession` instance and specify the endpoints::
First we set the environment variable to `true`, then create an :class:`AuthorizedSession`
instance and specify the endpoints::
regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
Expand Down Expand Up @@ -343,9 +350,11 @@ def __init__(
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_callback or from application default SSL credentials), a
:class:`_MutualTlsAdapter` instance will be mounted to "https://" prefix.
The function does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is
explicitly set to `true`. In this case if client certificate and key are
successfully obtained (from the given client_cert_callback or from application
default SSL credentials), a :class:`_MutualTlsAdapter` instance will be mounted
to "https://" prefix.
Args:
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
Expand All @@ -358,6 +367,13 @@ def configure_mtls_channel(self, client_cert_callback=None):
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
creation failed for any reason.
"""
use_client_cert = os.getenv(
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
)
if use_client_cert != "true":
self._is_mtls = False
return

try:
import OpenSSL
except ImportError as caught_exc:
Expand Down
27 changes: 22 additions & 5 deletions google/auth/transport/urllib3.py
Expand Up @@ -17,6 +17,7 @@
from __future__ import absolute_import

import logging
import os
import warnings

# Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle
Expand Down Expand Up @@ -45,6 +46,7 @@
import six
import urllib3.exceptions # pylint: disable=ungrouped-imports

from google.auth import environment_vars
from google.auth import exceptions
from google.auth import transport

Expand Down Expand Up @@ -202,13 +204,18 @@ 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_callback is provided, client certificate and private
method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
environment variable must be explicitly set to `true`, otherwise it does
nothing. Assume the environment is set to `true`, the method behaves in the
following manner:
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.
First we create an :class:`AuthorizedHttp` instance and specify the endpoints::
First we set the environment variable to `true`, then create an :class:`AuthorizedHttp`
instance and specify the endpoints::
regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
Expand Down Expand Up @@ -282,9 +289,13 @@ def __init__(

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.
application default SSL credentials. The behavior is controlled by
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable.
(1) If the environment variable value is `true`, the function returns True
if the channel is mutual TLS and False otherwise. The `http` provided
in the constructor will be overwritten.
(2) If the environment variable is not set or `false`, the function does
nothing and it always return False.
Args:
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
Expand All @@ -300,6 +311,12 @@ def configure_mtls_channel(self, client_cert_callback=None):
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
creation failed for any reason.
"""
use_client_cert = os.getenv(
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
)
if use_client_cert != "true":
return False

try:
import OpenSSL
except ImportError as caught_exc:
Expand Down
21 changes: 13 additions & 8 deletions system_tests/test_mtls_http.py
Expand Up @@ -18,6 +18,7 @@

import google.auth
import google.auth.credentials
from google.auth import environment_vars
from google.auth.transport import mtls
import google.auth.transport.requests
import google.auth.transport.urllib3
Expand All @@ -33,7 +34,8 @@ def test_requests():
)

authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
authed_session.configure_mtls_channel()
with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
authed_session.configure_mtls_channel()

# If the devices has default client cert source, then a mutual TLS channel
# is supposed to be created.
Expand All @@ -57,7 +59,8 @@ def test_urllib3():
)

authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
is_mtls = authed_http.configure_mtls_channel()
with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
is_mtls = authed_http.configure_mtls_channel()

# If the devices has default client cert source, then a mutual TLS channel
# is supposed to be created.
Expand All @@ -83,9 +86,10 @@ def test_requests_with_default_client_cert_source():
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()
)
with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
authed_session.configure_mtls_channel(
client_cert_callback=mtls.default_client_cert_source()
)

assert authed_session.is_mtls

Expand All @@ -105,9 +109,10 @@ def test_urllib3_with_default_client_cert_source():
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()
)
with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
assert authed_http.configure_mtls_channel(
client_cert_callback=mtls.default_client_cert_source()
)

# Sleep 1 second to avoid 503 error.
time.sleep(1)
Expand Down

0 comments on commit c0c995f

Please sign in to comment.