-
Notifications
You must be signed in to change notification settings - Fork 346
feat: add experimental enterprise cert support #1052
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,4 +45,6 @@ pytype_output/ | |
| .python-version | ||
| .DS_Store | ||
| cert_path | ||
| key_path | ||
| key_path | ||
| env/ | ||
| .vscode/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| # Copyright 2022 Google LLC | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """ | ||
| Code for configuring client side TLS to offload the signing operation to | ||
| signing libraries. | ||
| """ | ||
|
|
||
| import ctypes | ||
| import json | ||
| import logging | ||
| import os | ||
| import sys | ||
|
|
||
| import cffi # type: ignore | ||
| import six | ||
|
|
||
| from google.auth import exceptions | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| # C++ offload lib requires google-auth lib to provide the following callback: | ||
| # using SignFunc = int (*)(unsigned char *sig, size_t *sig_len, | ||
| # const unsigned char *tbs, size_t tbs_len) | ||
| # The bytes to be signed and the length are provided via `tbs` and `tbs_len`, | ||
| # the callback computes the signature, and write the signature and its length | ||
| # into `sig` and `sig_len`. | ||
| # If the signing is successful, the callback returns 1, otherwise it returns 0. | ||
| SIGN_CALLBACK_CTYPE = ctypes.CFUNCTYPE( | ||
| ctypes.c_int, # return type | ||
| ctypes.POINTER(ctypes.c_ubyte), # sig | ||
| ctypes.POINTER(ctypes.c_size_t), # sig_len | ||
| ctypes.POINTER(ctypes.c_ubyte), # tbs | ||
| ctypes.c_size_t, # tbs_len | ||
| ) | ||
|
|
||
|
|
||
| # Cast SSL_CTX* to void* | ||
| def _cast_ssl_ctx_to_void_p(ssl_ctx): | ||
| return ctypes.cast(int(cffi.FFI().cast("intptr_t", ssl_ctx)), ctypes.c_void_p) | ||
|
|
||
|
|
||
| # Load offload library and set up the function types. | ||
| def load_offload_lib(offload_lib_path): | ||
| _LOGGER.debug("loading offload library from %s", offload_lib_path) | ||
|
|
||
| # winmode parameter is only available for python 3.8+. | ||
| lib = ( | ||
| ctypes.CDLL(offload_lib_path, winmode=0) | ||
| if sys.version_info >= (3, 8) and os.name == "nt" | ||
| else ctypes.CDLL(offload_lib_path) | ||
| ) | ||
|
|
||
| # Set up types for: | ||
| # int ConfigureSslContext(SignFunc sign_func, const char *cert, SSL_CTX *ctx) | ||
| lib.ConfigureSslContext.argtypes = [ | ||
| SIGN_CALLBACK_CTYPE, | ||
| ctypes.c_char_p, | ||
| ctypes.c_void_p, | ||
| ] | ||
| lib.ConfigureSslContext.restype = ctypes.c_int | ||
|
|
||
| return lib | ||
|
|
||
|
|
||
| # Load signer library and set up the function types. | ||
| # See: https://github.com/googleapis/enterprise-certificate-proxy/blob/main/cshared/main.go | ||
| def load_signer_lib(signer_lib_path): | ||
| _LOGGER.debug("loading signer library from %s", signer_lib_path) | ||
|
|
||
| # winmode parameter is only available for python 3.8+. | ||
| lib = ( | ||
| ctypes.CDLL(signer_lib_path, winmode=0) | ||
| if sys.version_info >= (3, 8) and os.name == "nt" | ||
| else ctypes.CDLL(signer_lib_path) | ||
| ) | ||
|
|
||
| # Set up types for: | ||
| # func GetCertPemForPython(configFilePath *C.char, certHolder *byte, certHolderLen int) | ||
| lib.GetCertPemForPython.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int] | ||
| # Returns: certLen | ||
| lib.GetCertPemForPython.restype = ctypes.c_int | ||
|
|
||
| # Set up types for: | ||
| # func SignForPython(configFilePath *C.char, digest *byte, digestLen int, | ||
| # sigHolder *byte, sigHolderLen int) | ||
| lib.SignForPython.argtypes = [ | ||
| ctypes.c_char_p, | ||
| ctypes.c_char_p, | ||
| ctypes.c_int, | ||
| ctypes.c_char_p, | ||
| ctypes.c_int, | ||
| ] | ||
| # Returns: the signature length | ||
| lib.SignForPython.restype = ctypes.c_int | ||
|
|
||
| return lib | ||
|
|
||
|
|
||
| # Computes SHA256 hash. | ||
| def _compute_sha256_digest(to_be_signed, to_be_signed_len): | ||
| from cryptography.hazmat.primitives import hashes | ||
|
|
||
| data = ctypes.string_at(to_be_signed, to_be_signed_len) | ||
| hash = hashes.Hash(hashes.SHA256()) | ||
| hash.update(data) | ||
| return hash.finalize() | ||
|
|
||
|
|
||
| # Create the signing callback. The actual signing work is done by the | ||
| # `SignForPython` method from the signer lib. | ||
| def get_sign_callback(signer_lib, config_file_path): | ||
| def sign_callback(sig, sig_len, tbs, tbs_len): | ||
| _LOGGER.debug("calling sign callback...") | ||
|
|
||
| digest = _compute_sha256_digest(tbs, tbs_len) | ||
| digestArray = ctypes.c_char * len(digest) | ||
|
|
||
| # reserve 2000 bytes for the signature, shoud be more then enough. | ||
| # RSA signature is 256 bytes, EC signature is 70~72. | ||
| sig_holder_len = 2000 | ||
| sig_holder = ctypes.create_string_buffer(sig_holder_len) | ||
|
|
||
| signature_len = signer_lib.SignForPython( | ||
| config_file_path.encode(), # configFilePath | ||
| digestArray.from_buffer(bytearray(digest)), # digest | ||
| len(digest), # digestLen | ||
| sig_holder, # sigHolder | ||
| sig_holder_len, # sigHolderLen | ||
| ) | ||
|
|
||
| if signature_len == 0: | ||
| # signing failed, return 0 | ||
| return 0 | ||
|
|
||
| sig_len[0] = signature_len | ||
| bs = bytearray(sig_holder) | ||
| for i in range(signature_len): | ||
| sig[i] = bs[i] | ||
|
|
||
| return 1 | ||
|
|
||
| return SIGN_CALLBACK_CTYPE(sign_callback) | ||
|
|
||
|
|
||
| # Obtain the certificate bytes by calling the `GetCertPemForPython` method from | ||
| # the signer lib. The method is called twice, the first time is to compute the | ||
| # cert length, then we create a buffer to hold the cert, and call it again to | ||
| # fill the buffer. | ||
| def get_cert(signer_lib, config_file_path): | ||
| # First call to calculate the cert length | ||
| cert_len = signer_lib.GetCertPemForPython( | ||
| config_file_path.encode(), # configFilePath | ||
| None, # certHolder | ||
| 0, # certHolderLen | ||
| ) | ||
| if cert_len == 0: | ||
| raise exceptions.MutualTLSChannelError("failed to get certificate") | ||
|
|
||
| # Then we create an array to hold the cert, and call again to fill the cert | ||
| cert_holder = ctypes.create_string_buffer(cert_len) | ||
| signer_lib.GetCertPemForPython( | ||
| config_file_path.encode(), # configFilePath | ||
| cert_holder, # certHolder | ||
| cert_len, # certHolderLen | ||
| ) | ||
| return bytes(cert_holder) | ||
|
|
||
|
|
||
| class CustomTlsSigner(object): | ||
| def __init__(self, enterprise_cert_file_path): | ||
| """ | ||
| This class loads the offload and signer library, and calls APIs from | ||
| these libraries to obtain the cert and a signing callback, and attach | ||
| them to SSL context. The cert and the signing callback will be used | ||
| for client authentication in TLS handshake. | ||
|
|
||
| Args: | ||
| enterprise_cert_file_path (str): the path to a enterprise cert JSON | ||
| file. The file should contain the following field: | ||
|
|
||
| { | ||
| "libs": { | ||
| "signer_library": "...", | ||
| "offload_library": "..." | ||
| } | ||
| } | ||
| """ | ||
| self._enterprise_cert_file_path = enterprise_cert_file_path | ||
| self._cert = None | ||
| self._sign_callback = None | ||
|
|
||
| def load_libraries(self): | ||
| try: | ||
| with open(self._enterprise_cert_file_path, "r") as f: | ||
| enterprise_cert_json = json.load(f) | ||
| libs = enterprise_cert_json["libs"] | ||
| signer_library = libs["signer_library"] | ||
| offload_library = libs["offload_library"] | ||
| except (KeyError, ValueError) as caught_exc: | ||
| new_exc = exceptions.MutualTLSChannelError( | ||
| "enterprise cert file is invalid", caught_exc | ||
| ) | ||
| six.raise_from(new_exc, caught_exc) | ||
| self._offload_lib = load_offload_lib(offload_library) | ||
| self._signer_lib = load_signer_lib(signer_library) | ||
|
|
||
| def set_up_custom_key(self): | ||
| # We need to keep a reference of the cert and sign callback so it won't | ||
| # be garbage collected, otherwise it will crash when used by signer lib. | ||
| self._cert = get_cert(self._signer_lib, self._enterprise_cert_file_path) | ||
| self._sign_callback = get_sign_callback( | ||
| self._signer_lib, self._enterprise_cert_file_path | ||
| ) | ||
|
|
||
| def attach_to_ssl_context(self, ctx): | ||
| # In the TLS handshake, the signing operation will be done by the | ||
| # sign_callback. | ||
| if not self._offload_lib.ConfigureSslContext( | ||
| self._sign_callback, | ||
| ctypes.c_char_p(self._cert), | ||
arithmetic1728 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| _cast_ssl_ctx_to_void_p(ctx._ctx._context), | ||
| ): | ||
| raise exceptions.MutualTLSChannelError("failed to configure SSL context") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "libs": {} | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "libs": { | ||
| "signer_library": "/path/to/signer/lib", | ||
| "offload_library": "/path/to/offload/lib" | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.