Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
19f9699
refactor(auth): replace pyOpenSSL with standard ssl and cryptography
nbayati May 7, 2026
48f4e26
fix: resolve mypy and lint issues
nbayati Jun 10, 2026
41282b8
fix: suppress interactive OpenSSL stdin passphrase prompts during mTL…
nbayati Jun 10, 2026
1704dd8
add unit tests to mtls_helper
nbayati Jun 10, 2026
a6882e4
refactor(auth): use os.fdopen for writing to memfd in mtls helper and…
nbayati Jun 17, 2026
f9a741a
test(auth): fix failing test by updating mock_mds_mtls_config asserti…
nbayati Jun 17, 2026
04776d6
fix nox failures
nbayati Jun 17, 2026
980527e
test: add edge case and error handling tests for _mtls_helper functions
nbayati Jun 17, 2026
105b02a
fix lint error, again!
nbayati Jun 17, 2026
0a9e02d
refactor(auth): address PR comments on imports, exit call, and safety…
nbayati Jun 18, 2026
7b77812
docs/refactor(auth): improve secure_cert_key_paths docstrings and ref…
nbayati Jun 18, 2026
31ae507
refactor(auth): simplify fallback logic using custom exception and cl…
nbayati Jun 18, 2026
759f6c2
refactor: add type annotations to paths variable in _mtls_helper.py
nbayati Jun 18, 2026
9c31523
fix: unpack cryptography_base_require in DEPENDENCIES
nbayati Jun 25, 2026
30dd701
fix(auth): wrap callback, certificate load and trust chain read error…
nbayati Jun 25, 2026
3eaaf22
verify certificate path readability to prevent AppArmor/LSM crashes
nbayati Jun 25, 2026
56cde12
wrap secure_cert_key_paths inside transport exception handlers
nbayati Jun 25, 2026
22e7f0d
fix: catch ConnectionError in MDS client to allow HTTP fallback on co…
nbayati Jun 25, 2026
1305d2f
raise MutualTLSChannelError if custom TLS signer is used on unsupport…
nbayati Jun 25, 2026
3ff3750
fix lint and mypy failre
nbayati Jun 25, 2026
21d0254
fix(auth): handle FileNotFoundError for trust chain and mock os.acces…
nbayati Jun 25, 2026
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
54 changes: 17 additions & 37 deletions packages/google-auth/google/auth/aio/transport/mtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,42 +17,17 @@
"""

import asyncio
import contextlib
import logging
import os
import ssl
import tempfile
from typing import Optional

from google.auth import exceptions
import google.auth.transport._mtls_helper
from google.auth.transport._mtls_helper import secure_cert_key_paths
import google.auth.transport.mtls

_LOGGER = logging.getLogger(__name__)


@contextlib.contextmanager
def _create_temp_file(content: bytes):
"""Creates a temporary file with the given content.

Args:
content (bytes): The content to write to the file.

Yields:
str: The path to the temporary file.
"""
# Create a temporary file that is readable only by the owner.
fd, file_path = tempfile.mkstemp()
try:
with os.fdopen(fd, "wb") as f:
f.write(content)
yield file_path
finally:
# Securely delete the file after use.
if os.path.exists(file_path):
os.remove(file_path)


def make_client_cert_ssl_context(
cert_bytes: bytes, key_bytes: bytes, passphrase: Optional[bytes] = None
) -> ssl.SSLContext:
Expand All @@ -71,19 +46,24 @@ def make_client_cert_ssl_context(
Raises:
google.auth.exceptions.TransportError: If there is an error loading the certificate.
"""
with _create_temp_file(cert_bytes) as cert_path, _create_temp_file(
key_bytes
) as key_path:
try:
try:
with secure_cert_key_paths(cert_bytes, key_bytes, passphrase=passphrase) as (
cert_path,
key_path,
passphrase_val,
):
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
context.load_cert_chain(
certfile=cert_path, keyfile=key_path, password=passphrase
)
if cert_path:
context.load_cert_chain(
certfile=cert_path,
keyfile=key_path,
password=passphrase_val or "",
)
return context
except (ssl.SSLError, OSError, IOError, ValueError, RuntimeError) as exc:
raise exceptions.TransportError(
"Failed to load client certificate and key for mTLS."
) from exc
except (ssl.SSLError, OSError, IOError, ValueError, RuntimeError) as exc:
raise exceptions.TransportError(
"Failed to load client certificate and key for mTLS."
) from exc


async def _run_in_executor(func, *args):
Expand Down
3 changes: 2 additions & 1 deletion packages/google-auth/google/auth/compute_engine/_mtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def __init__(
self.ssl_context = ssl.create_default_context()
self.ssl_context.load_verify_locations(cafile=mds_mtls_config.ca_cert_path)
self.ssl_context.load_cert_chain(
certfile=mds_mtls_config.client_combined_cert_path
certfile=mds_mtls_config.client_combined_cert_path, password=""
Comment thread
nbayati marked this conversation as resolved.
)
super(MdsMtlsAdapter, self).__init__(*args, **kwargs)

Expand All @@ -146,6 +146,7 @@ def send(self, request, **kwargs):
ssl.SSLError,
requests.exceptions.SSLError,
requests.exceptions.HTTPError,
requests.exceptions.ConnectionError,
) as e:
_LOGGER.warning(
"mTLS connection to Compute Engine Metadata server failed. "
Expand Down
40 changes: 20 additions & 20 deletions packages/google-auth/google/auth/identity_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,15 @@ def __init__(self, trust_chain_path, leaf_cert_callback):

@_helpers.copy_docstring(SubjectTokenSupplier)
def get_subject_token(self, context, request):
# Import OpennSSL inline because it is an extra import only required by customers
# using mTLS.
from OpenSSL import crypto
from cryptography import x509

leaf_cert = crypto.load_certificate(
crypto.FILETYPE_PEM, self._leaf_cert_callback()
)
try:

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.

This wraps too much code in the parse error.

The callback can fail for config or file-read reasons, but because it is inside this try, those errors become Failed to parse leaf certificate.

Can we call the callback before this parse try, so only actual cert parsing errors get this message?

leaf_cert_data = self._leaf_cert_callback()
if isinstance(leaf_cert_data, str):
leaf_cert_data = leaf_cert_data.encode("utf-8")
leaf_cert = x509.load_pem_x509_certificate(leaf_cert_data)
except Exception as e:
raise exceptions.RefreshError("Failed to parse leaf certificate.") from e
trust_chain = self._read_trust_chain()
cert_chain = []

Expand All @@ -184,9 +186,7 @@ def get_subject_token(self, context, request):
return json.dumps(cert_chain)

def _read_trust_chain(self):
# Import OpennSSL inline because it is an extra import only required by customers
# using mTLS.
from OpenSSL import crypto
from cryptography import x509

certificate_trust_chain = []
# If no trust chain path was provided, return an empty list.
Expand All @@ -204,9 +204,7 @@ def _read_trust_chain(self):
cert_data = b"-----BEGIN CERTIFICATE-----" + cert_block
try:
# Load each certificate and add it to the trust chain.
cert = crypto.load_certificate(
crypto.FILETYPE_PEM, cert_data
)
cert = x509.load_pem_x509_certificate(cert_data)
Comment thread
nbayati marked this conversation as resolved.
certificate_trust_chain.append(cert)
except Exception as e:
raise exceptions.RefreshError(
Expand All @@ -215,19 +213,21 @@ def _read_trust_chain(self):
)
) from e
return certificate_trust_chain
except FileNotFoundError:
except FileNotFoundError as e:
raise exceptions.RefreshError(
"Trust chain file '{}' was not found.".format(self._trust_chain_path)
)
) from e
except OSError as e:
raise exceptions.RefreshError(
"Error accessing trust chain file '{}'.".format(self._trust_chain_path)
) from e

def _encode_cert(cert):
# Import OpennSSL inline because it is an extra import only required by customers
# using mTLS.
from OpenSSL import crypto
from cryptography.hazmat.primitives import serialization

return base64.b64encode(
crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
).decode("utf-8")
return base64.b64encode(cert.public_bytes(serialization.Encoding.DER)).decode(
"utf-8"
)


def _parse_token_data(token_content, format_type="text", subject_token_field_name=None):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
import os
import sys

import cffi # type: ignore

from google.auth import exceptions

_LOGGER = logging.getLogger(__name__)
Expand All @@ -45,13 +43,12 @@
)


# Cast SSL_CTX* to void*
def _cast_ssl_ctx_to_void_p_pyopenssl(ssl_ctx):
return ctypes.cast(int(cffi.FFI().cast("intptr_t", ssl_ctx)), ctypes.c_void_p)


# Cast SSL_CTX* to void*
def _cast_ssl_ctx_to_void_p_stdlib(context):
if sys.implementation.name != "cpython" or hasattr(sys, "getobjects"):
raise exceptions.MutualTLSChannelError(
"Custom TLS signing is only supported on standard release CPython runtimes."
)
return ctypes.c_void_p.from_address(
id(context) + ctypes.sizeof(ctypes.c_void_p) * 2
)
Expand Down Expand Up @@ -274,7 +271,7 @@ def attach_to_ssl_context(self, ctx):
if not self._offload_lib.ConfigureSslContext(
self._sign_callback,
ctypes.c_char_p(self._cert),
_cast_ssl_ctx_to_void_p_pyopenssl(ctx._ctx._context),
_cast_ssl_ctx_to_void_p_stdlib(ctx),

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.

The stdlib C pointer arithmetic hack id(context) + ctypes.sizeof(ctypes.c_void_p) * 2 used in _cast_ssl_ctx_to_void_p_stdlib is highly brittle and CPython-specific.

  1. On PyPy, id() returns virtual IDs instead of actual memory addresses, leading to immediate segmentation faults or undefined memory access.
  2. On CPython debug builds (compiled with Py_DEBUG / Py_TRACE_REFS), the object header is larger due to reference tracing pointers, which shifts the offset of the raw OpenSSL ctx pointer from 2 * sizeof(void*) to 4 * sizeof(void*). Running this on a debug build will cast the ob_type pointer to a void pointer, causing a crash.

Remediation:
Add a strict environment check to verify a standard release CPython environment before performing pointer casting, raising a graceful MutualTLSChannelError otherwise:

import sys
if sys.implementation.name != "cpython" or hasattr(sys, "getobjects"):
    raise exceptions.MutualTLSChannelError(
        "Custom TLS signing is only supported on standard release CPython runtimes."
    )

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.

Done! I'm glad we're catching some of these existing issues and cleaning things up as part of this PR 🎉

):
raise exceptions.MutualTLSChannelError(
"failed to configure ECP Offload SSL context"
Expand Down
Loading
Loading