Skip to content

Commit 3368f27

Browse 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

6 files changed

Lines changed: 304 additions & 16 deletions

File tree

Lines 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+
importasyncio
20+
importlogging
21+
22+
fromgoogle.authimportexceptions
23+
importgoogle.auth.transport._mtls_helper
24+
importgoogle.auth.transport.mtls
25+
26+
_LOGGER=logging.getLogger(__name__)
27+
28+
29+
asyncdef_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+
returnawaitasyncio.to_thread(func, *args)
37+
exceptAttributeError:
38+
# Fallback for older Python versions
39+
loop=asyncio.get_running_loop()
40+
returnawaitloop.run_in_executor(None, func, *args)
41+
42+
43+
defdefault_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+
ifnotgoogle.auth.transport.mtls.has_default_client_cert_source(
55+
include_context_aware=False
56+
):
57+
raiseexceptions.MutualTLSChannelError(
58+
"Default client cert source doesn't exist"
59+
)
60+
61+
asyncdefcallback():
62+
try:
63+
_, cert_bytes, key_bytes=awaitget_client_cert_and_key()
64+
except (OSError, RuntimeError, ValueError) ascaught_exc:
65+
new_exc=exceptions.MutualTLSChannelError(caught_exc)
66+
raisenew_excfromcaught_exc
67+
68+
returncert_bytes, key_bytes
69+
70+
returncallback
71+
72+
73+
asyncdefget_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+
ifcertandkey:
103+
returnTrue, cert, key, None
104+
105+
returnFalse, None, None, None
106+
107+
108+
asyncdefget_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+
ifclient_cert_callback:
129+
result=client_cert_callback()
130+
try:
131+
cert, key=awaitresult
132+
exceptTypeError:
133+
cert, key=result
134+
returnTrue, cert, key
135+
136+
has_cert, cert, key, _=awaitget_client_ssl_credentials()
137+
returnhas_cert, cert, key

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
fromgoogle.authimportexceptions
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
returnjson_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
ifcert_pathisNoneandkey_pathisNone:
127135
returnNone, 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-
ifenv_pathisnotNoneandenv_path!="":
168+
ifinclude_context_awareandenv_pathisnotNoneandenv_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
returncertificate_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
ifabsolute_pathisNone:
172182
returnNone, None
173183

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@
2020
fromgoogle.auth.transportimport_mtls_helper
2121

2222

23-
defhas_default_client_cert_source():
23+
defhas_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
isnotNone
3237
):
3338
returnTrue
@@ -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-
ifnothas_default_client_cert_source():
66+
ifnothas_default_client_cert_source(include_context_aware=True):
6267
raiseexceptions.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-
ifnothas_default_client_cert_source():
102+
ifnothas_default_client_cert_source(include_context_aware=True):
98103
raiseexceptions.MutualTLSChannelError(
99104
"Default client encrypted cert source doesn't exist"
100105
)

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

Lines 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
assertinfo["email"] ==credentials.service_account_email
4343
info_scopes=_helpers.string_to_scopes(info["scope"])
44-
assertset(info_scopes)==set(
44+
assertset(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
deftest_iam_signer(http_request, credentials):
5252
credentials=credentials.with_scopes(

0 commit comments

Comments
 (0)