FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat: add mTLS ADC support for HTTP (#457) · googleapis/google-cloud-python@dce9fc6 · GitHub

Commit dce9fc6

Browse files
feat: add mTLS ADC support for HTTP (#457)
feat: add mTLS ADC support for HTTP
1 parent 87ce34d commit dce9fc6

10 files changed

Lines changed: 642 additions & 37 deletions

File tree

‎packages/google-auth/google/auth/transport/_mtls_helper.py‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ def get_client_ssl_credentials(metadata_json):
8686
Raises:
8787
OSError: If the cert provider command failed to run.
8888
RuntimeError: If the cert provider command has a runtime error.
89-
ValueError: If the metadata json file doesn't contain the cert provider command or if the command doesn't produce both the client certificate and client key.
89+
ValueError: If the metadata json file doesn't contain the cert provider
90+
command or if the command doesn't produce both the client certificate
91+
and client key.
9092
"""
9193
# TODO: implement an in-memory cache of cert and key so we don't have to
9294
# run cert provider command every time.
@@ -114,3 +116,39 @@ def get_client_ssl_credentials(metadata_json):
114116
if len(key_match) != 1:
115117
raise ValueError("Client SSL key is missing or invalid")
116118
return cert_match[0], key_match[0]
119+
120+
121+
def get_client_cert_and_key(client_cert_callback=None):
122+
"""Returns the client side certificate and private key. The function first
123+
tries to get certificate and key from client_cert_callback; if the callback
124+
is None or doesn't provide certificate and key, the function tries application
125+
default SSL credentials.
126+
127+
Args:
128+
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): An
129+
optional callback which returns client certificate bytes and private
130+
key bytes both in PEM format.
131+
132+
Returns:
133+
Tuple[bool, bytes, bytes]:
134+
A boolean indicating if cert and key are obtained, the cert bytes
135+
and key bytes both in PEM format.
136+
137+
Raises:
138+
OSError: If the cert provider command failed to run.
139+
RuntimeError: If the cert provider command has a runtime error.
140+
ValueError: If the metadata json file doesn't contain the cert provider
141+
command or if the command doesn't produce both the client certificate
142+
and client key.
143+
"""
144+
if client_cert_callback:
145+
cert, key = client_cert_callback()
146+
return True, cert, key
147+
148+
metadata_path = _check_dca_metadata_path(CONTEXT_AWARE_METADATA_PATH)
149+
if metadata_path:
150+
metadata = _read_dca_metadata_file(metadata_path)
151+
cert, key = get_client_ssl_credentials(metadata)
152+
return True, cert, key
153+
154+
return False, None, None

‎packages/google-auth/google/auth/transport/requests.py‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,14 @@
3535
)
3636
import requests.adapters # pylint: disable=ungrouped-imports
3737
import requests.exceptions # pylint: disable=ungrouped-imports
38+
from requests.packages.urllib3.util.ssl_ import (
39+
create_urllib3_context,
40+
) # pylint: disable=ungrouped-imports
3841
import six # pylint: disable=ungrouped-imports
3942

4043
from google.auth import exceptions
4144
from google.auth import transport
45+
import google.auth.transport._mtls_helper
4246

4347
_LOGGER = logging.getLogger(__name__)
4448

@@ -182,6 +186,52 @@ def __call__(
182186
six.raise_from(new_exc, caught_exc)
183187

184188

189+
class _MutualTlsAdapter(requests.adapters.HTTPAdapter):
190+
"""
191+
A TransportAdapter that enables mutual TLS.
192+
193+
Args:
194+
cert (bytes): client certificate in PEM format
195+
key (bytes): client private key in PEM format
196+
197+
Raises:
198+
ImportError: if certifi or pyOpenSSL is not installed
199+
OpenSSL.crypto.Error: if client cert or key is invalid
200+
"""
201+
202+
def __init__(self, cert, key):
203+
import certifi
204+
from OpenSSL import crypto
205+
import urllib3.contrib.pyopenssl
206+
207+
urllib3.contrib.pyopenssl.inject_into_urllib3()
208+
209+
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
210+
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
211+
212+
ctx_poolmanager = create_urllib3_context()
213+
ctx_poolmanager.load_verify_locations(cafile=certifi.where())
214+
ctx_poolmanager._ctx.use_certificate(x509)
215+
ctx_poolmanager._ctx.use_privatekey(pkey)
216+
self._ctx_poolmanager = ctx_poolmanager
217+
218+
ctx_proxymanager = create_urllib3_context()
219+
ctx_proxymanager.load_verify_locations(cafile=certifi.where())
220+
ctx_proxymanager._ctx.use_certificate(x509)
221+
ctx_proxymanager._ctx.use_privatekey(pkey)
222+
self._ctx_proxymanager = ctx_proxymanager
223+
224+
super(_MutualTlsAdapter, self).__init__()
225+
226+
def init_poolmanager(self, *args, **kwargs):
227+
kwargs["ssl_context"] = self._ctx_poolmanager
228+
super(_MutualTlsAdapter, self).init_poolmanager(*args, **kwargs)
229+
230+
def proxy_manager_for(self, *args, **kwargs):
231+
kwargs["ssl_context"] = self._ctx_proxymanager
232+
return super(_MutualTlsAdapter, self).proxy_manager_for(*args, **kwargs)
233+
234+
185235
class AuthorizedSession(requests.Session):
186236
"""A Requests Session class with credentials.
187237
@@ -198,6 +248,48 @@ class AuthorizedSession(requests.Session):
198248
The underlying :meth:`request` implementation handles adding the
199249
credentials' headers to the request and refreshing credentials as needed.
200250
251+
This class also supports mutual TLS via :meth:`configure_mtls_channel`
252+
method. If client_cert_callabck is provided, client certificate and private
253+
key are loaded using the callback; if client_cert_callabck is None,
254+
application default SSL credentials will be used. Exceptions are raised if
255+
there are problems with the certificate, private key, or the loading process,
256+
so it should be called within a try/except block.
257+
258+
First we create an :class:`AuthorizedSession` instance and specify the endpoints::
259+
260+
regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
261+
mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
262+
263+
authed_session = AuthorizedSession(credentials)
264+
265+
Now we can pass a callback to :meth:`configure_mtls_channel`::
266+
267+
def my_cert_callback():
268+
# some code to load client cert bytes and private key bytes, both in
269+
# PEM format.
270+
some_code_to_load_client_cert_and_key()
271+
if loaded:
272+
return cert, key
273+
raise MyClientCertFailureException()
274+
275+
# Always call configure_mtls_channel within a try/except block.
276+
try:
277+
authed_session.configure_mtls_channel(my_cert_callback)
278+
except:
279+
# handle exceptions.
280+
281+
if authed_session.is_mtls:
282+
response = authed_session.request('GET', mtls_endpoint)
283+
else:
284+
response = authed_session.request('GET', regular_endpoint)
285+
286+
You can alternatively use application default SSL credentials like this::
287+
288+
try:
289+
authed_session.configure_mtls_channel()
290+
except:
291+
# handle exceptions.
292+
201293
Args:
202294
credentials (google.auth.credentials.Credentials): The credentials to
203295
add to the request.
@@ -229,6 +321,7 @@ def __init__(
229321
self._refresh_status_codes = refresh_status_codes
230322
self._max_refresh_attempts = max_refresh_attempts
231323
self._refresh_timeout = refresh_timeout
324+
self._is_mtls = False
232325

233326
if auth_request is None:
234327
auth_request_session = requests.Session()
@@ -247,6 +340,39 @@ def __init__(
247340
# credentials.refresh).
248341
self._auth_request = auth_request
249342

343+
def configure_mtls_channel(self, client_cert_callback=None):
344+
"""Configure the client certificate and key for SSL connection.
345+
346+
If client certificate and key are successfully obtained (from the given
347+
client_cert_callabck or from application default SSL credentials), a
348+
:class:`_MutualTlsAdapter` instance will be mounted to "https://" prefix.
349+
350+
Args:
351+
client_cert_callabck (Optional[Callable[[], (bytes, bytes)]]):
352+
The optional callback returns the client certificate and private
353+
key bytes both in PEM format.
354+
If the callback is None, application default SSL credentials
355+
will be used.
356+
357+
Raises:
358+
ImportError: If certifi or pyOpenSSL is not installed.
359+
OpenSSL.crypto.Error: If client cert or key is invalid.
360+
OSError: If the cert provider command launch fails during the
361+
application default SSL credentials loading process.
362+
RuntimeError: If the cert provider command has a runtime error during
363+
the application default SSL credentials loading process.
364+
ValueError: If the context aware metadata file is malformed or the
365+
cert provider command doesn't produce both client certicate and
366+
key during the application default SSL credentials loading process.
367+
"""
368+
self._is_mtls, cert, key = google.auth.transport._mtls_helper.get_client_cert_and_key(
369+
client_cert_callback
370+
)
371+
372+
if self._is_mtls:
373+
mtls_adapter = _MutualTlsAdapter(cert, key)
374+
self.mount("https://", mtls_adapter)
375+
250376
def request(
251377
self,
252378
method,
@@ -361,3 +487,8 @@ def request(
361487
)
362488

363489
return response
490+
491+
@property
492+
def is_mtls(self):
493+
"""Indicates if the created SSL channel is mutual TLS."""
494+
return self._is_mtls

‎packages/google-auth/google/auth/transport/urllib3.py‎

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

1919
import logging
20-
20+
import warnings
2121

2222
# Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle
2323
# to verify HTTPS requests, and certifi is the recommended and most reliable
@@ -149,6 +149,39 @@ def _make_default_http():
149149
return urllib3.PoolManager()
150150

151151

152+
def _make_mutual_tls_http(cert, key):
153+
"""Create a mutual TLS HTTP connection with the given client cert and key.
154+
See https://github.com/urllib3/urllib3/issues/474#issuecomment-253168415
155+
156+
Args:
157+
cert (bytes): client certificate in PEM format
158+
key (bytes): client private key in PEM format
159+
160+
Returns:
161+
urllib3.PoolManager: Mutual TLS HTTP connection.
162+
163+
Raises:
164+
ImportError: If certifi or pyOpenSSL is not installed.
165+
OpenSSL.crypto.Error: If the cert or key is invalid.
166+
"""
167+
import certifi
168+
from OpenSSL import crypto
169+
import urllib3.contrib.pyopenssl
170+
171+
urllib3.contrib.pyopenssl.inject_into_urllib3()
172+
ctx = urllib3.util.ssl_.create_urllib3_context()
173+
ctx.load_verify_locations(cafile=certifi.where())
174+
175+
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
176+
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
177+
178+
ctx._ctx.use_certificate(x509)
179+
ctx._ctx.use_privatekey(pkey)
180+
181+
http = urllib3.PoolManager(ssl_context=ctx)
182+
return http
183+
184+
152185
class AuthorizedHttp(urllib3.request.RequestMethods):
153186
"""A urllib3 HTTP class with credentials.
154187
@@ -168,6 +201,48 @@ class AuthorizedHttp(urllib3.request.RequestMethods):
168201
The underlying :meth:`urlopen` implementation handles adding the
169202
credentials' headers to the request and refreshing credentials as needed.
170203
204+
This class also supports mutual TLS via :meth:`configure_mtls_channel`
205+
method. If client_cert_callabck is provided, client certificate and private
206+
key are loaded using the callback; if client_cert_callabck is None,
207+
application default SSL credentials will be used. Exceptions are raised if
208+
there are problems with the certificate, private key, or the loading process,
209+
so it should be called within a try/except block.
210+
211+
First we create an :class:`AuthorizedHttp` instance and specify the endpoints::
212+
213+
regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics'
214+
mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics'
215+
216+
authed_http = AuthorizedHttp(credentials)
217+
218+
Now we can pass a callback to :meth:`configure_mtls_channel`::
219+
220+
def my_cert_callback():
221+
# some code to load client cert bytes and private key bytes, both in
222+
# PEM format.
223+
some_code_to_load_client_cert_and_key()
224+
if loaded:
225+
return cert, key
226+
raise MyClientCertFailureException()
227+
228+
# Always call configure_mtls_channel within a try/except block.
229+
try:
230+
is_mtls = authed_http.configure_mtls_channel(my_cert_callback)
231+
except:
232+
# handle exceptions.
233+
234+
if is_mtls:
235+
response = authed_http.request('GET', mtls_endpoint)
236+
else:
237+
response = authed_http.request('GET', regular_endpoint)
238+
239+
You can alternatively use application default SSL credentials like this::
240+
241+
try:
242+
is_mtls = authed_http.configure_mtls_channel()
243+
except:
244+
# handle exceptions.
245+
171246
Args:
172247
credentials (google.auth.credentials.Credentials): The credentials to
173248
add to the request.
@@ -189,12 +264,14 @@ def __init__(
189264
refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES,
190265
max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS,
191266
):
192-
193267
if http is None:
194-
http = _make_default_http()
268+
self.http = _make_default_http()
269+
self._has_user_provided_http = False
270+
else:
271+
self.http = http
272+
self._has_user_provided_http = True
195273

196274
self.credentials = credentials
197-
self.http = http
198275
self._refresh_status_codes = refresh_status_codes
199276
self._max_refresh_attempts = max_refresh_attempts
200277
# Request instance used by internal methods (for example,
@@ -203,6 +280,50 @@ def __init__(
203280

204281
super(AuthorizedHttp, self).__init__()
205282

283+
def configure_mtls_channel(self, client_cert_callabck=None):
284+
"""Configures mutual TLS channel using the given client_cert_callabck 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.
288+
289+
Args:
290+
client_cert_callabck (Optional[Callable[[], (bytes, bytes)]]):
291+
The optional callback returns the client certificate and private
292+
key bytes both in PEM format.
293+
If the callback is None, application default SSL credentials
294+
will be used.
295+
296+
Returns:
297+
True if the channel is mutual TLS and False otherwise.
298+
299+
Raises:
300+
ImportError: If certifi or pyOpenSSL is not installed.
301+
OpenSSL.crypto.Error: If client cert or key is invalid.
302+
OSError: If the cert provider command launch fails during the
303+
application default SSL credentials loading process.
304+
RuntimeError: If the cert provider command has a runtime error during
305+
the application default SSL credentials loading process.
306+
ValueError: If the context aware metadata file is malformed or the
307+
cert provider command doesn't produce both client certicate and
308+
key during the application default SSL credentials loading process.
309+
"""
310+
found_cert_key, cert, key = transport._mtls_helper.get_client_cert_and_key(
311+
client_cert_callabck
312+
)
313+
314+
if found_cert_key:
315+
self.http = _make_mutual_tls_http(cert, key)
316+
else:
317+
self.http = _make_default_http()
318+
319+
if self._has_user_provided_http:
320+
self._has_user_provided_http = False
321+
warnings.warn(
322+
"`http` provided in the constructor is overwritten", UserWarning
323+
)
324+
325+
return found_cert_key
326+
206327
def urlopen(self, method, url, body=None, headers=None, **kwargs):
207328
"""Implementation of urllib3's urlopen."""
208329
# pylint: disable=arguments-differ

‎packages/google-auth/noxfile.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"freezegun",
2020
"mock",
2121
"oauth2client",
22+
"pyopenssl",
2223
"pytest",
2324
"pytest-cov",
2425
"pytest-localserver",

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL