Skip to content

Commit 247e2ad

Browse files
authored
refactor(auth): replace pyOpenSSL with standard ssl and cryptography (#16976)
Replace pyOpenSSL with standard library ssl for mTLS transport and update key decryption to use cryptography library. This change also enhances security for handling private keys by: - Using Linux memfd_create for RAM-backed in-memory files to avoid writing secrets to physical storage. - Encrypting plaintext keys on-the-fly before writing to fallback temporary files on disk. - Securely wiping temporary files with null bytes before deletion. Fixes #16920 Closes #16921 fixes #17412
1 parent fd0e4b6 commit 247e2ad

17 files changed

Lines changed: 1308 additions & 226 deletions

File tree

packages/google-auth/google/auth/aio/transport/mtls.py

Lines changed: 25 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,42 +17,18 @@
1717
"""
1818

1919
import asyncio
20-
import contextlib
20+
import inspect
2121
import logging
22-
import os
2322
import ssl
24-
import tempfile
2523
from typing import Optional
2624

2725
from google.auth import exceptions
28-
import google.auth.transport._mtls_helper
26+
from google.auth.transport._mtls_helper import secure_cert_key_paths
2927
import google.auth.transport.mtls
3028

3129
_LOGGER = logging.getLogger(__name__)
3230

3331

34-
@contextlib.contextmanager
35-
def _create_temp_file(content: bytes):
36-
"""Creates a temporary file with the given content.
37-
38-
Args:
39-
content (bytes): The content to write to the file.
40-
41-
Yields:
42-
str: The path to the temporary file.
43-
"""
44-
# Create a temporary file that is readable only by the owner.
45-
fd, file_path = tempfile.mkstemp()
46-
try:
47-
with os.fdopen(fd, "wb") as f:
48-
f.write(content)
49-
yield file_path
50-
finally:
51-
# Securely delete the file after use.
52-
if os.path.exists(file_path):
53-
os.remove(file_path)
54-
55-
5632
def make_client_cert_ssl_context(
5733
cert_bytes: bytes, key_bytes: bytes, passphrase: Optional[bytes] = None
5834
) -> ssl.SSLContext:
@@ -71,19 +47,29 @@ def make_client_cert_ssl_context(
7147
Raises:
7248
google.auth.exceptions.TransportError: If there is an error loading the certificate.
7349
"""
74-
with _create_temp_file(cert_bytes) as cert_path, _create_temp_file(
75-
key_bytes
76-
) as key_path:
77-
try:
50+
try:
51+
with secure_cert_key_paths(cert_bytes, key_bytes, passphrase=passphrase) as (
52+
cert_path,
53+
key_path,
54+
passphrase_val,
55+
):
7856
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
79-
context.load_cert_chain(
80-
certfile=cert_path, keyfile=key_path, password=passphrase
81-
)
57+
if cert_path:
58+
password = (
59+
passphrase_val.decode("utf-8")
60+
if isinstance(passphrase_val, bytes)
61+
else passphrase_val
62+
)
63+
context.load_cert_chain(
64+
certfile=cert_path,
65+
keyfile=key_path,
66+
password=password,
67+
)
8268
return context
83-
except (ssl.SSLError, OSError, IOError, ValueError, RuntimeError) as exc:
84-
raise exceptions.TransportError(
85-
"Failed to load client certificate and key for mTLS."
86-
) from exc
69+
except (ssl.SSLError, OSError, IOError, ValueError, RuntimeError, TypeError) as exc:
70+
raise exceptions.TransportError(
71+
"Failed to load client certificate and key for mTLS."
72+
) from exc
8773

8874

8975
async def _run_in_executor(func, *args):
@@ -187,9 +173,9 @@ async def get_client_cert_and_key(client_cert_callback=None):
187173
"""
188174
if client_cert_callback:
189175
result = client_cert_callback()
190-
try:
176+
if inspect.isawaitable(result):
191177
cert, key = await result
192-
except TypeError:
178+
else:
193179
cert, key = result
194180
return True, cert, key
195181

packages/google-auth/google/auth/compute_engine/_mtls.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,9 @@ def __init__(
120120
self.ssl_context = ssl.create_default_context()
121121
self.ssl_context.load_verify_locations(cafile=mds_mtls_config.ca_cert_path)
122122
self.ssl_context.load_cert_chain(
123-
certfile=mds_mtls_config.client_combined_cert_path
123+
certfile=mds_mtls_config.client_combined_cert_path, password=""
124124
)
125+
self._fallback_adapter = HTTPAdapter()
125126
super(MdsMtlsAdapter, self).__init__(*args, **kwargs)
126127

127128
def init_poolmanager(self, *args, **kwargs):
@@ -146,6 +147,8 @@ def send(self, request, **kwargs):
146147
ssl.SSLError,
147148
requests.exceptions.SSLError,
148149
requests.exceptions.HTTPError,
150+
requests.exceptions.ConnectionError,
151+
requests.exceptions.Timeout,
149152
) as e:
150153
_LOGGER.warning(
151154
"mTLS connection to Compute Engine Metadata server failed. "
@@ -157,6 +160,9 @@ def send(self, request, **kwargs):
157160
http_fallback_url = urlunparse(parsed_original_url._replace(scheme="http"))
158161
request.url = http_fallback_url
159162

160-
# Use a standard HTTPAdapter for the fallback
161-
http_adapter = HTTPAdapter()
162-
return http_adapter.send(request, **kwargs)
163+
# Use the cached standard HTTPAdapter for the fallback
164+
return self._fallback_adapter.send(request, **kwargs)
165+
166+
def close(self):
167+
self._fallback_adapter.close()
168+
super(MdsMtlsAdapter, self).close()

packages/google-auth/google/auth/identity_pool.py

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -152,28 +152,34 @@ def __init__(self, trust_chain_path, leaf_cert_callback):
152152

153153
@_helpers.copy_docstring(SubjectTokenSupplier)
154154
def get_subject_token(self, context, request):
155-
# Import OpennSSL inline because it is an extra import only required by customers
156-
# using mTLS.
157-
from OpenSSL import crypto
155+
from cryptography import x509
158156

159-
leaf_cert = crypto.load_certificate(
160-
crypto.FILETYPE_PEM, self._leaf_cert_callback()
161-
)
157+
try:
158+
leaf_cert_data = self._leaf_cert_callback()
159+
except Exception as e:
160+
raise exceptions.RefreshError("Failed to retrieve leaf certificate.") from e
161+
162+
try:
163+
if isinstance(leaf_cert_data, str):
164+
leaf_cert_data = leaf_cert_data.encode("utf-8")
165+
leaf_cert = x509.load_pem_x509_certificate(leaf_cert_data)
166+
except Exception as e:
167+
raise exceptions.RefreshError("Failed to parse leaf certificate.") from e
162168
trust_chain = self._read_trust_chain()
163169
cert_chain = []
164170

165-
cert_chain.append(_X509Supplier._encode_cert(leaf_cert))
171+
cert_chain.append(_encode_cert(leaf_cert))
166172

167173
if trust_chain is None or len(trust_chain) == 0:
168174
return json.dumps(cert_chain)
169175

170176
# Append the first cert if it is not the leaf cert.
171-
first_cert = _X509Supplier._encode_cert(trust_chain[0])
177+
first_cert = _encode_cert(trust_chain[0])
172178
if first_cert != cert_chain[0]:
173179
cert_chain.append(first_cert)
174180

175181
for i in range(1, len(trust_chain)):
176-
encoded = _X509Supplier._encode_cert(trust_chain[i])
182+
encoded = _encode_cert(trust_chain[i])
177183
# Check if the current cert is the leaf cert and raise an exception if it is.
178184
if encoded == cert_chain[0]:
179185
raise exceptions.RefreshError(
@@ -184,9 +190,7 @@ def get_subject_token(self, context, request):
184190
return json.dumps(cert_chain)
185191

186192
def _read_trust_chain(self):
187-
# Import OpennSSL inline because it is an extra import only required by customers
188-
# using mTLS.
189-
from OpenSSL import crypto
193+
from cryptography import x509
190194

191195
certificate_trust_chain = []
192196
# If no trust chain path was provided, return an empty list.
@@ -204,9 +208,7 @@ def _read_trust_chain(self):
204208
cert_data = b"-----BEGIN CERTIFICATE-----" + cert_block
205209
try:
206210
# Load each certificate and add it to the trust chain.
207-
cert = crypto.load_certificate(
208-
crypto.FILETYPE_PEM, cert_data
209-
)
211+
cert = x509.load_pem_x509_certificate(cert_data)
210212
certificate_trust_chain.append(cert)
211213
except Exception as e:
212214
raise exceptions.RefreshError(
@@ -215,19 +217,22 @@ def _read_trust_chain(self):
215217
)
216218
) from e
217219
return certificate_trust_chain
218-
except FileNotFoundError:
220+
except FileNotFoundError as e:
219221
raise exceptions.RefreshError(
220222
"Trust chain file '{}' was not found.".format(self._trust_chain_path)
221-
)
223+
) from e
224+
except OSError as e:
225+
raise exceptions.RefreshError(
226+
"Error accessing trust chain file '{}'.".format(self._trust_chain_path)
227+
) from e
228+
222229

223-
def _encode_cert(cert):
224-
# Import OpennSSL inline because it is an extra import only required by customers
225-
# using mTLS.
226-
from OpenSSL import crypto
230+
def _encode_cert(cert):
231+
from cryptography.hazmat.primitives import serialization
227232

228-
return base64.b64encode(
229-
crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
230-
).decode("utf-8")
233+
return base64.b64encode(cert.public_bytes(serialization.Encoding.DER)).decode(
234+
"utf-8"
235+
)
231236

232237

233238
def _parse_token_data(token_content, format_type="text", subject_token_field_name=None):

packages/google-auth/google/auth/transport/_custom_tls_signer.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
import json
2222
import logging
2323
import os
24+
import ssl
2425
import sys
25-
26-
import cffi # type: ignore
26+
import sysconfig
2727

2828
from google.auth import exceptions
2929

@@ -45,16 +45,23 @@
4545
)
4646

4747

48-
# Cast SSL_CTX* to void*
49-
def _cast_ssl_ctx_to_void_p_pyopenssl(ssl_ctx):
50-
return ctypes.cast(int(cffi.FFI().cast("intptr_t", ssl_ctx)), ctypes.c_void_p)
51-
52-
5348
# Cast SSL_CTX* to void*
5449
def _cast_ssl_ctx_to_void_p_stdlib(context):
55-
return ctypes.c_void_p.from_address(
56-
id(context) + ctypes.sizeof(ctypes.c_void_p) * 2
57-
)
50+
if not issubclass(type(context), ssl.SSLContext):
51+
raise TypeError("context must be an instance of ssl.SSLContext, not a mock")
52+
53+
if (
54+
sys.implementation.name != "cpython"
55+
or hasattr(sys, "getobjects")
56+
or sysconfig.get_config_var("Py_DEBUG")
57+
or sysconfig.get_config_var("Py_GIL_DISABLED") == 1
58+
):
59+
raise exceptions.MutualTLSChannelError(
60+
"Custom TLS signing is only supported on standard release CPython runtimes."
61+
)
62+
63+
offset = sys.getsizeof(object())
64+
return ctypes.c_void_p.from_address(id(context) + offset)
5865

5966

6067
# Load offload library and set up the function types.
@@ -274,7 +281,7 @@ def attach_to_ssl_context(self, ctx):
274281
if not self._offload_lib.ConfigureSslContext(
275282
self._sign_callback,
276283
ctypes.c_char_p(self._cert),
277-
_cast_ssl_ctx_to_void_p_pyopenssl(ctx._ctx._context),
284+
_cast_ssl_ctx_to_void_p_stdlib(ctx),
278285
):
279286
raise exceptions.MutualTLSChannelError(
280287
"failed to configure ECP Offload SSL context"

0 commit comments

Comments
 (0)