Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3368f27

Browse filesBrowse files
feat: Add helper methods for asynchronous x.509 certificate discovery (#1956)
This PR introduces the google.auth.aio.transport.mtls module, providing asynchronous helper methods for mTLS certificate discovery [go/caa:x509-async-support](http://goto.google.com/caa:x509-async-support). These helpers are designed to be for x.509 certs discovery and non-blocking, ensuring that disk I/O operations are async during mTLS handshake process. Plus, added unit tests respectively for the helper functions. Please note: Only x.509 creds are in scope of this project currently. Context aware or ECP credentials are not in scope of this project currently. Next Steps: Will create a followup PR will that will utilize these helpers to implement `configure_mtls_channel` within the `AsyncAuthorizedSession` class. --------- Signed-off-by: Radhika Agrawal <agrawalradhika@google.com>
1 parent 89fc6f2 commit 3368f27
Copy full SHA for 3368f27

6 files changed

+304-16Lines changed: 304 additions & 16 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file
+137Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Helper functions for mTLS in async for discovery of certs.
17+
"""
18+
19+
import asyncio
20+
import logging
21+
22+
from google.auth import exceptions
23+
import google.auth.transport._mtls_helper
24+
import google.auth.transport.mtls
25+
26+
_LOGGER = logging.getLogger(__name__)
27+
28+
29+
async def _run_in_executor(func, *args):
30+
"""Run a blocking function in an executor to avoid blocking the event loop.
31+
32+
This implements the non-blocking execution strategy for disk I/O operations.
33+
"""
34+
try:
35+
# For python versions 3.9 and newer versions
36+
return await asyncio.to_thread(func, *args)
37+
except AttributeError:
38+
# Fallback for older Python versions
39+
loop = asyncio.get_running_loop()
40+
return await loop.run_in_executor(None, func, *args)
41+
42+
43+
def default_client_cert_source():
44+
"""Get a callback which returns the default client SSL credentials.
45+
46+
Returns:
47+
Awaitable[Callable[[], [bytes, bytes]]]: A callback which returns the default
48+
client certificate bytes and private key bytes, both in PEM format.
49+
50+
Raises:
51+
google.auth.exceptions.DefaultClientCertSourceError: If the default
52+
client SSL credentials don't exist or are malformed.
53+
"""
54+
if not google.auth.transport.mtls.has_default_client_cert_source(
55+
include_context_aware=False
56+
):
57+
raise exceptions.MutualTLSChannelError(
58+
"Default client cert source doesn't exist"
59+
)
60+
61+
async def callback():
62+
try:
63+
_, cert_bytes, key_bytes = await get_client_cert_and_key()
64+
except (OSError, RuntimeError, ValueError) as caught_exc:
65+
new_exc = exceptions.MutualTLSChannelError(caught_exc)
66+
raise new_exc from caught_exc
67+
68+
return cert_bytes, key_bytes
69+
70+
return callback
71+
72+
73+
async def get_client_ssl_credentials(
74+
certificate_config_path=None,
75+
):
76+
"""Returns the client side certificate, private key and passphrase.
77+
78+
We look for certificates and keys with the following order of priority:
79+
1. Certificate and key specified by certificate_config.json.
80+
Currently, only X.509 workload certificates are supported.
81+
82+
Args:
83+
certificate_config_path (str): The certificate_config.json file path.
84+
85+
Returns:
86+
Tuple[bool, bytes, bytes, bytes]:
87+
A boolean indicating if cert, key and passphrase are obtained, the
88+
cert bytes and key bytes both in PEM format, and passphrase bytes.
89+
90+
Raises:
91+
google.auth.exceptions.ClientCertError: if problems occurs when getting
92+
the cert, key and passphrase.
93+
"""
94+
95+
# Attempt to retrieve X.509 Workload cert and key.
96+
cert, key = await _run_in_executor(
97+
google.auth.transport._mtls_helper._get_workload_cert_and_key,
98+
certificate_config_path,
99+
False,
100+
)
101+
102+
if cert and key:
103+
return True, cert, key, None
104+
105+
return False, None, None, None
106+
107+
108+
async def get_client_cert_and_key(client_cert_callback=None):
109+
"""Returns the client side certificate and private key. The function first
110+
tries to get certificate and key from client_cert_callback; if the callback
111+
is None or doesn't provide certificate and key, the function tries application
112+
default SSL credentials.
113+
114+
Args:
115+
client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): An
116+
optional callback which returns client certificate bytes and private
117+
key bytes both in PEM format.
118+
119+
Returns:
120+
Tuple[bool, bytes, bytes]:
121+
A boolean indicating if cert and key are obtained, the cert bytes
122+
and key bytes both in PEM format.
123+
124+
Raises:
125+
google.auth.exceptions.ClientCertError: if problems occurs when getting
126+
the cert and key.
127+
"""
128+
if client_cert_callback:
129+
result = client_cert_callback()
130+
try:
131+
cert, key = await result
132+
except TypeError:
133+
cert, key = result
134+
return True, cert, key
135+
136+
has_cert, cert, key, _ = await get_client_ssl_credentials()
137+
return has_cert, cert, key
Collapse file

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

Copy file name to clipboardExpand all lines: packages/google-auth/google/auth/transport/_mtls_helper.py
+16-6Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from google.auth import exceptions
2626

2727
CONTEXT_AWARE_METADATA_PATH = "~/.secureConnect/context_aware_metadata.json"
28+
29+
# Default gcloud config path, to be used with path.expanduser for cross-platform compatibility.
2830
CERTIFICATE_CONFIGURATION_DEFAULT_PATH = "~/.config/gcloud/certificate_config.json"
2931
_CERT_PROVIDER_COMMAND = "cert_provider_command"
3032
_CERT_REGEX = re.compile(
@@ -103,14 +105,18 @@ def _load_json_file(path):
103105
return json_data
104106

105107

106-
def _get_workload_cert_and_key(certificate_config_path=None):
108+
def _get_workload_cert_and_key(
109+
certificate_config_path=None, include_context_aware=True
110+
):
107111
"""Read the workload identity cert and key files specified in the certificate config provided.
108112
If no config path is provided, check the environment variable: "GOOGLE_API_CERTIFICATE_CONFIG"
109113
first, then the well known gcloud location: "~/.config/gcloud/certificate_config.json".
110114
111115
Args:
112116
certificate_config_path (string): The certificate config path. If no path is provided,
113117
the environment variable will be checked first, then the well known gcloud location.
118+
include_context_aware (bool): If context aware metadata path should be checked for the
119+
SecureConnect mTLS configuration.
114120
115121
Returns:
116122
Tuple[Optional[bytes], Optional[bytes]]: client certificate bytes in PEM format and key
@@ -121,15 +127,17 @@ def _get_workload_cert_and_key(certificate_config_path=None):
121127
the certificate or key information.
122128
"""
123129

124-
cert_path, key_path = _get_workload_cert_and_key_paths(certificate_config_path)
130+
cert_path, key_path = _get_workload_cert_and_key_paths(
131+
certificate_config_path, include_context_aware
132+
)
125133

126134
if cert_path is None and key_path is None:
127135
return None, None
128136

129137
return _read_cert_and_key_files(cert_path, key_path)
130138

131139

132-
def _get_cert_config_path(certificate_config_path=None):
140+
def _get_cert_config_path(certificate_config_path=None, include_context_aware=True):
133141
"""Get the certificate configuration path based on the following order:
134142
135143
1: Explicit override, if set
@@ -141,6 +149,8 @@ def _get_cert_config_path(certificate_config_path=None):
141149
Args:
142150
certificate_config_path (string): The certificate config path. If provided, the well known
143151
location and environment variable will be ignored.
152+
include_context_aware (bool): If context aware metadata path should be checked for the
153+
SecureConnect mTLS configuration.
144154
145155
Returns:
146156
The absolute path of the certificate config file, and None if the file does not exist.
@@ -155,7 +165,7 @@ def _get_cert_config_path(certificate_config_path=None):
155165
environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH,
156166
None,
157167
)
158-
if env_path is not None and env_path != "":
168+
if include_context_aware and env_path is not None and env_path != "":
159169
certificate_config_path = env_path
160170
else:
161171
certificate_config_path = CERTIFICATE_CONFIGURATION_DEFAULT_PATH
@@ -166,8 +176,8 @@ def _get_cert_config_path(certificate_config_path=None):
166176
return certificate_config_path
167177

168178

169-
def _get_workload_cert_and_key_paths(config_path):
170-
absolute_path = _get_cert_config_path(config_path)
179+
def _get_workload_cert_and_key_paths(config_path, include_context_aware=True):
180+
absolute_path = _get_cert_config_path(config_path, include_context_aware)
171181
if absolute_path is None:
172182
return None, None
173183

Collapse file

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

Copy file name to clipboardExpand all lines: packages/google-auth/google/auth/transport/mtls.py
+9-4Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@
2020
from google.auth.transport import _mtls_helper
2121

2222

23-
def has_default_client_cert_source():
23+
def has_default_client_cert_source(include_context_aware=True):
2424
"""Check if default client SSL credentials exists on the device.
2525
26+
Args:
27+
include_context_aware (bool): include_context_aware indicates if context_aware
28+
path location will be checked or should it be skipped.
29+
2630
Returns:
2731
bool: indicating if the default client cert source exists.
2832
"""
2933
if (
30-
_mtls_helper._check_config_path(_mtls_helper.CONTEXT_AWARE_METADATA_PATH)
34+
include_context_aware
35+
and _mtls_helper._check_config_path(_mtls_helper.CONTEXT_AWARE_METADATA_PATH)
3136
is not None
3237
):
3338
return True
@@ -58,7 +63,7 @@ def default_client_cert_source():
5863
google.auth.exceptions.DefaultClientCertSourceError: If the default
5964
client SSL credentials don't exist or are malformed.
6065
"""
61-
if not has_default_client_cert_source():
66+
if not has_default_client_cert_source(include_context_aware=True):
6267
raise exceptions.MutualTLSChannelError(
6368
"Default client cert source doesn't exist"
6469
)
@@ -94,7 +99,7 @@ def default_client_encrypted_cert_source(cert_path, key_path):
9499
google.auth.exceptions.DefaultClientCertSourceError: If any problem
95100
occurs when loading or saving the client certificate and key.
96101
"""
97-
if not has_default_client_cert_source():
102+
if not has_default_client_cert_source(include_context_aware=True):
98103
raise exceptions.MutualTLSChannelError(
99104
"Default client encrypted cert source doesn't exist"
100105
)
Collapse file

‎packages/google-auth/system_tests/system_tests_sync/test_service_account.py‎

Copy file name to clipboardExpand all lines: packages/google-auth/system_tests/system_tests_sync/test_service_account.py
+2-2Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ def test_refresh_success(http_request, credentials, token_info):
4141

4242
assert info["email"] == credentials.service_account_email
4343
info_scopes = _helpers.string_to_scopes(info["scope"])
44-
assert set(info_scopes) == set(
44+
assert set(info_scopes).issubset(set(
4545
[
4646
"https://www.googleapis.com/auth/userinfo.email",
4747
"https://www.googleapis.com/auth/userinfo.profile",
4848
]
49-
)
49+
))
5050

5151
def test_iam_signer(http_request, credentials):
5252
credentials = credentials.with_scopes(

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.