feat(auth): support ECP hardware-backed certificates for X.509 WIF - #18222
feat(auth): support ECP hardware-backed certificates for X.509 WIF#18222attharva-24 wants to merge 3 commits into
Conversation
- Add is_ecp_config in _mtls_helper to detect ECP configurations - Add get_cert_from_custom_tls_signer in _custom_tls_signer to extract cert bytes from hardware keystores - Update identity_pool._get_cert_bytes to retrieve certificate bytes via ECP when no file path is present - Update external_account._perform_refresh_token to delegate mTLS signing to the transport adapter when using hardware keys - Add comprehensive unit tests covering ECP detection, cert extraction, and token exchange Fixes googleapis#17967 Refs: b/541419974
There was a problem hiding this comment.
Code Review
This pull request introduces support for Enterprise Certificate Proxy (ECP) configurations in external account and identity pool credentials. It adds helper functions to detect ECP configurations, retrieve certificates using custom TLS signers, and updates credential refresh logic to handle hardware-backed keys. The review feedback highlights a potential path resolution mismatch in is_ecp_config that could lead to runtime errors, recommends specifying an explicit UTF-8 encoding when opening configuration files for cross-platform safety, and suggests type-validating the signer_library value from untrusted JSON inputs to prevent type errors.
- Add encoding='utf-8' when opening enterprise cert file - Validate signer_library is a non-empty string before loading - Streamline is_ecp_config to avoid redundant fallback resolution - Format all modified files with black
| assert not auth_session.is_mtls | ||
|
|
||
| @mock.patch( | ||
| "google.auth.transport._mtls_helper._get_cert_config_path", |
There was a problem hiding this comment.
Why is this patch needed? I suspect it is covering up a real issue with the implementation change to check_use_client_cert where we now return true in the ECP case even when certificates aren't present in the workload configuration.
There was a problem hiding this comment.
Reverted check_use_client_cert() back to only checking "workload"; removed mock from test_requests.py.
There was a problem hiding this comment.
Maybe I'm missing something but isn't check_use_client_cert used in numerous places to determine if we should be using mTLS or not? Presumably if ECP cert is configured we'd still want to use mTLS but the current impl would not do so - e.g. configure_mtls_channel would return False -
Is that truly working as intended? If not, what are we missing with test coverage
| ) | ||
|
|
||
| signer_lib = load_signer_lib(signer_library) | ||
| return get_cert(signer_lib, enterprise_cert_file_path) |
There was a problem hiding this comment.
Are we sure the only thing these two calls may raise is a "MutualTLSChannelError" exception which is what the docstring states.
There was a problem hiding this comment.
Wrapped both calls in try...except (OSError, AttributeError, TypeError, ValueError) re-raising MutualTLSChannelError
| ) | ||
| def test_configure_mtls_channel_without_client_cert_env( | ||
| self, get_client_cert_and_key | ||
| self, get_client_cert_and_key, mock_get_cert_config_path |
There was a problem hiding this comment.
I think the need to mock here is another indicator of an issue with the changes to the implementation of check_use_client_cert()
There was a problem hiding this comment.
Removed mock patch from test_requests.py. File now matches main with 0 diffs.
| "CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH": "", | ||
| }, | ||
| ) | ||
| def test_configure_mtls_channel_without_client_cert_env( |
There was a problem hiding this comment.
I think the need to mock here is another indicator of an issue with the changes to the implementation of check_use_client_cert()
There was a problem hiding this comment.
Removed mock patch from test_urllib3.py. File now matches main with 0 diffs.
| mock_get_path.return_value = "/path/to/config.json" | ||
| mock_load_json.return_value = { | ||
| "cert_configs": { | ||
| "workload": {"cert_path": "cert.pem", "key_path": "key.pem"} |
There was a problem hiding this comment.
This omits any ECP section meaning that even if the workload section was completely removed from is_ecp_config, this test would still pass because "has_ecp_section" would evaluate to false.
There was a problem hiding this comment.
Added "pkcs11" alongside "workload" in test JSON to genuinely test precedence.
| with pytest.raises( | ||
| exceptions.MutualTLSChannelError, match="missing signer library" | ||
| ): | ||
| _custom_tls_signer.get_cert_from_custom_tls_signer(INVALID_ENTERPRISE_CERT_FILE) |
There was a problem hiding this comment.
This is the same as "libs": {} - see https://github.com/googleapis/google-cloud-python/blob/082a99a2c4a3e8d5df28eaeab9b2c710dd4296d5/packages/google-auth/tests/data/enterprise_cert_invalid.json - I think we can clean up duplication here
There was a problem hiding this comment.
Reused INVALID_ENTERPRISE_CERT_FILE fixture and tested missing "libs" key separately.
| credentials = self.make_credentials( | ||
| credential_source=self.CREDENTIAL_SOURCE_CERTIFICATE.copy() | ||
| ) | ||
| cert_bytes = credentials._get_cert_bytes() |
There was a problem hiding this comment.
Should we really be testing by calling private, internal helpers like this directly instead of calling the public API?
There was a problem hiding this comment.
Refactored tests to call public credentials.retrieve_subject_token(None) and asserted Subject Token format.
Description
Fixes #17967
Refs: b/541419974, go/x509-wif-tpm
Reference Implementation: google-cloud-go PR #20138
This PR adds support for Enterprise Certificate Proxy (ECP) and hardware-backed certificates (Apple Secure Enclave, Linux TPM 2.0, Windows Certificate Store, and PKCS#11 HSMs) in X.509 Workload Identity Federation (WIF) within
google-auth.Background & Problem Statement
Previously, X.509 Workload Identity Federation in
google-authassumed that client certificates and private keys were always stored as plaintext files on disk (workload.cert_pathandworkload.key_path).In enterprise environments enforcing Zero Trust (e.g. BeyondCorp), private keys are non-exportable and reside exclusively in hardware security chips (TPM, Secure Enclave) accessed via ECP drivers. Attempting to use X.509 WIF with an ECP-configured machine previously failed because
identity_pool.pyexpected file paths on disk, raisingClientCertError, andexternal_account.pyinjected invalid cert tuples (cert=(None, None)) into the STS request.Summary of Changes
google/auth/transport/_mtls_helper.py:_ECP_SECTIONS = ("pkcs11", "windows_store", "macos_keychain").is_ecp_config(certificate_config_path=None)helper to detect ECP hardware configurations (validating keystore sections andlibswhile ensuring local file"workload"configurations take priority).google/auth/transport/_custom_tls_signer.py:get_cert_from_custom_tls_signer(enterprise_cert_file_path)to load the ECP signer library and retrieve the certificate bytes directly from the hardware keystore viaGetCertPemForPython.try...except (OSError, AttributeError, TypeError, ValueError)blocks, re-raising asMutualTLSChannelError.google/auth/identity_pool.py:_get_cert_bytes(): When no local file path exists on disk, it checksis_ecp_config()and retrieves the certificate bytes dynamically via ECP.refresh()exception handling to catchMutualTLSChannelErrorand re-raise asRefreshErrorin compliance with public interface contracts.google/auth/external_account.py:_perform_refresh_token(): Only injectscert=(cert_path, key_path)whenkey_path is not None. When using hardware-backed keys (key_path is None), the request delegates mTLS handshake signing to the session's native ECP transport adapter (_MutualTlsOffloadAdapter).Unit Tests:
TestIsECPConfigintests/transport/test__mtls_helper.pycovering PKCS#11, macOS Keychain, Windows Store, workload priority, missing libs, and corrupted configs.get_cert_from_custom_tls_signer()intests/transport/test__custom_tls_signer.pytesting library loading, symbol missing errors, and fixture deduplication.retrieve_subject_token) for ECP fallback and error propagation intests/test_identity_pool.py.test_refresh_with_mtls_ecp_no_keyintests/test_external_account.py.Verification & Test Results
1. Automated Test Suite
Ran pytest across all affected test suites (451 passing tests):
2. Manual End-to-End Verification (Live Hardware Keystore)
Tested end-to-end against a machine configured with enterprise certificate proxy and hardware keystores:
is_ecp_config()correctly identifies the system's ECP certificate configuration.get_cert_from_custom_tls_signer()successfully invokes the ECP native library (GetCertPemForPython) and retrieves PEM certificate bytes from the hardware module.identity_pool.Credentialswithuse_default_certificate_config=Trueformats a valid X.509 Base64 DER JSON array Subject Token.Sanitized Output:
3. Manual Edge Cases Verification
Tested all boundary conditions and defensive fallbacks: