Skip to content

feat: Add helper methods for asynchronous x.509 certificate discovery#1956

Open
agrawalradhika-cell wants to merge 6 commits intomainfrom
add-mtls-b-helper
Open

feat: Add helper methods for asynchronous x.509 certificate discovery#1956
agrawalradhika-cell wants to merge 6 commits intomainfrom
add-mtls-b-helper

Conversation

@agrawalradhika-cell
Copy link
Contributor

@agrawalradhika-cell agrawalradhika-cell commented Feb 9, 2026

This PR introduces the google.auth.aio.transport.mtls module, providing asynchronous helper methods for mTLS certificate discovery go/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>
Signed-off-by: Radhika Agrawal <agrawalradhika@google.com>
Signed-off-by: Radhika Agrawal <agrawalradhika@google.com>
Signed-off-by: Radhika Agrawal <agrawalradhika@google.com>
@agrawalradhika-cell agrawalradhika-cell requested review from a team as code owners February 9, 2026 23:29
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @agrawalradhika-cell, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the google-auth library by integrating asynchronous capabilities for mTLS certificate discovery. The new module google.auth.aio.transport.mtls provides helper functions that prevent blocking the event loop during disk I/O operations required for X.509 certificate retrieval, thereby improving the responsiveness and efficiency of applications utilizing mTLS. This lays the groundwork for future asynchronous mTLS channel configuration.

Highlights

  • New Asynchronous mTLS Module: Introduced the google.auth.aio.transport.mtls module to provide asynchronous helper methods for mTLS certificate discovery.
  • Non-blocking Disk I/O: Implemented mechanisms, including _run_in_executor utilizing asyncio.to_thread (or loop.run_in_executor for older Python versions), to ensure disk I/O operations during mTLS handshake are non-blocking.
  • X.509 Certificate Discovery: Added asynchronous functions like get_client_ssl_credentials and get_client_cert_and_key to retrieve X.509 client certificates and private keys.
  • Unit Tests: Included comprehensive unit tests for all new asynchronous mTLS helper functions to ensure their correctness and robustness.
Changelog
  • google/auth/aio/transport/mtls.py
    • Added _check_config_path function to validate and expand certificate configuration file paths.
    • Added _run_in_executor asynchronous utility to execute blocking functions in a separate thread.
    • Added has_default_client_cert_source function to check for the presence of default client SSL credentials.
    • Added get_client_ssl_credentials asynchronous function to retrieve client-side certificate, private key, and passphrase.
    • Added get_client_cert_and_key asynchronous function to obtain client certificate and key, supporting both callbacks and default credentials.
  • tests/transport/test_aio_mtls_helper.py
    • Added unit tests for _check_config_path covering cases where the path exists and does not exist.
    • Added unit tests for has_default_client_cert_source to verify environment variable handling.
    • Added asynchronous unit tests for get_client_ssl_credentials to confirm successful retrieval and proper error propagation.
    • Added asynchronous unit tests for get_client_cert_and_key to validate callback functionality, default credential retrieval, and exception handling.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces asynchronous helper methods for mTLS certificate discovery, a valuable addition for non-blocking I/O during the mTLS handshake process. However, a high-severity Path Traversal vulnerability was identified in google/auth/aio/transport/mtls.py where user-provided file paths are not properly validated, potentially allowing attackers to read arbitrary files and disclose sensitive information. Additionally, the review suggests improving consistency with existing synchronous helpers, enhancing robustness by handling async callbacks, and refining the API by removing unused parameters to make the new aio module more reliable and intuitive.

… handling async and removing encrypted_key complication

Signed-off-by: Radhika Agrawal <agrawalradhika@google.com>
@agrawalradhika-cell
Copy link
Contributor Author

agrawalradhika-cell commented Feb 10, 2026

Note - Python3.9 check is failing due to some nox dependency missing for python 3.9, IIUC unrelated to my change

"""
if _check_config_path(CERTIFICATE_CONFIGURATION_DEFAULT_PATH) is not None:
return True
cert_config_path = getenv("GOOGLE_API_CERTIFICATE_CONFIG")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we use the value from env var if set and fallback to default?

Copy link
Contributor Author

@agrawalradhika-cell agrawalradhika-cell Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on AIP-4114 - Users should enable Device Certificate Authentication through ADC instead of manual configuration via client options.
Thus, ~/.config/gcloud/certificate_config.json checks if the certs are present at standard location, and
if the certs are not present at default location, then certs are checked for specific custom config.

Same logic in synchronous operations - see url

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that's right? That section uses the word should, not must.

If we put the default check above the env var check, that completely removes the ability to manually override, which doesn't seem like the intended approach to me, even if automatic detection is the best practice

But I just skimmed the doc, so let me know if I'm missing something

return await loop.run_in_executor(None, func, *args)


def has_default_client_cert_source():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we reuse this method from sync implementation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, has_default_client_cert_source from synchronous flow checks for CONTEXT_AWARE_METADATA path too, which is not implemented for async flow yet. Sync method flow would return True if the certificate was present in that location, which would be misleading in a way that Async flow wouldn't implement mTLS configuration in that case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it. thanks.

Copy link
Collaborator

@daniel-sanche daniel-sanche Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a flag to the sync helper called check_context_aware_metadata, and turn it off when calling from async? It seems like that could cut down on logic duplication

Copy link
Collaborator

@daniel-sanche daniel-sanche Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And wait, don't has_default_client_cert_source and _get_cert_config_path also already do essentially the same thing? Can we condense this some more?

I know some of this logic was already in place, but I think we need to be careful about duplication when adding async, otherwise it can get confusing fast

@daniel-sanche daniel-sanche changed the title feat: Add helper methods for asynchronous x.509 certificate discovery in google-auth feat: Add helper methods for asynchronous x.509 certificate discovery Feb 11, 2026
from google.auth import exceptions
import google.auth.transport._mtls_helper

CERTIFICATE_CONFIGURATION_DEFAULT_PATH = "~/.config/gcloud/certificate_config.json"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems weird to me... What environment are we expecting to run? Should we be setting this up to work with windows? Or is this for Cloud Shell or some other Google-managed environment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the well-known location, which is for all OS.
Please note there is also GOOGLE_API_CERTIFICATE_CONFIG taken into consideration as well to cater to custom cert provided by user.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ~ threw me off, but it looks like it's being passed into path.expanduser.

Assuming that path is right:

I'd probably suggest using path.expanduser here, so it's already converted (it can always be turned back into a string for _check_config_path). Or at the very least, add a comment like # default gcloud config path, to be used with path.expanduser?

These docs seem to say that windows uses %APPDATA%\gcloud though, which would not be found by this. Are you sure this is right?

Copy link
Collaborator

@daniel-sanche daniel-sanche Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize now that this is coming from _mtls_helper. Maybe windows isn't in scope?'

It would be good if we can import this variable from the helper instead of duplciating it. But even better if we can share the sync logic so this isn't needed

"""
if _check_config_path(CERTIFICATE_CONFIGURATION_DEFAULT_PATH) is not None:
return True
cert_config_path = getenv("GOOGLE_API_CERTIFICATE_CONFIG")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that's right? That section uses the word should, not must.

If we put the default check above the env var check, that completely removes the ability to manually override, which doesn't seem like the intended approach to me, even if automatic detection is the best practice

But I just skimmed the doc, so let me know if I'm missing something

Signed-off-by: Radhika Agrawal <agrawalradhika@google.com>

async def callback():
try:
_, cert_bytes, key_bytes = await get_client_cert_and_key()
Copy link
Collaborator

@daniel-sanche daniel-sanche Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a little risky to me that it uses has_default_client_cert_source to check the location of the keys, but then uses a completely different code path to actually read them. I worry these could get out of sync in the future. Especially since we have logic split between the sync and async implementations

It looks like has_default_client_source reads:

  • CERTIFICATE_CONFIGURATION_DEFAULT_PATH
  • GOOGLE_API_CERTIFICATE_CONFIG

and get_client_cert_and_key uses _get_client_cert_config_path(None), which reads

  • GOOGLE_API_CERTIFICATE_CONFIG
  • CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH
  • CERTIFICATE_CONFIGURATION_DEFAULT_PATH

Didn't you mention that CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH shouldn't be used by the async code?

Copy link
Collaborator

@daniel-sanche daniel-sanche Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if we could refactor the logic that describes where to find the cert into one helper, like this:

def _found_cert_path(include_context_aware=True) -> str | None:
  # a single source of truth where we search for the cert locations, and return the place it was found, or None

def has_default_client_cert_source():
  return _found_cert_path() is not None

async def has_default_client_cert_source():
  return _found_cert_path(include_context_aware=False) is not None

def get_client_cert_and_key():
  cert_path = _found_cert_path
  # read certs from path

That way, checking for existence vs reading; and async vs sync, it would all flow through the same logic

Copy link
Collaborator

@daniel-sanche daniel-sanche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully my comments aren't too confusing, I was kind of learning the codebase as I went

In general, my feed back is that it would be great if we can re-use logic already in the sync implemtation as much as possible. I see a lot of implementations that are 90% the same as what we implemented elsewhere, with minor critical differences. And that can be very hard to build upon safely in the future

cert, key = await client_cert_callback()
except TypeError:
# If it's not awaitable (e.g., a tuple), result is already the data.
cert, key = client_cert_callback()
Copy link
Collaborator

@daniel-sanche daniel-sanche Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, actually looking at this again, I think we do need to use a result variable, like you had before. Otherwise if it's sync, it'll be called twice:

    if client_cert_callback:
        result = client_cert_callback()
        try:
            # handle coroutine callback
            cert, key = await result
        except TypeError:
            # unpack non-async callback result
            cert, key = result

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants