refactor(auth): replace pyOpenSSL with standard ssl and cryptography#16976
refactor(auth): replace pyOpenSSL with standard ssl and cryptography#16976nbayati wants to merge 21 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the library to remove the pyOpenSSL dependency, migrating certificate and key management to the cryptography library and Python's standard ssl module. It introduces a secure three-tier fallback strategy for handling mTLS credentials via the new secure_cert_key_paths utility, which utilizes RAM-backed virtual files on Linux to minimize disk exposure. Review feedback suggests simplifying the manual context manager orchestration within this utility and using os.fdopen instead of os.write for more robust and idiomatic file descriptor operations.
|
@nbayati , Please could you address the failing presubmits? |
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.
…S cert loading fallbacks
f7e5146 to
1704dd8
Compare
… update tests accordingly
…on to include empty password for client_combined_cert_path
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request removes the dependency on pyOpenSSL across the library, replacing it with Python's standard ssl library and the cryptography package. It also introduces a secure three-tier fallback strategy for handling client certificates and private keys. The review feedback highlights a potential NameError in urllib3.py due to an unimported module reference, suggests simplifying context manager exit logic in _mtls_helper.py, recommends verifying write permissions for /dev/shm to prevent failures in restricted environments, and advises defensively encoding leaf certificate callback values to bytes.
| cleanup_fds.append(fd_key) | ||
| with os.fdopen(fd_key, "wb", closefd=False) as f: | ||
| f.write(key_bytes) | ||
| key_path = f"/proc/self/fd/{fd_key}" |
There was a problem hiding this comment.
nit: I wonder if making a helper or loop for these could be cleaner?
There was a problem hiding this comment.
Done! Added a loop.
…actor file writing into loops
…ean standard with block
|
I tested the refactored paths manually by using the External Account X.509 provider to exchange certificates for access tokens and retrieve GCS buckets across different transports and credential initiation paths. The default transport (requests), urllib3, and aiohttp (async) are all verified and working as expected after the refactoring. I've also verified the encrypted private key loading/decryption flow using the new cryptography-based helper. A copy of the test script can be found at: paste/5209556048281600 |
…s in RefreshError
| ctx.load_cert_chain( | ||
| certfile=cert_path, | ||
| keyfile=key_path, | ||
| password=passphrase or "", |
There was a problem hiding this comment.
Nit: Based on the definition of load_cert_chain, password can safely be None (https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_cert_chain) so I don't think we need to do this type casting here.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request removes the dependency on pyOpenSSL and cffi across the codebase, transitioning to the standard library ssl module and the cryptography library for handling mTLS certificates and keys. It introduces a new secure context manager, secure_cert_key_paths, in _mtls_helper.py to securely manage certificate and key paths using Linux in-memory virtual files (memfd_create) or encrypted temporary files as a fallback. The setup configuration, tests, and environment files are updated accordingly. The feedback recommends removing a redundant import os statement inside the _secure_wipe_and_remove function since os is already imported at the module level.
| import os | ||
|
|
||
| if not os.path.exists(file_path): |
There was a problem hiding this comment.
The os module is already imported at the module level (line 20). Please remove the redundant import os inside _secure_wipe_and_remove to adhere to the project's coding standards.
| import os | |
| if not os.path.exists(file_path): | |
| if not os.path.exists(file_path): |
References
- Do not import modules or classes inside functions if they are already imported at the module level.
There was a problem hiding this comment.
1. Critical Packaging Bug in setup.py
- File:
packages/google-auth/setup.py(PR-level) - Technical Note:
The listcryptography_base_requireis placed directly inside theDEPENDENCIEStuple without unpacking on line 27. This results in a nested structure:("pyasn1-modules>=0.2.1", ["cryptography >= 38.0.3"]). Whensetuptoolsparsesinstall_requiresduring packaging or installation, it iterates over the requirements and expects each item to be a flat string specifier. Encountering a nested list triggers aTypeError(e.g.,TypeError: expected string or bytes-like object), completely blocking building or installing the package from source. Unpack the list insideDEPENDENCIES:DEPENDENCIES = ( "pyasn1-modules>=0.2.1", *cryptography_base_require, )
2. Standard SSLContext Pointer Alignment and Safety
- File:
packages/google-auth/google/auth/transport/_custom_tls_signer.py(Line 270) - Technical Note:
The pointer calculation in_cast_ssl_ctx_to_void_p_stdlibrelies on standard CPython release alignmentid(context) + ctypes.sizeof(ctypes.c_void_p) * 2to obtain the raw pointer to the underlying OpenSSL structureSSL_CTX. This offset assumes a 2-pointerPyObject_HEADheader structure. This alignment is highly fragile and shifts under debug builds withPy_TRACE_REFSenabled (increasing header size to 4 pointers), free-threaded CPython (Python 3.13+, PEP 703), or alternative runtimes (PyPy). Reading from the wrong offset results in arbitrary memory pointer retrieval and uncatchable segmentation faults. Reject unsupported environments or enforce safe alternative C-API casting.
3. Missing Type Validation on Context Parameter
- File:
packages/google-auth/google/auth/transport/_custom_tls_signer.py(Line 270) - Technical Note:
Inattach_to_ssl_context, there is no type validation onctx. If a caller passesNoneor an invalid object type,_cast_ssl_ctx_to_void_p_stdlibcalculates a memory address offset beyond the bounds of the object structure and reads raw heap garbage as anSSL_CTXpointer. When passed to the Go offload libraryConfigureSslContext, this bogus address is dereferenced, causing a hard segmentation fault that crashes the entire Python host process instead of raising a clean PythonTypeError. Add an explicit assertion before pointer casting:if not isinstance(ctx, ssl.SSLContext): raise TypeError("ctx must be an instance of ssl.SSLContext")
| def _encrypt_key_if_plaintext( | ||
| key_bytes: bytes, passphrase: Optional[bytes] | ||
| ) -> Tuple[bytes, Optional[bytes]]: | ||
| """Encrypts a plaintext PEM key if necessary, returning the bytes and passphrase. |
There was a problem hiding this comment.
We are returning plaintext if encryption fails. Is that intentional? If so should be documented
| OpenSSL.crypto.Error, | ||
| ValueError, | ||
| ) as caught_exc: | ||
| self._is_mtls = False |
There was a problem hiding this comment.
Can we make this atomic: build the new adapter first, then update is_mtls and mount it only after success?
| OpenSSL.crypto.Error, | ||
| ValueError, | ||
| ) as caught_exc: | ||
| self._is_mtls = False |
There was a problem hiding this comment.
Same issue here - I think if mTLS was already configured and a later configure call fails, _is_mtls becomes false but self.http is still the old mTLS PoolManager.
| leaf_cert = crypto.load_certificate( | ||
| crypto.FILETYPE_PEM, self._leaf_cert_callback() | ||
| ) | ||
| try: |
There was a problem hiding this comment.
This wraps too much code in the parse error.
The callback can fail for config or file-read reasons, but because it is inside this try, those errors become Failed to parse leaf certificate.
Can we call the callback before this parse try, so only actual cert parsing errors get this message?
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:
Fixes #16920
Closes #16921
fixes #17412