diff options
Diffstat (limited to 'plugin/auth_gssapi')
23 files changed, 2253 insertions, 0 deletions
diff --git a/plugin/auth_gssapi/CMakeLists.txt b/plugin/auth_gssapi/CMakeLists.txt new file mode 100644 index 00000000..f84f0aaf --- /dev/null +++ b/plugin/auth_gssapi/CMakeLists.txt @@ -0,0 +1,65 @@ + +SET(CPACK_RPM_gssapi-server_PACKAGE_SUMMARY "GSSAPI authentication plugin for MariaDB server" PARENT_SCOPE) +SET(CPACK_RPM_gssapi-server_PACKAGE_DESCRIPTION "The gssapi authentication plugin allows the user to authenticate with services +that use the Generic Security Services Application Program Interface (GSSAPI). +The gssapi authentication plugin is most often used for authenticating with Microsoft Active Directory." PARENT_SCOPE) + +IF (WIN32) + SET(USE_SSPI 1) +ENDIF() + +IF(USE_SSPI) + SET(GSSAPI_LIBS secur32) + ADD_DEFINITIONS(-DPLUGIN_SSPI) + SET(GSSAPI_CLIENT sspi_client.cc) + SET(GSSAPI_SERVER sspi_server.cc) + SET(GSSAPI_ERRMSG sspi_errmsg.cc) +ELSE() + SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) + FIND_PACKAGE(GSSAPI) + SET_PACKAGE_PROPERTIES(GSSAPI PROPERTIES TYPE OPTIONAL) + IF(GSSAPI_FOUND) + INCLUDE_DIRECTORIES(${GSSAPI_INCS}) + ADD_DEFINITIONS(-DPLUGIN_GSSAPI) + SET(GSSAPI_CLIENT gssapi_client.cc) + SET(GSSAPI_SERVER gssapi_server.cc) + SET(GSSAPI_ERRMSG gssapi_errmsg.cc) + + IF(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + SET_SOURCE_FILES_PROPERTIES( + ${GSSAPI_CLIENT} ${GSSAPI_SERVER} ${GSSAPI_ERRMSG} + PROPERTY COMPILE_FLAGS "-Wno-deprecated-declarations") + ENDIF() + SET(CMAKE_REQUIRED_INCLUDES ${GSSAPI_INCS}) + SET(CMAKE_REQUIRED_LIBRARIES ${GSSAPI_LIBS}) + SET(CMAKE_REQUIRED_FLAGS "-Werror -Wall") + INCLUDE(CheckCXXSymbolExists) + CHECK_CXX_SYMBOL_EXISTS(krb5_xfree "krb5.h" HAVE_KRB5_XFREE) + IF(HAVE_KRB5_XFREE) + ADD_DEFINITIONS(-DHAVE_KRB5_XFREE=1) + ENDIF() + MY_CHECK_AND_SET_COMPILER_FLAG(-Wl,--as-needed) + + ELSE() + # Can't build plugin + RETURN() + ENDIF() +ENDIF () + +IF(WIN32) + SET(AUTH_GSSAPI_DEFAULT DEFAULT) +ELSE() + SET(AUTH_GSSAPI_DEFAULT) +ENDIF() +MYSQL_ADD_PLUGIN(auth_gssapi server_plugin.cc ${GSSAPI_SERVER} ${GSSAPI_ERRMSG} + LINK_LIBRARIES ${GSSAPI_LIBS} + ${AUTH_GSSAPI_DEFAULT} + COMPONENT gssapi-server) + +# disabled in favor of libmariadb/plugins/auth/auth_gssapi_client.c +# +#MYSQL_ADD_PLUGIN(auth_gssapi_client client_plugin.cc ${GSSAPI_CLIENT} ${GSSAPI_ERRMSG} +# LINK_LIBRARIES ${GSSAPI_LIBS COMPONENT ClientPlugins} +# COMPONENT gssapi-client +# CLIENT +# MODULE_ONLY) diff --git a/plugin/auth_gssapi/README.md b/plugin/auth_gssapi/README.md new file mode 100644 index 00000000..ea8deaaf --- /dev/null +++ b/plugin/auth_gssapi/README.md @@ -0,0 +1,129 @@ +# GSSAPI/SSPI authentication for MariaDB + +This article gives instructions on configuring GSSAPI authentication plugin +for MariaDB for passwordless login. + +On Unix systems, GSSAPI is usually synonymous with Kerberos authentication. +Windows has slightly different but very similar API called SSPI, that along with Kerberos, also supports NTLM authentication. + +This plugin includes support for Kerberos on Unix, but also can be used as for Windows authentication with or without domain +environment. + +## Server-side preparations on Unix +To use the plugin, some preparation need to be done on the server side on Unixes. +MariaDB server will need read access to the Kerberos keytab file, that contains service principal name for the MariaDB server. + + +If you are using **Unix Kerberos KDC (MIT,Heimdal)** + +- Create service principal using kadmin tool + +``` +kadmin -q "addprinc -randkey mariadb/host.domain.com" +``` + +(replace host.domain.com with fully qualified DNS name for the server host) + +- Export the newly created user to the keytab file + +``` +kadmin -q "ktadd -k /path/to/mariadb.keytab mariadb/host.domain.com" +``` + +More details can be found [here](http://www.microhowto.info/howto/create_a_service_principal_using_mit_kerberos.html) +and [here](http://www.microhowto.info/howto/add_a_host_or_service_principal_to_a_keytab_using_mit_kerberos.html) + +If you are using **Windows Active Directory KDC** +you can need to create keytab using ktpass.exe tool on Windows, map principal user to an existing domain user like this + +``` +ktpass.exe /princ mariadb/host.domain.com@DOMAIN.COM /mapuser someuser /pass MyPas$w0rd /out mariadb.keytab /crypto all /ptype KRB5_NT_PRINCIPAL /mapop set +``` + +and then transfer the keytab file to the Unix server. See [Microsoft documentation](https://technet.microsoft.com/en-us/library/cc753771.aspx) for details. + + +## Server side preparations on Windows. +Usually nothing need to be done. MariaDB server should to run on a domain joined machine, either as NetworkService account +(which is default if it runs as service) or run under any other domain account credentials. +Creating service principal is not required here (but you can still do it using [_setspn_](https://technet.microsoft.com/en-us/library/cc731241.aspx) tool) + + +# Installing plugin +- Start the server + +- On Unix, edit my the my.cnf/my.ini configuration file, set the parameter gssapi-keytab-path to point to previously +created keytab path. + +``` + gssapi-keytab-path=/path/to/mariadb.keytab +``` + +- Optionally on Unix, in case the service principal name differs from default mariadb/host.domain.com@REALM, +configure alternative principal name with + +``` + gssapi-principal-name=alternative/principalname@REALM +``` + +- In mysql command line client, execute + +``` + INSTALL SONAME 'auth_gssapi' +``` + +#Creating users + +Now, you can create a user for GSSAPI/SSPI authentication. CREATE USER command, for Kerberos user +would be like this (*long* form, see below for short one) + +``` +CREATE USER usr1 IDENTIFIED WITH gssapi AS 'usr1@EXAMPLE.COM'; +``` + +(replace with real username and realm) + +The part after AS is mechanism specific, and needs to be ``machine\\usr1`` for Windows users identified with NTLM. + +You may also use alternative *short* form of CREATE USER + +``` +CREATE USER usr1 IDENTIFIED WITH gssapi; +``` + +If this syntax is used, realm part is *not* used for comparison +thus 'usr1@EXAMPLE.COM', 'usr1@EXAMPLE.CO.UK' and 'mymachine\usr1' will all identify as 'usr1'. + +#Login as GSSAPI user with command line clients + +Using command line client, do + +``` +mysql --plugin-dir=/path/to/plugin-dir -u usr1 +``` + +#Plugin variables +- **gssapi-keytab-path** (Unix only) - Path to the server keytab file +- **gssapi-principal-name** - name of the service principal. +- **gssapi-mech-name** (Windows only) - Name of the SSPI package used by server. Can be either 'Kerberos' or 'Negotiate'. + Defaults to 'Negotiate' (both Kerberos and NTLM users can connect) + Set it to 'Kerberos', to prevent less secure NTLM in domain environments, but leave it as default(Negotiate) + to allow non-domain environment (e.g if server does not run in domain environment). + + +#Implementation + +Overview of the protocol between client and server + +1. Server : Construct gssapi-principal-name if not set in my.cnf. On Unixes defaults to hostbased name for service "mariadb". On Windows to user's or machine's domain names. +Acquire credentials for gssapi-principal-name with ```gss_acquire_cred() / AcquireSecurityCredentials()```. +Send packet with principal name and mech ```"gssapi-principal-name\0gssapi-mech-name\0"``` to client ( on Unix, empty string used for gssapi-mech) + +2. Client: execute ```gss_init_sec_context() / InitializeSecurityContext()``` passing gssapi-principal-name / gssapi-mech-name parameters. +Send resulting GSSAPI blob to server. + +3. Server : receive blob from client, execute ```gss_accept_sec_context()/ AcceptSecurityContext()```, send resulting blob back to client + +4. Perform 2. and 3. can until both client and server decide that authentication is done, or until some error occurred. If authentication was successful, GSSAPI context (an opaque structure) is generated on both client and server sides. + +5. Server : Client name is extracted from the context, and compared to the name provided by client(with or without realm). If name matches, plugin returns success. diff --git a/plugin/auth_gssapi/client_plugin.cc b/plugin/auth_gssapi/client_plugin.cc new file mode 100644 index 00000000..0ab619a0 --- /dev/null +++ b/plugin/auth_gssapi/client_plugin.cc @@ -0,0 +1,112 @@ +/* Copyright (c) 2015, Shuang Qiu, Robbie Harwood, +Vladislav Vaintroub & MariaDB Corporation + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + @file + + GSSAPI authentication plugin, client side +*/ +#include <string.h> +#include <stdarg.h> +#include <mysqld_error.h> +#include <mysql/client_plugin.h> +#include <mysql.h> +#include <stdio.h> +#include "common.h" + +extern int auth_client(char *principal_name, + char *mech, + MYSQL *mysql, + MYSQL_PLUGIN_VIO *vio); + +static void parse_server_packet(char *packet, size_t packet_len, char *spn, char *mech) +{ + size_t spn_len; + spn_len = strnlen(packet, packet_len); + strncpy(spn, packet, PRINCIPAL_NAME_MAX); + if (spn_len == packet_len - 1) + { + /* Mechanism not included into packet */ + *mech = 0; + } + else + { + strncpy(mech, packet + spn_len + 1, MECH_NAME_MAX); + } +} + +/** + Set client error message. + */ +void log_client_error(MYSQL *mysql, const char *format, ...) +{ + NET *net= &mysql->net; + va_list args; + + net->last_errno= ER_UNKNOWN_ERROR; + va_start(args, format); + vsnprintf(net->last_error, sizeof(net->last_error) - 1, + format, args); + va_end(args); + memcpy(net->sqlstate, "HY000", sizeof(net->sqlstate)); +} + +/** + The main client function of the GSSAPI plugin. + */ +static int gssapi_auth_client(MYSQL_PLUGIN_VIO *vio, MYSQL *mysql) +{ + int packet_len; + unsigned char *packet; + char spn[PRINCIPAL_NAME_MAX + 1]; + char mech[MECH_NAME_MAX + 1]; + + /* read from server for service principal name */ + packet_len= vio->read_packet(vio, &packet); + if (packet_len < 0) + { + return CR_ERROR; + } + parse_server_packet((char *)packet, (size_t)packet_len, spn, mech); + return auth_client(spn, mech, mysql, vio); +} + + +/* register client plugin */ +mysql_declare_client_plugin(AUTHENTICATION) + "auth_gssapi_client", + "Shuang Qiu, Robbie Harwood, Vladislav Vaintroub", + "GSSAPI/SSPI based authentication", + {0, 1, 0}, + "BSD", + NULL, + NULL, + NULL, + NULL, + gssapi_auth_client +mysql_end_client_plugin; diff --git a/plugin/auth_gssapi/cmake/FindGSSAPI.cmake b/plugin/auth_gssapi/cmake/FindGSSAPI.cmake new file mode 100644 index 00000000..78d1ec84 --- /dev/null +++ b/plugin/auth_gssapi/cmake/FindGSSAPI.cmake @@ -0,0 +1,102 @@ +# - Try to detect the GSSAPI support +# Once done this will define +# +# GSSAPI_FOUND - system supports GSSAPI +# GSSAPI_INCS - the GSSAPI include directory +# GSSAPI_LIBS - the libraries needed to use GSSAPI +# GSSAPI_FLAVOR - the type of API - MIT or HEIMDAL + +# Copyright (c) 2006, Pino Toscano, <toscano.pino@tiscali.it> +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +if(GSSAPI_LIBS AND GSSAPI_FLAVOR) + + # in cache already + set(GSSAPI_FOUND TRUE) + +else(GSSAPI_LIBS AND GSSAPI_FLAVOR) + + find_program(KRB5_CONFIG NAMES krb5-config heimdal-krb5-config PATHS + /opt/local/bin /usr/lib/mit/bin + ONLY_CMAKE_FIND_ROOT_PATH # this is required when cross compiling with cmake 2.6 and ignored with cmake 2.4, Alex + ) + mark_as_advanced(KRB5_CONFIG) + + #reset vars + set(GSSAPI_INCS) + set(GSSAPI_LIBS) + set(GSSAPI_FLAVOR) + + if(KRB5_CONFIG) + + set(HAVE_KRB5_GSSAPI TRUE) + exec_program(${KRB5_CONFIG} ARGS --libs gssapi RETURN_VALUE _return_VALUE OUTPUT_VARIABLE GSSAPI_LIBS) + if(_return_VALUE) + message(STATUS "GSSAPI configure check failed.") + set(HAVE_KRB5_GSSAPI FALSE) + endif(_return_VALUE) + IF(CMAKE_SYSTEM_NAME MATCHES AIX) + string(REGEX REPLACE "-Wl[A-Za-z0-9_/,:-]*[ $]?" "" GSSAPI_LIBS "${GSSAPI_LIBS}") + string(REGEX REPLACE "-L[A-Za-z0-9_/,:-]*[ $]?" "" GSSAPI_LIBS "${GSSAPI_LIBS}") + ENDIF() + + exec_program(${KRB5_CONFIG} ARGS --cflags gssapi RETURN_VALUE _return_VALUE OUTPUT_VARIABLE GSSAPI_INCS) + string(REGEX REPLACE "(\r?\n)+$" "" GSSAPI_INCS "${GSSAPI_INCS}") + string(REGEX REPLACE " *-I" ";" GSSAPI_INCS "${GSSAPI_INCS}") + + exec_program(${KRB5_CONFIG} ARGS --vendor RETURN_VALUE _return_VALUE OUTPUT_VARIABLE gssapi_flavor_tmp) + set(GSSAPI_FLAVOR_MIT) + if(gssapi_flavor_tmp MATCHES ".*Massachusetts.*") + set(GSSAPI_FLAVOR "MIT") + else(gssapi_flavor_tmp MATCHES ".*Massachusetts.*") + set(GSSAPI_FLAVOR "HEIMDAL") + endif(gssapi_flavor_tmp MATCHES ".*Massachusetts.*") + + if(NOT HAVE_KRB5_GSSAPI) + if (gssapi_flavor_tmp MATCHES "Sun Microsystems.*") + message(STATUS "Solaris Kerberos does not have GSSAPI; this is normal.") + set(GSSAPI_LIBS) + set(GSSAPI_INCS) + else(gssapi_flavor_tmp MATCHES "Sun Microsystems.*") + message(WARNING "${KRB5_CONFIG} failed unexpectedly.") + endif(gssapi_flavor_tmp MATCHES "Sun Microsystems.*") + endif(NOT HAVE_KRB5_GSSAPI) + + if(GSSAPI_LIBS) # GSSAPI_INCS can be also empty, so don't rely on that + set(GSSAPI_FOUND TRUE CACHE STRING "") + message(STATUS "Found GSSAPI: ${GSSAPI_LIBS}") + + set(GSSAPI_INCS ${GSSAPI_INCS} CACHE STRING "") + set(GSSAPI_LIBS ${GSSAPI_LIBS} CACHE STRING "") + set(GSSAPI_FLAVOR ${GSSAPI_FLAVOR} CACHE STRING "") + + mark_as_advanced(GSSAPI_INCS GSSAPI_LIBS GSSAPI_FLAVOR) + + endif(GSSAPI_LIBS) + + endif(KRB5_CONFIG) + +endif(GSSAPI_LIBS AND GSSAPI_FLAVOR) diff --git a/plugin/auth_gssapi/common.h b/plugin/auth_gssapi/common.h new file mode 100644 index 00000000..c04241ac --- /dev/null +++ b/plugin/auth_gssapi/common.h @@ -0,0 +1,4 @@ +/** Maximal length of the target name */ +#define PRINCIPAL_NAME_MAX 256 +/** Maximal length of the mech string */ +#define MECH_NAME_MAX 30 diff --git a/plugin/auth_gssapi/gssapi_client.cc b/plugin/auth_gssapi/gssapi_client.cc new file mode 100644 index 00000000..a05ea158 --- /dev/null +++ b/plugin/auth_gssapi/gssapi_client.cc @@ -0,0 +1,127 @@ +/* Copyright (c) 2015, Shuang Qiu, Robbie Harwood, +Vladislav Vaintroub & MariaDB Corporation + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#include <gssapi/gssapi.h> +#include <string.h> +#include <stdio.h> +#include <mysql/plugin_auth.h> +#include <mysqld_error.h> +#include <mysql.h> +#include "gssapi_errmsg.h" + +extern void log_client_error(MYSQL *mysql,const char *fmt,...); + + +/* This sends the error to the client */ +static void log_error(MYSQL *mysql, OM_uint32 major, OM_uint32 minor, const char *msg) +{ + if (GSS_ERROR(major)) + { + char sysmsg[1024]; + gssapi_errmsg(major, minor, sysmsg, sizeof(sysmsg)); + log_client_error(mysql, + "Client GSSAPI error (major %u, minor %u) : %s - %s", + major, minor, msg, sysmsg); + } + else + { + log_client_error(mysql, "Client GSSAPI error : %s", msg); + } +} + +int auth_client(char *principal_name, char *mech, MYSQL *mysql, MYSQL_PLUGIN_VIO *vio) +{ + + int ret= CR_ERROR; + OM_uint32 major= 0, minor= 0; + gss_ctx_id_t ctxt= GSS_C_NO_CONTEXT; + gss_name_t service_name= GSS_C_NO_NAME; + + if (principal_name && principal_name[0]) + { + /* import principal from plain text */ + gss_buffer_desc principal_name_buf; + principal_name_buf.length= strlen(principal_name); + principal_name_buf.value= (void *) principal_name; + major= gss_import_name(&minor, &principal_name_buf, GSS_C_NT_USER_NAME, &service_name); + if (GSS_ERROR(major)) + { + log_error(mysql, major, minor, "gss_import_name"); + return CR_ERROR; + } + } + + gss_buffer_desc input= {0,0}; + do + { + gss_buffer_desc output= {0,0}; + major= gss_init_sec_context(&minor, GSS_C_NO_CREDENTIAL, &ctxt, service_name, + GSS_C_NO_OID, 0, 0, GSS_C_NO_CHANNEL_BINDINGS, + &input, NULL, &output, NULL, NULL); + if (output.length) + { + /* send credential */ + if(vio->write_packet(vio, (unsigned char *)output.value, output.length)) + { + /* Server error packet contains detailed message. */ + ret= CR_OK_HANDSHAKE_COMPLETE; + gss_release_buffer (&minor, &output); + goto cleanup; + } + } + gss_release_buffer (&minor, &output); + + if (GSS_ERROR(major)) + { + log_error(mysql, major, minor,"gss_init_sec_context"); + goto cleanup; + } + + if (major & GSS_S_CONTINUE_NEEDED) + { + int len= vio->read_packet(vio, (unsigned char **) &input.value); + if (len <= 0) + { + /* Server error packet contains detailed message. */ + ret= CR_OK_HANDSHAKE_COMPLETE; + goto cleanup; + } + input.length= len; + } + } while (major & GSS_S_CONTINUE_NEEDED); + + ret= CR_OK; + +cleanup: + if (service_name != GSS_C_NO_NAME) + gss_release_name(&minor, &service_name); + if (ctxt != GSS_C_NO_CONTEXT) + gss_delete_sec_context(&minor, &ctxt, GSS_C_NO_BUFFER); + + return ret; +} diff --git a/plugin/auth_gssapi/gssapi_errmsg.cc b/plugin/auth_gssapi/gssapi_errmsg.cc new file mode 100644 index 00000000..3e23c573 --- /dev/null +++ b/plugin/auth_gssapi/gssapi_errmsg.cc @@ -0,0 +1,80 @@ +/* Copyright (c) 2015, Shuang Qiu, Robbie Harwood, +Vladislav Vaintroub & MariaDB Corporation + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#if defined(__FreeBSD__) || defined(SOLARIS) || defined(__sun) +#include <gssapi/gssapi.h> +#else +#include <gssapi.h> +#endif +#include <string.h> + +void gssapi_errmsg(OM_uint32 major, OM_uint32 minor, char *buf, size_t size) +{ + OM_uint32 message_context; + OM_uint32 status_code; + OM_uint32 maj_status; + OM_uint32 min_status; + gss_buffer_desc status_string; + char *p= buf; + char *end= buf + size - 1; + int types[] = {GSS_C_GSS_CODE,GSS_C_MECH_CODE}; + + for(int i= 0; i < 2;i++) + { + message_context= 0; + status_code= types[i] == GSS_C_GSS_CODE?major:minor; + + if(!status_code) + continue; + do + { + maj_status = gss_display_status( + &min_status, + status_code, + types[i], + GSS_C_NO_OID, + &message_context, + &status_string); + + if(maj_status) + break; + + if(p + status_string.length + 2 < end) + { + memcpy(p,status_string.value, status_string.length); + p += status_string.length; + *p++ = '.'; + *p++ = ' '; + } + + gss_release_buffer(&min_status, &status_string); + } + while (message_context != 0); + } + *p= 0; +} diff --git a/plugin/auth_gssapi/gssapi_errmsg.h b/plugin/auth_gssapi/gssapi_errmsg.h new file mode 100644 index 00000000..26db8439 --- /dev/null +++ b/plugin/auth_gssapi/gssapi_errmsg.h @@ -0,0 +1,29 @@ +/* Copyright (c) 2015, Shuang Qiu, Robbie Harwood, +Vladislav Vaintroub & MariaDB Corporation + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +extern void gssapi_errmsg(OM_uint32 major, OM_uint32 minor, char *buf, size_t size); diff --git a/plugin/auth_gssapi/gssapi_server.cc b/plugin/auth_gssapi/gssapi_server.cc new file mode 100644 index 00000000..db7bda9a --- /dev/null +++ b/plugin/auth_gssapi/gssapi_server.cc @@ -0,0 +1,270 @@ +#include <my_config.h> +#include <gssapi/gssapi.h> +#include <stdio.h> +#include <mysql/plugin_auth.h> +#include <mysqld_error.h> +#include <string.h> +#include "server_plugin.h" +#include "gssapi_errmsg.h" + +static gss_name_t service_name = GSS_C_NO_NAME; + +/* This sends the error to the client */ +static void log_error( OM_uint32 major, OM_uint32 minor, const char *msg) +{ + if (GSS_ERROR(major)) + { + char sysmsg[1024]; + gssapi_errmsg(major, minor, sysmsg, sizeof(sysmsg)); + my_printf_error(ER_UNKNOWN_ERROR,"Server GSSAPI error (major %u, minor %u) : %s -%s", + 0, major, minor, msg, sysmsg); + } + else + { + my_printf_error(ER_UNKNOWN_ERROR, "Server GSSAPI error : %s", 0, msg); + } +} + + +/* + Generate default principal service name formatted as principal name "mariadb/server.fqdn@REALM" +*/ +#include <krb5.h> +#ifdef HAVE_KRB5_XFREE +#define krb5_free_unparsed_name(a,b) krb5_xfree(b) +#endif +static char* get_default_principal_name() +{ + static char default_name[1024]; + char *unparsed_name= NULL; + krb5_context context= NULL; + krb5_principal principal= NULL; + krb5_keyblock *key= NULL; + + if(krb5_init_context(&context)) + { + my_printf_error(1, "GSSAPI plugin : krb5_init_context failed", + ME_ERROR_LOG | ME_WARNING); + goto cleanup; + } + + if (krb5_sname_to_principal(context, NULL, "mariadb", KRB5_NT_SRV_HST, &principal)) + { + my_printf_error(1, "GSSAPI plugin : krb5_sname_to_principal failed", + ME_ERROR_LOG | ME_WARNING); + goto cleanup; + } + + if (krb5_unparse_name(context, principal, &unparsed_name)) + { + my_printf_error(1, "GSSAPI plugin : krb5_unparse_name failed", + ME_ERROR_LOG | ME_WARNING); + goto cleanup; + } + + /* Check for entry in keytab */ + if (krb5_kt_read_service_key(context, NULL, principal, 0, (krb5_enctype)0, &key)) + { + my_printf_error(1, "GSSAPI plugin : default principal '%s' not found in keytab", + ME_ERROR_LOG | ME_WARNING, unparsed_name); + goto cleanup; + } + + strncpy(default_name, unparsed_name, sizeof(default_name)-1); + +cleanup: + if (key) + krb5_free_keyblock(context, key); + if (unparsed_name) + krb5_free_unparsed_name(context, unparsed_name); + if (principal) + krb5_free_principal(context, principal); + if (context) + krb5_free_context(context); + + return default_name; +} + + +int plugin_init() +{ + gss_buffer_desc principal_name_buf; + OM_uint32 major= 0, minor= 0; + gss_cred_id_t cred= GSS_C_NO_CREDENTIAL; + + if(srv_keytab_path && srv_keytab_path[0]) + { + setenv("KRB5_KTNAME", srv_keytab_path, 1); + } + + if(!srv_principal_name || !srv_principal_name[0]) + srv_principal_name= get_default_principal_name(); + + /* import service principal from plain text */ + if(srv_principal_name && srv_principal_name[0]) + { + my_printf_error(1, "GSSAPI plugin : using principal name '%s'", + ME_ERROR_LOG | ME_NOTE, srv_principal_name); + principal_name_buf.length= strlen(srv_principal_name); + principal_name_buf.value= srv_principal_name; + major= gss_import_name(&minor, &principal_name_buf, GSS_C_NT_USER_NAME, &service_name); + if(GSS_ERROR(major)) + { + log_error(major, minor, "gss_import_name"); + return -1; + } + } + else + { + service_name= GSS_C_NO_NAME; + } + + /* Check if SPN configuration is OK */ + major= gss_acquire_cred(&minor, service_name, GSS_C_INDEFINITE, + GSS_C_NO_OID_SET, GSS_C_ACCEPT, &cred, NULL, + NULL); + + if (GSS_ERROR(major)) + { + log_error(major, minor, "gss_acquire_cred failed"); + return -1; + } + gss_release_cred(&minor, &cred); + + return 0; +} + +int plugin_deinit() +{ + if (service_name != GSS_C_NO_NAME) + { + OM_uint32 minor; + gss_release_name(&minor, &service_name); + } + return 0; +} + + +int auth_server(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *auth_info) +{ + + int rc= CR_ERROR; /* return code */ + + /* GSSAPI related fields */ + OM_uint32 major= 0, minor= 0, flags= 0; + gss_cred_id_t cred= GSS_C_NO_CREDENTIAL; /* credential identifier */ + gss_ctx_id_t ctxt= GSS_C_NO_CONTEXT; /* context identifier */ + gss_name_t client_name; + gss_buffer_desc client_name_buf, input, output; + char *client_name_str; + const char *user= 0; + size_t userlen= 0; + int use_full_name= 0; + + /* server acquires credential */ + major= gss_acquire_cred(&minor, service_name, GSS_C_INDEFINITE, + GSS_C_NO_OID_SET, GSS_C_ACCEPT, &cred, NULL, + NULL); + + if (GSS_ERROR(major)) + { + log_error(major, minor, "gss_acquire_cred failed"); + goto cleanup; + } + + input.length= 0; + input.value= NULL; + do + { + /* receive token from peer */ + int len= vio->read_packet(vio, (unsigned char **) &input.value); + if (len < 0) + { + log_error(0, 0, "fail to read token from client"); + goto cleanup; + } + if (!user) + { + if (auth_info->auth_string_length > 0) + { + use_full_name= 1; + user= auth_info->auth_string; + userlen= auth_info->auth_string_length; + } + else + { + use_full_name= 0; + user= auth_info->user_name; + userlen= auth_info->user_name_length; + } + } + + input.length= len; + major= gss_accept_sec_context(&minor, &ctxt, cred, &input, + GSS_C_NO_CHANNEL_BINDINGS, &client_name, + NULL, &output, &flags, NULL, NULL); + if (GSS_ERROR(major)) + { + + log_error(major, minor, "gss_accept_sec_context"); + rc= CR_ERROR; + goto cleanup; + } + + /* send token to peer */ + if (output.length) + { + if (vio->write_packet(vio, (const unsigned char *) output.value, output.length)) + { + gss_release_buffer(&minor, &output); + log_error(major, minor, "communication error(write)"); + goto cleanup; + } + gss_release_buffer(&minor, &output); + } + } while (major & GSS_S_CONTINUE_NEEDED); + + /* extract plain text client name */ + major= gss_display_name(&minor, client_name, &client_name_buf, NULL); + if (GSS_ERROR(major)) + { + log_error(major, minor, "gss_display_name"); + goto cleanup; + } + + client_name_str= (char *)client_name_buf.value; + + /* + * Compare input user name with the actual one. Return success if + * the names match exactly, or if use_full_name parameter is not set + * up to the '@' separator. + */ + if ((userlen == client_name_buf.length) || + (!use_full_name + && userlen < client_name_buf.length + && client_name_str[userlen] == '@')) + { + if (user && strncmp(client_name_str, user, userlen) == 0) + { + rc= CR_OK; + } + } + + if(rc != CR_OK) + { + my_printf_error(ER_ACCESS_DENIED_ERROR, + "GSSAPI name mismatch, requested '%s', actual name '%.*s'", + 0, user, (int)client_name_buf.length, client_name_str); + } + + gss_release_buffer(&minor, &client_name_buf); + + +cleanup: + if (ctxt != GSS_C_NO_CONTEXT) + gss_delete_sec_context(&minor, &ctxt, GSS_C_NO_BUFFER); + if (cred != GSS_C_NO_CREDENTIAL) + gss_release_cred(&minor, &cred); + + return(rc); +} diff --git a/plugin/auth_gssapi/mysql-test/auth_gssapi/basic.result b/plugin/auth_gssapi/mysql-test/auth_gssapi/basic.result new file mode 100644 index 00000000..09be092d --- /dev/null +++ b/plugin/auth_gssapi/mysql-test/auth_gssapi/basic.result @@ -0,0 +1,22 @@ +CREATE USER 'GSSAPI_SHORTNAME' IDENTIFIED WITH gssapi; +connect con1,localhost,$GSSAPI_SHORTNAME,,; +SELECT USER(),CURRENT_USER(); +USER() CURRENT_USER() +GSSAPI_SHORTNAME@localhost GSSAPI_SHORTNAME@% +disconnect con1; +connection default; +DROP USER 'GSSAPI_SHORTNAME'; +CREATE USER nosuchuser IDENTIFIED WITH gssapi; +ERROR 28000: GSSAPI name mismatch, requested 'nosuchuser', actual name 'GSSAPI_SHORTNAME' +DROP USER nosuchuser; +CREATE USER usr1 IDENTIFIED WITH gssapi as 'GSSAPI_FULLNAME'; +connect con1,localhost,usr1,,; +SELECT USER(),CURRENT_USER(); +USER() CURRENT_USER() +usr1@localhost usr1@% +disconnect con1; +connection default; +DROP USER usr1; +CREATE USER nosuchuser IDENTIFIED WITH gssapi AS 'nosuchuser@EXAMPLE.COM'; +ERROR 28000: GSSAPI name mismatch, requested 'nosuchuser@EXAMPLE.COM', actual name 'GSSAPI_FULLNAME' +DROP USER nosuchuser; diff --git a/plugin/auth_gssapi/mysql-test/auth_gssapi/basic.test b/plugin/auth_gssapi/mysql-test/auth_gssapi/basic.test new file mode 100644 index 00000000..dc242e3a --- /dev/null +++ b/plugin/auth_gssapi/mysql-test/auth_gssapi/basic.test @@ -0,0 +1,41 @@ +# +# CREATE USER without 'AS' clause +# +--replace_result $GSSAPI_SHORTNAME GSSAPI_SHORTNAME +eval CREATE USER '$GSSAPI_SHORTNAME' IDENTIFIED WITH gssapi; +connect (con1,localhost,$GSSAPI_SHORTNAME,,); +--replace_result $GSSAPI_SHORTNAME GSSAPI_SHORTNAME +SELECT USER(),CURRENT_USER(); +disconnect con1; + +connection default; +--replace_result $GSSAPI_SHORTNAME GSSAPI_SHORTNAME +eval DROP USER '$GSSAPI_SHORTNAME'; + +CREATE USER nosuchuser IDENTIFIED WITH gssapi; +--disable_query_log +--replace_regex /actual name '.*'/actual name 'GSSAPI_SHORTNAME'/ +--error ER_ACCESS_DENIED_ERROR +connect (con1,localhost,nosuchuser,,); +--enable_query_log +DROP USER nosuchuser; + +# +# CREATE USER with 'AS' clause +# +--replace_result $GSSAPI_FULLNAME GSSAPI_FULLNAME +eval CREATE USER usr1 IDENTIFIED WITH gssapi as '$GSSAPI_FULLNAME'; +connect (con1,localhost,usr1,,); +--replace_result $GSSAPI_FULLNAME GSSAPI_FULLNAME +SELECT USER(),CURRENT_USER(); +disconnect con1; +connection default; +DROP USER usr1; + +CREATE USER nosuchuser IDENTIFIED WITH gssapi AS 'nosuchuser@EXAMPLE.COM'; +--disable_query_log +--replace_regex /actual name '.*'/actual name 'GSSAPI_FULLNAME'/ +--error ER_ACCESS_DENIED_ERROR +connect (con1,localhost,nosuchuser,,); +--enable_query_log +DROP USER nosuchuser; diff --git a/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.result b/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.result new file mode 100644 index 00000000..38244eca --- /dev/null +++ b/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.result @@ -0,0 +1,40 @@ +CREATE USER 'nosuchgroup' IDENTIFIED WITH gssapi AS 'GROUP:nosuchgroup'; +connect(localhost,nosuchuser,,test,MASTER_MYPORT,MASTER_MYSOCK); +connect con1,localhost,nosuchuser,,; +ERROR 28000: Access denied for user 'nosuchuser'@'localhost' (using password: NO) +DROP USER nosuchgroup; +CREATE USER 'nullsid' IDENTIFIED WITH gssapi AS 'SID:S-1-0-0'; +connect(localhost,nullsid,,test,MASTER_MYPORT,MASTER_MYSOCK); +connect con1,localhost,nullsid,,; +ERROR 28000: Access denied for user 'nullsid'@'localhost' (using password: NO) +DROP USER nullsid; +CREATE USER 'anonymous' IDENTIFIED WITH gssapi AS 'SID:AN'; +connect(localhost,anonymous,,test,MASTER_MYPORT,MASTER_MYSOCK); +connect con1,localhost,anonymous,,; +ERROR 28000: Access denied for user 'anonymous'@'localhost' (using password: NO) +DROP USER anonymous; +CREATE USER 'group_everyone' IDENTIFIED WITH gssapi AS 'GROUP:Everyone'; +connect con1,localhost,group_everyone,,; +disconnect con1; +connection default; +DROP USER group_everyone; +CREATE USER 'sid_wd' IDENTIFIED WITH gssapi AS 'SID:WD'; +connect con1,localhost,sid_wd,,; +disconnect con1; +connection default; +DROP USER sid_wd; +CREATE USER 'S_1_1_0' IDENTIFIED WITH gssapi AS 'SID:S-1-1-0'; +connect con1,localhost,S_1_1_0,,; +disconnect con1; +connection default; +DROP USER S_1_1_0; +CREATE USER 'me_short' IDENTIFIED WITH gssapi AS 'GROUP:GSSAPI_SHORTNAME'; +connect con1,localhost,me_short,,; +disconnect con1; +connection default; +DROP USER me_short; +CREATE USER 'me_sid' IDENTIFIED WITH gssapi AS 'SID:MY-SID'; +connect con1,localhost,me_sid,,; +disconnect con1; +connection default; +DROP USER me_sid; diff --git a/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.test b/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.test new file mode 100644 index 00000000..647e0204 --- /dev/null +++ b/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.test @@ -0,0 +1,70 @@ +source include/windows.inc; +--replace_regex /name '[^']+'/name 'localhost'/ + + + +# Invalid group name +CREATE USER 'nosuchgroup' IDENTIFIED WITH gssapi AS 'GROUP:nosuchgroup'; +replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT; +error ER_ACCESS_DENIED_ERROR; +connect (con1,localhost,nosuchuser,,); +DROP USER nosuchgroup; + +# Group with no members, NULL SID +CREATE USER 'nullsid' IDENTIFIED WITH gssapi AS 'SID:S-1-0-0'; +replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT; +error ER_ACCESS_DENIED_ERROR; +connect (con1,localhost,nullsid,,); +DROP USER nullsid; + + +# Anonymous +CREATE USER 'anonymous' IDENTIFIED WITH gssapi AS 'SID:AN'; +replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT; +error ER_ACCESS_DENIED_ERROR; +connect (con1,localhost,anonymous,,); +DROP USER anonymous; + + +# Positive tests + +# Everyone group +CREATE USER 'group_everyone' IDENTIFIED WITH gssapi AS 'GROUP:Everyone'; +replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT; +connect (con1,localhost,group_everyone,,); +disconnect con1; +connection default; +DROP USER group_everyone; + +# Everyone AS well-known SID name +CREATE USER 'sid_wd' IDENTIFIED WITH gssapi AS 'SID:WD'; +replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT; +connect (con1,localhost,sid_wd,,); +disconnect con1; +connection default; +DROP USER sid_wd; + +# Everyone AS SID S-1-1-0 +CREATE USER 'S_1_1_0' IDENTIFIED WITH gssapi AS 'SID:S-1-1-0'; +replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT; +connect (con1,localhost,S_1_1_0,,); +disconnect con1; +connection default; +DROP USER S_1_1_0; + +replace_result $GSSAPI_SHORTNAME GSSAPI_SHORTNAME; +eval CREATE USER 'me_short' IDENTIFIED WITH gssapi AS 'GROUP:$GSSAPI_SHORTNAME'; +replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT; +connect (con1,localhost,me_short,,); +disconnect con1; +connection default; +DROP USER me_short; + + +replace_result $SID MY-SID; +eval CREATE USER 'me_sid' IDENTIFIED WITH gssapi AS 'SID:$SID'; +replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT; +connect (con1,localhost,me_sid,,); +disconnect con1; +connection default; +DROP USER me_sid;
\ No newline at end of file diff --git a/plugin/auth_gssapi/mysql-test/auth_gssapi/multiauth.result b/plugin/auth_gssapi/mysql-test/auth_gssapi/multiauth.result new file mode 100644 index 00000000..41d27073 --- /dev/null +++ b/plugin/auth_gssapi/mysql-test/auth_gssapi/multiauth.result @@ -0,0 +1,30 @@ +CREATE USER 'nosuchuser' IDENTIFIED WITH gssapi OR mysql_native_password as password("good"); +connect(localhost,nosuchuser,,test,MASTER_MYPORT,MASTER_MYSOCK); +connect con1,localhost,nosuchuser,,; +ERROR 28000: Access denied for user 'nosuchuser'@'localhost' (using password: NO) +connect con1,localhost,nosuchuser,good,; +SELECT USER(),CURRENT_USER(); +USER() CURRENT_USER() +nosuchuser@localhost nosuchuser@% +disconnect con1; +connection default; +DROP USER nosuchuser; +CREATE USER 'nosuchuser' IDENTIFIED WITH mysql_native_password as password("good") OR gssapi; +connect(localhost,nosuchuser,,test,MASTER_MYPORT,MASTER_MYSOCK); +connect con1,localhost,nosuchuser,,; +ERROR 28000: GSSAPI name mismatch, requested 'nosuchuser', actual name 'GSSAPI_SHORTNAME' +connect con1,localhost,nosuchuser,good,; +SELECT USER(),CURRENT_USER(); +USER() CURRENT_USER() +nosuchuser@localhost nosuchuser@% +disconnect con1; +connection default; +DROP USER nosuchuser; +CREATE USER 'GSSAPI_SHORTNAME' IDENTIFIED WITH mysql_native_password as password("good") OR gssapi; +connect con1,localhost,$GSSAPI_SHORTNAME,,; +SELECT USER(),CURRENT_USER(); +USER() CURRENT_USER() +GSSAPI_SHORTNAME@localhost GSSAPI_SHORTNAME@% +disconnect con1; +connection default; +DROP USER 'GSSAPI_SHORTNAME'; diff --git a/plugin/auth_gssapi/mysql-test/auth_gssapi/multiauth.test b/plugin/auth_gssapi/mysql-test/auth_gssapi/multiauth.test new file mode 100644 index 00000000..0257946b --- /dev/null +++ b/plugin/auth_gssapi/mysql-test/auth_gssapi/multiauth.test @@ -0,0 +1,32 @@ +# gssapi,password +CREATE USER 'nosuchuser' IDENTIFIED WITH gssapi OR mysql_native_password as password("good"); +replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT; +error ER_ACCESS_DENIED_ERROR; +connect (con1,localhost,nosuchuser,,); +connect (con1,localhost,nosuchuser,good,); +SELECT USER(),CURRENT_USER(); +disconnect con1; +connection default; +DROP USER nosuchuser; + +# password,gssapi +CREATE USER 'nosuchuser' IDENTIFIED WITH mysql_native_password as password("good") OR gssapi; +replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT $GSSAPI_SHORTNAME GSSAPI_SHORTNAME; +error ER_ACCESS_DENIED_ERROR; +connect (con1,localhost,nosuchuser,,); +connect (con1,localhost,nosuchuser,good,); +SELECT USER(),CURRENT_USER(); +disconnect con1; +connection default; +DROP USER nosuchuser; + +replace_result $GSSAPI_SHORTNAME GSSAPI_SHORTNAME; +eval CREATE USER '$GSSAPI_SHORTNAME' IDENTIFIED WITH mysql_native_password as password("good") OR gssapi; +connect (con1,localhost,$GSSAPI_SHORTNAME,,); +replace_result $GSSAPI_SHORTNAME GSSAPI_SHORTNAME; +SELECT USER(),CURRENT_USER(); +disconnect con1; +connection default; +replace_result $GSSAPI_SHORTNAME GSSAPI_SHORTNAME; +eval DROP USER '$GSSAPI_SHORTNAME'; + diff --git a/plugin/auth_gssapi/mysql-test/auth_gssapi/suite.opt b/plugin/auth_gssapi/mysql-test/auth_gssapi/suite.opt new file mode 100644 index 00000000..c9a5d522 --- /dev/null +++ b/plugin/auth_gssapi/mysql-test/auth_gssapi/suite.opt @@ -0,0 +1 @@ +--loose-gssapi=ON --loose-gssapi-keytab-path=$GSSAPI_KEYTAB_PATH --loose-gssapi-principal-name=$GSSAPI_PRINCIPAL_NAME --plugin-load-add=$AUTH_GSSAPI_SO diff --git a/plugin/auth_gssapi/mysql-test/auth_gssapi/suite.pm b/plugin/auth_gssapi/mysql-test/auth_gssapi/suite.pm new file mode 100644 index 00000000..af13b76e --- /dev/null +++ b/plugin/auth_gssapi/mysql-test/auth_gssapi/suite.pm @@ -0,0 +1,53 @@ + +package My::Suite::AuthGSSAPI; + +@ISA = qw(My::Suite); + +return "No AUTH_GSSAPI plugin" unless ($ENV{AUTH_GSSAPI_SO} or $::mysqld_variables{gssapi} eq "ON"); + +return "Not run for embedded server" if $::opt_embedded_server; + +# Following environment variables may need to be set +if ($^O eq "MSWin32") +{ + chomp(my $whoami =`whoami /UPN 2>NUL` || `whoami`); + my $fullname = $whoami; + $fullname =~ s/\\/\\\\/; # SQL escaping for backslash + $ENV{'GSSAPI_FULLNAME'} = $fullname; + $ENV{'GSSAPI_SHORTNAME'} = $ENV{'USERNAME'}; + chomp(my $sid = `powershell -Command "([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value"`); + $ENV{'SID'} = $sid; + +} +else +{ + if (!$ENV{'GSSAPI_FULLNAME'}) + { + my $s = `klist 2>/dev/null |grep 'Default principal: '`; + if ($s) + { + chomp($s); + my $fullname = substr($s,19); + $ENV{'GSSAPI_FULLNAME'} = $fullname; + } + } + $ENV{'GSSAPI_SHORTNAME'} = (split /@/, $ENV{'GSSAPI_FULLNAME'}) [0]; +} + + +if (!$ENV{'GSSAPI_FULLNAME'} || !$ENV{'GSSAPI_SHORTNAME'}) +{ + return "Environment variable GSSAPI_SHORTNAME and GSSAPI_FULLNAME need to be set" +} + +if ($::opt_verbose) +{ + foreach $var ('GSSAPI_SHORTNAME','GSSAPI_FULLNAME','GSSAPI_KEYTAB_PATH','GSSAPI_PRINCIPAL_NAME') + { + print "$var=$ENV{$var}\n"; + } +} +sub is_default { 1 } + +bless { }; + diff --git a/plugin/auth_gssapi/server_plugin.cc b/plugin/auth_gssapi/server_plugin.cc new file mode 100644 index 00000000..c45452ce --- /dev/null +++ b/plugin/auth_gssapi/server_plugin.cc @@ -0,0 +1,161 @@ +/* Copyright (c) 2015, Shuang Qiu, Robbie Harwood, + Vladislav Vaintroub & MariaDB Corporation + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +/** + @file + + GSSAPI authentication plugin, server side +*/ + +typedef unsigned long long my_ulonglong; + +#include <stdlib.h> +#include <mysqld_error.h> +#include <typelib.h> +#include <mysql/plugin_auth.h> +#include "string.h" +#include "server_plugin.h" +#include "common.h" + +/* First packet sent from server to client, contains srv_principal_name\0mech\0 */ +static char first_packet[PRINCIPAL_NAME_MAX + MECH_NAME_MAX +2]; +static int first_packet_len; + +/* + Target name in GSSAPI/SSPI , for Kerberos it is service principal name + (often user principal name of the server user will work) +*/ +char *srv_principal_name; +char *srv_keytab_path; +const char *srv_mech_name=""; +unsigned long srv_mech; + +/** + The main server function of the GSSAPI plugin. + */ +static int gssapi_auth(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *auth_info) +{ + /* Send first packet with target name and mech name */ + if (vio->write_packet(vio, (unsigned char *)first_packet, first_packet_len)) + return CR_ERROR; + + return auth_server(vio, auth_info); +} + +static int initialize_plugin(void *unused) +{ + int rc; + rc = plugin_init(); + if (rc) + return rc; + + strcpy(first_packet, srv_principal_name); + strcpy(first_packet + strlen(srv_principal_name) + 1,srv_mech_name); + first_packet_len = (int)(strlen(srv_principal_name) + strlen(srv_mech_name) + 2); + + return 0; +} + +static int deinitialize_plugin(void *unused) +{ + return plugin_deinit(); +} + +#ifdef PLUGIN_GSSAPI +/* system variable */ +static MYSQL_SYSVAR_STR(keytab_path, srv_keytab_path, + PLUGIN_VAR_RQCMDARG|PLUGIN_VAR_READONLY, + "Keytab file path for Kerberos authentication", + NULL, + NULL, + ""); +#endif + +static MYSQL_SYSVAR_STR(principal_name, srv_principal_name, + PLUGIN_VAR_RQCMDARG|PLUGIN_VAR_READONLY, + "GSSAPI target name - service principal name for Kerberos authentication.", + NULL, + NULL, + ""); +#ifdef PLUGIN_SSPI +static const char* mech_names[] = { + "Kerberos", + "Negotiate", + "", + NULL +}; +static TYPELIB mech_name_typelib = { + 3, + "mech_name_typelib", + mech_names, + NULL +}; +static MYSQL_SYSVAR_ENUM(mech_name, srv_mech, + PLUGIN_VAR_RQCMDARG|PLUGIN_VAR_READONLY, + "GSSAPI mechanism", + NULL, + NULL, + 2,&mech_name_typelib); +#endif + +static struct st_mysql_sys_var *system_variables[]= { + MYSQL_SYSVAR(principal_name), +#ifdef PLUGIN_SSPI + MYSQL_SYSVAR(mech_name), +#endif +#ifdef PLUGIN_GSSAPI + MYSQL_SYSVAR(keytab_path), +#endif + NULL +}; + +/* Register authentication plugin */ +static struct st_mysql_auth server_handler= { + MYSQL_AUTHENTICATION_INTERFACE_VERSION, + "auth_gssapi_client", + gssapi_auth, NULL, NULL +}; + +maria_declare_plugin(auth_gssapi) +{ + MYSQL_AUTHENTICATION_PLUGIN, + &server_handler, + "gssapi", + "Shuang Qiu, Robbie Harwood, Vladislav Vaintroub", + "Plugin for GSSAPI/SSPI based authentication.", + PLUGIN_LICENSE_BSD, + initialize_plugin, + deinitialize_plugin, /* destructor */ + 0x0100, /* version */ + NULL, /* status variables */ + system_variables, /* system variables */ + "1.0", + MariaDB_PLUGIN_MATURITY_STABLE +} +maria_declare_plugin_end; + diff --git a/plugin/auth_gssapi/server_plugin.h b/plugin/auth_gssapi/server_plugin.h new file mode 100644 index 00000000..84552d3a --- /dev/null +++ b/plugin/auth_gssapi/server_plugin.h @@ -0,0 +1,51 @@ +/* Copyright (c) 2015, Shuang Qiu, Robbie Harwood, +Vladislav Vaintroub & MariaDB Corporation + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +/* Plugin variables*/ +#include <mysql/plugin_auth.h> +typedef enum +{ + PLUGIN_MECH_KERBEROS = 0, + PLUGIN_MECH_SPNEGO = 1, + PLUGIN_MECH_DEFAULT = 2 +}PLUGIN_MECH; + +extern unsigned long srv_mech; +extern char *srv_principal_name; +extern const char *srv_mech_name; +extern char *srv_keytab_path; +/* + Check, with GSSAPI/SSPI username of logged on user. + + Depending on use_full_name parameter, compare either full name + (principal name like user@real), or local name (first component) +*/ +int plugin_init(); +int plugin_deinit(); + +int auth_server(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *auth_info); diff --git a/plugin/auth_gssapi/sspi.h b/plugin/auth_gssapi/sspi.h new file mode 100644 index 00000000..34b8a56a --- /dev/null +++ b/plugin/auth_gssapi/sspi.h @@ -0,0 +1,38 @@ +/* Copyright (c) 2015, Shuang Qiu, Robbie Harwood, +Vladislav Vaintroub & MariaDB Corporation + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#define SECURITY_WIN32 +#include <windows.h> +#include <sspi.h> +#include <SecExt.h> +#include <stdarg.h> +#include <stdio.h> + +#define SSPI_MAX_TOKEN_SIZE 50000 +#define SEC_ERROR(err) (err < 0) +extern void sspi_errmsg(int err, char *buf, size_t size);
\ No newline at end of file diff --git a/plugin/auth_gssapi/sspi_client.cc b/plugin/auth_gssapi/sspi_client.cc new file mode 100644 index 00000000..61249dd1 --- /dev/null +++ b/plugin/auth_gssapi/sspi_client.cc @@ -0,0 +1,183 @@ +/* Copyright (c) 2015, Shuang Qiu, Robbie Harwood, +Vladislav Vaintroub & MariaDB Corporation + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#define SECURITY_WIN32 +#include <windows.h> +#include <sspi.h> +#include <SecExt.h> +#include <stdarg.h> +#include <stdio.h> + +#include <mysql/plugin_auth.h> +#include <mysql.h> +#include <mysqld_error.h> + +#include "sspi.h" + +extern void log_client_error(MYSQL *mysql, const char *fmt, ...); +static void log_error(MYSQL *mysql, SECURITY_STATUS err, const char *msg) +{ + if (err) + { + char buf[1024]; + sspi_errmsg(err, buf, sizeof(buf)); + log_client_error(mysql, "SSPI client error 0x%x - %s - %s", err, msg, buf); + } + else + { + log_client_error(mysql, "SSPI client error %s", msg); + } +} + + +/** Client side authentication*/ +int auth_client(char *principal_name, char *mech, MYSQL *mysql, MYSQL_PLUGIN_VIO *vio) +{ + + int ret; + CredHandle cred; + CtxtHandle ctxt; + ULONG attribs = 0; + TimeStamp lifetime; + SECURITY_STATUS sspi_err; + + SecBufferDesc inbuf_desc; + SecBuffer inbuf; + SecBufferDesc outbuf_desc; + SecBuffer outbuf; + PBYTE out = NULL; + + ret= CR_ERROR; + SecInvalidateHandle(&ctxt); + SecInvalidateHandle(&cred); + + if (!mech || strcmp(mech, "Negotiate") != 0) + { + mech= "Kerberos"; + } + + sspi_err = AcquireCredentialsHandle( + NULL, + mech, + SECPKG_CRED_OUTBOUND, + NULL, + NULL, + NULL, + NULL, + &cred, + &lifetime); + + if (SEC_ERROR(sspi_err)) + { + log_error(mysql, sspi_err, "AcquireCredentialsHandle"); + return CR_ERROR; + } + + out = (PBYTE)malloc(SSPI_MAX_TOKEN_SIZE); + if (!out) + { + log_error(mysql, SEC_E_OK, "memory allocation error"); + goto cleanup; + } + + /* Prepare buffers */ + inbuf_desc.ulVersion = SECBUFFER_VERSION; + inbuf_desc.cBuffers = 1; + inbuf_desc.pBuffers = &inbuf; + inbuf.BufferType = SECBUFFER_TOKEN; + inbuf.cbBuffer = 0; + inbuf.pvBuffer = NULL; + + outbuf_desc.ulVersion = SECBUFFER_VERSION; + outbuf_desc.cBuffers = 1; + outbuf_desc.pBuffers = &outbuf; + outbuf.BufferType = SECBUFFER_TOKEN; + outbuf.pvBuffer = out; + + do + { + outbuf.cbBuffer= SSPI_MAX_TOKEN_SIZE; + sspi_err= InitializeSecurityContext( + &cred, + SecIsValidHandle(&ctxt) ? &ctxt : NULL, + principal_name, + 0, + 0, + SECURITY_NATIVE_DREP, + inbuf.cbBuffer ? &inbuf_desc : NULL, + 0, + &ctxt, + &outbuf_desc, + &attribs, + &lifetime); + if (SEC_ERROR(sspi_err)) + { + log_error(mysql, sspi_err, "InitializeSecurityContext"); + goto cleanup; + } + if (sspi_err != SEC_E_OK && sspi_err != SEC_I_CONTINUE_NEEDED) + { + log_error(mysql, sspi_err, "Unexpected response from InitializeSecurityContext"); + goto cleanup; + } + + if (outbuf.cbBuffer) + { + /* send credential to server */ + if (vio->write_packet(vio, (unsigned char *)outbuf.pvBuffer, outbuf.cbBuffer)) + { + /* Server error packet contains detailed message. */ + ret= CR_OK_HANDSHAKE_COMPLETE; + goto cleanup; + } + } + + if (sspi_err == SEC_I_CONTINUE_NEEDED) + { + int len= vio->read_packet(vio, (unsigned char **)&inbuf.pvBuffer); + if (len <= 0) + { + /* Server side error is in the last server packet. */ + ret= CR_OK_HANDSHAKE_COMPLETE; + goto cleanup; + } + inbuf.cbBuffer= len; + } + } while (sspi_err == SEC_I_CONTINUE_NEEDED); + + ret= CR_OK; + +cleanup: + + if (SecIsValidHandle(&ctxt)) + DeleteSecurityContext(&ctxt); + if (SecIsValidHandle(&cred)) + FreeCredentialsHandle(&cred); + free(out); + return ret; +} diff --git a/plugin/auth_gssapi/sspi_errmsg.cc b/plugin/auth_gssapi/sspi_errmsg.cc new file mode 100644 index 00000000..8e59da6f --- /dev/null +++ b/plugin/auth_gssapi/sspi_errmsg.cc @@ -0,0 +1,150 @@ +/* Copyright (c) 2015, Shuang Qiu, Robbie Harwood, +Vladislav Vaintroub & MariaDB Corporation + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#include <windows.h> +#include <stdio.h> + +#define ERRSYM(x) {x, #x} +static struct { + int error; + const char *sym; +} error_symbols[] = +{ + ERRSYM(SEC_E_OK), + ERRSYM(SEC_E_INSUFFICIENT_MEMORY), + ERRSYM(SEC_E_INVALID_HANDLE), + ERRSYM(SEC_E_UNSUPPORTED_FUNCTION), + ERRSYM(SEC_E_TARGET_UNKNOWN), + ERRSYM(SEC_E_INTERNAL_ERROR), + ERRSYM(SEC_E_SECPKG_NOT_FOUND), + ERRSYM(SEC_E_NOT_OWNER), + ERRSYM(SEC_E_CANNOT_INSTALL), + ERRSYM(SEC_E_INVALID_TOKEN), + ERRSYM(SEC_E_CANNOT_PACK), + ERRSYM(SEC_E_QOP_NOT_SUPPORTED), + ERRSYM(SEC_E_NO_IMPERSONATION), + ERRSYM(SEC_E_LOGON_DENIED), + ERRSYM(SEC_E_UNKNOWN_CREDENTIALS), + ERRSYM(SEC_E_NO_CREDENTIALS), + ERRSYM(SEC_E_MESSAGE_ALTERED), + ERRSYM(SEC_E_OUT_OF_SEQUENCE), + ERRSYM(SEC_E_NO_AUTHENTICATING_AUTHORITY), + ERRSYM(SEC_E_BAD_PKGID), + ERRSYM(SEC_E_CONTEXT_EXPIRED), + ERRSYM(SEC_E_INCOMPLETE_MESSAGE), + ERRSYM(SEC_E_INCOMPLETE_CREDENTIALS), + ERRSYM(SEC_E_BUFFER_TOO_SMALL), + ERRSYM(SEC_E_WRONG_PRINCIPAL), + ERRSYM(SEC_E_TIME_SKEW), + ERRSYM(SEC_E_UNTRUSTED_ROOT), + ERRSYM(SEC_E_ILLEGAL_MESSAGE), + ERRSYM(SEC_E_CERT_UNKNOWN), + ERRSYM(SEC_E_CERT_EXPIRED), + ERRSYM(SEC_E_ENCRYPT_FAILURE), + ERRSYM(SEC_E_DECRYPT_FAILURE), + ERRSYM(SEC_E_ALGORITHM_MISMATCH), + ERRSYM(SEC_E_SECURITY_QOS_FAILED), + ERRSYM(SEC_E_UNFINISHED_CONTEXT_DELETED), + ERRSYM(SEC_E_NO_TGT_REPLY), + ERRSYM(SEC_E_NO_IP_ADDRESSES), + ERRSYM(SEC_E_WRONG_CREDENTIAL_HANDLE), + ERRSYM(SEC_E_CRYPTO_SYSTEM_INVALID), + ERRSYM(SEC_E_MAX_REFERRALS_EXCEEDED), + ERRSYM(SEC_E_MUST_BE_KDC), + ERRSYM(SEC_E_STRONG_CRYPTO_NOT_SUPPORTED), + ERRSYM(SEC_E_TOO_MANY_PRINCIPALS), + ERRSYM(SEC_E_NO_PA_DATA), + ERRSYM(SEC_E_PKINIT_NAME_MISMATCH), + ERRSYM(SEC_E_SMARTCARD_LOGON_REQUIRED), + ERRSYM(SEC_E_SHUTDOWN_IN_PROGRESS), + ERRSYM(SEC_E_KDC_INVALID_REQUEST), + ERRSYM(SEC_E_KDC_UNABLE_TO_REFER), + ERRSYM(SEC_E_KDC_UNKNOWN_ETYPE), + ERRSYM(SEC_E_UNSUPPORTED_PREAUTH), + ERRSYM(SEC_E_DELEGATION_REQUIRED), + ERRSYM(SEC_E_BAD_BINDINGS), + ERRSYM(SEC_E_MULTIPLE_ACCOUNTS), + ERRSYM(SEC_E_NO_KERB_KEY), + ERRSYM(SEC_E_CERT_WRONG_USAGE), + ERRSYM(SEC_E_DOWNGRADE_DETECTED), + ERRSYM(SEC_E_SMARTCARD_CERT_REVOKED), + ERRSYM(SEC_E_ISSUING_CA_UNTRUSTED), + ERRSYM(SEC_E_REVOCATION_OFFLINE_C), + ERRSYM(SEC_E_PKINIT_CLIENT_FAILURE), + ERRSYM(SEC_E_SMARTCARD_CERT_EXPIRED), + ERRSYM(SEC_E_NO_S4U_PROT_SUPPORT), + ERRSYM(SEC_E_CROSSREALM_DELEGATION_FAILURE), + ERRSYM(SEC_E_REVOCATION_OFFLINE_KDC), + ERRSYM(SEC_E_ISSUING_CA_UNTRUSTED_KDC), + ERRSYM(SEC_E_KDC_CERT_EXPIRED), + ERRSYM(SEC_E_KDC_CERT_REVOKED), + ERRSYM(SEC_E_INVALID_PARAMETER), + ERRSYM(SEC_E_DELEGATION_POLICY), + ERRSYM(SEC_E_POLICY_NLTM_ONLY), + ERRSYM(SEC_E_NO_CONTEXT), + ERRSYM(SEC_E_PKU2U_CERT_FAILURE), + ERRSYM(SEC_E_MUTUAL_AUTH_FAILED), + ERRSYM(SEC_E_NO_SPM), + ERRSYM(SEC_E_NOT_SUPPORTED), + {0,0} +}; + +void sspi_errmsg(int err, char *buf, size_t size) +{ + buf[size - 1] = 0; + size_t len; + + for (size_t i= 0; error_symbols[i].sym; i++) + { + if (error_symbols[i].error == err) + { + size_t len= strlen(error_symbols[i].sym); + if (len + 2 < size) + { + memcpy(buf, error_symbols[i].sym, len); + buf[len]= ' '; + buf += len + 1; + size-= len + 1; + } + break; + } + } + + len = FormatMessageA( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, + err, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), + buf, (DWORD)size, NULL); + + if(len > 0) + { + /* Trim trailing \n\r*/ + char *p; + for(p= buf + len;p > buf && (*p == '\n' || *p=='\r' || *p == 0);p--) + *p= 0; + } +} diff --git a/plugin/auth_gssapi/sspi_server.cc b/plugin/auth_gssapi/sspi_server.cc new file mode 100644 index 00000000..4a195808 --- /dev/null +++ b/plugin/auth_gssapi/sspi_server.cc @@ -0,0 +1,463 @@ +/* Copyright (c) 2015, Shuang Qiu, Robbie Harwood, +Vladislav Vaintroub & MariaDB Corporation + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "sspi.h" +#include "common.h" +#include "server_plugin.h" +#include <mysql/plugin_auth.h> +#include <mysqld_error.h> +#include <sddl.h> + +/* This sends the error to the client */ +static void log_error(SECURITY_STATUS err, const char *msg) +{ + if (err) + { + char buf[1024]; + sspi_errmsg(err, buf, sizeof(buf)); + my_printf_error(ER_UNKNOWN_ERROR, "SSPI server error 0x%x - %s - %s", 0, err, msg, buf); + } + else + { + my_printf_error(ER_UNKNOWN_ERROR, "SSPI server error %s", 0, msg); + } + +} + +static char INVALID_KERBEROS_PRINCIPAL[] = "localhost"; + +static char *get_default_principal_name() +{ + static char default_principal[PRINCIPAL_NAME_MAX +1]; + ULONG size= sizeof(default_principal); + + if (GetUserNameEx(NameUserPrincipal,default_principal,&size)) + return default_principal; + + size= sizeof(default_principal); + if (GetUserNameEx(NameServicePrincipal,default_principal,&size)) + return default_principal; + + char domain[PRINCIPAL_NAME_MAX+1]; + char host[PRINCIPAL_NAME_MAX+1]; + size= sizeof(domain); + if (GetComputerNameEx(ComputerNameDnsDomain,domain,&size) && size > 0) + { + size= sizeof(host); + if (GetComputerNameEx(ComputerNameDnsHostname,host,&size)) + { + _snprintf(default_principal,sizeof(default_principal),"%s$@%s",host, domain); + return default_principal; + } + } + /* Unable to retrieve useful name, return something */ + return INVALID_KERBEROS_PRINCIPAL; +} + + +/* Extract client name from SSPI context */ +static int get_client_name_from_context(CtxtHandle *ctxt, + char *name, + size_t name_len, + int use_full_name) +{ + SecPkgContext_NativeNames native_names; + SECURITY_STATUS sspi_ret; + char *p; + + sspi_ret= QueryContextAttributes(ctxt, SECPKG_ATTR_NATIVE_NAMES, &native_names); + if (sspi_ret == SEC_E_OK) + { + /* Extract user from Kerberos principal name user@realm */ + if(!use_full_name) + { + p = strrchr(native_names.sClientName,'@'); + if(p) + *p = 0; + } + strncpy(name, native_names.sClientName, name_len); + + if (native_names.sClientName) + FreeContextBuffer(native_names.sClientName); + if (native_names.sServerName) + FreeContextBuffer(native_names.sServerName); + + return CR_OK; + } + + sspi_ret= ImpersonateSecurityContext(ctxt); + if (sspi_ret == SEC_E_OK) + { + ULONG len= (ULONG)name_len; + if (!GetUserNameEx(NameSamCompatible, name, &len)) + { + log_error(GetLastError(), "GetUserNameEx"); + RevertSecurityContext(ctxt); + return CR_ERROR; + } + RevertSecurityContext(ctxt); + + /* Extract user from Windows name realm\user */ + if (!use_full_name) + { + p = strrchr(name, '\\'); + if (p) + { + p++; + memmove(name, p, name + len + 1 - p); + } + } + return CR_OK; + } + + log_error(sspi_ret, "ImpersonateSecurityContext"); + return CR_ERROR; +} + + +/* + Check if username from SSPI context matches the name requested + in MYSQL_SERVER_AUTH_INFO + + There are 2 ways to specify SSPI username + username, of auth_string. + + if auth_string is used, we compare full name (i.e , with user+domain) + if not, we match just the user name. +*/ +static bool check_username_match(CtxtHandle *ctxt, + MYSQL_SERVER_AUTH_INFO *auth_info) +{ + char client_name[MYSQL_USERNAME_LENGTH + 1]; + const char *user= 0; + int compare_full_name;; + if (auth_info->auth_string_length > 0) + { + compare_full_name= 1; + user= auth_info->auth_string; + } + else + { + compare_full_name= 0; + user= auth_info->user_name; + } + if (get_client_name_from_context(ctxt, client_name, MYSQL_USERNAME_LENGTH, + compare_full_name) != CR_OK) + { + return false; + } + + /* Always compare case-insensitive on Windows. */ + if (_stricmp(client_name, user)) + { + my_printf_error(ER_ACCESS_DENIED_ERROR, + "GSSAPI name mismatch, requested '%s', actual name '%s'", 0, user, + client_name); + return false; + } + return true; +} + + +/* + Checks the security token extracted from SSPI context + for membership in specfied group. + + @param ctxt - SSPI context + @param group_name - group name to check membership against + NOTE: this can also be a user's name + + @param use_sid - whether name is SID + @last_error - will be set, if the function returns false, and + some of the API's have failed. + @failing_api - name of the API that has failed(for error logging) +*/ +static bool check_group_match(CtxtHandle *ctxt, const char *name, + bool name_is_sid) +{ + BOOL is_member= FALSE; + bool is_impersonating= false; + bool free_sid= false; + PSID sid= 0; + + +#define FAIL(msg) \ + do \ + { \ + log_error(GetLastError(), msg); \ + goto cleanup; \ + } while (0) + + /* Get the group SID.*/ + if (name_is_sid) + { + if (!ConvertStringSidToSidA(name, &sid)) + FAIL("ConvertStringSidToSid"); + free_sid= true; + } + else + { + /* Get the SID of the specified group via LookupAccountName().*/ + char sid_buf[SECURITY_MAX_SID_SIZE]; + char domain[256]; + DWORD sid_size= sizeof(sid_buf); + DWORD domain_size= sizeof(domain); + + SID_NAME_USE sid_name_use; + sid= (PSID) sid_buf; + + if (!LookupAccountName(0, name, sid, &sid_size, domain, + &domain_size, &sid_name_use)) + { + FAIL("LookupAccountName"); + } + } + + /* Impersonate, to check group membership */ + if (ImpersonateSecurityContext(ctxt)) + FAIL("ImpersonateSecurityContext"); + is_impersonating= true; + + if (!CheckTokenMembership(GetCurrentThreadToken(), sid, &is_member)) + FAIL("CheckTokenMembership"); + +cleanup: + if (is_impersonating) + RevertSecurityContext(ctxt); + if (free_sid) + LocalFree(sid); + return is_member; +} + +static SECURITY_STATUS sspi_get_context(MYSQL_PLUGIN_VIO *vio, + CtxtHandle *ctxt, CredHandle *cred) +{ + SECURITY_STATUS sspi_ret= SEC_E_OK; + ULONG attribs= 0; + TimeStamp lifetime; + SecBufferDesc inbuf_desc; + SecBuffer inbuf; + SecBufferDesc outbuf_desc; + SecBuffer outbuf; + void* out= NULL; + + SecInvalidateHandle(cred); + SecInvalidateHandle(ctxt); + + out= malloc(SSPI_MAX_TOKEN_SIZE); + if (!out) + { + log_error(SEC_E_OK, "memory allocation failed"); + sspi_ret= SEC_E_INSUFFICIENT_MEMORY; + goto cleanup; + } + sspi_ret= AcquireCredentialsHandle( + srv_principal_name, + (LPSTR)srv_mech_name, + SECPKG_CRED_INBOUND, + NULL, + NULL, + NULL, + NULL, + cred, + &lifetime); + + if (SEC_ERROR(sspi_ret)) + { + log_error(sspi_ret, "AcquireCredentialsHandle failed"); + goto cleanup; + } + + inbuf.cbBuffer= 0; + inbuf.BufferType= SECBUFFER_TOKEN; + inbuf.pvBuffer= NULL; + inbuf_desc.ulVersion= SECBUFFER_VERSION; + inbuf_desc.cBuffers= 1; + inbuf_desc.pBuffers= &inbuf; + + outbuf.BufferType= SECBUFFER_TOKEN; + outbuf.cbBuffer= SSPI_MAX_TOKEN_SIZE; + outbuf.pvBuffer= out; + + outbuf_desc.ulVersion= SECBUFFER_VERSION; + outbuf_desc.cBuffers= 1; + outbuf_desc.pBuffers= &outbuf; + + do + { + /* Read SSPI blob from client. */ + int len= vio->read_packet(vio, (unsigned char **)&inbuf.pvBuffer); + if (len < 0) + { + log_error(SEC_E_OK, "communication error(read)"); + goto cleanup; + } + inbuf.cbBuffer= len; + outbuf.cbBuffer= SSPI_MAX_TOKEN_SIZE; + sspi_ret= AcceptSecurityContext( + cred, + SecIsValidHandle(ctxt) ? ctxt : NULL, + &inbuf_desc, + attribs, + SECURITY_NATIVE_DREP, + ctxt, + &outbuf_desc, + &attribs, + &lifetime); + + if (SEC_ERROR(sspi_ret)) + { + log_error(sspi_ret, "AcceptSecurityContext"); + goto cleanup; + } + if (sspi_ret != SEC_E_OK && sspi_ret != SEC_I_CONTINUE_NEEDED) + { + log_error(sspi_ret, "AcceptSecurityContext unexpected return value"); + goto cleanup; + } + if (outbuf.cbBuffer) + { + /* Send generated blob to client. */ + if (vio->write_packet(vio, (unsigned char *)outbuf.pvBuffer, outbuf.cbBuffer)) + { + log_error(SEC_E_OK, "communicaton error(write)"); + goto cleanup; + } + } + } while (sspi_ret == SEC_I_CONTINUE_NEEDED); + +cleanup: + free(out); + return sspi_ret; +} + + +int auth_server(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *auth_info) +{ + int ret= CR_ERROR; + const char* group = 0; + bool use_sid = 0; + + CtxtHandle ctxt; + CredHandle cred; + if (sspi_get_context(vio, &ctxt, &cred) != SEC_E_OK) + goto cleanup; + + + /* + Authentication done, now test user name, or group + membership. + First, find out if matching group was requested. + */ + static struct + { + const char *str; + size_t len; + bool sid; + } prefixes[]= {{"GROUP:", sizeof("GROUP:") - 1, false}, + {"SID:", sizeof("SID:") - 1, true}}; + group= 0; + for (auto &prefix : prefixes) + { + if (auth_info->auth_string_length >= prefix.len && + !strncmp(auth_info->auth_string, prefix.str, prefix.len)) + { + group= auth_info->auth_string + prefix.len; + use_sid= prefix.sid; + break; + } + } + + if (group) + { + /* Test group membership.*/ + ret= check_group_match(&ctxt, group, use_sid) ? CR_OK : CR_ERROR; + } + else + { + /* Compare username. */ + ret= check_username_match(&ctxt, auth_info) ? CR_OK : CR_ERROR; + } + +cleanup: + if (SecIsValidHandle(&ctxt)) + DeleteSecurityContext(&ctxt); + + if (SecIsValidHandle(&cred)) + FreeCredentialsHandle(&cred); + + return ret; +} + +int plugin_init() +{ + CredHandle cred; + SECURITY_STATUS ret; + + /* + Use negotiate by default, which accepts raw kerberos + and also NTLM. + */ + if (srv_mech == PLUGIN_MECH_DEFAULT) + srv_mech= PLUGIN_MECH_SPNEGO; + + if(srv_mech == PLUGIN_MECH_KERBEROS) + srv_mech_name= "Kerberos"; + else if(srv_mech == PLUGIN_MECH_SPNEGO ) + srv_mech_name= "Negotiate"; + + if(!srv_principal_name[0]) + { + srv_principal_name= get_default_principal_name(); + } + my_printf_error(ER_UNKNOWN_ERROR, "SSPI: using principal name '%s', mech '%s'", + ME_ERROR_LOG | ME_NOTE, srv_principal_name, srv_mech_name); + + ret = AcquireCredentialsHandle( + srv_principal_name, + (LPSTR)srv_mech_name, + SECPKG_CRED_INBOUND, + NULL, + NULL, + NULL, + NULL, + &cred, + NULL); + if (SEC_ERROR(ret)) + { + log_error(ret, "AcquireCredentialsHandle"); + return -1; + } + FreeCredentialsHandle(&cred); + return 0; +} + +int plugin_deinit() +{ + return 0; +} |