From b1ec8f3ac8a8eef4a51ea24ebe2e72f42febddcf Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Sun, 2 Aug 2026 12:46:18 -0400 Subject: [PATCH 1/4] Use ECP with WIF x.509 --- .../google-auth/google/auth/external_account.py | 11 ++++++++--- .../google/auth/transport/_custom_tls_signer.py | 16 ++++++++++++++++ .../google/auth/transport/_mtls_helper.py | 8 +++++--- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/google-auth/google/auth/external_account.py b/packages/google-auth/google/auth/external_account.py index b90fcab4c0ee..a608058b90bd 100644 --- a/packages/google-auth/google/auth/external_account.py +++ b/packages/google-auth/google/auth/external_account.py @@ -479,9 +479,14 @@ def _perform_refresh_token(self, request, cert_fingerprint=None): # Inject client certificate into request. if self._mtls_required(): - request = functools.partial( - request, cert=self._get_mtls_cert_and_key_paths() - ) + # Only inject cert= when key_path is not None. For hardware- + # backed keys (TPM/Secure Enclave) key_path is None and the + # mTLS adapter handles signing via ECP callbacks instead. + _cert_path, _key_path = self._get_mtls_cert_and_key_paths() + if _key_path is not None: + request = functools.partial( + request, cert=(_cert_path, _key_path) + ) if self._should_initialize_impersonated_credentials(): with self._impersonation_lock: diff --git a/packages/google-auth/google/auth/transport/_custom_tls_signer.py b/packages/google-auth/google/auth/transport/_custom_tls_signer.py index 90143101ab07..17a714ca5e70 100644 --- a/packages/google-auth/google/auth/transport/_custom_tls_signer.py +++ b/packages/google-auth/google/auth/transport/_custom_tls_signer.py @@ -195,6 +195,22 @@ def get_cert(signer_lib, config_file_path): 0, # certHolderLen ) if cert_len == 0: + # Fallback: ECP may not be able to access the keystore directly + # (e.g. Secure Enclave keys on macOS). Try reading the cert from + # the cert_path specified in the certificate config. + try: + import json as _json + + with open(config_file_path, "r") as _f: + _cfg = _json.load(_f) + _cert_path = ( + _cfg.get("cert_configs", {}).get("workload", {}).get("cert_path") + ) + if _cert_path: + with open(_cert_path, "rb") as _cf: + return _cf.read() + except Exception: + pass raise exceptions.MutualTLSChannelError("failed to get certificate") # Then we create an array to hold the cert, and call again to fill the cert diff --git a/packages/google-auth/google/auth/transport/_mtls_helper.py b/packages/google-auth/google/auth/transport/_mtls_helper.py index eb0600740c0d..70fd0bb99d16 100644 --- a/packages/google-auth/google/auth/transport/_mtls_helper.py +++ b/packages/google-auth/google/auth/transport/_mtls_helper.py @@ -476,14 +476,16 @@ def _get_workload_cert_and_key_paths(config_path, include_context_aware=True): return None, None workload = cert_configs["workload"] - if "cert_path" not in workload or "key_path" not in workload: + if "cert_path" not in workload: raise exceptions.ClientCertError( - 'Workload certificate configuration is missing "cert_path" or "key_path" in {}'.format( + 'Workload certificate configuration is missing "cert_path" in {}'.format( absolute_path ) ) cert_path = workload["cert_path"] - key_path = workload["key_path"] + # key_path is optional — hardware-backed keys (TPM, Secure Enclave) + # have no extractable private key file on disk. + key_path = workload.get("key_path") return cert_path, key_path From f100831383d2b5676bd319c6481290ee0ff99489 Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Sun, 2 Aug 2026 14:20:51 -0400 Subject: [PATCH 2/4] mTLS aware transport --- .../google/auth/external_account.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/google-auth/google/auth/external_account.py b/packages/google-auth/google/auth/external_account.py index a608058b90bd..fb202a6790e7 100644 --- a/packages/google-auth/google/auth/external_account.py +++ b/packages/google-auth/google/auth/external_account.py @@ -479,14 +479,31 @@ def _perform_refresh_token(self, request, cert_fingerprint=None): # Inject client certificate into request. if self._mtls_required(): - # Only inject cert= when key_path is not None. For hardware- - # backed keys (TPM/Secure Enclave) key_path is None and the - # mTLS adapter handles signing via ECP callbacks instead. _cert_path, _key_path = self._get_mtls_cert_and_key_paths() if _key_path is not None: + # File-based key: inject cert= directly. request = functools.partial( request, cert=(_cert_path, _key_path) ) + else: + # Hardware-backed key (TPM / Secure Enclave): the private + # key cannot be extracted, so cert= won't work. Instead, + # create an mTLS-aware transport that delegates signing to + # the ECP TLS offload library via _MutualTlsOffloadAdapter. + from google.auth.transport.requests import ( # lazy import + Request as _Request, + _MutualTlsOffloadAdapter, + ) + import requests as _requests + + _session = _requests.Session() + _session.mount( + "https://", + _MutualTlsOffloadAdapter( + self._certificate_config_location + ), + ) + request = _Request(session=_session) if self._should_initialize_impersonated_credentials(): with self._impersonation_lock: From 6208aa64e77b8db6fff76773dbdcd88a5b6442c5 Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Sun, 2 Aug 2026 14:42:05 -0400 Subject: [PATCH 3/4] config path fix --- .../google/auth/external_account.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/google-auth/google/auth/external_account.py b/packages/google-auth/google/auth/external_account.py index fb202a6790e7..f7f959191416 100644 --- a/packages/google-auth/google/auth/external_account.py +++ b/packages/google-auth/google/auth/external_account.py @@ -497,11 +497,23 @@ def _perform_refresh_token(self, request, cert_fingerprint=None): import requests as _requests _session = _requests.Session() + # Resolve certificate config path: explicit location, + # GOOGLE_API_CERTIFICATE_CONFIG env var, or default path. + _config_path = getattr( + self, "_certificate_config_location", None + ) + if not _config_path: + import os as _os + + _config_path = _os.environ.get( + "GOOGLE_API_CERTIFICATE_CONFIG", + _os.path.expanduser( + "~/.config/gcloud/certificate_config.json" + ), + ) _session.mount( "https://", - _MutualTlsOffloadAdapter( - self._certificate_config_location - ), + _MutualTlsOffloadAdapter(_config_path), ) request = _Request(session=_session) From c8f27a62169224dc2be1d0d2db200ddb412d91ce Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Mon, 3 Aug 2026 08:15:09 -0400 Subject: [PATCH 4/4] configure_mtls_offload_channel public method --- .../google/auth/transport/requests.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/google-auth/google/auth/transport/requests.py b/packages/google-auth/google/auth/transport/requests.py index 822cf687f5d0..450d923b6126 100644 --- a/packages/google-auth/google/auth/transport/requests.py +++ b/packages/google-auth/google/auth/transport/requests.py @@ -562,6 +562,45 @@ def configure_mtls_channel(self, client_cert_callback=None): if hasattr(self, "_cached_cert"): del self._cached_cert + def configure_mtls_offload_channel(self, enterprise_cert_file_path): + """Configure mTLS using ECP hardware-backed signing offload. + + This mounts a ``_MutualTlsOffloadAdapter`` that delegates TLS client + authentication to the Enterprise Certificate Proxy (ECP), which + performs signing using hardware-backed keys (TPM, Secure Enclave, + CNG, etc.). + + Unlike :meth:`configure_mtls_channel`, which requires software + cert and key bytes, this method offloads all cryptographic + operations to the ECP shared libraries specified in the + enterprise cert configuration file. + + Args: + enterprise_cert_file_path (str): Path to the enterprise + certificate configuration JSON file. The file must + contain a ``libs`` section pointing to the ECP shared + libraries:: + + { + "libs": { + "ecp_client": "/path/to/libecp.so", + "tls_offload": "/path/to/libtls_offload.so" + } + } + + Raises: + google.auth.exceptions.MutualTLSChannelError: If the + offload adapter could not be created (e.g. missing or + invalid ECP libraries). + """ + adapter = _MutualTlsOffloadAdapter(enterprise_cert_file_path) + self.mount("https://", adapter) + self._is_mtls = True + + if self._auth_request_session is not None: + auth_adapter = _MutualTlsOffloadAdapter(enterprise_cert_file_path) + self._auth_request_session.mount("https://", auth_adapter) + def request( self, method,