| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -86,7 +86,9 @@ def get_client_ssl_credentials(metadata_json): | |||
| 86 | 86 | Raises: | |
| 87 | 87 | OSError: If the cert provider command failed to run. | |
| 88 | 88 | 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. | ||
| 90 | 92 | """ | |
| 91 | 93 | # TODO: implement an in-memory cache of cert and key so we don't have to | |
| 92 | 94 | # run cert provider command every time. | |
@@ -114,3 +116,39 @@ def get_client_ssl_credentials(metadata_json): | |||
| 114 | 116 | if len(key_match) != 1: | |
| 115 | 117 | raise ValueError("Client SSL key is missing or invalid") | |
| 116 | 118 | 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 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -35,10 +35,14 @@ | |||
| 35 | 35 | ) | |
| 36 | 36 | import requests.adapters # pylint: disable=ungrouped-imports | |
| 37 | 37 | import requests.exceptions # pylint: disable=ungrouped-imports | |
| 38 | + from requests.packages.urllib3.util.ssl_ import ( | ||
| 39 | + create_urllib3_context, | ||
| 40 | + ) # pylint: disable=ungrouped-imports | ||
| 38 | 41 | import six # pylint: disable=ungrouped-imports | |
| 39 | 42 | ||
| 40 | 43 | from google.auth import exceptions | |
| 41 | 44 | from google.auth import transport | |
| 45 | + import google.auth.transport._mtls_helper | ||
| 42 | 46 | ||
| 43 | 47 | _LOGGER = logging.getLogger(__name__) | |
| 44 | 48 | ||
@@ -182,6 +186,52 @@ def __call__( | |||
| 182 | 186 | six.raise_from(new_exc, caught_exc) | |
| 183 | 187 | ||
| 184 | 188 | ||
| 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 | + | ||
| 185 | 235 | class AuthorizedSession(requests.Session): | |
| 186 | 236 | """A Requests Session class with credentials. | |
| 187 | 237 | ||
@@ -198,6 +248,48 @@ class AuthorizedSession(requests.Session): | |||
| 198 | 248 | The underlying :meth:`request` implementation handles adding the | |
| 199 | 249 | credentials' headers to the request and refreshing credentials as needed. | |
| 200 | 250 | ||
| 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 | + | ||
| 201 | 293 | Args: | |
| 202 | 294 | credentials (google.auth.credentials.Credentials): The credentials to | |
| 203 | 295 | add to the request. | |
@@ -229,6 +321,7 @@ def __init__( | |||
| 229 | 321 | self._refresh_status_codes = refresh_status_codes | |
| 230 | 322 | self._max_refresh_attempts = max_refresh_attempts | |
| 231 | 323 | self._refresh_timeout = refresh_timeout | |
| 324 | + self._is_mtls = False | ||
| 232 | 325 | ||
| 233 | 326 | if auth_request is None: | |
| 234 | 327 | auth_request_session = requests.Session() | |
@@ -247,6 +340,39 @@ def __init__( | |||
| 247 | 340 | # credentials.refresh). | |
| 248 | 341 | self._auth_request = auth_request | |
| 249 | 342 | ||
| 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 | + | ||
| 250 | 376 | def request( | |
| 251 | 377 | self, | |
| 252 | 378 | method, | |
@@ -361,3 +487,8 @@ def request( | |||
| 361 | 487 | ) | |
| 362 | 488 | ||
| 363 | 489 | 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 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -17,7 +17,7 @@ | |||
| 17 | 17 | from __future__ import absolute_import | |
| 18 | 18 | ||
| 19 | 19 | import logging | |
| 20 | - | ||
| 20 | + import warnings | ||
| 21 | 21 | ||
| 22 | 22 | # Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle | |
| 23 | 23 | # to verify HTTPS requests, and certifi is the recommended and most reliable | |
@@ -149,6 +149,39 @@ def _make_default_http(): | |||
| 149 | 149 | return urllib3.PoolManager() | |
| 150 | 150 | ||
| 151 | 151 | ||
| 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 | + | ||
| 152 | 185 | class AuthorizedHttp(urllib3.request.RequestMethods): | |
| 153 | 186 | """A urllib3 HTTP class with credentials. | |
| 154 | 187 | ||
@@ -168,6 +201,48 @@ class AuthorizedHttp(urllib3.request.RequestMethods): | |||
| 168 | 201 | The underlying :meth:`urlopen` implementation handles adding the | |
| 169 | 202 | credentials' headers to the request and refreshing credentials as needed. | |
| 170 | 203 | ||
| 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 | + | ||
| 171 | 246 | Args: | |
| 172 | 247 | credentials (google.auth.credentials.Credentials): The credentials to | |
| 173 | 248 | add to the request. | |
@@ -189,12 +264,14 @@ def __init__( | |||
| 189 | 264 | refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES, | |
| 190 | 265 | max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS, | |
| 191 | 266 | ): | |
| 192 | - | ||
| 193 | 267 | 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 | ||
| 195 | 273 | ||
| 196 | 274 | self.credentials = credentials | |
| 197 | - self.http = http | ||
| 198 | 275 | self._refresh_status_codes = refresh_status_codes | |
| 199 | 276 | self._max_refresh_attempts = max_refresh_attempts | |
| 200 | 277 | # Request instance used by internal methods (for example, | |
@@ -203,6 +280,50 @@ def __init__( | |||
| 203 | 280 | ||
| 204 | 281 | super(AuthorizedHttp, self).__init__() | |
| 205 | 282 | ||
| 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 | + | ||
| 206 | 327 | def urlopen(self, method, url, body=None, headers=None, **kwargs): | |
| 207 | 328 | """Implementation of urllib3's urlopen.""" | |
| 208 | 329 | # pylint: disable=arguments-differ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,6 +19,7 @@ | |||
| 19 | 19 | "freezegun", | |
| 20 | 20 | "mock", | |
| 21 | 21 | "oauth2client", | |
| 22 | + "pyopenssl", | ||
| 22 | 23 | "pytest", | |
| 23 | 24 | "pytest-cov", | |
| 24 | 25 | "pytest-localserver", | |
| Back | FazBrowse Home | New Git URL |
0 commit comments