Skip to content

Commit 395e405

Browse files
feat: Enable mTLS if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set, if the MWID/X.509 cert sources detected (#1848)
The Python SDK will use a hybrid approach for mTLS enablement: - If the GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable is set (either true or false), the SDK will respect that setting. This is necessary for test scenarios and users who need to explicitly control mTLS behavior. - If the GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable is not set, the SDK will automatically enable mTLS only if it detects Managed Workload Identity (MWID) or X.509 Workforce Identity Federation (WIF) certificate sources. In other cases where the variable is not set, mTLS will remain disabled. ** This change also adds the helper method `check_use_client_cert` and it's unit test, which will be used for checking the criteria for setting the mTLS to true ** This change is only for Auth-Library, other changes will be created for Client-Library use-cases. --------- Signed-off-by: Radhika Agrawal <agrawalradhika@google.com> Co-authored-by: Daniel Sanche <d.sanche14@gmail.com>
1 parent f2708b2 commit 395e405

File tree

5 files changed

+121
-21
lines changed

5 files changed

+121
-21
lines changed

google/auth/transport/_mtls_helper.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import json
1818
import logging
19-
from os import environ, path
19+
from os import environ, getenv, path
2020
import re
2121
import subprocess
2222

@@ -405,3 +405,47 @@ def client_cert_callback():
405405

406406
# Then dump the decrypted key bytes
407407
return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
408+
409+
410+
def check_use_client_cert():
411+
"""Returns the value of the GOOGLE_API_USE_CLIENT_CERTIFICATE variable,
412+
or an inferred value('true' or 'false') if unset.
413+
414+
This value is meant to be interpreted as a "true" or "false" value
415+
representing whether the client certificate should be used, but could be any
416+
arbitrary string.
417+
418+
If GOOGLE_API_USE_CLIENT_CERTIFICATE is unset, the value value will be
419+
inferred by reading a file pointed at by GOOGLE_API_CERTIFICATE_CONFIG, and
420+
verifying it contains a "workload" section. If so, the function will return
421+
"true", otherwise "false".
422+
423+
Returns:
424+
str: The value of GOOGLE_API_USE_CLIENT_CERTIFICATE, or an inferred value
425+
("true" or "false") if unset. This string should contain a value, but may
426+
be an any arbitrary string read from the user's set
427+
GOOGLE_API_USE_CLIENT_CERTIFICATE.
428+
"""
429+
use_client_cert = getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE")
430+
# Check if the value of GOOGLE_API_USE_CLIENT_CERTIFICATE is set.
431+
if use_client_cert:
432+
return use_client_cert.lower()
433+
else:
434+
# Check if the value of GOOGLE_API_CERTIFICATE_CONFIG is set.
435+
cert_path = getenv("GOOGLE_API_CERTIFICATE_CONFIG")
436+
if cert_path:
437+
try:
438+
with open(cert_path, "r") as f:
439+
content = json.load(f)
440+
# verify json has workload key
441+
content["cert_configs"]["workload"]
442+
return "true"
443+
except (
444+
FileNotFoundError,
445+
OSError,
446+
KeyError,
447+
TypeError,
448+
json.JSONDecodeError,
449+
) as e:
450+
_LOGGER.debug("error decoding certificate: %s", e)
451+
return "false"

google/auth/transport/grpc.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717
from __future__ import absolute_import
1818

1919
import logging
20-
import os
2120

22-
from google.auth import environment_vars
2321
from google.auth import exceptions
2422
from google.auth.transport import _mtls_helper
2523
from google.oauth2 import service_account
@@ -256,9 +254,7 @@ def my_client_cert_callback():
256254

257255
# If SSL credentials are not explicitly set, try client_cert_callback and ADC.
258256
if not ssl_credentials:
259-
use_client_cert = os.getenv(
260-
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
261-
)
257+
use_client_cert = _mtls_helper.check_use_client_cert()
262258
if use_client_cert == "true" and client_cert_callback:
263259
# Use the callback if provided.
264260
cert, key = client_cert_callback()
@@ -295,9 +291,7 @@ class SslCredentials:
295291
"""
296292

297293
def __init__(self):
298-
use_client_cert = os.getenv(
299-
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
300-
)
294+
use_client_cert = _mtls_helper.check_use_client_cert()
301295
if use_client_cert != "true":
302296
self._is_mtls = False
303297
else:

google/auth/transport/requests.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import functools
2020
import logging
2121
import numbers
22-
import os
2322
import time
2423

2524
try:
@@ -35,7 +34,6 @@
3534
) # pylint: disable=ungrouped-imports
3635

3736
from google.auth import _helpers
38-
from google.auth import environment_vars
3937
from google.auth import exceptions
4038
from google.auth import transport
4139
import google.auth.transport._mtls_helper
@@ -444,13 +442,10 @@ def configure_mtls_channel(self, client_cert_callback=None):
444442
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
445443
creation failed for any reason.
446444
"""
447-
use_client_cert = os.getenv(
448-
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
449-
)
445+
use_client_cert = google.auth.transport._mtls_helper.check_use_client_cert()
450446
if use_client_cert != "true":
451447
self._is_mtls = False
452448
return
453-
454449
try:
455450
import OpenSSL
456451
except ImportError as caught_exc:

google/auth/transport/urllib3.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from __future__ import absolute_import
1818

1919
import logging
20-
import os
2120
import warnings
2221

2322
# Certifi is Mozilla's certificate bundle. Urllib3 needs a certificate bundle
@@ -51,7 +50,6 @@
5150

5251

5352
from google.auth import _helpers
54-
from google.auth import environment_vars
5553
from google.auth import exceptions
5654
from google.auth import transport
5755
from google.oauth2 import service_account
@@ -335,12 +333,9 @@ def configure_mtls_channel(self, client_cert_callback=None):
335333
google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
336334
creation failed for any reason.
337335
"""
338-
use_client_cert = os.getenv(
339-
environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false"
340-
)
336+
use_client_cert = transport._mtls_helper.check_use_client_cert()
341337
if use_client_cert != "true":
342338
return False
343-
344339
try:
345340
import OpenSSL
346341
except ImportError as caught_exc:

tests/transport/test__mtls_helper.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import json
1516
import os
1617
import re
1718

@@ -638,3 +639,74 @@ def test_crypto_error(self):
638639
_mtls_helper.decrypt_private_key(
639640
ENCRYPTED_EC_PRIVATE_KEY, b"wrong_password"
640641
)
642+
643+
def test_check_use_client_cert(self, monkeypatch):
644+
monkeypatch.setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true")
645+
use_client_cert = _mtls_helper.check_use_client_cert()
646+
assert use_client_cert == "true"
647+
648+
def test_check_use_client_cert_for_workload_with_config_file(self, monkeypatch):
649+
config_data = {
650+
"version": 1,
651+
"cert_configs": {
652+
"workload": {
653+
"cert_path": "path/to/cert/file",
654+
"key_path": "path/to/key/file",
655+
}
656+
},
657+
}
658+
config_filename = "mock_certificate_config.json"
659+
config_file_content = json.dumps(config_data)
660+
monkeypatch.setenv("GOOGLE_API_CERTIFICATE_CONFIG", config_filename)
661+
monkeypatch.setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "")
662+
# Use mock_open to simulate the file in memory
663+
mock_file_handle = mock.mock_open(read_data=config_file_content)
664+
with mock.patch("builtins.open", mock_file_handle):
665+
use_client_cert = _mtls_helper.check_use_client_cert()
666+
assert use_client_cert == "true"
667+
668+
def test_check_use_client_cert_false(self, monkeypatch):
669+
monkeypatch.setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")
670+
use_client_cert = _mtls_helper.check_use_client_cert()
671+
assert use_client_cert == "false"
672+
673+
def test_check_use_client_cert_for_workload_with_config_file_not_found(
674+
self, monkeypatch
675+
):
676+
monkeypatch.setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "")
677+
use_client_cert = _mtls_helper.check_use_client_cert()
678+
assert use_client_cert == "false"
679+
680+
def test_check_use_client_cert_for_workload_with_config_file_not_json(
681+
self, monkeypatch
682+
):
683+
config_filename = "mock_certificate_config.json"
684+
config_file_content = "not_valid_json"
685+
monkeypatch.setenv("GOOGLE_API_CERTIFICATE_CONFIG", config_filename)
686+
monkeypatch.setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "")
687+
# Use mock_open to simulate the file in memory
688+
mock_file_handle = mock.mock_open(read_data=config_file_content)
689+
with mock.patch("builtins.open", mock_file_handle):
690+
use_client_cert = _mtls_helper.check_use_client_cert()
691+
assert use_client_cert == "false"
692+
693+
def test_check_use_client_cert_for_workload_with_config_file_no_workload(
694+
self, monkeypatch
695+
):
696+
config_data = {"version": 1, "cert_configs": {"dummy_key": {}}}
697+
config_filename = "mock_certificate_config.json"
698+
config_file_content = json.dumps(config_data)
699+
monkeypatch.setenv("GOOGLE_API_CERTIFICATE_CONFIG", config_filename)
700+
monkeypatch.setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "")
701+
# Use mock_open to simulate the file in memory
702+
mock_file_handle = mock.mock_open(read_data=config_file_content)
703+
with mock.patch("builtins.open", mock_file_handle):
704+
use_client_cert = _mtls_helper.check_use_client_cert()
705+
assert use_client_cert == "false"
706+
707+
def test_check_use_client_cert_when_file_does_not_exist(self, monkeypatch):
708+
config_filename = "mock_certificate_config.json"
709+
monkeypatch.setenv("GOOGLE_API_CERTIFICATE_CONFIG", config_filename)
710+
monkeypatch.setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "")
711+
use_client_cert = _mtls_helper.check_use_client_cert()
712+
assert use_client_cert == "false"

0 commit comments

Comments
 (0)