Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 105 additions & 53 deletions packages/google-auth/google/auth/_agent_identity_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import base64
import hashlib
import json
import os
import re
import stat
Expand Down Expand Up @@ -73,13 +74,32 @@ def _is_certificate_file_ready(path):
return False


def _is_in_well_known_dir(path):
"""Checks if the given path is inside the well-known Agent Identity directory."""
if not path:
return False
well_known_dir = os.path.dirname(_WELL_KNOWN_CERT_PATH)
try:
real_path = os.path.realpath(path)
real_well_known_dir = os.path.realpath(well_known_dir)
return (
os.path.commonpath([real_well_known_dir, real_path]) == real_well_known_dir
)
except ValueError:
return False


def get_agent_identity_certificate_path():
"""Gets the agent certificate path from the certificate config file.
"""Gets the agent certificate path from the certificate config file or GKE bundle.
The path to the certificate config file is read from the
GOOGLE_API_CERTIFICATE_CONFIG environment variable. This function
can optionally trigger polling to handle cases where the environment
variable is set before the files are available on the filesystem.
When GOOGLE_API_CERTIFICATE_CONFIG and
CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH are unset and no
implicit certificate_config.json is present, it falls back to checking
the GKE workload credential bundle path without polling.
Returns:
Optional[str]: The path to the agent's certificate file, or None if unavailable.
Expand All @@ -88,26 +108,26 @@ def get_agent_identity_certificate_path():
google.auth.exceptions.RefreshError: If the certificate config file
or the certificate file cannot be found after retries.
"""
from google.auth.transport import _mtls_helper

cert_config_path = os.environ.get(environment_vars.GOOGLE_API_CERTIFICATE_CONFIG)

if not cert_config_path:
if (
not _mtls_helper._has_explicit_cert_config_env()
and _mtls_helper._should_use_gke_credential_bundle(
_mtls_helper._get_cert_config_path()
)
):
return _mtls_helper._GKE_CREDENTIAL_BUNDLE_PATH
return None

# We trigger polling only if the config path points to the well-known directory.
# Cloud Run dynamically generates these files in this directory, and both the
# config file and the certificate file may experience a brief startup latency.
# For all other paths, we return early to avoid introducing unnecessary startup
# delays.
well_known_dir = os.path.dirname(_WELL_KNOWN_CERT_PATH)
try:
abs_cert_path = os.path.abspath(cert_config_path)
abs_well_known_dir = os.path.abspath(well_known_dir)
should_poll = (
os.path.commonpath([abs_well_known_dir, abs_cert_path])
== abs_well_known_dir
)
except ValueError:
should_poll = False
should_poll = _is_in_well_known_dir(cert_config_path)

return _get_cert_path_with_optional_polling(cert_config_path, should_poll)

Expand Down Expand Up @@ -141,9 +161,9 @@ def _get_cert_path_with_optional_polling(cert_config_path, should_poll):
if _is_certificate_file_ready(cert_path):
return cert_path

# The config was parsed, but the cert file is not ready yet
if not should_poll:
# If polling is disabled, return early.
# The config was parsed, but the cert file is not ready yet.
# Only poll if both the config path and cert path are in the well-known directory.
if not (should_poll and _is_in_well_known_dir(cert_path)):
return None

if not has_logged_cert_warning:
Expand Down Expand Up @@ -182,7 +202,7 @@ def _get_cert_path_with_optional_polling(cert_config_path, should_poll):
raise exceptions.RefreshError(
"Certificate config or certificate file not found after multiple retries. "
f"Token binding protection is failing. You can turn off this protection by setting "
f"{environment_vars.GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES} to false "
f"{environment_vars.GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN} to false "
"to fall back to unbound tokens."
)

Expand All @@ -203,8 +223,6 @@ def _parse_cert_path_from_config(cert_config_path):
KeyError: If the certificate config file does not contain the
expected structure.
"""
import json

with open(cert_config_path, "r", encoding="utf-8") as f:
cert_config = json.load(f)

Expand All @@ -221,64 +239,105 @@ def _parse_cert_path_from_config(cert_config_path):
return workload_config["cert_path"]


def get_and_parse_agent_identity_certificate():
def _is_bound_token_opted_out():
"""Returns True only if bound tokens are explicitly disabled via env vars."""
val = os.environ.get(
environment_vars.GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN, ""
).strip()
if not val:
# Fall back to the deprecated env var for backward compatibility
val = os.environ.get(
environment_vars.GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES,
"true",
).strip()
return val.lower() == "false"


def get_agent_identity_certificate_and_bytes():
"""Gets and parses the agent identity certificate if not opted out.
Checks if the user has opted out of certificate-bound tokens. If not,
it gets the certificate path, reads the file, and parses it.
Returns:
The parsed certificate object if found and not opted out, otherwise None.
Tuple[Optional[cryptography.x509.Certificate], Optional[bytes]]: A tuple
of (parsed certificate object, certificate bytes) if found and not
opted out, otherwise (None, None).
"""
from google.auth.transport import _mtls_helper

# If the user has opted out of cert bound tokens, there is no need to
# look up the certificate.
is_opted_out = (
os.environ.get(
environment_vars.GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES,
"true",
).lower()
== "false"
)
if is_opted_out:
return None
if _is_bound_token_opted_out():
return None, None

# Respect explicit opt-out of mTLS / client certs
from google.auth.transport import _mtls_helper

env_override = _mtls_helper._check_use_client_cert_env()
if env_override is False:
return None
return None, None

cert_path = get_agent_identity_certificate_path()
if not cert_path:
return None
return None, None

try:
with open(cert_path, "rb") as cert_file:
cert_bytes = cert_file.read()
except PermissionError as e:
raw_bytes = cert_file.read()
except OSError as e:
warnings.warn(
f"Failed to read agent identity certificate file at {cert_path}: {e}. "
"Token binding protection cannot be enabled. Falling back to unbound tokens."
)
return None
return None, None

return parse_certificate(cert_bytes)
cert_blocks = _mtls_helper._CERT_REGEX.findall(raw_bytes)
if not cert_blocks:
warnings.warn(
f"No PEM certificate blocks found in {cert_path}. "
"Token binding protection cannot be enabled. Falling back to unbound tokens."
)
return None, None

cert_bytes = b"\n".join(block.strip() for block in cert_blocks) + b"\n"
try:
return parse_certificate(raw_bytes), cert_bytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Pass cert_bytes instead of raw_bytes to parse_certificate. Since cert_bytes has already been cleaned of non-certificate blocks (like private keys or bag attributes) and validated, parsing it directly is safer and avoids redundant regex parsing inside parse_certificate.

Suggested change
return parse_certificate(raw_bytes), cert_bytes
return parse_certificate(cert_bytes), cert_bytes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rejecting the suggestion. Keeping raw_bytes here is intentional: cert_bytes is built from regex matches of complete PEM blocks and strips out any trailing block missing an END marker, so passing raw_bytes is required for parse_certificate to detect truncated certificate chains.

except (ValueError, ImportError) as e:
warnings.warn(
f"Failed to parse agent identity certificate at {cert_path}: {e}. "
"Token binding protection cannot be enabled. Falling back to unbound tokens."
)
return None, None


def parse_certificate(cert_bytes):
"""Parses a PEM-encoded certificate.
"""Parses a PEM-encoded certificate or certificate chain and returns the leaf certificate.
Args:
cert_bytes (bytes): The PEM-encoded certificate bytes.
Returns:
cryptography.x509.Certificate: The parsed certificate object.
cryptography.x509.Certificate: The leaf (first) parsed certificate object.
Raises:
ValueError: If no certificates are found or any certificate in the chain
is malformed.
ImportError: If the cryptography library is not installed.
"""
from google.auth.transport import _mtls_helper

if not cert_bytes:
raise ValueError("Certificate bytes cannot be empty or None.")

try:
from cryptography import x509

return x509.load_pem_x509_certificate(cert_bytes)
cert_blocks = _mtls_helper._CERT_REGEX.findall(cert_bytes)
if not cert_blocks:
return x509.load_pem_x509_certificate(cert_bytes)
if _mtls_helper._has_unmatched_pem_markers(cert_bytes, cert_blocks):
raise ValueError("Malformed or truncated PEM certificate chain.")
certs = [x509.load_pem_x509_certificate(block) for block in cert_blocks]
return certs[0]
except ImportError as e:
raise ImportError(CRYPTOGRAPHY_NOT_FOUND_ERROR) from e

Expand Down Expand Up @@ -350,34 +409,27 @@ def calculate_certificate_fingerprint(cert):
def should_request_bound_token(cert):
"""Determines if a bound token should be requested.
This is based on the GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES
environment variable and whether the certificate is an agent identity cert.
This is based on the GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN env var
(falls back to the deprecated GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES
if unset or empty) and whether the certificate is an agent identity cert.
Args:
cert (cryptography.x509.Certificate): The parsed certificate object.
Returns:
bool: True if a bound token should be requested, False otherwise.
"""
is_agent_cert = _is_agent_identity_certificate(cert)
is_opted_in = (
os.environ.get(
environment_vars.GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES,
"true",
).lower()
== "true"
)
if not (is_agent_cert and is_opted_in):
from google.auth.transport import _mtls_helper

if _is_bound_token_opted_out():
return False

# Respect explicit opt-out of mTLS / client certs
from google.auth.transport import _mtls_helper

env_override = _mtls_helper._check_use_client_cert_env()
if env_override is False:
return False

return True
return _is_agent_identity_certificate(cert)


def get_cached_cert_fingerprint(cached_cert):
Expand Down
16 changes: 3 additions & 13 deletions packages/google-auth/google/auth/aio/transport/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import inspect
import logging
import time
import urllib.parse
import warnings
from contextlib import asynccontextmanager

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Explicitly import _mtls_helper from google.auth.transport instead of relying on implicit package attribute binding via google.auth.transport._mtls_helper.

Suggested change
from contextlib import asynccontextmanager
from google.auth.transport import _mtls_helper
from contextlib import asynccontextmanager

@nbayati nbayati Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

False positive: google.auth.transport._mtls_helper is already explicitly imported on line 27, so this does not rely on implicit package attribute binding.

from typing import TYPE_CHECKING, Mapping, Optional, Union
Expand All @@ -44,11 +43,6 @@
ClientTimeout = None

_LOGGER = logging.getLogger(__name__)
_MTLS_URL_PREFIXES = [
"mtls.googleapis.com",
"mtls.sandbox.googleapis.com",
"p.googleapis.com",
]

# Tracks the internal aiohttp installation and usage
try:
Expand Down Expand Up @@ -482,13 +476,9 @@ async def request(
async def _recover_auth_state():
is_mtls_endpoint = False
if self._is_mtls:
hostname = urllib.parse.urlsplit(url).hostname
if hostname:
is_mtls_endpoint = any(
hostname == prefix
or hostname.endswith("." + prefix)
for prefix in _MTLS_URL_PREFIXES
)
is_mtls_endpoint = (
google.auth.transport._mtls_helper.is_mtls_endpoint(url)
)
Comment on lines +479 to +481

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Use the explicitly imported _mtls_helper module.

Suggested change
is_mtls_endpoint = (
google.auth.transport._mtls_helper.is_mtls_endpoint(url)
)
is_mtls_endpoint = (
_mtls_helper.is_mtls_endpoint(url)
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rejecting the suggestion: Keeping google.auth.transport._mtls_helper to match the existing import on line 27 and usage on line 304.

# Snapshot the stale certificate state BEFORE acquiring the lock.
# This represents the cert that caused the 401 rejection.
if is_mtls_endpoint:
Expand Down
Loading
Loading