Skip to content

Commit c0c995f

Browse files
feat: add GOOGLE_API_USE_CLIENT_CERTIFICATE support (#592)
1 parent ae27b49 commit c0c995f

File tree

9 files changed

+326
-54
lines changed

9 files changed

+326
-54
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
google.auth.transport.mtls module
2+
=================================
3+
4+
.. automodule:: google.auth.transport.mtls
5+
:members:
6+
:inherited-members:
7+
:show-inheritance:

google/auth/environment_vars.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,9 @@
5353
GCE_METADATA_IP = "GCE_METADATA_IP"
5454
"""Environment variable providing an alternate ip:port to be used for ip-only
5555
GCE metadata requests."""
56+
57+
GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
58+
"""Environment variable controlling whether to use client certificate or not.
59+
60+
The default value is false. Users have to explicitly set this value to true
61+
in order to use client certificate to establish a mutual TLS channel."""

google/auth/transport/grpc.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
from __future__ import absolute_import
1818

1919
import logging
20+
import os
2021

2122
import six
2223

24+
from google.auth import environment_vars
2325
from google.auth import exceptions
2426
from google.auth.transport import _mtls_helper
2527

@@ -96,6 +98,9 @@ def secure_authorized_channel(
9698
9799
This creates a channel with SSL and :class:`AuthMetadataPlugin`. This
98100
channel can be used to create a stub that can make authorized requests.
101+
Users can configure client certificate or rely on device certificates to
102+
establish a mutual TLS channel, if the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
103+
variable is explicitly set to `true`.
99104
100105
Example::
101106
@@ -138,7 +143,9 @@ def secure_authorized_channel(
138143
ssl_credentials=regular_ssl_credentials)
139144
140145
Option 2: create a mutual TLS channel by calling a callback which returns
141-
the client side certificate and the key::
146+
the client side certificate and the key (Note that
147+
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
148+
set to `true`)::
142149
143150
def my_client_cert_callback():
144151
code_to_load_client_cert_and_key()
@@ -155,7 +162,9 @@ def my_client_cert_callback():
155162
156163
Option 3: use application default SSL credentials. It searches and uses
157164
the command in a context aware metadata file, which is available on devices
158-
with endpoint verification support.
165+
with endpoint verification support (Note that
166+
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be explicitly
167+
set to `true`).
159168
See https://cloud.google.com/endpoint-verification/docs/overview::
160169
161170
try:
@@ -174,7 +183,8 @@ def my_client_cert_callback():
174183
ssl_credentials=default_ssl_credentials)
175184
176185
Option 4: not setting ssl_credentials and client_cert_callback. For devices
177-
without endpoint verification support, a regular TLS channel is created;
186+
without endpoint verification support or `GOOGLE_API_USE_CLIENT_CERTIFICATE`
187+
environment variable is not `true`, a regular TLS channel is created;
178188
otherwise, a mutual TLS channel is created, however, the call should be
179189
wrapped in a try/except block in case of malformed context aware metadata.
180190
@@ -205,13 +215,15 @@ def my_client_cert_callback():
205215
This argument is mutually exclusive with client_cert_callback;
206216
providing both will raise an exception.
207217
If ssl_credentials and client_cert_callback are None, application
208-
default SSL credentials will be used.
218+
default SSL credentials are used if `GOOGLE_API_USE_CLIENT_CERTIFICATE`
219+
environment variable is explicitly set to `true`, otherwise one way TLS
220+
SSL credentials are used.
209221
client_cert_callback (Callable[[], (bytes, bytes)]): Optional
210222
callback function to obtain client certicate and key for mutual TLS
211223
connection. This argument is mutually exclusive with
212224
ssl_credentials; providing both will raise an exception.
213-
If ssl_credentials and client_cert_callback are None, application
214-
default SSL credentials will be used.
225+
This argument does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE`
226+
environment variable is explicitly set to `true`.
215227
kwargs: Additional arguments to pass to :func:`grpc.secure_channel`.
216228
217229
Returns:
@@ -235,16 +247,21 @@ def my_client_cert_callback():
235247

236248
# If SSL credentials are not explicitly set, try client_cert_callback and ADC.
237249
if not ssl_credentials:
238-
if client_cert_callback:
250+
use_client_cert = os.getenv(
251+
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
252+
)
253+
if use_client_cert == "true" and client_cert_callback:
239254
# Use the callback if provided.
240255
cert, key = client_cert_callback()
241256
ssl_credentials = grpc.ssl_channel_credentials(
242257
certificate_chain=cert, private_key=key
243258
)
244-
else:
259+
elif use_client_cert == "true":
245260
# Use application default SSL credentials.
246261
adc_ssl_credentils = SslCredentials()
247262
ssl_credentials = adc_ssl_credentils.ssl_credentials
263+
else:
264+
ssl_credentials = grpc.ssl_channel_credentials()
248265

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

265288
def __init__(self):
266-
# Load client SSL credentials.
267-
metadata_path = _mtls_helper._check_dca_metadata_path(
268-
_mtls_helper.CONTEXT_AWARE_METADATA_PATH
289+
use_client_cert = os.getenv(
290+
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
269291
)
270-
self._is_mtls = metadata_path is not None
292+
if use_client_cert != "true":
293+
self._is_mtls = False
294+
else:
295+
# Load client SSL credentials.
296+
metadata_path = _mtls_helper._check_dca_metadata_path(
297+
_mtls_helper.CONTEXT_AWARE_METADATA_PATH
298+
)
299+
self._is_mtls = metadata_path is not None
271300

272301
@property
273302
def ssl_credentials(self):

google/auth/transport/requests.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import functools
2020
import logging
2121
import numbers
22+
import os
2223
import time
2324

2425
try:
@@ -40,6 +41,7 @@
4041
) # pylint: disable=ungrouped-imports
4142
import six # pylint: disable=ungrouped-imports
4243

44+
from google.auth import environment_vars
4345
from google.auth import exceptions
4446
from google.auth import transport
4547
import google.auth.transport._mtls_helper
@@ -249,13 +251,18 @@ class AuthorizedSession(requests.Session):
249251
credentials' headers to the request and refreshing credentials as needed.
250252
251253
This class also supports mutual TLS via :meth:`configure_mtls_channel`
252-
method. If client_cert_callback is provided, client certificate and private
254+
method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
255+
environment variable must be explicitly set to `true`, otherwise it does
256+
nothing. Assume the environment is set to `true`, the method behaves in the
257+
following manner:
258+
If client_cert_callback is provided, client certificate and private
253259
key are loaded using the callback; if client_cert_callback is None,
254260
application default SSL credentials will be used. Exceptions are raised if
255261
there are problems with the certificate, private key, or the loading process,
256262
so it should be called within a try/except block.
257263
258-
First we create an :class:`AuthorizedSession` instance and specify the endpoints::
264+
First we set the environment variable to `true`, then create an :class:`AuthorizedSession`
265+
instance and specify the endpoints::
259266
260267
regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
261268
mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
@@ -343,9 +350,11 @@ def __init__(
343350
def configure_mtls_channel(self, client_cert_callback=None):
344351
"""Configure the client certificate and key for SSL connection.
345352
346-
If client certificate and key are successfully obtained (from the given
347-
client_cert_callback or from application default SSL credentials), a
348-
:class:`_MutualTlsAdapter` instance will be mounted to "https://" prefix.
353+
The function does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is
354+
explicitly set to `true`. In this case if client certificate and key are
355+
successfully obtained (from the given client_cert_callback or from application
356+
default SSL credentials), a :class:`_MutualTlsAdapter` instance will be mounted
357+
to "https://" prefix.
349358
350359
Args:
351360
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
@@ -358,6 +367,13 @@ def configure_mtls_channel(self, client_cert_callback=None):
358367
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
359368
creation failed for any reason.
360369
"""
370+
use_client_cert = os.getenv(
371+
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
372+
)
373+
if use_client_cert != "true":
374+
self._is_mtls = False
375+
return
376+
361377
try:
362378
import OpenSSL
363379
except ImportError as caught_exc:

google/auth/transport/urllib3.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from __future__ import absolute_import
1818

1919
import logging
20+
import os
2021
import warnings
2122

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

49+
from google.auth import environment_vars
4850
from google.auth import exceptions
4951
from google.auth import transport
5052

@@ -202,13 +204,18 @@ class AuthorizedHttp(urllib3.request.RequestMethods):
202204
credentials' headers to the request and refreshing credentials as needed.
203205
204206
This class also supports mutual TLS via :meth:`configure_mtls_channel`
205-
method. If client_cert_callback is provided, client certificate and private
207+
method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE`
208+
environment variable must be explicitly set to `true`, otherwise it does
209+
nothing. Assume the environment is set to `true`, the method behaves in the
210+
following manner:
211+
If client_cert_callback is provided, client certificate and private
206212
key are loaded using the callback; if client_cert_callback is None,
207213
application default SSL credentials will be used. Exceptions are raised if
208214
there are problems with the certificate, private key, or the loading process,
209215
so it should be called within a try/except block.
210216
211-
First we create an :class:`AuthorizedHttp` instance and specify the endpoints::
217+
First we set the environment variable to `true`, then create an :class:`AuthorizedHttp`
218+
instance and specify the endpoints::
212219
213220
regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
214221
mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
@@ -282,9 +289,13 @@ def __init__(
282289

283290
def configure_mtls_channel(self, client_cert_callback=None):
284291
"""Configures mutual TLS channel using the given client_cert_callback or
285-
application default SSL credentials. Returns True if the channel is
286-
mutual TLS and False otherwise. Note that the `http` provided in the
287-
constructor will be overwritten.
292+
application default SSL credentials. The behavior is controlled by
293+
`GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable.
294+
(1) If the environment variable value is `true`, the function returns True
295+
if the channel is mutual TLS and False otherwise. The `http` provided
296+
in the constructor will be overwritten.
297+
(2) If the environment variable is not set or `false`, the function does
298+
nothing and it always return False.
288299
289300
Args:
290301
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]):
@@ -300,6 +311,12 @@ def configure_mtls_channel(self, client_cert_callback=None):
300311
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
301312
creation failed for any reason.
302313
"""
314+
use_client_cert = os.getenv(
315+
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
316+
)
317+
if use_client_cert != "true":
318+
return False
319+
303320
try:
304321
import OpenSSL
305322
except ImportError as caught_exc:

system_tests/test_mtls_http.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import google.auth
2020
import google.auth.credentials
21+
from google.auth import environment_vars
2122
from google.auth.transport import mtls
2223
import google.auth.transport.requests
2324
import google.auth.transport.urllib3
@@ -33,7 +34,8 @@ def test_requests():
3334
)
3435

3536
authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
36-
authed_session.configure_mtls_channel()
37+
with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
38+
authed_session.configure_mtls_channel()
3739

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

5961
authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
60-
is_mtls = authed_http.configure_mtls_channel()
62+
with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
63+
is_mtls = authed_http.configure_mtls_channel()
6164

6265
# If the devices has default client cert source, then a mutual TLS channel
6366
# is supposed to be created.
@@ -83,9 +86,10 @@ def test_requests_with_default_client_cert_source():
8386
authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
8487

8588
if mtls.has_default_client_cert_source():
86-
authed_session.configure_mtls_channel(
87-
client_cert_callback=mtls.default_client_cert_source()
88-
)
89+
with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
90+
authed_session.configure_mtls_channel(
91+
client_cert_callback=mtls.default_client_cert_source()
92+
)
8993

9094
assert authed_session.is_mtls
9195

@@ -105,9 +109,10 @@ def test_urllib3_with_default_client_cert_source():
105109
authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials)
106110

107111
if mtls.has_default_client_cert_source():
108-
assert authed_http.configure_mtls_channel(
109-
client_cert_callback=mtls.default_client_cert_source()
110-
)
112+
with mock.patch.dict(os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}):
113+
assert authed_http.configure_mtls_channel(
114+
client_cert_callback=mtls.default_client_cert_source()
115+
)
111116

112117
# Sleep 1 second to avoid 503 error.
113118
time.sleep(1)

0 commit comments

Comments
 (0)