Skip to content

feat(auth): support ECP hardware-backed certificates for X.509 WIF - #18222

Open
attharva-24 wants to merge 3 commits into
googleapis:mainfrom
attharva-24:feat-x509-wif-ecp
Open

feat(auth): support ECP hardware-backed certificates for X.509 WIF#18222
attharva-24 wants to merge 3 commits into
googleapis:mainfrom
attharva-24:feat-x509-wif-ecp

Conversation

@attharva-24

@attharva-24 attharva-24 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

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-auth assumed that client certificates and private keys were always stored as plaintext files on disk (workload.cert_path and workload.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.py expected file paths on disk, raising ClientCertError, and external_account.py injected invalid cert tuples (cert=(None, None)) into the STS request.


Summary of Changes

  1. google/auth/transport/_mtls_helper.py:

    • Added _ECP_SECTIONS = ("pkcs11", "windows_store", "macos_keychain").
    • Added is_ecp_config(certificate_config_path=None) helper to detect ECP hardware configurations (validating keystore sections and libs while ensuring local file "workload" configurations take priority).
  2. google/auth/transport/_custom_tls_signer.py:

    • Added 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 via GetCertPemForPython.
    • Safely wrapped C-driver loading and symbol execution in defensive try...except (OSError, AttributeError, TypeError, ValueError) blocks, re-raising as MutualTLSChannelError.
  3. google/auth/identity_pool.py:

    • Updated _get_cert_bytes(): When no local file path exists on disk, it checks is_ecp_config() and retrieves the certificate bytes dynamically via ECP.
    • Updated refresh() exception handling to catch MutualTLSChannelError and re-raise as RefreshError in compliance with public interface contracts.
  4. google/auth/external_account.py:

    • Updated _perform_refresh_token(): Only injects cert=(cert_path, key_path) when key_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).
  5. Unit Tests:

    • Added TestIsECPConfig in tests/transport/test__mtls_helper.py covering PKCS#11, macOS Keychain, Windows Store, workload priority, missing libs, and corrupted configs.
    • Added unit tests for get_cert_from_custom_tls_signer() in tests/transport/test__custom_tls_signer.py testing library loading, symbol missing errors, and fixture deduplication.
    • Added public API testing (retrieve_subject_token) for ECP fallback and error propagation in tests/test_identity_pool.py.
    • Added test_refresh_with_mtls_ecp_no_key in tests/test_external_account.py.

Verification & Test Results

1. Automated Test Suite

Ran pytest across all affected test suites (451 passing tests):

======================= 451 passed, 24 warnings in 23.47s =======================

2. Manual End-to-End Verification (Live Hardware Keystore)

Tested end-to-end against a machine configured with enterprise certificate proxy and hardware keystores:

  1. ECP Configuration Detection: Verified is_ecp_config() correctly identifies the system's ECP certificate configuration.
  2. Hardware Certificate Extraction: Verified get_cert_from_custom_tls_signer() successfully invokes the ECP native library (GetCertPemForPython) and retrieves PEM certificate bytes from the hardware module.
  3. Subject Token Generation: Verified identity_pool.Credentials with use_default_certificate_config=True formats a valid X.509 Base64 DER JSON array Subject Token.

Sanitized Output:

1. ECP Configuration Detection:
   -> is_ecp_config: True

2. Extracting Certificate from Hardware Keystore via ECP:
   -> Successfully loaded certificate from hardware keystore via libecp.so!
   -> Certificate Header: -----BEGIN CERTIFICATE-----
   -> Total Certificate Bytes: 1298

3. Generating X.509 Subject Token from Hardware Certificate:
   -> Subject Token format: Valid JSON array
   -> Certificate Chain items: 1
   -> Base64 Certificate Payload: MIID8TCCAtmgAwIBAgIJAJK... [SANITIZED]

SUCCESS: Hardware X.509 WIF verified end-to-end!

3. Manual Edge Cases Verification

Tested all boundary conditions and defensive fallbacks:

🔹 Edge Case 1: Both Local File ('workload') AND Hardware ECP exist in config
   -> Priority Check: Local File ('workload') won over ECP as expected! ✅

🔹 Edge Case 2: Config file path does not exist (/nonexistent/path.json)
   -> Clean Failure: Raised expected ClientCertError! ✅

🔹 Edge Case 3: Malformed / Corrupted JSON config file
   -> Clean Failure: Caught corrupted JSON and raised MutualTLSChannelError! ✅

🔹 Edge Case 4: ECP section exists, but 'libs' driver section is missing
   -> Incomplete Config Check: Correctly rejected config without 'libs'! ✅

🔹 Edge Case 5: 'libs.ecp_client' points to a non-existent shared library (.so)
   -> Exception Wrapping: Cleanly caught and raised RefreshError (Failed to retrieve certificate bytes for external account credentials)! ✅

🔹 Edge Case 6: Non-X.509 WIF (URL/OIDC) while ECP exists on the machine
   -> Independence Check: URL/OIDC credential is fully isolated from ECP! ✅

ALL 6 EDGE CASES PASSED WITH 100% SUCCESS AND DEFENSIVE SAFETY!

- 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
@attharva-24
attharva-24 requested review from a team as code owners August 26, 2026 00:29

@gemini-code-assist gemini-code-assist Bot left a comment

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.

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.

Comment thread packages/google-auth/google/auth/transport/_mtls_helper.py
Comment thread packages/google-auth/google/auth/transport/_custom_tls_signer.py Outdated
Comment thread packages/google-auth/google/auth/transport/_custom_tls_signer.py Outdated
- 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",

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.

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.

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.

Reverted check_use_client_cert() back to only checking "workload"; removed mock from test_requests.py.

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.

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)

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.

Are we sure the only thing these two calls may raise is a "MutualTLSChannelError" exception which is what the docstring states.

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.

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

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.

I think the need to mock here is another indicator of an issue with the changes to the implementation of check_use_client_cert()

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.

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(

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.

I think the need to mock here is another indicator of an issue with the changes to the implementation of check_use_client_cert()

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.

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"}

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 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.

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.

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)

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.

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.

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()

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.

Should we really be testing by calling private, internal helpers like this directly instead of calling the public API?

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.

Refactored tests to call public credentials.retrieve_subject_token(None) and asserted Subject Token format.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[google-auth] Feat: ECP and hardware-backed cert support for X.509 workloads

2 participants