From 28dadb01797c22c81069e92803619729d0deb1fa Mon Sep 17 00:00:00 2001 From: Georg Richter Date: Thu, 11 Jun 2015 13:21:25 +0200 Subject: [PATCH 1/2] - OpenSSL security: report an error if client requires SSL but server doesn't support SSL (MTM attack) new options MARIADB_OPT_SSL_FP for fingerprint of server certificate MARIADB_OPT_SSL_FP_LIST for white list of finger prints. --- include/ma_common.h | 2 + include/ma_secure.h | 1 + include/mysql.h | 7 +- libmariadb/libmariadb.c | 26 +++++-- libmariadb/ma_secure.c | 108 ++++++++++++++++++++++++++++- libmariadb/my_auth.c | 47 +++++++++++++ unittest/libmariadb/CMakeLists.txt | 11 +++ unittest/libmariadb/ssl.c.in | 54 +++++++++++++++ 8 files changed, 248 insertions(+), 8 deletions(-) diff --git a/include/ma_common.h b/include/ma_common.h index 599d6398..1871c734 100644 --- a/include/ma_common.h +++ b/include/ma_common.h @@ -49,6 +49,8 @@ struct st_mysql_options_extension { const char *proc_info, unsigned int proc_info_length); MARIADB_DB_DRIVER *db_driver; + char *ssl_fp; /* finger print of server certificate */ + char *ssl_fp_list; /* white list of finger prints */ }; diff --git a/include/ma_secure.h b/include/ma_secure.h index 148d65bd..b380630a 100644 --- a/include/ma_secure.h +++ b/include/ma_secure.h @@ -36,6 +36,7 @@ size_t my_ssl_write(Vio *vio, const uchar* buf, size_t size); SSL *my_ssl_init(MYSQL *mysql); int my_ssl_connect(SSL *ssl); int my_ssl_verify_server_cert(SSL *ssl); +int ma_ssl_verify_fingerprint(SSL *ssl); int my_ssl_start(MYSQL *mysql); void my_ssl_end(void); diff --git a/include/mysql.h b/include/mysql.h index 12a7ee87..515c3b87 100644 --- a/include/mysql.h +++ b/include/mysql.h @@ -209,12 +209,15 @@ extern unsigned int mariadb_deinitialize_ssl; /* MariaDB specific */ MYSQL_PROGRESS_CALLBACK=5999, MYSQL_OPT_NONBLOCK, - MYSQL_DATABASE_DRIVER=7000 + /* MariaDB Connector/C specific */ + MYSQL_DATABASE_DRIVER=7000, + MARIADB_OPT_SSL_FP, /* single finger print for server certificate verification */ + MARIADB_OPT_SSL_FP_LIST /* finger print white list for server certificate verification */ }; enum mysql_status { MYSQL_STATUS_READY, MYSQL_STATUS_GET_RESULT, - MYSQL_STATUS_USE_RESULT, + MYSQL_STATUS_USE_RESULT, MYSQL_STATUS_QUERY_SENT, MYSQL_STATUS_SENDING_LOAD_DATA, MYSQL_STATUS_FETCHING_DATA, diff --git a/libmariadb/libmariadb.c b/libmariadb/libmariadb.c index 1d8b2e80..f434a847 100644 --- a/libmariadb/libmariadb.c +++ b/libmariadb/libmariadb.c @@ -844,6 +844,7 @@ static const char *default_options[]= "ssl-cipher", "max-allowed-packet", "protocol", "shared-memory-base-name", "multi-results", "multi-statements", "multi-queries", "secure-auth", "report-data-truncation", "plugin-dir", "default-auth", "database-type", + "ssl-fp", "ssl-fp-list", NULL }; @@ -857,7 +858,8 @@ enum option_val OPT_connect_timeout, OPT_local_infile, OPT_disable_local_infile, OPT_ssl_cipher, OPT_max_allowed_packet, OPT_protocol, OPT_shared_memory_base_name, OPT_multi_results, OPT_multi_statements, OPT_multi_queries, OPT_secure_auth, - OPT_report_data_truncation, OPT_plugin_dir, OPT_default_auth, OPT_db_type + OPT_report_data_truncation, OPT_plugin_dir, OPT_default_auth, OPT_db_type, + OPT_ssl_fp, OPT_ssl_fp_list }; #define CHECK_OPT_EXTENSION_SET(OPTS)\ @@ -990,7 +992,7 @@ static void mysql_read_default_options(struct st_mysql_options *options, case OPT_return_found_rows: options->client_flag|=CLIENT_FOUND_ROWS; break; - #ifdef HAVE_OPENSSL +#ifdef HAVE_OPENSSL case OPT_ssl_key: my_free(options->ssl_key); options->ssl_key = my_strdup(opt_arg, MYF(MY_WME)); @@ -1009,12 +1011,20 @@ static void mysql_read_default_options(struct st_mysql_options *options, break; case OPT_ssl_cipher: break; + case OPT_ssl_fp: + OPT_SET_EXTENDED_VALUE(options, ssl_fp, opt_arg, 1); + break; + case OPT_ssl_fp_list: + OPT_SET_EXTENDED_VALUE(options, ssl_fp_list, opt_arg, 1); + break; #else case OPT_ssl_key: case OPT_ssl_cert: case OPT_ssl_ca: case OPT_ssl_capath: case OPT_ssl_cipher: + case OPT_ssl_fp: + case OPT_ssl_fp_list: break; #endif /* HAVE_OPENSSL */ case OPT_charset_dir: @@ -2190,6 +2200,8 @@ static void mysql_close_options(MYSQL *mysql) my_free(mysql->options.extension->db_driver); my_free(mysql->options.extension->ssl_crl); my_free(mysql->options.extension->ssl_crlpath); + my_free(mysql->options.extension->ssl_fp); + my_free(mysql->options.extension->ssl_fp_list); if(hash_inited(&mysql->options.extension->connect_attrs)) hash_free(&mysql->options.extension->connect_attrs); } @@ -3035,6 +3047,12 @@ mysql_optionsv(MYSQL *mysql,enum mysql_option option, ...) case MYSQL_OPT_SSL_CRLPATH: OPT_SET_EXTENDED_VALUE(&mysql->options, ssl_crlpath, (char *)arg1, 1); break; + case MARIADB_OPT_SSL_FP: + OPT_SET_EXTENDED_VALUE(&mysql->options, ssl_fp, (char *)arg1, 1); + break; + case MARIADB_OPT_SSL_FP_LIST: + OPT_SET_EXTENDED_VALUE(&mysql->options, ssl_fp_list, (char *)arg1, 1); + break; case MYSQL_OPT_CONNECT_ATTR_DELETE: { uchar *p; @@ -3492,8 +3510,8 @@ ulong STDCALL mysql_hex_string(char *to, const char *from, while (len--) { - *to++= hexdigits[*from >> 4]; - *to++= hexdigits[*from & 0x0F]; + *to++= hexdigits[((unsigned char)*from) >> 4]; + *to++= hexdigits[((unsigned char)*from) & 0x0F]; from++; } *to= 0; diff --git a/libmariadb/ma_secure.c b/libmariadb/ma_secure.c index 8c5f7eef..cda76f38 100644 --- a/libmariadb/ma_secure.c +++ b/libmariadb/ma_secure.c @@ -298,7 +298,28 @@ error: DBUG_RETURN(1); } -static int my_verify_callback(int ok, X509_STORE_CTX *ctx) +static unsigned int ma_get_cert_fingerprint(X509 *cert, EVP_MD *digest, + unsigned char *fingerprint, unsigned int *fp_length) +{ + if (*fp_length < EVP_MD_size(digest)) + return 0; + if (!X509_digest(cert, digest, fingerprint, fp_length)) + return 0; + return *fp_length; +} + +static my_bool ma_check_fingerprint(char *fp1, unsigned int fp1_len, + char *fp2, unsigned int fp2_len) +{ + char hexstr[fp1_len * 2 + 1]; + + fp1_len= (unsigned int)mysql_hex_string(hexstr, fp1, fp1_len); + if (strncasecmp(hexstr, fp2, fp1_len) != 0) + return 1; + return 0; +} + +int my_verify_callback(int ok, X509_STORE_CTX *ctx) { X509 *check_cert; SSL *ssl; @@ -400,7 +421,7 @@ int my_ssl_connect(SSL *ssl) { my_bool blocking; MYSQL *mysql; - int rc; + long rc; DBUG_ENTER("my_ssl_connect"); @@ -444,6 +465,89 @@ int my_ssl_connect(SSL *ssl) DBUG_RETURN(0); } +int ma_ssl_verify_fingerprint(SSL *ssl) +{ + X509 *cert= SSL_get_peer_certificate(ssl); + MYSQL *mysql= (MYSQL *)SSL_get_app_data(ssl); + unsigned char fingerprint[EVP_MAX_MD_SIZE]; + EVP_MD *digest; + unsigned int fp_length; + + DBUG_ENTER("ma_ssl_verify_fingerprint"); + + if (!cert) + { + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Unable to get server certificate"); + DBUG_RETURN(1); + } + + digest= (EVP_MD *)EVP_sha1(); + fp_length= sizeof(fingerprint); + + if (!ma_get_cert_fingerprint(cert, digest, fingerprint, &fp_length)) + { + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Unable to get finger print of server certificate"); + DBUG_RETURN(1); + } + + /* single finger print was specified */ + if (mysql->options.extension->ssl_fp) + { + if (ma_check_fingerprint(fingerprint, fp_length, mysql->options.extension->ssl_fp, + strlen(mysql->options.extension->ssl_fp))) + { + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "invalid finger print of server certificate"); + DBUG_RETURN(1); + } + } + + /* white list of finger prints was specified */ + if (mysql->options.extension->ssl_fp_list) + { + FILE *fp; + char buff[255]; + + if (!(fp = my_fopen(mysql->options.extension->ssl_fp_list ,O_RDONLY, MYF(0)))) + { + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Can't open finger print list"); + DBUG_RETURN(1); + } + + while (fgets(buff, sizeof(buff)-1, fp)) + { + /* remove trailing new line character */ + char *pos= strchr(buff, '\r'); + if (!pos) + pos= strchr(buff, '\n'); + if (pos) + *pos= '\0'; + + if (!ma_check_fingerprint(fingerprint, fp_length, buff, strlen(buff))) + { + /* finger print is valid: close file and exit */ + my_fclose(fp, MYF(0)); + DBUG_RETURN(0); + } + } + + /* No finger print matched - close file and return error */ + my_fclose(fp, MYF(0)); + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "invalid finger print of server certificate"); + DBUG_RETURN(1); + } + DBUG_RETURN(0); +} + /* verify server certificate diff --git a/libmariadb/my_auth.c b/libmariadb/my_auth.c index e5ddfe20..08300ce2 100644 --- a/libmariadb/my_auth.c +++ b/libmariadb/my_auth.c @@ -1,3 +1,26 @@ +/************************************************************************************ + Copyright (C) 2012-2015 Monty Program AB, MariaDB Corporation AB, + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not see + or write to the Free Software Foundation, Inc., + 51 Franklin St., Fifth Floor, Boston, MA 02110, USA + + Part of this code includes code from the PHP project which + is freely available from http://www.php.net + + Originally written by Sergei Golubchik +*************************************************************************************/ #include #include #include @@ -253,6 +276,22 @@ static int send_client_reply_packet(MCPVIO_EXT *mpvio, mysql->options.use_ssl= 1; if (mysql->options.use_ssl) mysql->client_flag|= CLIENT_SSL; + + /* if server doesn't support SSL and verification of server certificate + was set to mandator, we need to return an error */ + if (mysql->options.use_ssl && !(mysql->server_capabilities & CLIENT_SSL)) + { + if ((mysql->client_flag & CLIENT_SSL_VERIFY_SERVER_CERT) || + (mysql->options.extension && (mysql->options.extension->ssl_fp || + mysql->options.extension->ssl_fp_list))) + { + my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, + ER(CR_SSL_CONNECTION_ERROR), + "Server doesn't support SSL"); + goto error; + } + } + #endif /* HAVE_OPENSSL && !EMBEDDED_LIBRARY*/ if (mpvio->db) mysql->client_flag|= CLIENT_CONNECT_WITH_DB; @@ -294,6 +333,7 @@ static int send_client_reply_packet(MCPVIO_EXT *mpvio, #endif ) mysql->options.use_ssl= 1; + if (mysql->options.use_ssl && (mysql->client_flag & CLIENT_SSL)) { @@ -322,6 +362,13 @@ static int send_client_reply_packet(MCPVIO_EXT *mpvio, goto error; } + if (mysql->options.extension && + (mysql->options.extension->ssl_fp || mysql->options.extension->ssl_fp_list)) + { + if (ma_ssl_verify_fingerprint(ssl)) + goto error; + } + if ((mysql->options.ssl_ca || mysql->options.ssl_capath) && (mysql->client_flag & CLIENT_SSL_VERIFY_SERVER_CERT) && my_ssl_verify_server_cert(ssl)) diff --git a/unittest/libmariadb/CMakeLists.txt b/unittest/libmariadb/CMakeLists.txt index 969349d8..086f0416 100644 --- a/unittest/libmariadb/CMakeLists.txt +++ b/unittest/libmariadb/CMakeLists.txt @@ -21,8 +21,19 @@ INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/unittest/mytap) ADD_DEFINITIONS(-DLIBMARIADB) +# Get finger print from server certificate +EXECUTE_PROCESS(COMMAND openssl x509 -in server-cert.pem -sha1 -fingerprint -noout + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/unittest/libmariadb/certs + OUTPUT_VARIABLE FINGER_PRINT) +STRING(REPLACE "SHA1 Fingerprint=" "" FINGER_PRINT ${FINGER_PRINT}) +STRING(REPLACE "\n" "" FINGER_PRINT ${FINGER_PRINT}) +STRING(REPLACE ":" "" SSL_CERT_FINGER_PRINT ${FINGER_PRINT}) +MESSAGE(STATUS "FINGER_PRINT ${SSL_CERT_FINGER_PRINT}") + CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/unittest/libmariadb/ssl.c.in ${CMAKE_SOURCE_DIR}/unittest/libmariadb/ssl.c) +CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/unittest/libmariadb/fingerprint.list.in + ${CMAKE_SOURCE_DIR}/unittest/libmariadb/fingerprint.list) SET(API_TESTS "async" "basic-t" "fetch" "charset" "logs" "cursor" "errors" "view" "ps" "ps_bugs" "sp" "result" "connection" "misc" "ssl" "ps_new" "sqlite3" "thread" "dyncol") diff --git a/unittest/libmariadb/ssl.c.in b/unittest/libmariadb/ssl.c.in index 71f63c4c..0ba3dd25 100644 --- a/unittest/libmariadb/ssl.c.in +++ b/unittest/libmariadb/ssl.c.in @@ -380,6 +380,7 @@ static int test_conc127(MYSQL *my) mysql_real_connect(mysql, hostname, ssluser, sslpw, schema, port, socketname, 0); + diag("Error: %s", mysql_error(mysql)); FAIL_IF(mysql_errno(mysql) == 0, "Error expected (invalid certificate)"); mysql_close(mysql); @@ -615,10 +616,63 @@ static int test_conc_102(MYSQL *mysql) return OK; } +const char *ssl_cert_finger_print= "@SSL_CERT_FINGER_PRINT@"; + +static int test_ssl_fp(MYSQL *unused) +{ + MYSQL *my; + char *cipher; + + if (check_skip_ssl()) + return SKIP; + + my= mysql_init(NULL); + FAIL_IF(!my, "mysql_init() failed"); + + mysql_ssl_set(my,0, 0, "@CMAKE_SOURCE_DIR@/unittest/libmariadb/certs/ca-cert.pem", 0, 0); + + mysql_options(my, MARIADB_OPT_SSL_FP, ssl_cert_finger_print); + + FAIL_IF(!mysql_real_connect(my, hostname, ssluser, sslpw, schema, + port, socketname, 0), mysql_error(my)); + + cipher= (char *)mysql_get_ssl_cipher(my); + FAIL_IF(strcmp(cipher, "DHE-RSA-AES256-SHA") != 0, "Cipher != DHE-RSA-AES256-SHA"); + mysql_close(my); + return OK; +} + +static int test_ssl_fp_list(MYSQL *unused) +{ + MYSQL *my; + char *cipher; + + if (check_skip_ssl()) + return SKIP; + + my= mysql_init(NULL); + FAIL_IF(!my, "mysql_init() failed"); + + mysql_ssl_set(my,0, 0, "@CMAKE_SOURCE_DIR@/unittest/libmariadb/certs/ca-cert.pem", 0, 0); + + mysql_options(my, MARIADB_OPT_SSL_FP_LIST, "./fingerprint.list"); + + FAIL_IF(!mysql_real_connect(my, hostname, ssluser, sslpw, schema, + port, socketname, 0), mysql_error(my)); + + cipher= (char *)mysql_get_ssl_cipher(my); + FAIL_IF(strcmp(cipher, "DHE-RSA-AES256-SHA") != 0, "Cipher != DHE-RSA-AES256-SHA"); + mysql_close(my); + return OK; +} + + struct my_tests_st my_tests[] = { {"test_ssl", test_ssl, TEST_CONNECTION_NEW, 0, NULL, NULL}, {"test_conc127", test_conc127, TEST_CONNECTION_NEW, 0, NULL, NULL}, + {"test_ssl_fp", test_ssl_fp, TEST_CONNECTION_NEW, 0, NULL, NULL}, + {"test_ssl_fp_list", test_ssl_fp_list, TEST_CONNECTION_NEW, 0, NULL, NULL}, {"test_conc50", test_conc50, TEST_CONNECTION_NEW, 0, NULL, NULL}, {"test_conc50_1", test_conc50_1, TEST_CONNECTION_NEW, 0, NULL, NULL}, {"test_conc50_2", test_conc50_2, TEST_CONNECTION_NEW, 0, NULL, NULL}, From ae96108081525a495139337049bc33598fd25e44 Mon Sep 17 00:00:00 2001 From: Georg Richter Date: Wed, 17 Jun 2015 09:54:31 +0200 Subject: [PATCH 2/2] added missing fingerprint white list --- unittest/libmariadb/fingerprint.list.in | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 unittest/libmariadb/fingerprint.list.in diff --git a/unittest/libmariadb/fingerprint.list.in b/unittest/libmariadb/fingerprint.list.in new file mode 100644 index 00000000..06f2be28 --- /dev/null +++ b/unittest/libmariadb/fingerprint.list.in @@ -0,0 +1,4 @@ +86DD6D764C0CA47C5014E1E7802674BFDB674ED3 +@SSL_CERT_FINGER_PRINT@ +73F1FEC1FE041473563BFF2D624B88F6CFCFE626 +