-
Notifications
You must be signed in to change notification settings - Fork 303
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add experimental enterprise cert support (#1052)
* feat: add experimental enterprise cert support * fix test issue * resolve comments
- Loading branch information
1 parent
f609246
commit dda7dda
Showing
9 changed files
with
582 additions
and
1 deletion.
There are no files selected for viewing
This file contains 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 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), | ||
_cast_ssl_ctx_to_void_p(ctx._ctx._context), | ||
): | ||
raise exceptions.MutualTLSChannelError("failed to configure SSL context") |
This file contains 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 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 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 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 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.