SSL handshake now successfully completes when a client connects with SSL enabled.

This commit is contained in:
Markus Makela 2015-06-01 13:50:22 +03:00
parent 0f814d3e73
commit a2768955e7
6 changed files with 253 additions and 179 deletions

View File

@ -0,0 +1,13 @@
# MaxScale and SSL
MaxScale supports client side SSL connections. Enabling is done on a per service basis and each service has its own set of certificates.
## SSL Options
Here are the options which relate to SSL and certificates.
Parameter|Values|Description
----------------------------
ssl | disabled, enabled, required |`disable` disables SSL, `enabled` enables SSL for client connections but still allows non-SSL connections and `required` requires SSL from all client connections. With the `required` option, client connections that do not use SSL will be rejected.
ssl_cert | <path to file> |Path to server certificate
ssl_key | <path to file> |Path to server private key
ssl_ca_cert | <path to file> |Path to Certificate Authority file

View File

@ -941,18 +941,12 @@ int dcb_read_SSL(
/** Handle closed client socket */
if (dcb_isclient(dcb))
{
char c;
int l_errno = 0;
char c = 0;
int r = -1;
/* try to read 1 byte, without consuming the socket buffer */
r = recv(dcb->fd, &c, sizeof(char), MSG_PEEK);
l_errno = errno;
if (r <= 0 &&
l_errno != EAGAIN &&
l_errno != EWOULDBLOCK &&
l_errno != 0)
r = SSL_peek(ssl, &c, sizeof(char));
if (r <= 0)
{
n = -1;
goto return_n;
@ -989,13 +983,15 @@ int dcb_read_SSL(
n = -1;
goto return_n;
}
GW_NOINTR_CALL(n = SSL_read(ssl, GWBUF_DATA(buffer), bufsize);
dcb->stats.n_reads++);
n = SSL_read(ssl, GWBUF_DATA(buffer), bufsize);
dcb->stats.n_reads++;
int ssl_errno = 0;
if (n <= 0)
{
int ssl_errno = ERR_get_error();
if(ssl_errno != SSL_ERROR_WANT_READ)
ssl_errno = ERR_get_error();
if(ssl_errno != SSL_ERROR_WANT_READ && ssl_errno != SSL_ERROR_NONE)
{
LOGIF(LE, (skygw_log_write_flush(
LOGFILE_ERROR,
@ -1023,6 +1019,8 @@ int dcb_read_SSL(
dcb->fd)));
/*< Append read data to the gwbuf */
*head = gwbuf_append(*head, buffer);
if(ssl_errno == SSL_ERROR_WANT_READ || ssl_errno == SSL_ERROR_NONE)
break;
} /*< while (true) */
return_n:
return n;

View File

@ -196,7 +196,9 @@ static bool resolve_maxscale_conf_fname(
static char* check_dir_access(char* dirname,bool,bool);
static int set_user();
static void maxscale_ssl_lock(int mode,int n,const char* file, int line);
static unsigned long maxscale_ssl_id();
static SPINLOCK* ssl_locks;
/**
* Handler for SIGHUP signal. Reload the configuration for the
* gateway.
@ -1370,7 +1372,23 @@ int main(int argc, char **argv)
rc = MAXSCALE_INTERNALERROR;
goto return_main;
}
/** OpenSSL initialization */
SSL_library_init();
SSL_load_error_strings();
int n_locks = CRYPTO_num_locks();
if((ssl_locks = malloc(n_locks*sizeof(SPINLOCK))) == NULL)
{
rc = MAXSCALE_INTERNALERROR;
goto return_main;
}
for(i = 0;i<n_locks;i++)
spinlock_init(&ssl_locks[i]);
CRYPTO_set_locking_callback(maxscale_ssl_lock);
CRYPTO_set_id_callback(maxscale_ssl_id);
/* register exit function for embedded MySQL library */
l = atexit(libmysqld_done);
@ -2002,3 +2020,20 @@ static int set_user(char* user)
return rval;
}
static void maxscale_ssl_lock(int mode,int n,const char* file, int line)
{
if(mode & CRYPTO_LOCK)
{
spinlock_acquire(&ssl_locks[n]);
}
else
{
spinlock_release(&ssl_locks[n]);
}
}
static unsigned long maxscale_ssl_id()
{
return (unsigned long)pthread_self();
}

View File

@ -1843,7 +1843,7 @@ int serviceInitSSL(SERVICE* service)
SSL_CTX_set_verify(service->ctx,SSL_VERIFY_PEER,NULL);
/* Set the verification depth to 1 */
SSL_CTX_set_verify_depth(service->ctx,10);
SSL_CTX_set_verify_depth(service->ctx,1);
service->ssl_init_done = true;
}
return 0;

View File

@ -99,10 +99,11 @@ typedef enum {
MYSQL_AUTH_RECV,
MYSQL_AUTH_FAILED,
MYSQL_HANDSHAKE_FAILED,
MYSQL_AUTH_SSL_REQ, /*< client requested SSL */
MYSQL_AUTH_SSL_EXCHANGE_DONE, /*< SSL handshake done */
MYSQL_AUTH_SSL_EXCHANGE_ERR, /*< SSL handshake failure */
MYSQL_AUTH_SSL_RECV, /*< */
MYSQL_AUTH_SSL_REQ, /*< client requested SSL but SSL_accept hasn't beed called */
MYSQL_AUTH_SSL_HANDSHAKE_DONE, /*< SSL handshake has been fully completed */
MYSQL_AUTH_SSL_HANDSHAKE_FAILED, /*< SSL handshake failed for any reason */
MYSQL_AUTH_SSL_HANDSHAKE_ONGOING, /*< SSL_accept has been called but the
* SSL handshake hasn't been completed */
MYSQL_IDLE
} mysql_auth_state_t;

View File

@ -117,8 +117,6 @@ version()
void
ModuleInit()
{
SSL_library_init();
SSL_load_error_strings();
}
/**
@ -466,7 +464,7 @@ static int gw_mysql_do_authentication(DCB *dcb, GWBUF *queue) {
&protocol->client_capabilities);
*/
if(protocol->protocol_auth_state == MYSQL_AUTH_SSL_EXCHANGE_DONE)
if(protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_DONE)
goto ssl_hs_done;
ssl = protocol->client_capabilities & GW_MYSQL_CAPABILITIES_SSL;
@ -637,48 +635,6 @@ gw_MySQLWrite_client_SSL(DCB *dcb, GWBUF *queue)
return dcb_write_SSL(dcb, protocol->ssl, queue);
}
int gw_read_client_event_SSL(
DCB* dcb)
{
SESSION *session = NULL;
ROUTER_OBJECT *router = NULL;
ROUTER *router_instance = NULL;
void *rsession = NULL;
MySQLProtocol *protocol = NULL;
GWBUF *read_buffer = NULL;
int rc = 0;
int nbytes_read = 0;
uint8_t cap = 0;
bool stmt_input = false; /*< router input type */
CHK_DCB(dcb);
protocol = DCB_PROTOCOL(dcb, MySQLProtocol);
CHK_PROTOCOL(protocol);
if(protocol->protocol_auth_state == MYSQL_AUTH_SSL_REQ)
{
if(do_ssl_accept(protocol) == 1)
{
spinlock_acquire(&protocol->protocol_lock);
protocol->protocol_auth_state = MYSQL_AUTH_SSL_EXCHANGE_DONE;
spinlock_release(&protocol->protocol_lock);
spinlock_acquire(&dcb->authlock);
dcb->func.read = gw_read_client_event_SSL;
dcb->func.write = gw_MySQLWrite_client_SSL;
dcb->func.write_ready = gw_write_client_event;
spinlock_release(&dcb->authlock);
}
goto return_rc;
}
return_rc:
return rc;
}
/**
* Client read event triggered by EPOLLIN
*
@ -703,26 +659,26 @@ int gw_read_client_event(
protocol = DCB_PROTOCOL(dcb, MySQLProtocol);
CHK_PROTOCOL(protocol);
/** Let the OpenSSL API do the reading from the socket */
if(protocol->protocol_auth_state == MYSQL_AUTH_SSL_REQ)
if(protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_ONGOING ||
protocol->protocol_auth_state == MYSQL_AUTH_SSL_REQ)
{
if(do_ssl_accept(protocol) == 1)
switch(do_ssl_accept(protocol))
{
spinlock_acquire(&protocol->protocol_lock);
protocol->protocol_auth_state = MYSQL_AUTH_SSL_EXCHANGE_DONE;
protocol->use_ssl = true;
spinlock_release(&protocol->protocol_lock);
spinlock_acquire(&dcb->authlock);
//dcb->func.read = gw_read_client_event_SSL;
//dcb->func.write = gw_MySQLWrite_client_SSL;
//dcb->func.write_ready = gw_write_client_event_SSL;
spinlock_release(&dcb->authlock);
case 0:
return 0;
break;
case 1:
return 0;
break;
case -1:
return 1;
break;
default:
return 1;
break;
}
goto return_rc;
}
if(protocol->use_ssl)
{
rc = dcb_read_SSL(dcb,protocol->ssl, &read_buffer);
@ -880,8 +836,13 @@ int gw_read_client_event(
auth_val = gw_mysql_do_authentication(dcb, read_buffer);
if(protocol->protocol_auth_state == MYSQL_AUTH_SSL_REQ)
if(protocol->protocol_auth_state == MYSQL_AUTH_SSL_REQ ||
protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_ONGOING ||
protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_DONE ||
protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_FAILED)
{
break;
}
if (auth_val == 0)
{
@ -976,9 +937,103 @@ int gw_read_client_event(
}
break;
case MYSQL_AUTH_SSL_EXCHANGE_DONE:
case MYSQL_AUTH_SSL_HANDSHAKE_DONE:
{
protocol->protocol_auth_state = MYSQL_AUTH_SSL_EXCHANGE_DONE;
int auth_val;
auth_val = gw_mysql_do_authentication(dcb, read_buffer);
if (auth_val == 0)
{
SESSION *session;
protocol->protocol_auth_state = MYSQL_AUTH_RECV;
/**
* Create session, and a router session for it.
* If successful, there will be backend connection(s)
* after this point.
*/
session = session_alloc(dcb->service, dcb);
if (session != NULL)
{
CHK_SESSION(session);
ss_dassert(session->state != SESSION_STATE_ALLOC);
protocol->protocol_auth_state = MYSQL_IDLE;
/**
* Send an AUTH_OK packet to the client,
* packet sequence is # 2
*/
mysql_send_ok(dcb, 2, 0, NULL);
}
else
{
protocol->protocol_auth_state = MYSQL_AUTH_FAILED;
LOGIF(LD, (skygw_log_write(
LOGFILE_DEBUG,
"%lu [gw_read_client_event] session "
"creation failed. fd %d, "
"state = MYSQL_AUTH_FAILED.",
pthread_self(),
protocol->owner_dcb->fd)));
/** Send ERR 1045 to client */
mysql_send_auth_error(
dcb,
2,
0,
"failed to create new session");
dcb_close(dcb);
}
}
else
{
char* fail_str = NULL;
protocol->protocol_auth_state = MYSQL_AUTH_FAILED;
if (auth_val == 2) {
/** Send error 1049 to client */
int message_len = 25 + MYSQL_DATABASE_MAXLEN;
fail_str = calloc(1, message_len+1);
snprintf(fail_str, message_len, "Unknown database '%s'",
(char*)((MYSQL_session *)dcb->data)->db);
modutil_send_mysql_err_packet(dcb, 2, 0, 1049, "42000", fail_str);
} else {
/** Send error 1045 to client */
fail_str = create_auth_fail_str((char *)((MYSQL_session *)dcb->data)->user,
dcb->remote,
(char*)((MYSQL_session *)dcb->data)->client_sha1,
(char*)((MYSQL_session *)dcb->data)->db);
modutil_send_mysql_err_packet(dcb, 2, 0, 1045, "28000", fail_str);
}
if (fail_str)
free(fail_str);
LOGIF(LD, (skygw_log_write(
LOGFILE_DEBUG,
"%lu [gw_read_client_event] after "
"gw_mysql_do_authentication, fd %d, "
"state = MYSQL_AUTH_FAILED.",
protocol->owner_dcb->fd,
pthread_self())));
/**
* Release MYSQL_session since it is not used anymore.
*/
if (!DCB_IS_CLONE(dcb))
{
free(dcb->data);
}
dcb->data = NULL;
dcb_close(dcb);
}
read_buffer = gwbuf_consume(read_buffer, nbytes_read);
}
break;
@ -1821,111 +1876,83 @@ return_rc:
return rc;
}
/**
* Create a character array including the query string.
* GWBUF given as input includes either one complete or partial query.
* Length of buffer is at most the query length+4 (length of packet header).
* Do the SSL authentication handshake.
* This functions
* @param protocol
* @return
*/
#if defined(NOT_USED)
static char* gw_get_or_create_querystr (
void* data,
bool* new_allocation)
{
GWBUF* buf = (GWBUF *)data;
size_t buflen; /*< first gw buffer data length */
size_t packetlen; /*< length of mysql packet */
size_t querylen; /*< total buffer length-<length of type indicator> */
size_t nbytes_copied;
char* startpos; /*< first byte of query in gw buffer */
char* str; /*< resulting query string */
CHK_GWBUF(buf);
packetlen = MYSQL_GET_PACKET_LEN((uint8_t *)GWBUF_DATA(buf));
str = (char *)malloc(packetlen); /*< leave space for terminating null */
if (str == NULL)
{
goto return_str;
}
*new_allocation = true;
/**
* First buffer includes 4 bytes header and a type indicator byte.
*/
buflen = GWBUF_LENGTH(buf);
querylen = packetlen-1;
ss_dassert(buflen<=querylen+5); /*< 5 == header+type indicator */
startpos = (char *)GWBUF_DATA(buf)+5;
nbytes_copied = MIN(querylen, buflen-5);
memcpy(str, startpos, nbytes_copied);
memset(&str[querylen-1], 0, 1);
buf = gwbuf_consume(buf, querylen-1);
/**
* In case of multi-packet statement whole buffer consists of query
* string.
*/
while (buf != NULL)
{
buflen = GWBUF_LENGTH(buf);
memcpy(str+nbytes_copied, GWBUF_DATA(buf), buflen);
nbytes_copied += buflen;
buf = gwbuf_consume(buf, buflen);
}
ss_dassert(str[querylen-1] == 0);
return_str:
return str;
}
#endif
int do_ssl_accept(MySQLProtocol* protocol)
{
int rval,errnum;
char errbuf[2014];
switch((rval = SSL_accept(protocol->ssl)))
{
case 0:
errnum = SSL_get_error(protocol->ssl,rval);
skygw_log_write_flush(LT,"SSL_accept ongoing for %s@%s",
protocol->owner_dcb->user,
protocol->owner_dcb->remote);
break;
case 1:
protocol->protocol_auth_state = MYSQL_AUTH_SSL_EXCHANGE_DONE;
rval = 1;
protocol->use_ssl = true;
skygw_log_write_flush(LT,"SSL_accept done for %s@%s",
protocol->owner_dcb->user,
protocol->owner_dcb->remote);
break;
DCB* dcb;
default:
errnum = SSL_get_error(protocol->ssl,rval);
if(errnum == SSL_ERROR_WANT_READ)
{
/** Not all of the data has been read. Go back to the poll
queue and wait for more.*/
rval = 0;
protocol->protocol_auth_state = MYSQL_AUTH_SSL_REQ;
skygw_log_write_flush(LT,"SSL_accept partially done for %s@%s",
protocol->owner_dcb->user,
protocol->owner_dcb->remote);
}
else
{
skygw_log_write_flush(LE,
"Error: Fatal error in SSL_accept for %s@%s: %s",
protocol->owner_dcb->user,
protocol->owner_dcb->remote,
ERR_error_string(errnum,NULL));
protocol->protocol_auth_state = MYSQL_AUTH_SSL_EXCHANGE_ERR;
rval = -1;
}
break;
rval = SSL_accept(protocol->ssl);
}
switch(rval)
{
case 0:
errnum = SSL_get_error(protocol->ssl,rval);
skygw_log_write_flush(LT,"SSL_accept shutdown for %s@%s",
protocol->owner_dcb->user,
protocol->owner_dcb->remote);
return -1;
break;
case 1:
spinlock_acquire(&protocol->protocol_lock);
dcb = protocol->owner_dcb;
protocol->protocol_auth_state = MYSQL_AUTH_SSL_HANDSHAKE_DONE;
protocol->use_ssl = true;
spinlock_release(&protocol->protocol_lock);
spinlock_acquire(&dcb->authlock);
dcb->func.write = gw_MySQLWrite_client_SSL;
dcb->func.write_ready = gw_write_client_event_SSL;
spinlock_release(&dcb->authlock);
rval = 1;
skygw_log_write_flush(LT,"SSL_accept done for %s@%s",
protocol->owner_dcb->user,
protocol->owner_dcb->remote);
break;
case -1:
errnum = SSL_get_error(protocol->ssl,rval);
if(errnum == SSL_ERROR_WANT_READ || errnum == SSL_ERROR_WANT_WRITE ||
errnum == SSL_ERROR_WANT_X509_LOOKUP)
{
/** Not all of the data has been read. Go back to the poll
queue and wait for more.*/
rval = 0;
skygw_log_write_flush(LT,"SSL_accept ongoing for %s@%s",
protocol->owner_dcb->user,
protocol->owner_dcb->remote);
}
else
{
spinlock_acquire(&protocol->protocol_lock);
protocol->protocol_auth_state = MYSQL_AUTH_SSL_HANDSHAKE_FAILED;
spinlock_release(&protocol->protocol_lock);
rval = -1;
skygw_log_write_flush(LE,
"Error: Fatal error in SSL_accept for %s@%s: %s",
protocol->owner_dcb->user,
protocol->owner_dcb->remote,
ERR_error_string(errnum,NULL));
}
break;
default:
skygw_log_write_flush(LE,
"Error: Fatal error in SSL_accept, returned value was %d.",
rval);
break;
}
return rval;
}