Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions packages/google-auth/google/auth/transport/_mtls_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -911,8 +911,10 @@ def check_parameters_for_unauthorized_response(cached_cert):

def call_client_cert_callback():
"""Calls the client cert callback and returns the certificate and key."""
_, cert_bytes, key_bytes, passphrase = get_client_ssl_credentials(
generate_encrypted_key=True
# Callers load the key without a passphrase, so request an unencrypted
# key, as get_client_cert_and_key() does.
_, cert_bytes, key_bytes, _ = get_client_ssl_credentials(
generate_encrypted_key=False
)
return cert_bytes, key_bytes

Expand Down
45 changes: 43 additions & 2 deletions packages/google-auth/tests/transport/test__mtls_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -1332,17 +1332,58 @@ def test_call_client_cert_callback(self, mock_get_client_ssl_credentials):
True,
b"cert_bytes",
b"key_bytes",
b"passphrase",
None,
)

cert, key = _mtls_helper.call_client_cert_callback()

assert cert == b"cert_bytes"
assert key == b"key_bytes"
mock_get_client_ssl_credentials.assert_called_once_with(
generate_encrypted_key=True
generate_encrypted_key=False
)

@mock.patch("subprocess.Popen", autospec=True)
@mock.patch("google.auth.transport._mtls_helper._load_json_file", autospec=True)
@mock.patch("google.auth.transport._mtls_helper._check_config_path", autospec=True)
@mock.patch(
"google.auth.transport._mtls_helper._get_workload_cert_and_key", autospec=True
)
def test_call_client_cert_callback_secure_connect_unencrypted_key(
self,
mock_get_workload_cert_and_key,
mock_check_config_path,
mock_load_json_file,
mock_popen,
):
mock_get_workload_cert_and_key.return_value = (None, None)
mock_check_config_path.return_value = "/path/to/config"
mock_load_json_file.return_value = {"cert_provider_command": ["command"]}

# Like a SecureConnect cert provider, return an encrypted key and a
# passphrase only when --with_passphrase is passed.
def popen(command, **kwargs):
if "--with_passphrase" in command:

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.

Two small things in this test:

  1. The test doesn't need the --with_passphrase branch in popen to fail without the fix. Returning just the cert and plaintext key already makes the old encrypted-key path raise ClientCertError("Passphrase is missing or invalid"), and with the fix in place that branch never runs, leaving line 1367 uncovered.

  2. serialization.load_pem_private_key(key, password=None) on line 1385 runs right after asserting key == pytest.private_key_bytes, so it only re-parses the static test fixture.

Could we drop both?

output = (
pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE
)
else:
output = pytest.public_cert_bytes + pytest.private_key_bytes
process = mock.Mock()
process.configure_mock(
**{"communicate.return_value": (output, b""), "returncode": 0}
)
return process

mock_popen.side_effect = popen

cert, key = _mtls_helper.call_client_cert_callback()

assert cert == pytest.public_cert_bytes
assert key == pytest.private_key_bytes
# The 401 rotation path loads this key without a passphrase.
serialization.load_pem_private_key(key, password=None)


class TestSecureCertKeyPaths(object):
def test_tier1_pass_through(self):
Expand Down
Loading