diff --git a/gssapi/raw/__init__.py b/gssapi/raw/__init__.py index a74b74e9..b2320991 100644 --- a/gssapi/raw/__init__.py +++ b/gssapi/raw/__init__.py @@ -111,7 +111,7 @@ except ImportError: pass -# optional DCE (IOV/AEAD) support +# optional DCE (IOV) support try: from gssapi.raw.ext_dce import * # noqa # optional IOV MIC support (requires DCE support) @@ -119,6 +119,12 @@ except ImportError: pass +# optional DCE (AEAD) support +try: + from gssapi.raw.ext_dce_aead import * # noqa +except ImportError: + pass + # optional RFC 6680 support try: from gssapi.raw.ext_rfc6680 import * # noqa diff --git a/gssapi/raw/ext_dce.pyx b/gssapi/raw/ext_dce.pyx index 52d87e5d..db751141 100644 --- a/gssapi/raw/ext_dce.pyx +++ b/gssapi/raw/ext_dce.pyx @@ -16,39 +16,120 @@ from enum import IntEnum from gssapi.raw._enum_extensions import ExtendableEnum -cdef extern from "python_gssapi_ext.h": - # NB(directxman12): this wiki page has a different argument order - # than the header file, and uses size_t instead of int - # (this file matches the header file) - OM_uint32 gss_wrap_iov(OM_uint32 *min_stat, gss_ctx_id_t ctx_handle, - int conf_req_flag, gss_qop_t qop_req, int *conf_ret, - gss_iov_buffer_desc *iov, int iov_count) nogil - - OM_uint32 gss_unwrap_iov(OM_uint32 *min_stat, gss_ctx_id_t ctx_handle, - int* conf_ret, gss_qop_t *qop_ret, - gss_iov_buffer_desc *iov, int iov_count) nogil - - OM_uint32 gss_wrap_iov_length(OM_uint32 *min_stat, gss_ctx_id_t ctx_handle, - int conf_req, gss_qop_t qop_req, - int *conf_ret, gss_iov_buffer_desc *iov, - int iov_count) nogil - - OM_uint32 gss_release_iov_buffer(OM_uint32 *min_stat, - gss_iov_buffer_desc *iov, - int iov_count) nogil - - OM_uint32 gss_wrap_aead(OM_uint32 *min_stat, gss_ctx_id_t ctx_handle, - int conf_req, gss_qop_t qop_req, - gss_buffer_t input_assoc_buffer, - gss_buffer_t input_payload_buffer, int *conf_ret, - gss_buffer_t output_message_buffer) nogil - - OM_uint32 gss_unwrap_aead(OM_uint32 *min_stat, gss_ctx_id_t ctx_handle, - gss_buffer_t input_message_buffer, - gss_buffer_t input_assoc_buffer, - gss_buffer_t output_payload_buffer, - int *conf_ret, gss_qop_t *qop_ret) nogil +IF MACOS_FRAMEWORK_PATH: + # GSS.Framework (macOS) does not expose the 4 gss IOV functions in its + # header. Instead dynamically lookup the function address in the GSS + # library file based on the known symbol name. + + from posix.dlfcn cimport dlopen, dlerror, dlsym, dlclose, RTLD_NOW, \ + RTLD_GLOBAL + + cdef: + void *gss = NULL + void *wrap_iov_fn = NULL + void *unwrap_iov_fn = NULL + void *wrap_iov_length_fn = NULL + void *release_iov_buffer_fn = NULL + + cdef void * _dlsym(void *handle, name): + # dlsym() returns NULL for a symbol that is not found but it could + # also return NULL as a valid symbol address. The correct way to test + # on an error is to: + # 1. call dlerror() to clear any existing errors, + # 2. call dlsym, then + # 3. call dlerror() to check whether that is NULL or not + # https://man7.org/linux/man-pages/man3/dlsym.3.html + dlerror() + + hn = dlsym(handle, name) + err_msg = dlerror() + if err_msg != NULL: + raise ImportError(err_msg.decode('utf-8', errors='replace')) + + return hn + + gss = dlopen(MACOS_FRAMEWORK_PATH.encode(), RTLD_NOW | RTLD_GLOBAL) + if gss == NULL: + raise ImportError(dlerror().decode('utf-8', errors='replace')) + + try: + # The symbol names were found when inspecting the GSS library file + # 'nm -gU /path/GSS'. Can confirm these symbols are present in the + # latest (10.15) to at least 10.11. Online docs seem to confirm these + # symbols have been present since iOS 6.0 which is roughly when this + # framework was introduced. + wrap_iov_fn = _dlsym(gss, b'__ApplePrivate_gss_wrap_iov') + unwrap_iov_fn = _dlsym(gss, b'__ApplePrivate_gss_unwrap_iov') + wrap_iov_length_fn = _dlsym(gss, b'__ApplePrivate_gss_wrap_iov_length') + release_iov_buffer_fn = _dlsym( + gss, b'__ApplePrivate_gss_release_iov_buffer') + + finally: + dlclose(gss) + + cdef OM_uint32 gss_wrap_iov(OM_uint32 *min_stat, gss_ctx_id_t ctx_handle, + int conf_req_flag, gss_qop_t qop_req, + int *conf_ret, gss_iov_buffer_desc *iov, + int iov_count) nogil: + + return (wrap_iov_fn)( + min_stat, ctx_handle, conf_req_flag, qop_req, conf_ret, iov, + iov_count) + + cdef OM_uint32 gss_unwrap_iov(OM_uint32 *min_stat, gss_ctx_id_t ctx_handle, + int* conf_ret, gss_qop_t *qop_ret, + gss_iov_buffer_desc *iov, + int iov_count) nogil: + + return (unwrap_iov_fn)( + min_stat, ctx_handle, conf_ret, qop_ret, iov, iov_count) + + cdef OM_uint32 gss_wrap_iov_length(OM_uint32 *min_stat, + gss_ctx_id_t ctx_handle, int conf_req, + gss_qop_t qop_req, int *conf_ret, + gss_iov_buffer_desc *iov, + int iov_count) nogil: + + return (wrap_iov_length_fn)( + min_stat, ctx_handle, conf_req, qop_req, conf_ret, iov, iov_count) + + cdef OM_uint32 gss_release_iov_buffer(OM_uint32 *min_stat, + gss_iov_buffer_desc *iov, + int iov_count) nogil: + + return (release_iov_buffer_fn)( + min_stat, iov, iov_count) + +ELSE: + cdef extern from "python_gssapi_ext.h": + # NB(directxman12): this wiki page has a different argument order + # than the header file, and uses size_t instead of + # int (this file matches the header file) + OM_uint32 gss_wrap_iov(OM_uint32 *min_stat, gss_ctx_id_t ctx_handle, + int conf_req_flag, gss_qop_t qop_req, + int *conf_ret, gss_iov_buffer_desc *iov, + int iov_count) nogil + + OM_uint32 gss_unwrap_iov(OM_uint32 *min_stat, gss_ctx_id_t ctx_handle, + int* conf_ret, gss_qop_t *qop_ret, + gss_iov_buffer_desc *iov, int iov_count) nogil + + OM_uint32 gss_wrap_iov_length(OM_uint32 *min_stat, + gss_ctx_id_t ctx_handle, int conf_req, + gss_qop_t qop_req, int *conf_ret, + gss_iov_buffer_desc *iov, + int iov_count) nogil + + OM_uint32 gss_release_iov_buffer(OM_uint32 *min_stat, + gss_iov_buffer_desc *iov, + int iov_count) nogil + +cdef extern from "python_gssapi_ext.h": gss_iov_buffer_t GSS_C_NO_IOV_BUFFER OM_uint32 GSS_IOV_BUFFER_TYPE_EMPTY @@ -447,109 +528,3 @@ def wrap_iov_length(SecurityContext context not None, IOV message not None, return conf_used else: raise GSSError(maj_stat, min_stat) - - -def wrap_aead(SecurityContext context not None, bytes message not None, - bytes associated=None, confidential=True, qop=None): - """ - wrap_aead(context, message, associated=None, confidential=True, qop=None) - Wrap/Encrypt an AEAD message. - - This method takes an input message and associated data, - and outputs and AEAD message. - - Args: - context (SecurityContext): the current security context - message (bytes): the message to wrap or encrypt - associated (bytes): associated data to go with the message - confidential (bool): whether or not to encrypt the message (True), - or just wrap it with a MIC (False) - qop (int): the desired Quality of Protection - (or None for the default QoP) - - Returns: - WrapResult: the wrapped/encrypted total message, and whether or not - encryption was actually used - - Raises: - GSSError - """ - - cdef int conf_req = confidential - cdef gss_qop_t qop_req = qop if qop is not None else GSS_C_QOP_DEFAULT - cdef gss_buffer_desc message_buffer = gss_buffer_desc(len(message), - message) - - cdef gss_buffer_t assoc_buffer_ptr = GSS_C_NO_BUFFER - cdef gss_buffer_desc assoc_buffer - if associated is not None: - assoc_buffer = gss_buffer_desc(len(associated), associated) - assoc_buffer_ptr = &assoc_buffer - - cdef int conf_used - # GSS_C_EMPTY_BUFFER - cdef gss_buffer_desc output_buffer = gss_buffer_desc(0, NULL) - - cdef OM_uint32 maj_stat, min_stat - - with nogil: - maj_stat = gss_wrap_aead(&min_stat, context.raw_ctx, conf_req, qop_req, - assoc_buffer_ptr, &message_buffer, - &conf_used, &output_buffer) - - if maj_stat == GSS_S_COMPLETE: - output_message = (output_buffer.value)[:output_buffer.length] - gss_release_buffer(&min_stat, &output_buffer) - return WrapResult(output_message, conf_used) - else: - raise GSSError(maj_stat, min_stat) - - -def unwrap_aead(SecurityContext context not None, bytes message not None, - bytes associated=None): - """ - unwrap_aead(context, message, associated=None) - Unwrap/Decrypt an AEAD message. - - This method takes an encrpyted/wrapped AEAD message and some associated - data, and returns an unwrapped/decrypted message. - - Args: - context (SecurityContext): the current security context - message (bytes): the AEAD message to unwrap or decrypt - associated (bytes): associated data that goes with the message - - Returns: - UnwrapResult: the unwrapped/decrypted message, whether or on - encryption was used, and the QoP used - - Raises: - GSSError - """ - - cdef gss_buffer_desc input_buffer = gss_buffer_desc(len(message), message) - - cdef gss_buffer_t assoc_buffer_ptr = GSS_C_NO_BUFFER - cdef gss_buffer_desc assoc_buffer - if associated is not None: - assoc_buffer = gss_buffer_desc(len(associated), associated) - assoc_buffer_ptr = &assoc_buffer - - # GSS_C_EMPTY_BUFFER - cdef gss_buffer_desc output_buffer = gss_buffer_desc(0, NULL) - cdef int conf_state - cdef gss_qop_t qop_state - - cdef OM_uint32 maj_stat, min_stat - - with nogil: - maj_stat = gss_unwrap_aead(&min_stat, context.raw_ctx, &input_buffer, - assoc_buffer_ptr, &output_buffer, - &conf_state, &qop_state) - - if maj_stat == GSS_S_COMPLETE: - output_message = (output_buffer.value)[:output_buffer.length] - gss_release_buffer(&min_stat, &output_buffer) - return UnwrapResult(output_message, conf_state, qop_state) - else: - raise GSSError(maj_stat, min_stat) diff --git a/gssapi/raw/ext_dce_aead.pyx b/gssapi/raw/ext_dce_aead.pyx new file mode 100644 index 00000000..4272b27b --- /dev/null +++ b/gssapi/raw/ext_dce_aead.pyx @@ -0,0 +1,127 @@ +GSSAPI="BASE" # This ensures that a full module is generated by Cython + +from gssapi.raw.cython_types cimport * +from gssapi.raw.sec_contexts cimport SecurityContext + +from gssapi.raw.misc import GSSError +from gssapi.raw.named_tuples import WrapResult, UnwrapResult + + +cdef extern from "python_gssapi_ext.h": + OM_uint32 gss_wrap_aead(OM_uint32 *min_stat, gss_ctx_id_t ctx_handle, + int conf_req, gss_qop_t qop_req, + gss_buffer_t input_assoc_buffer, + gss_buffer_t input_payload_buffer, int *conf_ret, + gss_buffer_t output_message_buffer) nogil + + OM_uint32 gss_unwrap_aead(OM_uint32 *min_stat, gss_ctx_id_t ctx_handle, + gss_buffer_t input_message_buffer, + gss_buffer_t input_assoc_buffer, + gss_buffer_t output_payload_buffer, + int *conf_ret, gss_qop_t *qop_ret) nogil + + +def wrap_aead(SecurityContext context not None, bytes message not None, + bytes associated=None, confidential=True, qop=None): + """ + wrap_aead(context, message, associated=None, confidential=True, qop=None) + Wrap/Encrypt an AEAD message. + + This method takes an input message and associated data, + and outputs and AEAD message. + + Args: + context (SecurityContext): the current security context + message (bytes): the message to wrap or encrypt + associated (bytes): associated data to go with the message + confidential (bool): whether or not to encrypt the message (True), + or just wrap it with a MIC (False) + qop (int): the desired Quality of Protection + (or None for the default QoP) + + Returns: + WrapResult: the wrapped/encrypted total message, and whether or not + encryption was actually used + + Raises: + GSSError + """ + + cdef int conf_req = confidential + cdef gss_qop_t qop_req = qop if qop is not None else GSS_C_QOP_DEFAULT + cdef gss_buffer_desc message_buffer = gss_buffer_desc(len(message), + message) + + cdef gss_buffer_t assoc_buffer_ptr = GSS_C_NO_BUFFER + cdef gss_buffer_desc assoc_buffer + if associated is not None: + assoc_buffer = gss_buffer_desc(len(associated), associated) + assoc_buffer_ptr = &assoc_buffer + + cdef int conf_used + # GSS_C_EMPTY_BUFFER + cdef gss_buffer_desc output_buffer = gss_buffer_desc(0, NULL) + + cdef OM_uint32 maj_stat, min_stat + + with nogil: + maj_stat = gss_wrap_aead(&min_stat, context.raw_ctx, conf_req, qop_req, + assoc_buffer_ptr, &message_buffer, + &conf_used, &output_buffer) + + if maj_stat == GSS_S_COMPLETE: + output_message = (output_buffer.value)[:output_buffer.length] + gss_release_buffer(&min_stat, &output_buffer) + return WrapResult(output_message, conf_used) + else: + raise GSSError(maj_stat, min_stat) + + +def unwrap_aead(SecurityContext context not None, bytes message not None, + bytes associated=None): + """ + unwrap_aead(context, message, associated=None) + Unwrap/Decrypt an AEAD message. + + This method takes an encrpyted/wrapped AEAD message and some associated + data, and returns an unwrapped/decrypted message. + + Args: + context (SecurityContext): the current security context + message (bytes): the AEAD message to unwrap or decrypt + associated (bytes): associated data that goes with the message + + Returns: + UnwrapResult: the unwrapped/decrypted message, whether or on + encryption was used, and the QoP used + + Raises: + GSSError + """ + + cdef gss_buffer_desc input_buffer = gss_buffer_desc(len(message), message) + + cdef gss_buffer_t assoc_buffer_ptr = GSS_C_NO_BUFFER + cdef gss_buffer_desc assoc_buffer + if associated is not None: + assoc_buffer = gss_buffer_desc(len(associated), associated) + assoc_buffer_ptr = &assoc_buffer + + # GSS_C_EMPTY_BUFFER + cdef gss_buffer_desc output_buffer = gss_buffer_desc(0, NULL) + cdef int conf_state + cdef gss_qop_t qop_state + + cdef OM_uint32 maj_stat, min_stat + + with nogil: + maj_stat = gss_unwrap_aead(&min_stat, context.raw_ctx, &input_buffer, + assoc_buffer_ptr, &output_buffer, + &conf_state, &qop_state) + + if maj_stat == GSS_S_COMPLETE: + output_message = (output_buffer.value)[:output_buffer.length] + gss_release_buffer(&min_stat, &output_buffer) + return UnwrapResult(output_message, conf_state, qop_state) + else: + raise GSSError(maj_stat, min_stat) diff --git a/gssapi/tests/test_raw.py b/gssapi/tests/test_raw.py index bfa5f550..a7660bdd 100644 --- a/gssapi/tests/test_raw.py +++ b/gssapi/tests/test_raw.py @@ -1399,7 +1399,7 @@ def test_basic_wrap_unwrap(self): unwrapped_message.shouldnt_be_empty() unwrapped_message.should_be(b'test message') - @ktu.gssapi_extension_test('dce', 'DCE (IOV/AEAD)') + @ktu.gssapi_extension_test('dce', 'DCE (IOV)') def test_basic_iov_wrap_unwrap_prealloc(self): init_data = b'some encrypted data' init_other_data = b'some other encrypted data' @@ -1443,7 +1443,7 @@ def test_basic_iov_wrap_unwrap_prealloc(self): init_message[2].value.should_be(init_data) init_message[3].value.should_be(init_other_data) - @ktu.gssapi_extension_test('dce', 'DCE (IOV/AEAD)') + @ktu.gssapi_extension_test('dce', 'DCE (IOV)') def test_basic_iov_wrap_unwrap_autoalloc(self): init_data = b'some encrypted data' init_other_data = b'some other encrypted data' @@ -1475,7 +1475,7 @@ def test_basic_iov_wrap_unwrap_autoalloc(self): init_message[2].value.should_be(init_data) init_message[3].value.should_be(init_other_data) - @ktu.gssapi_extension_test('dce', 'DCE (IOV/AEAD)') + @ktu.gssapi_extension_test('dce_aead', 'DCE (AEAD)') def test_basic_aead_wrap_unwrap(self): assoc_data = b'some sig data' (wrapped_message, conf) = gb.wrap_aead(self.client_ctx, @@ -1501,7 +1501,7 @@ def test_basic_aead_wrap_unwrap(self): unwrapped_message.shouldnt_be_empty() unwrapped_message.should_be(b'test message') - @ktu.gssapi_extension_test('dce', 'DCE (IOV/AEAD)') + @ktu.gssapi_extension_test('dce_aead', 'DCE (AEAD)') def test_basic_aead_wrap_unwrap_no_assoc(self): (wrapped_message, conf) = gb.wrap_aead(self.client_ctx, b'test message') @@ -1525,7 +1525,7 @@ def test_basic_aead_wrap_unwrap_no_assoc(self): unwrapped_message.shouldnt_be_empty() unwrapped_message.should_be(b'test message') - @ktu.gssapi_extension_test('dce', 'DCE (IOV/AEAD)') + @ktu.gssapi_extension_test('dce_aead', 'DCE (AEAD)') def test_basic_aead_wrap_unwrap_bad_assoc_raises_error(self): assoc_data = b'some sig data' (wrapped_message, conf) = gb.wrap_aead(self.client_ctx, diff --git a/setup.py b/setup.py index 7cd3d9b7..3d9bdc20 100755 --- a/setup.py +++ b/setup.py @@ -133,6 +133,7 @@ def get_output(*args, **kwargs): ENABLE_SUPPORT_DETECTION = \ (os.environ.get('GSSAPI_SUPPORT_DETECT', 'true').lower() == 'true') +osx_framework_path = None if ENABLE_SUPPORT_DETECTION: import ctypes.util @@ -140,6 +141,7 @@ def get_output(*args, **kwargs): main_path = "" if main_lib is None and osx_has_gss_framework: main_lib = ctypes.util.find_library('GSS') + osx_framework_path = main_lib elif os.environ.get('MINGW_PREFIX'): main_lib = os.environ.get('MINGW_PREFIX')+'/bin/libgss-3.dll' elif sys.platform == 'msys': @@ -208,6 +210,7 @@ def ext_modules(self): if getattr(self, '_cythonized_ext_modules', None) is None: self._cythonized_ext_modules = cythonize( self._ext_modules, + compile_time_env={'MACOS_FRAMEWORK_PATH': osx_framework_path}, language_level=2, ) @@ -336,7 +339,10 @@ def gssapi_modules(lst): extension_file('rfc5588', 'gss_store_cred'), extension_file('rfc5801', 'gss_inquire_saslname_for_mech'), extension_file('cred_imp_exp', 'gss_import_cred'), - extension_file('dce', 'gss_wrap_iov'), + extension_file('dce', + '__ApplePrivate_gss_wrap_iov' if osx_framework_path + else 'gss_wrap_iov'), + extension_file('dce_aead', 'gss_wrap_aead'), extension_file('iov_mic', 'gss_get_mic_iov'), extension_file('ggf', 'gss_inquire_sec_context_by_oid'), extension_file('set_cred_opt', 'gss_set_cred_option'),