From 5d527fbdf5676d741909d764faf5464e7ad0aea9 Mon Sep 17 00:00:00 2001 From: Atharva Date: Thu, 6 Aug 2026 20:07:18 +0000 Subject: [PATCH 1/2] fix(auth): prevent TypeError and support home-dir cert fallback for X.509 WIF on ECP machines - Prevent TypeError crash in identity_pool.py by raising ClientCertError if _get_mtls_cert_and_key_paths() returns None for the certificate path. - Add fallback in _mtls_helper.py to check the default home directory configuration ~/.config/gcloud/certificate_config.json if the env-var-resolved config does not contain a workload block. - Add unit tests to cover both behaviors and verify they function correctly. Fixes: b/542359992 --- .../google-auth/google/auth/identity_pool.py | 4 ++ .../google/auth/transport/_mtls_helper.py | 8 ++++ .../google-auth/tests/test_identity_pool.py | 16 +++++++ .../tests/transport/test__mtls_helper.py | 47 +++++++++++++++++++ 4 files changed, 75 insertions(+) diff --git a/packages/google-auth/google/auth/identity_pool.py b/packages/google-auth/google/auth/identity_pool.py index 4b1aa393b2fa..b32de78add92 100644 --- a/packages/google-auth/google/auth/identity_pool.py +++ b/packages/google-auth/google/auth/identity_pool.py @@ -412,6 +412,10 @@ def _get_mtls_cert_and_key_paths(self): def _get_cert_bytes(self): cert_path, _ = self._get_mtls_cert_and_key_paths() + if cert_path is None: + raise exceptions.ClientCertError( + "Workload certificate configuration could not be found or does not contain workload certificate paths." + ) return _mtls_helper._read_cert_file(cert_path) def _mtls_required(self): diff --git a/packages/google-auth/google/auth/transport/_mtls_helper.py b/packages/google-auth/google/auth/transport/_mtls_helper.py index eb0600740c0d..3fd27dbaafb5 100644 --- a/packages/google-auth/google/auth/transport/_mtls_helper.py +++ b/packages/google-auth/google/auth/transport/_mtls_helper.py @@ -472,6 +472,14 @@ def _get_workload_cert_and_key_paths(config_path, include_context_aware=True): # and we want to gracefully fallback to testing other mTLS configurations # like SecureConnect instead of throwing an exception. + if "workload" not in cert_configs and config_path is None: + default_home_path = path.expanduser(CERTIFICATE_CONFIGURATION_DEFAULT_PATH) + if path.exists(default_home_path) and default_home_path != absolute_path: + home_data = _load_json_file(default_home_path) + if "cert_configs" in home_data and "workload" in home_data["cert_configs"]: + cert_configs = home_data["cert_configs"] + absolute_path = default_home_path + if "workload" not in cert_configs: return None, None workload = cert_configs["workload"] diff --git a/packages/google-auth/tests/test_identity_pool.py b/packages/google-auth/tests/test_identity_pool.py index 92659bd90b38..55fa7a177439 100644 --- a/packages/google-auth/tests/test_identity_pool.py +++ b/packages/google-auth/tests/test_identity_pool.py @@ -1784,6 +1784,22 @@ def test_get_mtls_certs_invalid(self): 'The credential is not configured to use mtls requests. The credential should include a "certificate" section in the credential source.' ) + @mock.patch( + "google.auth.transport._mtls_helper._get_workload_cert_and_key_paths", + return_value=(None, None), + ) + def test_get_cert_bytes_none_raises_error(self, mock_get_workload_cert_and_key_paths): + credentials = self.make_credentials( + credential_source=self.CREDENTIAL_SOURCE_CERTIFICATE.copy() + ) + + with pytest.raises(exceptions.ClientCertError) as excinfo: + credentials._get_cert_bytes() + + assert excinfo.match( + "Workload certificate configuration could not be found or does not contain workload certificate paths." + ) + @mock.patch("google.auth._agent_identity_utils.parse_certificate") @mock.patch( "google.auth._agent_identity_utils.should_request_bound_token", diff --git a/packages/google-auth/tests/transport/test__mtls_helper.py b/packages/google-auth/tests/transport/test__mtls_helper.py index 537ef47e7295..f472784a7098 100644 --- a/packages/google-auth/tests/transport/test__mtls_helper.py +++ b/packages/google-auth/tests/transport/test__mtls_helper.py @@ -511,6 +511,53 @@ def test_no_workload(self, mock_get_cert_config_path, mock_load_json_file): assert actual_cert is None assert actual_key is None + @mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True) + @mock.patch( + "google.auth.transport._mtls_helper._get_cert_config_path", autospec=True + ) + @mock.patch( + "google.auth.transport._mtls_helper._read_cert_and_key_files", autospec=True + ) + @mock.patch("os.path.exists", autospec=True) + def test_no_workload_fallback_to_home( + self, + mock_path_exists, + mock_read_cert_and_key_files, + mock_get_cert_config_path, + mock_load_json_file, + ): + ecp_path = "/etc/gcloud/certificate_config.json" + home_path = os.path.expanduser("~/.config/gcloud/certificate_config.json") + mock_get_cert_config_path.return_value = ecp_path + + def exists_side_effect(path): + if path == home_path: + return True + return False + + mock_path_exists.side_effect = exists_side_effect + + def load_json_side_effect(path): + if path == ecp_path: + return {"cert_configs": {"pkcs11": {}}} + elif path == home_path: + return { + "cert_configs": { + "workload": {"cert_path": "cert/path", "key_path": "key/path"} + } + } + return {} + + mock_load_json_file.side_effect = load_json_side_effect + mock_read_cert_and_key_files.return_value = ( + pytest.public_cert_bytes, + pytest.private_key_bytes, + ) + + actual_cert, actual_key = _mtls_helper._get_workload_cert_and_key(None) + assert actual_cert == pytest.public_cert_bytes + assert actual_key == pytest.private_key_bytes + @mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True) @mock.patch( "google.auth.transport._mtls_helper._get_cert_config_path", autospec=True From 8e68cf0aae95591292113b2d72ea9046df0382c6 Mon Sep 17 00:00:00 2001 From: Atharva Date: Thu, 6 Aug 2026 20:12:24 +0000 Subject: [PATCH 2/2] fix(auth): add defensive dictionary checks for loaded certificate configuration JSON --- .../google/auth/transport/_mtls_helper.py | 14 ++++++++------ .../tests/transport/test__mtls_helper.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/google-auth/google/auth/transport/_mtls_helper.py b/packages/google-auth/google/auth/transport/_mtls_helper.py index 3fd27dbaafb5..b97102cdaed2 100644 --- a/packages/google-auth/google/auth/transport/_mtls_helper.py +++ b/packages/google-auth/google/auth/transport/_mtls_helper.py @@ -459,7 +459,7 @@ def _get_workload_cert_and_key_paths(config_path, include_context_aware=True): data = _load_json_file(absolute_path) - if "cert_configs" not in data: + if not isinstance(data, dict) or "cert_configs" not in data: raise exceptions.ClientCertError( 'Certificate config file {} is in an invalid format, a "cert configs" object is expected'.format( absolute_path @@ -472,15 +472,17 @@ def _get_workload_cert_and_key_paths(config_path, include_context_aware=True): # and we want to gracefully fallback to testing other mTLS configurations # like SecureConnect instead of throwing an exception. - if "workload" not in cert_configs and config_path is None: + if (not isinstance(cert_configs, dict) or "workload" not in cert_configs) and config_path is None: default_home_path = path.expanduser(CERTIFICATE_CONFIGURATION_DEFAULT_PATH) if path.exists(default_home_path) and default_home_path != absolute_path: home_data = _load_json_file(default_home_path) - if "cert_configs" in home_data and "workload" in home_data["cert_configs"]: - cert_configs = home_data["cert_configs"] - absolute_path = default_home_path + if isinstance(home_data, dict): + home_cert_configs = home_data.get("cert_configs") + if isinstance(home_cert_configs, dict) and "workload" in home_cert_configs: + cert_configs = home_cert_configs + absolute_path = default_home_path - if "workload" not in cert_configs: + if not isinstance(cert_configs, dict) or "workload" not in cert_configs: return None, None workload = cert_configs["workload"] diff --git a/packages/google-auth/tests/transport/test__mtls_helper.py b/packages/google-auth/tests/transport/test__mtls_helper.py index f472784a7098..35ca94b7cf30 100644 --- a/packages/google-auth/tests/transport/test__mtls_helper.py +++ b/packages/google-auth/tests/transport/test__mtls_helper.py @@ -499,6 +499,22 @@ def test_no_cert_configs( with pytest.raises(exceptions.ClientCertError): _mtls_helper._get_workload_cert_and_key("") + @mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True) + @mock.patch( + "google.auth.transport._mtls_helper._get_cert_config_path", autospec=True + ) + @mock.patch("os.path.exists", autospec=True) + def test_malformed_json_returns_error( + self, mock_path_exists, mock_get_cert_config_path, mock_load_json_file + ): + mock_path_exists.return_value = True + mock_get_cert_config_path.return_value = "/path/to/cert" + + for val in [None, [], "invalid_string"]: + mock_load_json_file.return_value = val + with pytest.raises(exceptions.ClientCertError): + _mtls_helper._get_workload_cert_and_key("") + @mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True) @mock.patch( "google.auth.transport._mtls_helper._get_cert_config_path", autospec=True