/************************************************************************************ Copyright (C) 2012 Monty Program 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 *************************************************************************************/ unsigned int mariadb_deinitialize_ssl= 1; #ifdef HAVE_OPENSSL #include #include #include #include #include #include #include #include #include static my_bool my_ssl_initialized= FALSE; static SSL_CTX *SSL_context= NULL; #define MAX_SSL_ERR_LEN 100 extern pthread_mutex_t LOCK_ssl_config; static pthread_mutex_t *LOCK_crypto= NULL; /* SSL error handling */ static void my_SSL_error(MYSQL *mysql) { ulong ssl_errno= ERR_get_error(); char ssl_error[MAX_SSL_ERR_LEN]; const char *ssl_error_reason; DBUG_ENTER("my_SSL_error"); if (mysql_errno(mysql)) DBUG_VOID_RETURN; if (!ssl_errno) { my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, "Unknown SSL error"); DBUG_VOID_RETURN; } if ((ssl_error_reason= ERR_reason_error_string(ssl_errno))) { my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, ER(CR_SSL_CONNECTION_ERROR), ssl_error_reason); DBUG_VOID_RETURN; } my_snprintf(ssl_error, MAX_SSL_ERR_LEN, "SSL errno=%lu", ssl_errno, mysql->charset); my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, ER(CR_SSL_CONNECTION_ERROR), ssl_error); DBUG_VOID_RETURN; } /* thread safe callbacks for OpenSSL Crypto call back functions will be set during ssl_initialization */ #if OPENSSL_VERSION_NUMBER < 0x10100000 #if (OPENSSL_VERSION_NUMBER < 0x10000000) static unsigned long my_cb_threadid(void) { /* cast pthread_t to unsigned long */ return (unsigned long) pthread_self(); } #else static void my_cb_threadid(CRYPTO_THREADID *id) { CRYPTO_THREADID_set_numeric(id, (unsigned long)pthread_self()); } #endif static void my_cb_locking(int mode, int n, const char *file, int line) { if (mode & CRYPTO_LOCK) pthread_mutex_lock(&LOCK_crypto[n]); else pthread_mutex_unlock(&LOCK_crypto[n]); } static int ssl_crypto_init() { int i, rc= 1, max= CRYPTO_num_locks(); #if (OPENSSL_VERSION_NUMBER < 0x10000000) CRYPTO_set_id_callback(my_cb_threadid); #else rc= CRYPTO_THREADID_set_callback(my_cb_threadid); #endif /* if someone else already set callbacks * there is nothing do */ if (!rc) return 0; if (LOCK_crypto == NULL) { if (!(LOCK_crypto= (pthread_mutex_t *)my_malloc(sizeof(pthread_mutex_t) * max, MYF(0)))) return 1; for (i=0; i < max; i++) pthread_mutex_init(&LOCK_crypto[i], NULL); } CRYPTO_set_locking_callback(my_cb_locking); return 0; } #endif /* Initializes SSL and allocate global context SSL_context SYNOPSIS my_ssl_start mysql connection handle RETURN VALUES 0 success 1 error */ int my_ssl_start(MYSQL *mysql) { int rc= 0; DBUG_ENTER("my_ssl_start"); /* lock mutex to prevent multiple initialization */ pthread_mutex_lock(&LOCK_ssl_config); if (!my_ssl_initialized) { #if OPENSSL_VERSION_NUMBER < 0x10100000 if (ssl_crypto_init()) goto end; #endif #if OPENSSL_VERSION_NUMBER >= 0x10100000L OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, NULL); #else SSL_library_init(); #if SSLEAY_VERSION_NUMBER >= 0x00907000L OPENSSL_config(NULL); #endif #endif /* load errors */ SSL_load_error_strings(); /* digests and ciphers */ OpenSSL_add_all_algorithms(); #if OPENSSL_VERSION_NUMBER >= 0x10100000L if (!(SSL_context= SSL_CTX_new(TLS_client_method()))) #else if (!(SSL_context= SSL_CTX_new(SSLv23_client_method()))) #endif { my_SSL_error(mysql); rc= 1; goto end; } /* use server preferences instead of client preferences: client sends lowest tls version (=1.0) first, and will update to the version the server sent in client hellp response packet */ SSL_CTX_set_options(SSL_context, SSL_OP_ALL); my_ssl_initialized= TRUE; } end: pthread_mutex_unlock(&LOCK_ssl_config); DBUG_RETURN(rc); } /* Release SSL and free resources Will be automatically executed by mysql_server_end() function SYNOPSIS my_ssl_end() void RETURN VALUES void */ void my_ssl_end() { DBUG_ENTER("my_ssl_end"); pthread_mutex_lock(&LOCK_ssl_config); if (my_ssl_initialized) { int i; if (LOCK_crypto) { CRYPTO_set_locking_callback(NULL); CRYPTO_set_id_callback(NULL); for (i=0; i < CRYPTO_num_locks(); i++) pthread_mutex_destroy(&LOCK_crypto[i]); my_free(LOCK_crypto); LOCK_crypto= NULL; } if (SSL_context) { SSL_CTX_free(SSL_context); SSL_context= FALSE; } if (mariadb_deinitialize_ssl) { #if OPENSSL_VERSION_NUMBER < 0x10100000 ERR_remove_state(0); #endif EVP_cleanup(); CRYPTO_cleanup_all_ex_data(); ERR_free_strings(); CONF_modules_free(); CONF_modules_unload(1); } my_ssl_initialized= FALSE; } pthread_mutex_unlock(&LOCK_ssl_config); pthread_mutex_destroy(&LOCK_ssl_config); DBUG_VOID_RETURN; } /* Set certification stuff. */ static int my_ssl_set_certs(MYSQL *mysql) { char *certfile= mysql->options.ssl_cert, *keyfile= mysql->options.ssl_key; DBUG_ENTER("my_ssl_set_certs"); /* Make sure that ssl was allocated and ssl_system was initialized */ DBUG_ASSERT(my_ssl_initialized == TRUE); /* add cipher */ if ((mysql->options.ssl_cipher && mysql->options.ssl_cipher[0] != 0) && SSL_CTX_set_cipher_list(SSL_context, mysql->options.ssl_cipher) == 0) goto error; /* ca_file and ca_path */ if (SSL_CTX_load_verify_locations(SSL_context, mysql->options.ssl_ca, mysql->options.ssl_capath) == 0) { if (mysql->options.ssl_ca || mysql->options.ssl_capath) goto error; if (SSL_CTX_set_default_verify_paths(SSL_context) == 0) goto error; } if (keyfile && !certfile) certfile= keyfile; if (certfile && !keyfile) keyfile= certfile; /* set cert */ if (certfile && certfile[0] != 0) if (SSL_CTX_use_certificate_file(SSL_context, certfile, SSL_FILETYPE_PEM) != 1) goto error; /* set key */ if (keyfile && keyfile[0]) { if (SSL_CTX_use_PrivateKey_file(SSL_context, keyfile, SSL_FILETYPE_PEM) != 1) goto error; } /* verify key */ if (certfile && !SSL_CTX_check_private_key(SSL_context)) goto error; if (mysql->options.extension && (mysql->options.extension->ssl_crl || mysql->options.extension->ssl_crlpath)) { X509_STORE *certstore; if ((certstore= SSL_CTX_get_cert_store(SSL_context))) { if (X509_STORE_load_locations(certstore, mysql->options.extension->ssl_crl, mysql->options.extension->ssl_crlpath) == 0 || X509_STORE_set_flags(certstore, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL) == 0) goto error; } } DBUG_RETURN(0); error: my_SSL_error(mysql); DBUG_RETURN(1); } 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) { /* SHA1 fingerprint (160 bit) / 8 * 2 + 1 */ char hexstr[41]; fp1_len= (unsigned int)mysql_hex_string(hexstr, fp1, fp1_len); #ifdef _WIN32 if (_strnicmp(hexstr, fp2, fp1_len) != 0) #else if (strncasecmp(hexstr, fp2, fp1_len) != 0) #endif return 1; return 0; } /* allocates a new ssl object SYNOPSIS my_ssl_init mysql connection object RETURN VALUES NULL on error SSL new SSL object */ SSL *my_ssl_init(MYSQL *mysql) { SSL *ssl= NULL; DBUG_ENTER("my_ssl_init"); DBUG_ASSERT(mysql->net.vio->ssl == NULL); if (!my_ssl_initialized) my_ssl_start(mysql); pthread_mutex_lock(&LOCK_ssl_config); if (my_ssl_set_certs(mysql)) goto error; if (!(ssl= SSL_new(SSL_context))) goto error; if (!SSL_set_app_data(ssl, mysql)) goto error; pthread_mutex_unlock(&LOCK_ssl_config); DBUG_RETURN(ssl); error: pthread_mutex_unlock(&LOCK_ssl_config); if (ssl) SSL_free(ssl); DBUG_RETURN(NULL); } /* establish SSL connection between client and server SYNOPSIS my_ssl_connect ssl ssl object RETURN VALUES 0 success 1 error */ int my_ssl_connect(SSL *ssl) { my_bool blocking; MYSQL *mysql; long rc; my_bool try_connect= 1; DBUG_ENTER("my_ssl_connect"); DBUG_ASSERT(ssl != NULL); mysql= (MYSQL *)SSL_get_app_data(ssl); CLEAR_CLIENT_ERROR(mysql); /* Set socket to non blocking */ if (!(blocking= vio_is_blocking(mysql->net.vio))) vio_blocking(mysql->net.vio, FALSE, 0); SSL_clear(ssl); SSL_SESSION_set_timeout(SSL_get_session(ssl), mysql->options.connect_timeout); SSL_set_fd(ssl, mysql->net.vio->sd); while (try_connect && (rc= SSL_connect(ssl)) == -1) { switch(SSL_get_error(ssl, rc)) { case SSL_ERROR_WANT_READ: if (vio_wait_or_timeout(mysql->net.vio, TRUE, mysql->options.connect_timeout) < 1) try_connect= 0; break; case SSL_ERROR_WANT_WRITE: if (vio_wait_or_timeout(mysql->net.vio, TRUE, mysql->options.connect_timeout) < 1) try_connect= 0; break; default: try_connect= 0; } } if (rc != 1) { my_SSL_error(mysql); DBUG_RETURN(1); } if ((mysql->client_flag & CLIENT_SSL_VERIFY_SERVER_CERT)) { rc= SSL_get_verify_result(ssl); if (rc != X509_V_OK) { my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, ER(CR_SSL_CONNECTION_ERROR), X509_verify_cert_error_string(rc)); /* restore blocking mode */ if (!blocking) vio_blocking(mysql->net.vio, FALSE, 0); DBUG_RETURN(1); } } vio_reset(mysql->net.vio, VIO_TYPE_SSL, mysql->net.vio->sd, 0, 0); mysql->net.vio->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 SYNOPSIS my_ssl_verify_server_cert() MYSQL mysql mybool verify_server_cert; RETURN VALUES 1 Error 0 OK */ int my_ssl_verify_server_cert(SSL *ssl) { X509 *cert; MYSQL *mysql; X509_NAME *x509sn; int cn_pos; X509_NAME_ENTRY *cn_entry; ASN1_STRING *cn_asn1; const char *cn_str; DBUG_ENTER("my_ssl_verify_server_cert"); mysql= (MYSQL *)SSL_get_app_data(ssl); if (!mysql->host) { my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, ER(CR_SSL_CONNECTION_ERROR), "Invalid (empty) hostname"); DBUG_RETURN(1); } if (!(cert= SSL_get_peer_certificate(ssl))) { my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, ER(CR_SSL_CONNECTION_ERROR), "Unable to get server certificate"); DBUG_RETURN(1); } x509sn= X509_get_subject_name(cert); if ((cn_pos= X509_NAME_get_index_by_NID(x509sn, NID_commonName, -1)) < 0) goto error; if (!(cn_entry= X509_NAME_get_entry(x509sn, cn_pos))) goto error; if (!(cn_asn1 = X509_NAME_ENTRY_get_data(cn_entry))) goto error; cn_str = (char *)ASN1_STRING_data(cn_asn1); /* Make sure there is no embedded \0 in the CN */ if ((size_t)ASN1_STRING_length(cn_asn1) != strlen(cn_str)) goto error; if (strcmp(cn_str, mysql->host)) goto error; X509_free(cert); DBUG_RETURN(0); error: X509_free(cert); my_set_error(mysql, CR_SSL_CONNECTION_ERROR, SQLSTATE_UNKNOWN, ER(CR_SSL_CONNECTION_ERROR), "Validation of SSL server certificate failed"); DBUG_RETURN(1); } /* write to ssl socket SYNOPSIS my_ssl_write() vio vio buf write buffer size size of buffer RETURN VALUES bytes written */ size_t my_ssl_write(Vio *vio, const uchar* buf, size_t size) { size_t written; DBUG_ENTER("my_ssl_write"); if (vio->async_context && vio->async_context->active) written= my_ssl_write_async(vio->async_context, (SSL *)vio->ssl, buf, size); else written= SSL_write((SSL*) vio->ssl, buf, size); DBUG_RETURN(written); } /* read from ssl socket SYNOPSIS my_ssl_read() vio vio buf read buffer size_t max number of bytes to read RETURN VALUES number of bytes read */ size_t my_ssl_read(Vio *vio, uchar* buf, size_t size) { size_t read; DBUG_ENTER("my_ssl_read"); if (vio->async_context && vio->async_context->active) read= my_ssl_read_async(vio->async_context, (SSL *)vio->ssl, buf, size); else read= SSL_read((SSL*) vio->ssl, buf, size); DBUG_RETURN(read); } /* close ssl connection and free ssl object SYNOPSIS my_ssl_close() vio vio RETURN VALUES 1 ok 0 or -1 on error */ int my_ssl_close(Vio *vio) { int i, rc; DBUG_ENTER("my_ssl_close"); if (!vio || !vio->ssl) DBUG_RETURN(1); SSL_set_quiet_shutdown(vio->ssl, 1); /* 2 x pending + 2 * data = 4 */ for (i=0; i < 4; i++) if ((rc= SSL_shutdown(vio->ssl))) break; SSL_free(vio->ssl); vio->ssl= NULL; DBUG_RETURN(rc); } #endif /* HAVE_OPENSSL */