diff options
-rw-r--r-- | .travis.yml | 11 | ||||
-rw-r--r-- | Makefile.am | 24 | ||||
-rw-r--r-- | configure.ac | 97 | ||||
-rw-r--r-- | crawler.c | 4 | ||||
-rw-r--r-- | doc/tls.txt | 133 | ||||
-rw-r--r-- | logger.c | 4 | ||||
-rw-r--r-- | memcached.c | 350 | ||||
-rw-r--r-- | memcached.h | 34 | ||||
-rwxr-xr-x | t/binary.t | 34 | ||||
-rw-r--r-- | t/cacert.pem | 22 | ||||
-rw-r--r-- | t/cakey.pem | 30 | ||||
-rw-r--r-- | t/client_crt.pem | 68 | ||||
-rw-r--r-- | t/client_key.pem | 15 | ||||
-rw-r--r-- | t/idle-timeout.t | 8 | ||||
-rw-r--r-- | t/lib/MemcachedTest.pm | 86 | ||||
-rwxr-xr-x | t/misbehave.t | 2 | ||||
-rwxr-xr-x | t/multiversioning.t | 12 | ||||
-rw-r--r-- | t/noreply.t | 1 | ||||
-rw-r--r-- | t/server.pem | 79 | ||||
-rw-r--r-- | t/server_crt.pem | 67 | ||||
-rw-r--r-- | t/server_key.pem | 15 | ||||
-rw-r--r-- | t/ssl_cert_refresh.t | 76 | ||||
-rw-r--r-- | t/ssl_ports.t | 31 | ||||
-rw-r--r-- | t/ssl_settings.t | 36 | ||||
-rw-r--r-- | t/ssl_verify_modes.t | 22 | ||||
-rwxr-xr-x | t/stats.t | 15 | ||||
-rw-r--r-- | testapp.c | 204 | ||||
-rw-r--r-- | thread.c | 32 | ||||
-rw-r--r-- | tls.c | 195 | ||||
-rw-r--r-- | tls.h | 14 |
30 files changed, 1630 insertions, 91 deletions
diff --git a/.travis.yml b/.travis.yml index 3b00743..9686526 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ sudo: required -dist: trusty +dist: xenial language: cpp compiler: @@ -7,11 +7,14 @@ compiler: install: - sudo apt-get update -y - - sudo apt-get install -y build-essential automake1.11 autoconf libevent-dev libseccomp-dev git + - sudo apt-get install -y build-essential automake1.11 autoconf libevent-dev libseccomp-dev git tar wget libio-socket-ssl-perl + - wget https://www.openssl.org/source/openssl-1.1.0g.tar.gz && tar xzvf openssl-1.1.0g.tar.gz + - cd openssl-1.1.0g && ./config -Wl,--enable-new-dtags,-rpath,'$(LIBRPATH)' && make && sudo make install && openssl version && cd ../ + - rm -rf openssl-1.1.0g && rm openssl-1.1.0g.tar.gz* script: - ./autogen.sh - - ./configure --enable-seccomp + - ./configure --enable-seccomp --enable-tls - make -j - make test - + - make test_tls diff --git a/Makefile.am b/Makefile.am index 22ea696..594c794 100644 --- a/Makefile.am +++ b/Makefile.am @@ -53,6 +53,10 @@ memcached_SOURCES += extstore.c extstore.h \ slab_automove_extstore.c slab_automove_extstore.h endif +if ENABLE_TLS +memcached_SOURCES += tls.c tls.h +endif + memcached_debug_SOURCES = $(memcached_SOURCES) memcached_CPPFLAGS = -DNDEBUG memcached_debug_LDADD = @PROFILER_LDFLAGS@ @@ -102,9 +106,29 @@ EXTRA_DIST = doc scripts t memcached.spec memcached_dtrace.d version.m4 README.m MOSTLYCLEANFILES = *.gcov *.gcno *.gcda *.tcov +if ENABLE_TLS +test_tls: + $(MAKE) SSL_TEST=1 test + +test_basic_tls: + @if test $(SSL_TEST)1 != 1; then \ + echo "Running basic tests with TLS"; \ + $(srcdir)/testapp; \ + prove $(srcdir)/t/binary.t $(srcdir)/t/getset.t $(srcdir)/t/ssl*; \ + echo "Finished running basic TLS tests"; \ + else \ + echo "Set SSL_TEST=1 to enable TLS tests"; \ + fi +endif + test: memcached-debug sizes testapp $(srcdir)/sizes $(srcdir)/testapp +if ENABLE_TLS + @if test $(SSL_TEST)1 = 1; then \ + $(MAKE) SSL_TEST=1 test_basic_tls; \ + fi +endif @if test -n "${PARALLEL}"; then \ prove $(srcdir)/t -j ${PARALLEL}; \ else \ diff --git a/configure.ac b/configure.ac index abcb8ae..93046dc 100644 --- a/configure.ac +++ b/configure.ac @@ -100,6 +100,8 @@ AC_ARG_ENABLE(sasl_pwdb, AS_IF([test "x$enable_sasl_pwdb" = "xyes"], [enable_sasl=yes ]) +AC_ARG_ENABLE(tls, + [AS_HELP_STRING([--enable-tls], [Enable Transport Layer Security EXPERIMENTAL ])]) dnl ********************************************************************** @@ -190,6 +192,10 @@ if test "x$enable_extstore" = "xyes"; then AC_DEFINE([EXTSTORE],1,[Set to nonzero if you want to enable extstore]) fi +if test "x$enable_tls" = "xyes"; then + AC_DEFINE([TLS],1,[Set to nonzero if you want to enable TLS]) +fi + if test "x$enable_arm_crc32" = "xyes"; then AC_DEFINE([ARM_CRC32],1,[Set to nonzero if you want to enable ARMv8 crc32]) fi @@ -199,6 +205,8 @@ AM_CONDITIONAL([DTRACE_INSTRUMENT_OBJ],[test "$dtrace_instrument_obj" = "yes"]) AM_CONDITIONAL([ENABLE_SASL],[test "$enable_sasl" = "yes"]) AM_CONDITIONAL([ENABLE_EXTSTORE],[test "$enable_extstore" = "yes"]) AM_CONDITIONAL([ENABLE_ARM_CRC32],[test "$enable_arm_crc32" = "yes"]) +AM_CONDITIONAL([ENABLE_TLS],[test "$enable_tls" = "yes"]) + AC_SUBST(DTRACE) AC_SUBST(DTRACEFLAGS) @@ -354,6 +362,95 @@ if test $ac_cv_libevent_dir != "(system)"; then fi fi +trylibssldir="" +AC_ARG_WITH(libssl, + [ --with-libssl=PATH Specify path to libssl installation ], + [ + if test "x$withval" != "xno" ; then + trylibssldir=$withval + fi + ] +) + +dnl ---------------------------------------------------------------------------- +dnl libssl detection. swiped from libevent. modified for openssl detection. + +OPENSSL_URL=https://www.openssl.org/ +if test "x$enable_tls" = "xyes"; then + AC_CACHE_CHECK([for libssl directory], ac_cv_libssl_dir, [ + saved_LIBS="$LIBS" + saved_LDFLAGS="$LDFLAGS" + saved_CPPFLAGS="$CPPFLAGS" + le_found=no + for ledir in $trylibssldir "" $prefix /usr/local ; do + LDFLAGS="$saved_LDFLAGS" + LIBS="-lssl -lcrypto $saved_LIBS" + + # Skip the directory if it isn't there. + if test ! -z "$ledir" -a ! -d "$ledir" ; then + continue; + fi + if test ! -z "$ledir" ; then + if test -d "$ledir/lib" ; then + LDFLAGS="-L$ledir/lib $LDFLAGS" + else + LDFLAGS="-L$ledir $LDFLAGS" + fi + if test -d "$ledir/include" ; then + CPPFLAGS="-I$ledir/include $CPPFLAGS" + else + CPPFLAGS="-I$ledir $CPPFLAGS" + fi + fi + # Can I compile and link it? + AC_TRY_LINK([#include <sys/time.h> + #include <sys/types.h> + #include <assert.h> + #include <openssl/ssl.h>], [ SSL_CTX* ssl_ctx = SSL_CTX_new(TLS_server_method()); + assert(OPENSSL_VERSION_NUMBER >= 0x10100000L);], + [ libssl_linked=yes ], [ libssl_linked=no ]) + if test $libssl_linked = yes; then + if test ! -z "$ledir" ; then + ac_cv_libssl_dir=$ledir + _myos=`echo $target_os | cut -f 1 -d .` + AS_IF(test "$SUNCC" = "yes" -o "x$_myos" = "xsolaris2", + [saved_LDFLAGS="$saved_LDFLAGS -Wl,-R$ledir/lib"], + [AS_IF(test "$GCC" = "yes", + [saved_LDFLAGS="$saved_LDFLAGS -Wl,-rpath,$ledir/lib"])]) + else + ac_cv_libssl_dir="(system)" + fi + le_found=yes + break + fi + done + LIBS="$saved_LIBS" + LDFLAGS="$saved_LDFLAGS" + CPPFLAGS="$saved_CPPFLAGS" + if test $le_found = no ; then + AC_MSG_ERROR([libssl (at least version 1.1.0) is required. You can get it from $OPENSSL_URL + + If it's already installed, specify its path using --with-libssl=/dir/ + ]) + fi + ]) + LIBS="-lssl -lcrypto $LIBS" + if test $ac_cv_libssl_dir != "(system)"; then + if test -d "$ac_cv_libssl_dir/lib" ; then + LDFLAGS="-L$ac_cv_libssl_dir/lib $LDFLAGS" + le_libdir="$ac_cv_libssl_dir/lib" + else + LDFLAGS="-L$ac_cv_libssl_dir $LDFLAGS" + le_libdir="$ac_cv_libssl_dir" + fi + if test -d "$ac_cv_libssl_dir/include" ; then + CPPFLAGS="-I$ac_cv_libssl_dir/include $CPPFLAGS" + else + CPPFLAGS="-I$ac_cv_libssl_dir $CPPFLAGS" + fi + fi +fi + dnl ---------------------------------------------------------------------------- AC_SEARCH_LIBS(umem_cache_create, umem) @@ -293,7 +293,7 @@ static int lru_crawler_poll(crawler_client_t *c) { if (to_poll[0].revents & POLLIN) { char buf[1]; - int res = read(c->sfd, buf, 1); + int res = ((conn*)c->c)->read(c->c, buf, 1); if (res == 0 || (res == -1 && (errno != EAGAIN && errno != EWOULDBLOCK))) { lru_crawler_close_client(c); return -1; @@ -304,7 +304,7 @@ static int lru_crawler_poll(crawler_client_t *c) { lru_crawler_close_client(c); return -1; } else if (to_poll[0].revents & POLLOUT) { - int total = write(c->sfd, data, data_size); + int total = ((conn*)c->c)->write(c->c, data, data_size); if (total == -1) { if (errno != EAGAIN && errno != EWOULDBLOCK) { lru_crawler_close_client(c); diff --git a/doc/tls.txt b/doc/tls.txt new file mode 100644 index 0000000..fef56ab --- /dev/null +++ b/doc/tls.txt @@ -0,0 +1,133 @@ +Securing Memcached with TLS + +Requirements +------------ +We are required to encrypt Memcached network traffic as we deploy our servers in public cloud +environments. We decided to implement SSL/TLS for TCP at the network layer of Memcached +using OpenSSL libraries. This provides following benefits with the expense of added latency +and reduced throughput (to be quantified). + +# Encryption :Data is encrypted on the wire between Memcached client and server. +# Authentication : Optionally, both server and client authenticate each other. +# Integrity: Data is not tampered or altered when transmitted between client and server + +Following are a few additional features. +# Certificate refresh: when the server gets a new certificate, new connections +will use new certificates without a need of re-starting the server process. + +# Multiple ports with and without TLS : by default all TCP ports are secured. Optionally we can setup +the server to secure a specific TCP port. + +Note that initial implementation does not support session resumption or renegotiation. + +Design +------ +We experimented two options for implementing TLS, with SSL buffered events and directly using +OpenSSL API. + +Bufferevents can use the OpenSSL library to implement SSL/TLS. Our experiment used +a socket-based bufferevent that tells OpenSSL to communicate with the network directly over. +Unlike a worker thread sets callback on the socket, this uses a “bufferevent” object for +callbacks. Memcached still has to setup the SSL Context but SSL handshake and object +management is done via the “bufferevent_” API. While this was fairly easy to implement, +we noticed a higher memory usage as we don’t have much control over allocating evbuffer +objects in bufferevents. More over there is a discussion on removing the libevent dependency +from Memcached; hence this option was not chosen. + +OpenSSL library provides APIs for us to directly read/write from a socket. With this option, +we create an SSL Context and many SSL objects. The SSL Context object, created at the process level, +holds certificates, a private key, and options regarding the TLS protocol and algorithms. +SSL objects, created at the connection level, represents SSL sessions. SSL objects are responsible +for encryption, and session handshake among other things. + +There are two ways to do network IO over TLS, either only use SSL_read/SSL_write with a network socket or +use the API along with an output/input buffer pair. These buffers are referred as BIO +(Basic Input Output) buffers. + +We started with the first option, create SSL objects with the socket and only interact with SSL_read/SSL_write. + + +------+ +-----+ + |......|--> read(fd) --> BIO_write(rbio) -->|.....|--> SSL_read(ssl) --> IN + |......| |.....| + |.sock.| |.SSL.| + |......| |.....| + |......|<-- write(fd) <-- BIO_read(wbio) <--|.....|<-- SSL_write(ssl) <-- OUT + +------+ +-----+ + | | | | + |<-------------------------------->| |<------------------->| + | encrypted bytes | | unencrypted bytes | + + Figure 1 : Network sockets, BIO buffers and SSL_read/SSL_write + +(reference: https://gist.github.com/darrenjs/4645f115d10aa4b5cebf57483ec82eca) + +Memcached uses non blocking sockets and implements a rather complex state machine for +network IO. A listener thread does the TCP handshake and initiates the SSL handshake after +creating an SSL object based on the SSL Context object of the server. If there are no +fatal errors, the listener thread hands over the socket to a worker thread. A worker completes +the SSL handshake. + +----------- ---------------------- + | | + Client | | Memcached Server + | | + | |--------------------- + | | Listener thread | + | TCP connect | | + |---------------------> | (accept) | + | ClientHello | | + |---------------------> | (SSL_accept) | + | | | + | ServerHello and | | + | Certificate, | | + | ServerHelloDone | | + | <---------------------| | + | |--------------------- + | | | + | | V + | |------------------- + | | Worker thread | + | ClientKeyExchange, | | + | ChangeCipherSpec, | | + | Finished | | + |---------------------> | (SSL_read) | + | | | + | | | + | NewSessionTicket, | | + | ChangeCipherSpec, | | + | Finished | | + | <---------------------| | + | | | + | Memcached request/ | | + | response | | + | <-------------------> | (SSL_read/ | + | | SSL_write) | +----------- ------------------------- + + Figure 2 : The initial SSL handshake + + +Setting-up callbacks when the socket is ready for reading/writing is the same +for both TLS and non-TLS connections. When the socket is ready, the state machine kicks off +and issues a SSL_read/ SSL_write. Note that we implement a SSL_sendmsg wrapper on top of +SSL_write to simulate the sendmsg API. +This way we don't explicitly use BIO buffers or do BIO_write/BIO_read, but let OpenSSL +library to do it on our behalf. Existing state machine takes care of reading the correct amount +of bytes and do the error handling when needed. + +As a best practice, server certificates and keys are periodically refreshed by the PKI. +When this happens we want server to use the new certificate without restarting the process. +Memcached is a cache and restarting servers affects the latency of applications. We implement +the automatic certificate refresh through a command. Upon receiving the "refresh_certs" command, +the server reloads the certificates and key to the SSL Context object. Existing connection won't be +interrupted but new connections will use the new certificate. + +We understand not all users want to use TLS or have the OpenSSL dependency. Therefore +it's an optional module at the compile time. We can build a TLS capable Memcached server with +"./configure --enable-tls". Once the server is built with TLS support, we can enabled it with +"-Z" flag or "--enable-ssl". Certificate (-o ssl_chain_cert) and (-o ssl_key) are required +parameters while others are optional. Supported options can be listed through "memcached -h". + +Developers need to have libio-socket-ssl-perl installed for running unit tests. When the server is +built with TLS support, we can use "test_tls" make target to run all existing tests over TLS and some +additional TLS specific tests. The minimum required OpenSSL version is 1.1.0g. @@ -443,7 +443,7 @@ static int logger_thread_poll_watchers(int force_poll, int watcher) { */ if (watchers_pollfds[nfd].revents & POLLIN) { char buf[1]; - int res = read(w->sfd, buf, 1); + int res = ((conn*)w->c)->read(w->c, buf, 1); if (res == 0 || (res == -1 && (errno != EAGAIN && errno != EWOULDBLOCK))) { L_DEBUG("LOGGER: watcher closed remotely\n"); logger_thread_close_watcher(w); @@ -464,7 +464,7 @@ static int logger_thread_poll_watchers(int force_poll, int watcher) { total = fwrite(data, 1, data_size, stderr); break; case LOGGER_WATCHER_CLIENT: - total = write(w->sfd, data, data_size); + total = ((conn*)w->c)->write(w->c, data, data_size); break; } diff --git a/memcached.c b/memcached.c index 8d79eba..31555c5 100644 --- a/memcached.c +++ b/memcached.c @@ -55,6 +55,10 @@ #include <getopt.h> #endif +#ifdef TLS +#include "tls.h" +#endif + /* FreeBSD 4.x doesn't have IOV_MAX exposed. */ #ifndef IOV_MAX #if defined(__FreeBSD__) || defined(__APPLE__) || defined(__GNU__) @@ -73,6 +77,9 @@ static void drive_machine(conn *c); static int new_socket(struct addrinfo *ai); static int try_read_command(conn *c); +static ssize_t tcp_read(conn *arg, void *buf, size_t count); +static ssize_t tcp_sendmsg(conn *arg, struct msghdr *msg, int flags); +static ssize_t tcp_write(conn *arg, void *buf, size_t count); enum try_read_result { READ_DATA_RECEIVED, @@ -146,6 +153,22 @@ enum transmit_result { TRANSMIT_HARD_ERROR /** Can't write (c->state is set to conn_closing) */ }; +/* Default methods to read from/ write to a socket */ +ssize_t tcp_read(conn *c, void *buf, size_t count) { + assert (c != NULL); + return read(c->sfd, buf, count); +} + +ssize_t tcp_sendmsg(conn *c, struct msghdr *msg, int flags) { + assert (c != NULL); + return sendmsg(c->sfd, msg, flags); +} + +ssize_t tcp_write(conn *c, void *buf, size_t count) { + assert (c != NULL); + return write(c->sfd, buf, count); +} + static enum transmit_result transmit(conn *c); /* This reduces the latency without adding lots of extra wiring to be able to @@ -223,6 +246,18 @@ static void settings_init(void) { settings.access = 0700; settings.port = 11211; settings.udpport = 0; +#ifdef TLS + settings.ssl_enabled = false; + settings.ssl_ctx = NULL; + settings.ssl_chain_cert = NULL; + settings.ssl_key = NULL; + settings.ssl_verify_mode = SSL_VERIFY_NONE; + settings.ssl_keyformat = SSL_FILETYPE_PEM; + settings.ssl_ciphers = NULL; + settings.ssl_ca_cert = NULL; + settings.ssl_last_cert_refresh_time = current_time; + settings.ssl_wbuf_size = 16 * 1024; // default is 16KB (SSL max frame size is 17KB) +#endif /* By default this string should be NULL for getaddrinfo() */ settings.inter = NULL; settings.maxbytes = 64 * 1024 * 1024; /* default is 64MB */ @@ -502,7 +537,7 @@ void conn_worker_readd(conn *c) { conn *conn_new(const int sfd, enum conn_states init_state, const int event_flags, const int read_buffer_size, enum network_transport transport, - struct event_base *base) { + struct event_base *base, void *ssl) { conn *c; assert(sfd >= 0 && sfd < max_fds); @@ -517,7 +552,9 @@ conn *conn_new(const int sfd, enum conn_states init_state, return NULL; } MEMCACHED_CONN_CREATE(c); - + c->read = NULL; + c->sendmsg = NULL; + c->write = NULL; c->rbuf = c->wbuf = 0; c->ilist = 0; c->suffixlist = 0; @@ -597,7 +634,11 @@ conn *conn_new(const int sfd, enum conn_states init_state, assert(false); } } - +#ifdef TLS + c->ssl = NULL; + c->ssl_wbuf = NULL; + c->ssl_enabled = false; +#endif c->state = init_state; c->rlbytes = 0; c->cmd = -1; @@ -626,6 +667,25 @@ conn *conn_new(const int sfd, enum conn_states init_state, c->noreply = false; +#ifdef TLS + if (ssl) { + c->ssl = (SSL*)ssl; + c->read = ssl_read; + c->sendmsg = ssl_sendmsg; + c->write = ssl_write; + c->ssl_enabled = true; + SSL_set_info_callback(c->ssl, ssl_callback); + } else +#else + // This must be NULL if TLS is not enabled. + assert(ssl == NULL); +#endif + { + c->read = tcp_read; + c->sendmsg = tcp_sendmsg; + c->write = tcp_write; + } + event_set(&c->event, sfd, event_flags, event_handler, (void *)c); event_base_set(base, &c->event); c->ev_flags = event_flags; @@ -793,6 +853,11 @@ void conn_free(conn *c) { free(c->suffixlist); if (c->iov) free(c->iov); +#ifdef TLS + if (c->ssl_wbuf) + c->ssl_wbuf = NULL; +#endif + free(c); } } @@ -810,8 +875,13 @@ static void conn_close(conn *c) { MEMCACHED_CONN_RELEASE(c->sfd); conn_set_state(c, conn_closed); +#ifdef TLS + if (c->ssl) { + SSL_shutdown(c->ssl); + SSL_free(c->ssl); + } +#endif close(c->sfd); - pthread_mutex_lock(&conn_lock); allow_new_conns = true; pthread_mutex_unlock(&conn_lock); @@ -3217,6 +3287,13 @@ static void server_stats(ADD_STAT add_stats, conn *c) { APPEND_STAT("extstore_io_queue", "%llu", (unsigned long long)(st.io_queue)); } #endif +#ifdef TLS + if (settings.ssl_enabled) { + SSL_LOCK(); + APPEND_STAT("time_since_server_cert_refresh", "%u", now - settings.ssl_last_cert_refresh_time); + SSL_UNLOCK(); + } +#endif } static void process_stat_settings(ADD_STAT add_stats, void *c) { @@ -3288,6 +3365,16 @@ static void process_stat_settings(ADD_STAT add_stats, void *c) { APPEND_STAT("slab_automove_freeratio", "%.3f", settings.slab_automove_freeratio); APPEND_STAT("ext_drop_unread", "%s", settings.ext_drop_unread ? "yes" : "no"); #endif +#ifdef TLS + APPEND_STAT("ssl_enabled", "%s", settings.ssl_enabled ? "yes" : "no"); + APPEND_STAT("ssl_chain_cert", "%s", settings.ssl_chain_cert); + APPEND_STAT("ssl_key", "%s", settings.ssl_key); + APPEND_STAT("ssl_verify_mode", "%d", settings.ssl_verify_mode); + APPEND_STAT("ssl_keyformat", "%d", settings.ssl_keyformat); + APPEND_STAT("ssl_ciphers", "%s", settings.ssl_ciphers ? settings.ssl_ciphers : "NULL"); + APPEND_STAT("ssl_ca_cert", "%s", settings.ssl_ca_cert ? settings.ssl_ca_cert : "NULL"); + APPEND_STAT("ssl_wbuf_size", "%u", settings.ssl_wbuf_size); +#endif } static void conn_to_str(const conn *c, char *buf) { @@ -4927,6 +5014,17 @@ static void process_command(conn *c, char *command) { } else if (ntokens >= 3 && strcmp(tokens[COMMAND_TOKEN].value, "extstore") == 0) { process_extstore_command(c, tokens, ntokens); #endif +#ifdef TLS + } else if (ntokens == 2 && strcmp(tokens[COMMAND_TOKEN].value, "refresh_certs") == 0) { + set_noreply_maybe(c, tokens, ntokens); + char *errmsg = NULL; + if (refresh_certs(&errmsg)) { + out_string(c, "OK"); + } else { + write_and_free(c, errmsg, strlen(errmsg)); + } + return; +#endif } else { if (ntokens >= 2 && strncmp(tokens[ntokens - 2].value, "HTTP/", 5) == 0) { conn_set_state(c, conn_closing); @@ -5158,7 +5256,7 @@ static enum try_read_result try_read_network(conn *c) { } int avail = c->rsize - c->rbytes; - res = read(c->sfd, c->rbuf + c->rbytes, avail); + res = c->read(c, c->rbuf + c->rbytes, avail); if (res > 0) { pthread_mutex_lock(&c->thread->stats.mutex); c->thread->stats.bytes_read += res; @@ -5262,7 +5360,7 @@ static enum transmit_result transmit(conn *c) { ssize_t res; struct msghdr *m = &c->msglist[c->msgcurr]; - res = sendmsg(c->sfd, m, 0); + res = c->sendmsg(c, m, 0); if (res > 0) { pthread_mutex_lock(&c->thread->stats.mutex); c->thread->stats.bytes_written += res; @@ -5357,7 +5455,7 @@ static int read_into_chunked_item(conn *c) { } } else { /* now try reading from the socket */ - res = read(c->sfd, ch->data + ch->used, + res = c->read(c, ch->data + ch->used, (unused > c->rlbytes ? c->rlbytes : unused)); if (res > 0) { pthread_mutex_lock(&c->thread->stats.mutex); @@ -5458,8 +5556,47 @@ static void drive_machine(conn *c) { stats.rejected_conns++; STATS_UNLOCK(); } else { + void *ssl_v = NULL; +#ifdef TLS + SSL *ssl = NULL; + if (c->ssl_enabled) { + assert(IS_TCP(c->transport) && settings.ssl_enabled); + + if (settings.ssl_ctx == NULL) { + if (settings.verbose) { + fprintf(stderr, "SSL context is not initialized\n"); + } + close(sfd); + break; + } + SSL_LOCK(); + ssl = SSL_new(settings.ssl_ctx); + SSL_UNLOCK(); + if (ssl == NULL) { + if (settings.verbose) { + fprintf(stderr, "Failed to created the SSL object\n"); + } + close(sfd); + break; + } + SSL_set_fd(ssl, sfd); + int ret = SSL_accept(ssl); + if (ret < 0) { + int err = SSL_get_error(ssl, ret); + if (err == SSL_ERROR_SYSCALL || err == SSL_ERROR_SSL) { + if (settings.verbose) { + fprintf(stderr, "SSL connection failed with error code : %d : %s\n", err, strerror(errno)); + } + close(sfd); + break; + } + } + } + ssl_v = (void*) ssl; +#endif + dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST, - DATA_BUFFER_SIZE, c->transport); + DATA_BUFFER_SIZE, c->transport, ssl_v); } stop = true; @@ -5565,7 +5702,7 @@ static void drive_machine(conn *c) { } /* now try reading from the socket */ - res = read(c->sfd, c->ritem, c->rlbytes); + res = c->read(c, c->ritem, c->rlbytes); if (res > 0) { pthread_mutex_lock(&c->thread->stats.mutex); c->thread->stats.bytes_read += res; @@ -5635,7 +5772,7 @@ static void drive_machine(conn *c) { } /* now try reading from the socket */ - res = read(c->sfd, c->rbuf, c->rsize > c->sbytes ? c->sbytes : c->rsize); + res = c->read(c, c->rbuf, c->rsize > c->sbytes ? c->sbytes : c->rsize); if (res > 0) { pthread_mutex_lock(&c->thread->stats.mutex); c->thread->stats.bytes_read += res; @@ -5849,7 +5986,7 @@ static void maximize_sndbuf(const int sfd) { static int server_socket(const char *interface, int port, enum network_transport transport, - FILE *portnumber_file) { + FILE *portnumber_file, bool ssl_enabled) { int sfd; struct linger ling = {0, 0}; struct addrinfo *ai; @@ -5972,15 +6109,20 @@ static int server_socket(const char *interface, int per_thread_fd = c ? dup(sfd) : sfd; dispatch_conn_new(per_thread_fd, conn_read, EV_READ | EV_PERSIST, - UDP_READ_BUFFER_SIZE, transport); + UDP_READ_BUFFER_SIZE, transport, NULL); } } else { if (!(listen_conn_add = conn_new(sfd, conn_listening, EV_READ | EV_PERSIST, 1, - transport, main_base))) { + transport, main_base, NULL))) { fprintf(stderr, "failed to create listening connection\n"); exit(EXIT_FAILURE); } +#ifdef TLS + listen_conn_add->ssl_enabled = ssl_enabled; +#else + assert(ssl_enabled == false); +#endif listen_conn_add->next = listen_conn; listen_conn = listen_conn_add; } @@ -5994,8 +6136,15 @@ static int server_socket(const char *interface, static int server_sockets(int port, enum network_transport transport, FILE *portnumber_file) { + bool ssl_enabled = false; + +#ifdef TLS + const char *notls = "notls"; + ssl_enabled = settings.ssl_enabled; +#endif + if (settings.inter == NULL) { - return server_socket(settings.inter, port, transport, portnumber_file); + return server_socket(settings.inter, port, transport, portnumber_file, ssl_enabled); } else { // tokenize them and bind to each one of them.. char *b; @@ -6007,9 +6156,21 @@ static int server_sockets(int port, enum network_transport transport, return 1; } for (char *p = strtok_r(list, ";,", &b); - p != NULL; - p = strtok_r(NULL, ";,", &b)) { + p != NULL; + p = strtok_r(NULL, ";,", &b)) { int the_port = port; +#ifdef TLS + ssl_enabled = settings.ssl_enabled; + // "notls" option is valid only when memcached is run with SSL enabled. + if (strncmp(p, notls, strlen(notls)) == 0) { + if (!settings.ssl_enabled) { + fprintf(stderr, "'notls' option is valid only when SSL is enabled\n"); + return 1; + } + ssl_enabled = false; + p += strlen(notls) + 1; + } +#endif char *h = NULL; if (*p == '[') { @@ -6049,7 +6210,7 @@ static int server_sockets(int port, enum network_transport transport, if (strcmp(p, "*") == 0) { p = NULL; } - ret |= server_socket(p, the_port, transport, portnumber_file); + ret |= server_socket(p, the_port, transport, portnumber_file, ssl_enabled); } free(list); return ret; @@ -6126,7 +6287,7 @@ static int server_socket_unix(const char *path, int access_mask) { } if (!(listen_conn = conn_new(sfd, conn_listening, EV_READ | EV_PERSIST, 1, - local_transport, main_base))) { + local_transport, main_base, NULL))) { fprintf(stderr, "failed to create listening connection\n"); exit(EXIT_FAILURE); } @@ -6205,6 +6366,10 @@ static void usage(void) { "-A, --enable-shutdown enable ascii \"shutdown\" command\n" "-a, --unix-mask=<mask> access mask for UNIX socket, in octal (default: 0700)\n" "-l, --listen=<addr> interface to listen on (default: INADDR_ANY)\n" +#ifdef TLS + " if TLS/SSL is enabled, 'notls' prefix can be used to\n" + " disable for specific listeners (-l notls:<ip>:<port>) \n" +#endif "-d, --daemon run as a daemon\n" "-r, --enable-coredumps maximize core file limit\n" "-u, --user=<user> assume identity of <username> (only when run as root)\n" @@ -6241,6 +6406,9 @@ static void usage(void) { #endif printf("-F, --disable-flush-all disable flush_all command\n"); printf("-X, --disable-dumping disable stats cachedump and lru_crawler metadump\n"); +#ifdef TLS + printf("-Z, --enable-ssl enable TLS/SSL\n"); +#endif printf("-o, --extended comma separated list of extended options\n" " most options have a 'no_' prefix to disable\n" " - maxconns_fast: immediately close new connections after limit\n" @@ -6304,6 +6472,17 @@ static void usage(void) { " - slab_automove_freeratio: ratio of memory to hold free as buffer.\n" " (see doc/storage.txt for more info)\n" #endif +#ifdef TLS + " - ssl_chain_cert: certificate chain file in PEM format\n" + " - ssl_key: private key, if not part of the -ssl_chain_cert\n" + " - ssl_keyformat: private key format (PEM, DER or ENGINE) PEM default\n" + " - ssl_verify_mode: peer certificate verification mode, default is 0(None).\n" + " valid values are 0(None), 1(Request), 2(Require)\n" + " or 3(Once)\n" + " - ssl_ciphers: specify cipher list to be used\n" + " - ssl_ca_cert: PEM format file of acceptable client CA's\n" + " - ssl_wbuf_size: size in kilobytes of per-connection SSL output buffer\n" +#endif ); return; } @@ -6628,6 +6807,15 @@ int main (int argc, char **argv) { NO_LRU_MAINTAINER, NO_DROP_PRIVILEGES, DROP_PRIVILEGES, +#ifdef TLS + SSL_CERT, + SSL_KEY, + SSL_VERIFY_MODE, + SSL_KEYFORM, + SSL_CIPHERS, + SSL_CA_CERT, + SSL_WBUF_SIZE, +#endif #ifdef MEMCACHED_DEBUG RELAXED_PRIVILEGES, #endif @@ -6685,6 +6873,15 @@ int main (int argc, char **argv) { [NO_LRU_MAINTAINER] = "no_lru_maintainer", [NO_DROP_PRIVILEGES] = "no_drop_privileges", [DROP_PRIVILEGES] = "drop_privileges", +#ifdef TLS + [SSL_CERT] = "ssl_chain_cert", + [SSL_KEY] = "ssl_key", + [SSL_VERIFY_MODE] = "ssl_verify_mode", + [SSL_KEYFORM] = "ssl_keyformat", + [SSL_CIPHERS] = "ssl_ciphers", + [SSL_CA_CERT] = "ssl_ca_cert", + [SSL_WBUF_SIZE] = "ssl_wbuf_size", +#endif #ifdef MEMCACHED_DEBUG [RELAXED_PRIVILEGES] = "relaxed_privileges", #endif @@ -6711,7 +6908,7 @@ int main (int argc, char **argv) { return EX_OSERR; } - /* handle SIGINT and SIGTERM */ + /* handle SIGINT, SIGTERM */ signal(SIGINT, sig_handler); signal(SIGTERM, sig_handler); @@ -6745,6 +6942,7 @@ int main (int argc, char **argv) { char *shortopts = "a:" /* access mask for unix socket */ "A" /* enable admin shutdown command */ + "Z" /* enable SSL */ "p:" /* TCP port number to listen on */ "s:" /* unix socket path to listen on */ "U:" /* UDP port number to listen on */ @@ -6780,6 +6978,7 @@ int main (int argc, char **argv) { const struct option longopts[] = { {"unix-mask", required_argument, 0, 'a'}, {"enable-shutdown", no_argument, 0, 'A'}, + {"enable-ssl", no_argument, 0, 'Z'}, {"port", required_argument, 0, 'p'}, {"unix-socket", required_argument, 0, 's'}, {"udp-port", required_argument, 0, 'U'}, @@ -6822,12 +7021,19 @@ int main (int argc, char **argv) { /* enables "shutdown" command */ settings.shutdown_command = true; break; - + case 'Z': + /* enable secure communication*/ +#ifdef TLS + settings.ssl_enabled = true; +#else + fprintf(stderr, "This server is not built with TLS support.\n"); + exit(EX_USAGE); +#endif + break; case 'a': /* access for unix domain socket, as octal mask (like chmod)*/ settings.access= strtol(optarg,NULL,8); break; - case 'U': settings.udpport = atoi(optarg); udp_specified = true; @@ -7242,6 +7448,90 @@ int main (int argc, char **argv) { start_lru_maintainer = false; settings.lru_segmented = false; break; +#ifdef TLS + case SSL_CERT: + if (subopts_value == NULL) { + fprintf(stderr, "Missing ssl_chain_cert argument\n"); + return 1; + } + settings.ssl_chain_cert = strdup(subopts_value); + break; + case SSL_KEY: + if (subopts_value == NULL) { + fprintf(stderr, "Missing ssl_key argument\n"); + return 1; + } + settings.ssl_key = strdup(subopts_value); + break; + case SSL_VERIFY_MODE: + { + if (subopts_value == NULL) { + fprintf(stderr, "Missing ssl_verify_mode argument\n"); + return 1; + } + int verify = 0; + if (!safe_strtol(subopts_value, &verify)) { + fprintf(stderr, "could not parse argument to ssl_verify_mode\n"); + return 1; + } + switch(verify) { + case 0: + settings.ssl_verify_mode = SSL_VERIFY_NONE; + break; + case 1: + settings.ssl_verify_mode = SSL_VERIFY_PEER; + break; + case 2: + settings.ssl_verify_mode = SSL_VERIFY_PEER | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + break; + case 3: + settings.ssl_verify_mode = SSL_VERIFY_PEER | + SSL_VERIFY_FAIL_IF_NO_PEER_CERT | + SSL_VERIFY_CLIENT_ONCE; + break; + default: + fprintf(stderr, "Invalid ssl_verify_mode. Use help to see valid options.\n"); + return 1; + } + break; + } + case SSL_KEYFORM: + if (subopts_value == NULL) { + fprintf(stderr, "Missing ssl_keyformat argument\n"); + return 1; + } + if (!safe_strtol(subopts_value, &settings.ssl_keyformat)) { + fprintf(stderr, "could not parse argument to ssl_keyformat\n"); + return 1; + } + break; + case SSL_CIPHERS: + if (subopts_value == NULL) { + fprintf(stderr, "Missing ssl_ciphers argument\n"); + return 1; + } + settings.ssl_ciphers = strdup(subopts_value); + break; + case SSL_CA_CERT: + if (subopts_value == NULL) { + fprintf(stderr, "Missing ssl_ca_cert argument\n"); + return 1; + } + settings.ssl_ca_cert = strdup(subopts_value); + break; + case SSL_WBUF_SIZE: + if (subopts_value == NULL) { + fprintf(stderr, "Missing ssl_wbuf_size argument\n"); + return 1; + } + if (!safe_strtoul(subopts_value, &settings.ssl_wbuf_size)) { + fprintf(stderr, "could not parse argument to ssl_wbuf_size\n"); + return 1; + } + settings.ssl_wbuf_size *= 1024; /* kilobytes */ + break; +#endif #ifdef EXTSTORE case EXT_PAGE_SIZE: if (subopts_value == NULL) { @@ -7540,6 +7830,24 @@ int main (int argc, char **argv) { settings.port = settings.udpport; } + +#ifdef TLS + /* + * Setup SSL if enabled + */ + if (settings.ssl_enabled) { + if (!settings.port) { + fprintf(stderr, "ERROR: You cannot enable SSL without a TCP port.\n"); + exit(EX_USAGE); + } + // openssl init methods. + SSL_load_error_strings(); + SSLeay_add_ssl_algorithms(); + // Initiate the SSL context. + ssl_init(); + } +#endif + if (maxcore != 0) { struct rlimit rlim_new; /* diff --git a/memcached.h b/memcached.h index a8dd59d..0645287 100644 --- a/memcached.h +++ b/memcached.h @@ -31,6 +31,9 @@ #endif #include "sasl_defs.h" +#ifdef TLS +#include <openssl/ssl.h> +#endif /** Maximum length of a key. */ #define KEY_MAX_LENGTH 250 @@ -433,6 +436,18 @@ struct settings { /* per-slab-class free chunk limit */ unsigned int ext_free_memchunks[MAX_NUMBER_OF_SLAB_CLASSES]; #endif +#ifdef TLS + bool ssl_enabled; /* indicates whether SSL is enabled */ + SSL_CTX *ssl_ctx; /* holds the SSL server context which has the server certificate */ + char *ssl_chain_cert; /* path to the server SSL chain certificate */ + char *ssl_key; /* path to the server key */ + int ssl_verify_mode; /* client certificate verify mode */ + int ssl_keyformat; /* key format , defult is PEM */ + char *ssl_ciphers; /* list of SSL ciphers */ + char *ssl_ca_cert; /* certificate with CAs. */ + rel_time_t ssl_last_cert_refresh_time; /* time of the last server certificate refresh */ + unsigned int ssl_wbuf_size; /* size of the write buffer used by ssl_sendmsg method */ +#endif }; extern struct stats stats; @@ -563,6 +578,10 @@ typedef struct { #endif logger *l; /* logger buffer */ void *lru_bump_buf; /* async LRU bump buffer */ +#ifdef TLS + char *ssl_wbuf; +#endif + } LIBEVENT_THREAD; typedef struct conn conn; #ifdef EXTSTORE @@ -584,6 +603,11 @@ typedef struct _io_wrap { */ struct conn { int sfd; +#ifdef TLS + SSL *ssl; + char *ssl_wbuf; + bool ssl_enabled; +#endif sasl_conn_t *sasl_conn; bool sasl_started; bool authenticated; @@ -676,6 +700,9 @@ struct conn { int keylen; conn *next; /* Used for generating a list of conn structures */ LIBEVENT_THREAD *thread; /* Pointer to the thread object serving this connection */ + ssize_t (*read)(conn *c, void *buf, size_t count); + ssize_t (*sendmsg)(conn *c, struct msghdr *msg, int flags); + ssize_t (*write)(conn *c, void *buf, size_t count); }; /* array of conn structures, indexed by file descriptor */ @@ -716,7 +743,9 @@ enum delta_result_type do_add_delta(conn *c, const char *key, const int64_t delta, char *buf, uint64_t *cas, const uint32_t hv); enum store_item_type do_store_item(item *item, int comm, conn* c, const uint32_t hv); -conn *conn_new(const int sfd, const enum conn_states init_state, const int event_flags, const int read_buffer_size, enum network_transport transport, struct event_base *base); +conn *conn_new(const int sfd, const enum conn_states init_state, const int event_flags, const int read_buffer_size, + enum network_transport transport, struct event_base *base, void *ssl); + void conn_worker_readd(conn *c); extern int daemonize(int nochdir, int noclose); @@ -740,7 +769,8 @@ extern int daemonize(int nochdir, int noclose); */ void memcached_thread_init(int nthreads, void *arg); void redispatch_conn(conn *c); -void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags, int read_buffer_size, enum network_transport transport); +void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags, int read_buffer_size, + enum network_transport transport, void *ssl); void sidethread_conn_close(conn *c); /* Lock wrappers for cache functions that are called from main loop. */ @@ -441,7 +441,14 @@ $mc->silent_mutation(::CMD_ADDQ, 'silentadd', 'silentaddval'); my %stats = $mc->stats('settings'); is(1024, $stats{'maxconns'}); - isnt('NULL', $stats{'domain_socket'}); + # we run SSL tests over TCP; hence the domain_socket + # is expected to be NULL. + if (enabled_tls_testing()) { + is('NULL', $stats{'domain_socket'}); + } else { + isnt('NULL', $stats{'domain_socket'}); + } + is('on', $stats{'evictions'}); is('yes', $stats{'cas_enabled'}); is('yes', $stats{'flush_enabled'}); @@ -484,14 +491,14 @@ $mc->silent_mutation(::CMD_ADDQ, 'silentadd', 'silentaddval'); $data .= $mc->build_command(::CMD_SETQ, "alt_$k", "blah", 0, $extra, 0); if (length($data) > 2024) { for (my $j = 2024; $j < min(2096, length($data)); $j++) { - $mc->{socket}->send(substr($data, 0, $j)); + $mc->{socket}->syswrite(substr($data, 0, $j)); $mc->flush_socket; sleep(0.001); - $mc->{socket}->send(substr($data, $j)); + $mc->{socket}->syswrite(substr($data, $j)); $mc->flush_socket; } } else { - $mc->{socket}->send($data); + $mc->{socket}->syswrite($data); } $mc->flush_socket; $check->($k, 82, $v); @@ -571,9 +578,18 @@ sub send_command { my $full_msg = $self->build_command($cmd, $key, $val, $opaque, $extra_header, $cas); - my $sent = $self->{socket}->send($full_msg); - die("Send failed: $!") unless $sent; - if($sent != length($full_msg)) { + my $sent = 0; + my $data_len = length($full_msg); + while ($sent < $data_len) { + my $sent_bytes = $self->{socket}->syswrite($full_msg, + $data_len - $sent > MemcachedTest::MAX_READ_WRITE_SIZE ? + MemcachedTest::MAX_READ_WRITE_SIZE : ($data_len - $sent), + $sent); + last if ($sent_bytes <= 0); + $sent += $sent_bytes; + } + die("Send failed: $!") unless $data_len; + if($sent != $data_len) { die("only sent $sent of " . length($full_msg) . " bytes"); } } @@ -612,7 +628,7 @@ sub _handle_single_response { my $hdr = ""; while(::MIN_RECV_BYTES - length($hdr) > 0) { - $self->{socket}->recv(my $response, ::MIN_RECV_BYTES - length($hdr)); + $self->{socket}->sysread(my $response, ::MIN_RECV_BYTES - length($hdr)); $hdr .= $response; } Test::More::is(length($hdr), ::MIN_RECV_BYTES, "Expected read length"); @@ -628,7 +644,7 @@ sub _handle_single_response { # fetch the value my $rv=""; while($remaining - length($rv) > 0) { - $self->{socket}->recv(my $buf, $remaining - length($rv)); + $self->{socket}->sysread(my $buf, $remaining - length($rv)); $rv .= $buf; } if(length($rv) != $remaining) { diff --git a/t/cacert.pem b/t/cacert.pem new file mode 100644 index 0000000..522bd27 --- /dev/null +++ b/t/cacert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDqzCCApOgAwIBAgIJAOFc3ZmVKolRMA0GCSqGSIb3DQEBBQUAMIGLMSgwJgYD +VQQDDB9UZXN0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MQswCQYDVQQIDAJD +QTELMAkGA1UEBhMCVVMxHDAaBgkqhkiG9w0BCQEWDXJvb3RAdGVzdC5jb20xDTAL +BgNVBAoMBFRlc3QxGDAWBgNVBAsMD1Rlc3QgRGVwYXJ0bWVudDAeFw0xOTAxMDcx +ODIzMjlaFw0yNDAxMDYxODIzMjlaMIGLMSgwJgYDVQQDDB9UZXN0IFJvb3QgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MQswCQYDVQQIDAJDQTELMAkGA1UEBhMCVVMxHDAa +BgkqhkiG9w0BCQEWDXJvb3RAdGVzdC5jb20xDTALBgNVBAoMBFRlc3QxGDAWBgNV +BAsMD1Rlc3QgRGVwYXJ0bWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAJQcDfenVed0uJCykaWowL2qGUs9e5gyjGo9URoNhI5LDYSSq3ebxZGfbi1T +BcC7oL5OI/B0lCShPR9sJjKFkQ60vs43ltmkTLNSGoKLnxXMlBjdpCxweDDAMiF3 +p/vCG2hUa7auOkMWMYIkM81rcQsRB0qj0ilt3zcTsS860oKGzNrtPeAcz3KxbYWI +nJEhQVy3S4U59b7mm3cGz3/3m2NUjn+b8sA7J8F9K5mnFkCUCRva6zte6qmL2ruH +sGRav9ICLGxDqiJoic6Y2ReffgU77RDJO+sTuJme+VeTDE77vBIHvCVCSG+e0RSs +L+6nQYDtjHH5bLgoSXq9D3hgxEsCAwEAAaMQMA4wDAYDVR0TBAUwAwEB/zANBgkq +hkiG9w0BAQUFAAOCAQEARWcYJvkleq+qfOPPI1eQAuBf5OZwz8mthsBvMfsQMCLk ++wSxdlJvJyvPoFgZBv8YbTde2b40lQe94gTsGDUXGEkOsERrjt/jqHxO9X8n7a+z +M/okeSSYnam0Zcl1M9sa5L7BVXbGh/sE9j/bXrAhz64np5P773dZTLTPYjBf2Grp +NheCsGDtJbegJqn7pp5MfAKcyzLxnZAE0cilSVKZB7R3urISJVdwiRtkprJL7IwQ +oIu+XhUgdZbx7TQQnjTkq3COSIIof5+5oqFnhzTqfSgi/06dWWvCwl17Mz+vCMZ9 +1MA8L4cR+iNdJYlCQPbk30laJx1akfqnpv7qTXq7nQ== +-----END CERTIFICATE----- diff --git a/t/cakey.pem b/t/cakey.pem new file mode 100644 index 0000000..3396f25 --- /dev/null +++ b/t/cakey.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIigxGWoxgfJACAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECHgfMUpgAX2tBIIEyFcJT6kFw1TH +kmwmbtB+MPW3+zUxoKZuP+L7UCaMHQPHcQ4/AThVNPPhXa++/lgL0P1q33SBhqsm +HK5nRfzEe75bQIFoZ1FFg3YGaAB4KUPm5vJ4XhmyzCEiUvp75mgiw38fkL5ZCPqW +eitNKgyAYEl9zIrF1cRyN/s6O6mPR8NOPbVL2ZtCGGvy/LN+mhlolvuMFySEEI+s +nzu5682LU7QQOzLFB/q3Vqrt2tafzoIHYtj8YYVsPMO6bWkoKx1BWyTCuesFOLTN +agnXx3AKtmD8Q0C3vvdmpqNnWw1iP1Vc4r/BZwmD2MFXUh8cExBGRaPIfUr41BoR +p1bHqtcAsbipuU7qQ6wjnLyuqAvFQxy1LCBGNyBrzMMD25+lXh9BoeS9H5L+WW0N +pcAPA5FoD1RQc27ZXVf0kRGEy8Vj/UsE3Pf74Z8u16bC+6ojAUvvvc49ERm0t85O +T5rP9ql3sITtg4EvNdNhHtYdJGVkZQ7H8T9G1OXEf4dE/vqYNnqhtFm+5CyWb8Vk +Rly2XFbBs4I4UnVIi0Nt0ybIqqwAvvKAU5kSOIEy6Q9mvUYyvBZrhY5Va2KEwaTU +ig2yjWxWFoOArE1UVu2kJaslaOKqAr9OPqW510+6G9s+lREm7EYqUd6Ut1cwcXUv +/s1fdx/As6U6qApH9+TC0XqNm5yXNCuADVtK2PmOoanczpEXZTZGijylGFDROQQj +bofjoB0hiN80Xc+II0zClGwXwZz43Xy4uVIMbvMBi3yp03ct1RMwORPCGkiH/1uV +iazsfV4DAD8KdGmgUL57WfnMPZJCEfqpM4YrM7rKu3Md8v8hKyFGUAdeNsCFTwwY +g2E6NXWbtqGrUFC/r/70/axfijWRoyPaLcBoourc5HcZ+K/TmmY+uOxKBcJsfS/t +HiRZ5l48sva+lZVWN6KNG5N93pa3TqgCCfp56VPOo3o3kj4XAJ3aMK6RsK3QrGi7 +TTXlv/hKFYW3GUJ0IGNfinnARbdMoD6ww8nQLe3Id3HKzqh7xfQb4xJZpQhpSXVS +KJmf5H80GHQnJq54Xwi7daYmxpTS+yNHuxKq+ryBqO7WeotNkOCbG6Flc/GBO6a3 +7rbSOTmqpyJPTRPQEf1ogHNgCG9txPIbZRAi9PPJVn/gV/dKttUpj5OGI9fjDaSK +ILEwLuokdRwTAGeFRIkucKnvA0pzXQkZRMG4D4kYzxskutUQVvKFAsqvEkb2c6qL +j8MjKYfYZqxG44o68aOk8H42vR6lkemclj6byvLfnqbZPUka3MUnrG8oPI+vNLlg +N57QpU2Kw5joBf1oCSROTXEv3BT/JYE3qQ2T6q/NGgG0s8aXbROh/7HaZJrENmoD +pDJZp+bSGn6On6vmDu98F3fyubKgg611tA5pyDdWE4MSvJBGKHc2/HmqgxbLtK1c +gn2BBefAi+Qqzc+XSeJxh2nlAx7ohvgEcodO3Vqb9eSoPviMHyO9bap8QIFJqTNW +QK6rMqtou0QTEcXp5cIlVRW59zZHKEPpNpJ08fNtxB5n/Ngo+vo86LLVUysfbwkY +p7GK4kraBGhU3rEXzuFBXn1Cr7015VrAC8mLafKqar7OLhLduHDRPxGidXJc6oxE +2PpJudZFhS6P+0M5AkMq2Q== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/t/client_crt.pem b/t/client_crt.pem new file mode 100644 index 0000000..f9031ee --- /dev/null +++ b/t/client_crt.pem @@ -0,0 +1,68 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 2 (0x2) + Signature Algorithm: sha1WithRSAEncryption + Issuer: CN=Test Root Certificate Authority, ST=CA, C=US/emailAddress=root@test.com, O=Test, OU=Test Department + Validity + Not Before: Jan 7 19:26:14 2019 GMT + Not After : Jan 6 19:26:14 2024 GMT + Subject: CN=client.test.com, ST=CA, C=US/emailAddress=root@client.test.com, O=Test Client, OU=Subunit of Test Organization + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (1024 bit) + Modulus: + 00:a6:a4:24:19:14:9e:96:0a:3e:b8:14:7b:e9:04: + ca:a0:ef:61:1a:e6:63:84:51:34:10:58:9e:f2:6f: + 9d:27:64:e5:a9:32:02:e2:fb:f0:c5:47:d0:b2:8f: + f4:19:71:2a:de:f8:de:ae:cb:0b:41:cf:cc:76:63: + 9b:4a:9b:12:50:5f:b9:b4:fc:e3:fd:05:85:7f:a7: + 1c:ad:ec:d1:40:70:fa:4c:51:88:a3:d4:e5:49:b2: + 72:7c:2e:4d:c5:00:ae:40:96:15:84:34:5f:99:75: + 7b:6a:00:d3:ec:a0:7d:82:d3:71:a3:79:cc:d4:4c: + 3b:50:49:d0:9b:27:e4:0b:cb + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Key Usage: + Digital Signature, Non Repudiation, Key Encipherment + X509v3 Subject Alternative Name: + DNS:client.test.com, DNS:alt.client.test.com + Signature Algorithm: sha1WithRSAEncryption + 4f:11:ff:ed:05:e2:80:75:bd:e0:b3:f3:21:34:65:50:67:ff: + 3c:45:88:58:58:77:9a:6e:f6:1e:74:f9:cb:02:e1:31:e0:52: + d9:f9:63:e5:fb:01:d2:83:df:20:c1:77:a7:15:da:18:3a:e0: + ea:e0:66:ab:41:21:9e:36:9c:36:28:1c:cb:20:43:94:94:e8: + 9b:0d:3d:2a:ac:20:48:5b:b8:c0:45:0d:5c:30:91:be:ba:67: + b9:f6:bd:64:08:ab:af:35:a1:db:dd:54:e8:32:c9:3a:95:34: + 26:8f:a1:1b:a1:a2:32:47:a0:e0:a4:11:06:dc:d2:67:87:1a: + 51:50:bc:09:26:e3:1c:e7:83:a5:69:48:92:6c:87:94:46:f6: + b2:45:55:6f:5e:f2:6b:c8:9d:65:61:31:83:09:71:60:71:d5: + 9c:44:65:27:f6:3f:fd:fb:40:30:47:02:b1:6f:5a:ff:7a:c5: + 83:e0:80:52:53:a7:2c:24:71:51:81:df:3f:2f:1d:42:df:bc: + 86:b4:0c:18:64:8d:33:a7:c0:e8:f2:9e:f5:0b:92:c4:4d:f7: + 4b:2f:13:8d:81:25:f3:47:f5:72:71:c2:62:3d:36:09:3c:ec: + d1:15:6d:15:77:28:c6:de:f9:73:5d:5b:a0:a4:0f:f2:50:a0: + 00:20:87:fa +-----BEGIN CERTIFICATE----- +MIIDZTCCAk2gAwIBAgIBAjANBgkqhkiG9w0BAQUFADCBizEoMCYGA1UEAwwfVGVz +dCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eTELMAkGA1UECAwCQ0ExCzAJBgNV +BAYTAlVTMRwwGgYJKoZIhvcNAQkBFg1yb290QHRlc3QuY29tMQ0wCwYDVQQKDARU +ZXN0MRgwFgYDVQQLDA9UZXN0IERlcGFydG1lbnQwHhcNMTkwMTA3MTkyNjE0WhcN +MjQwMTA2MTkyNjE0WjCBljEYMBYGA1UEAwwPY2xpZW50LnRlc3QuY29tMQswCQYD +VQQIDAJDQTELMAkGA1UEBhMCVVMxIzAhBgkqhkiG9w0BCQEWFHJvb3RAY2xpZW50 +LnRlc3QuY29tMRQwEgYDVQQKDAtUZXN0IENsaWVudDElMCMGA1UECwwcU3VidW5p +dCBvZiBUZXN0IE9yZ2FuaXphdGlvbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC +gYEApqQkGRSelgo+uBR76QTKoO9hGuZjhFE0EFie8m+dJ2TlqTIC4vvwxUfQso/0 +GXEq3vjerssLQc/MdmObSpsSUF+5tPzj/QWFf6ccrezRQHD6TFGIo9TlSbJyfC5N +xQCuQJYVhDRfmXV7agDT7KB9gtNxo3nM1Ew7UEnQmyfkC8sCAwEAAaNLMEkwCQYD +VR0TBAIwADALBgNVHQ8EBAMCBeAwLwYDVR0RBCgwJoIPY2xpZW50LnRlc3QuY29t +ghNhbHQuY2xpZW50LnRlc3QuY29tMA0GCSqGSIb3DQEBBQUAA4IBAQBPEf/tBeKA +db3gs/MhNGVQZ/88RYhYWHeabvYedPnLAuEx4FLZ+WPl+wHSg98gwXenFdoYOuDq +4GarQSGeNpw2KBzLIEOUlOibDT0qrCBIW7jARQ1cMJG+ume59r1kCKuvNaHb3VTo +Msk6lTQmj6EboaIyR6DgpBEG3NJnhxpRULwJJuMc54OlaUiSbIeURvayRVVvXvJr +yJ1lYTGDCXFgcdWcRGUn9j/9+0AwRwKxb1r/esWD4IBSU6csJHFRgd8/Lx1C37yG +tAwYZI0zp8Do8p71C5LETfdLLxONgSXzR/VyccJiPTYJPOzRFW0VdyjG3vlzXVug +pA/yUKAAIIf6 +-----END CERTIFICATE----- diff --git a/t/client_key.pem b/t/client_key.pem new file mode 100644 index 0000000..22d1e37 --- /dev/null +++ b/t/client_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCmpCQZFJ6WCj64FHvpBMqg72Ea5mOEUTQQWJ7yb50nZOWpMgLi ++/DFR9Cyj/QZcSre+N6uywtBz8x2Y5tKmxJQX7m0/OP9BYV/pxyt7NFAcPpMUYij +1OVJsnJ8Lk3FAK5AlhWENF+ZdXtqANPsoH2C03GjeczUTDtQSdCbJ+QLywIDAQAB +AoGAXFVMrxzutiIdGHA5LCb8g2m/+2C2uYUo/PmtsJVJlZ9hZXuRf+WrRhSBvb7n +uQUshPmOoXld1mxmVR7h19fOsBbgKYqwDTlLZEnvdMVzaGiyeHiwDAvJgkkrK7pV +kod5JIjFd+UMho6+Qn3K7tzfttLze5xuBPNrMH3q2b8LgBECQQDbJVyF+gLk4gmd +BBInYl0BHPjYmGW9xfHfn0fhkKhQCZ9eNzH5Wbk1D8yqdlfmJ5nI8FkLQunzwzgv +P51m+USJAkEAwqpaJoVOaaRfmsgEXOsZop32DBCBHwOqOyqH9qMez8qfBPDHrUbA +TMtJN/TqLhi4VEPeaHbioUdUybA+1MggswJALWEgNrId1U2lVflY1QT+Y1OfiCKO +tux9eKQgG2p8IA7ODJF7bLoSqxU5eXcGHqfGpaB3n+hcT9j2Enqm2oL1mQJAIp53 +D9ivPDxeQEGH/RpWYcQjyLt6qxBUytbifSs/RIbtRsynRhqKAl44tDnbF72PsnSr +bfqOjU4JNyEf22mH3wJBAK1yx2ilG//KYHtcfFYtll0Rkkife+2It6bTMVdkeVCH +o5iDnW/+VsJCawhS9jHLZAjors+A9iyjygl1O6zLZTE= +-----END RSA PRIVATE KEY----- diff --git a/t/idle-timeout.t b/t/idle-timeout.t index 66558ac..bc908e9 100644 --- a/t/idle-timeout.t +++ b/t/idle-timeout.t @@ -30,7 +30,13 @@ is($stats->{idle_kicks}, "0", "check stats 2"); sleep(5); mem_stats($sock); # Network activity, so socket code will see dead socket sleep(1); -is($sock->connected(), undef, "check disconnected"); +# we run SSL tests over TCP; hence IO::Socket::SSL returns +# '' upon disconnecting with the server. +if (enabled_tls_testing()) { + is($sock->connected(),'', "check disconnected"); +} else { + is($sock->connected(),undef, "check disconnected"); +} $sock = $server->sock; $stats = mem_stats($sock); diff --git a/t/lib/MemcachedTest.pm b/t/lib/MemcachedTest.pm index 416daaf..72ebe08 100644 --- a/t/lib/MemcachedTest.pm +++ b/t/lib/MemcachedTest.pm @@ -15,7 +15,22 @@ my @unixsockets = (); @EXPORT = qw(new_memcached sleep mem_get_is mem_gets mem_gets_is mem_stats supports_sasl free_port supports_drop_priv supports_extstore - wait_ext_flush); + wait_ext_flush, supports_tls, enabled_tls_testing); + +use constant MAX_READ_WRITE_SIZE => 16384; +use constant SRV_CRT => "server_crt.pem"; +use constant SRV_KEY => "server_key.pem"; +use constant CLIENT_CRT => "client_crt.pem"; +use constant CLIENT_KEY => "client_key.pem"; +use constant CA_CRT => "cacert.pem"; + +my $testdir = $builddir . "/t/"; +my $client_crt = $testdir. CLIENT_CRT; +my $client_key = $testdir. CLIENT_KEY; +my $server_crt = $testdir . SRV_CRT; +my $server_key = $testdir . SRV_KEY; + +my $tls_checked = 0; sub sleep { my $n = shift; @@ -149,10 +164,20 @@ sub free_port { my $port; while (!$sock) { $port = int(rand(20000)) + 30000; - $sock = IO::Socket::INET->new(LocalAddr => '127.0.0.1', + if (enabled_tls_testing()) { + $sock = eval qq{ IO::Socket::SSL->new(LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => '$type', + ReuseAddr => 1, + SSL_verify_mode => SSL_VERIFY_NONE); + }; + die $@ if $@; # sanity check. + } else { + $sock = IO::Socket::INET->new(LocalAddr => '127.0.0.1', LocalPort => $port, Proto => $type, ReuseAddr => 1); + } } return $port; } @@ -175,6 +200,23 @@ sub supports_extstore { return 0; } +sub supports_tls { + my $output = `$builddir/memcached-debug -h`; + return 1 if $output =~ /enable-ssl/i; + return 0; +} + +sub enabled_tls_testing { + if ($tls_checked) { + return 1; + } elsif (supports_tls() && $ENV{SSL_TEST}) { + eval "use IO::Socket::SSL"; + croak("IO::Socket::SSL not installed or failed to load, cannot run SSL tests as requested") if $@; + $tls_checked = 1; + return 1; + } +} + sub supports_drop_priv { my $output = `$builddir/memcached-debug -h`; return 1 if $output =~ /no_drop_privileges/i; @@ -185,10 +227,21 @@ sub new_memcached { my ($args, $passed_port) = @_; my $port = $passed_port; my $host = '127.0.0.1'; + my $ssl_enabled = enabled_tls_testing(); if ($ENV{T_MEMD_USE_DAEMON}) { my ($host, $port) = ($ENV{T_MEMD_USE_DAEMON} =~ m/^([^:]+):(\d+)$/); - my $conn = IO::Socket::INET->new(PeerAddr => "$host:$port"); + my $conn; + if ($ssl_enabled) { + $conn = eval qq{IO::Socket::SSL->new(PeerAddr => "$host:$port", + SSL_verify_mode => SSL_VERIFY_NONE, + SSL_cert_file => '$client_crt', + SSL_key_file => '$client_key'); + }; + die $@ if $@; # sanity check. + } else { + $conn = IO::Socket::INET->new(PeerAddr => "$host:$port"); + } if ($conn) { return Memcached::Handle->new(conn => $conn, host => $host, @@ -203,13 +256,18 @@ sub new_memcached { $args .= " -o relaxed_privileges"; my $udpport; - if ($args =~ /-l (\S+)/) { - $port = free_port(); + if ($args =~ /-l (\S+)/ || ($ssl_enabled && ($args !~ /-s (\S+)/))) { + if (!$port) { + $port = free_port(); + } $udpport = free_port("udp"); $args .= " -p $port"; if (supports_udp()) { $args .= " -U $udpport"; } + if ($ssl_enabled) { + $args .= " -Z -o ssl_chain_cert=$server_crt -o ssl_key=$server_key"; + } } elsif ($args !~ /-s (\S+)/) { my $num = @unixsockets; my $file = "/tmp/memcachetest.$$.$num"; @@ -246,7 +304,17 @@ sub new_memcached { # sockets for (1..20) { - my $conn = IO::Socket::INET->new(PeerAddr => "127.0.0.1:$port"); + my $conn; + if ($ssl_enabled) { + $conn = eval qq{ IO::Socket::SSL->new(PeerAddr => "127.0.0.1:$port", + SSL_verify_mode => SSL_VERIFY_NONE, + SSL_cert_file => '$client_crt', + SSL_key_file => '$client_key'); + }; + die $@ if $@; # sanity check. + } else { + $conn = IO::Socket::INET->new(PeerAddr => "127.0.0.1:$port"); + } if ($conn) { return Memcached::Handle->new(pid => $childpid, conn => $conn, @@ -299,6 +367,12 @@ sub new_sock { my $self = shift; if ($self->{domainsocket}) { return IO::Socket::UNIX->new(Peer => $self->{domainsocket}); + } elsif (MemcachedTest::enabled_tls_testing()) { + return eval qq{ IO::Socket::SSL->new(PeerAddr => "$self->{host}:$self->{port}", + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE, + SSL_cert_file => '$client_crt', + SSL_key_file => '$client_key'); + }; } else { return IO::Socket::INET->new(PeerAddr => "$self->{host}:$self->{port}"); } diff --git a/t/misbehave.t b/t/misbehave.t index 13cb7f3..1312a27 100755 --- a/t/misbehave.t +++ b/t/misbehave.t @@ -7,7 +7,7 @@ use Socket qw(MSG_PEEK MSG_DONTWAIT); use lib "$Bin/lib"; use MemcachedTest; -if (supports_drop_priv()) { +if (!enabled_tls_testing() && supports_drop_priv()) { plan tests => 1; } else { plan skip_all => 'Privilege drop not supported'; diff --git a/t/multiversioning.t b/t/multiversioning.t index df9eab4..c65c2df 100755 --- a/t/multiversioning.t +++ b/t/multiversioning.t @@ -24,7 +24,17 @@ mem_get_is($sock, "big", $bigval, "big value got correctly"); print $sock "get big\r\n"; my $buf; -is(read($sock, $buf, $size / 2), $size / 2, "read half the answer back"); +my $read = 0; +my $to_read = $size / 2; +while ($read < $to_read) { + my $read_bytes = $sock->sysread($buf, + ($to_read - $read > MemcachedTest::MAX_READ_WRITE_SIZE ? + MemcachedTest::MAX_READ_WRITE_SIZE : $to_read - $read), + $read); + last if ($read_bytes <= 0); + $read += $read_bytes; +} +is($read, $size / 2, "read half the answer back"); like($buf, qr/VALUE big/, "buf has big value header in it"); like($buf, qr/abcdef/, "buf has some data in it"); unlike($buf, qr/abcde\]/, "buf doesn't yet close"); diff --git a/t/noreply.t b/t/noreply.t index 54a3f13..6aa3dc2 100644 --- a/t/noreply.t +++ b/t/noreply.t @@ -44,4 +44,3 @@ mem_get_is($sock, "noreply:foo", "7"); print $sock "delete noreply:foo noreply\r\n"; mem_get_is($sock, "noreply:foo"); - diff --git a/t/server.pem b/t/server.pem new file mode 100644 index 0000000..d4bc393 --- /dev/null +++ b/t/server.pem @@ -0,0 +1,79 @@ +subject= C = UK, O = OpenSSL Group, OU = FOR TESTING PURPOSES ONLY, CN = Test Server Cert +issuer= C = UK, O = OpenSSL Group, OU = FOR TESTING PURPOSES ONLY, CN = OpenSSL Test Intermediate CA +-----BEGIN CERTIFICATE----- +MIID0DCCArigAwIBAgIIcsOElVeHzfYwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UE +BhMCVUsxFjAUBgNVBAoMDU9wZW5TU0wgR3JvdXAxIjAgBgNVBAsMGUZPUiBURVNU +SU5HIFBVUlBPU0VTIE9OTFkxJTAjBgNVBAMMHE9wZW5TU0wgVGVzdCBJbnRlcm1l +ZGlhdGUgQ0EwIBcNMTgwNjE0MTI0NjI4WhgPMjExODA2MTQxMjQ2MjhaMGQxCzAJ +BgNVBAYTAlVLMRYwFAYDVQQKDA1PcGVuU1NMIEdyb3VwMSIwIAYDVQQLDBlGT1Ig +VEVTVElORyBQVVJQT1NFUyBPTkxZMRkwFwYDVQQDDBBUZXN0IFNlcnZlciBDZXJ0 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0jIZ8IZ4dIzBc+ZfdmG5 +n8G3JzRX99QvIqv52s4hFVfdzoa+AciKJpo9zkegWPmfsAVNa4uVceg/ZQt6qJsu +G/pxbQSZVnyjDQGtt7rgaDEbyUP0XJCnzyRdWSUjFS8yNZn4NkmZU01GlHtXdzWy +dEa5PaiTIwW0HI+bjjOEhwJ1hFuFqzlKHVKHA6DBzNcl6ly0E/q2kyslbR+0hq7p +NMqKvvuAxqgc//W8KvLDlKAt9D3t5zgh2+BrMPemrzjEaM97yHTogJo7+SKVDdUw +YQ7Br3xfyki9u2bUYib1BMSvLezxNP0qf/iU91z4xyLmMvOXE6W0D1WHwya1CfE7 +vwIDAQABo3gwdjAdBgNVHQ4EFgQU3ulCbvgfxej6rHnddMpBidwnLIIwHwYDVR0j +BBgwFoAUCgNEpWg658NdblSLsvg43EA1WwUwCQYDVR0TBAIwADATBgNVHSUEDDAK +BggrBgEFBQcDATAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQAD +ggEBAENMzaqJtmWED++W4KXFVwNBkQ87errBXe4jVeYKpjNb0JGMm60MS5ty54fb +r27SsR2EEk3EK2rcd85RR7TEKZCn9SvPykVtVf0tru7nOptQJgSbRvxIzyyq1UcE +K+BXDgN/I0f1X6qbk4Stb6uJF7yyAUabacjwKqgVifOOeKF9WJhVA8qJKoVq7HLN +k+uvm0geO1I4LKeULXVnQy8kwB6twcxN8iPyO45ZxbYIVeEKaYtbj/XPoq6KsLIb +5fj+mK1r/LkWk352ksNhf73r3alF8TBcSLqnbMoy1/ZvzlI4ksp9IGWtIU+CzP/f +VUjh00NOwDLd5jJbPoWW0oNp9m4= +-----END CERTIFICATE----- +subject= C = UK, O = OpenSSL Group, OU = FOR TESTING PURPOSES ONLY, CN = OpenSSL Test Intermediate CA +issuer= C = UK, O = OpenSSL Group, OU = FOR TESTING PURPOSES ONLY, CN = OpenSSL Test Root CA +-----BEGIN CERTIFICATE----- +MIIEPzCCAqegAwIBAgIILsaQqJAjK4IwDQYJKoZIhvcNAQELBQAwaDELMAkGA1UE +BhMCVUsxFjAUBgNVBAoMDU9wZW5TU0wgR3JvdXAxIjAgBgNVBAsMGUZPUiBURVNU +SU5HIFBVUlBPU0VTIE9OTFkxHTAbBgNVBAMMFE9wZW5TU0wgVGVzdCBSb290IENB +MCAXDTE4MDYxNDEyNDYyOFoYDzIxMTgwNjE0MTI0NjI4WjBwMQswCQYDVQQGEwJV +SzEWMBQGA1UECgwNT3BlblNTTCBHcm91cDEiMCAGA1UECwwZRk9SIFRFU1RJTkcg +UFVSUE9TRVMgT05MWTElMCMGA1UEAwwcT3BlblNTTCBUZXN0IEludGVybWVkaWF0 +ZSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANIpVng2wNFJp2kF +oJ6Yji25wy1YufnS8NxA82fk5OHdhGWj1CWqnQNotEqEQzcOUszQYrNxd8tEvoWk +Ik4JMBVoEcgBGedchftptTNulFWodWpi1yFaqA/Nz2BsVgcCJW4C+UWDT7VeHtGU +7tYKKr35lxp6io/a4jUDQXvO2nJA9YlrxOktunMqtoZSYqUz35ZXsdkn58o8Fbqm +dEpw6AqAr9aBgY5DSaGxbaX2lwNt9NvB+f9ucOqEnPP8AfTlPYc/ENwJ6u/H8RGw +d1im71mu2lHjcws3aHkbluH860U3vlKWx6Ff1qdQcH98e2HwElqxCK00xya8leu4 +u64nljkCAwEAAaNjMGEwHQYDVR0OBBYEFAoDRKVoOufDXW5Ui7L4ONxANVsFMB8G +A1UdIwQYMBaAFDZjTeLsQUG6KL9xuLhzXVdB4pkKMA8GA1UdEwEB/wQFMAMBAf8w +DgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBgQDZQJLA90ewVaS3E3du +gSjPkQ1xsHm8H1am+7zr5oZ81J+R8XYIZgMR+9ShVo38OradiYNqDLso+4iuVdxh +hzoSoQELoDXCficzWKnlAtWvwDDoczyK+/p94g3VKx14n2+GvQzoZ4kwQQgaFH1w +YI6w0oH9zwoklCxvihj8D069QrYyuTT8JGZ2m0FHqVJg6teuQKFahSgwYR2CUoIb +6PrpSUQeCVCH8TPkzlRT6UgtM3ERt7+TlQ+zZ80dSf4YTAsDv9Z/CJXiF/5wZr6/ +lWuFjWmX2HkpEW6Wiv5KF8QP6Ft7Z+RYua7RMtELCYvqYbWDBs7fXWGBkZ5xhB09 +jCxz+F7zOeRbyzacfFq9DhxCWCRbIrdgGGE/Of2ujJtmK/2p4M6E5IsKNAI2SJBW +iJXvIgQgR22ehPqy6er2Gog5LkWUwqB0kHZJJpbp1IW01IGTpD6YAJyVCEAlyMbo +Kto9+wQFLT3Auv/W5h6OwxkNdfAyZBYy0ZSFk4EE8OdWWY4= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA0jIZ8IZ4dIzBc+ZfdmG5n8G3JzRX99QvIqv52s4hFVfdzoa+ +AciKJpo9zkegWPmfsAVNa4uVceg/ZQt6qJsuG/pxbQSZVnyjDQGtt7rgaDEbyUP0 +XJCnzyRdWSUjFS8yNZn4NkmZU01GlHtXdzWydEa5PaiTIwW0HI+bjjOEhwJ1hFuF +qzlKHVKHA6DBzNcl6ly0E/q2kyslbR+0hq7pNMqKvvuAxqgc//W8KvLDlKAt9D3t +5zgh2+BrMPemrzjEaM97yHTogJo7+SKVDdUwYQ7Br3xfyki9u2bUYib1BMSvLezx +NP0qf/iU91z4xyLmMvOXE6W0D1WHwya1CfE7vwIDAQABAoIBAQC2HAo1RYvfDoQc +sh9LJWf5bZANO2Brqz4bP/x9AdHP+AyH/l1oliJ7R2785TmbXMppam6lGo4j3h/u +n39pzOip/NWAqldfgySRBD9Jy3LZUpLMUT/JYtrAsLTfozk+BWHu5rMR9boNXgok +Yqho8/DkpNGhBghUc4CUricLkL7laD3ziAHpx8yALL3tnLGOpgT9hNrA8Dm3yfUS +JEfiG12ILXvq1IP+vUNuaLpTLJZuqUmLpK8v+CBYgKxfd+TDnEjul4PqhhIIFK3A +xEZYQR2D/AXUwng9hP9uCbVm5lOY6vRbi9Fpbt+KRv+m25s1AnuhJFBOsL30h/Tb +iCKWm/nhAoGBAO0bFqMvZHjaT2KiwOwG/Ze9NsjynFPVltiuCqNj8HE5wM6imC5J +SdB+jMkgN6ERXALWrtr8Uf2pqzfeMsi6pekOOVTWLe/8c4bAZRxaCZn/BlZRysZI +vB9Gb7m7Oymw5iDSqrYywgOiUu+oIiCrmPOealhmn7zmHzHaETvdL9zDAoGBAOLy +DVT1csoexnuHVIWqnp7FK7lv6eOGZSdXpfJ3XYjmKJLK2hpVZe+J/mFOL1wsKSt4 +0k/V0dnkHR7V4Pa4ECiCthkWMWrBVIHe7+ZnZ0ocKQSC+EEecavOiZ57S/qnUlT6 +NtQP4cSy4DHzzFZdTZnn+2oymapPZpb2mvSN/GVVAoGADrIlHwwq8Aqn7Pclefuc +8DC8GoxfABs29EslQadKGdp4htYxFH1aY9/UHgsvJ36J82sW/1+wPUas5BOTljlr +WxyUlRuJUVyWVH3MRouWGMNjwynipZOQhWe6OQrPye+688Ha7twKhmsjNNN4+glo +u4DQGpaRxAWHXXGkq88zzj0CgYEAsICEceD7R8srnwMfb13FQ8IhQXWSuAvcO/7k +53CCZGhsgc4WVoi4YNY360G9f7gwxMiQ+NpY/Vd2dnbtIbUBjCAss9IY2OhHa0IR +3mXpZTAFjqa1oR+mVHKrgYBvFSBw3fpEDiXT9wEPcIomD709D0fmty9nZ5edOCfP +WAfdlokCgYEAqXuMuAg3NMMgEv+eBfsf43v3hRwBqPYanE26wcO3GoT/S8BpB6wy +vBoPZOlO5ZfsD2jaTec60GLay+MofxC7qNXIjzHOw50ry4bqHqqoQbn2cONE1k+0 +ov7H2keTcG9FEGgL7dRUq3pRUo/W12WmRuDN17IEgkzAeisJnoiPtaQ= +-----END RSA PRIVATE KEY----- diff --git a/t/server_crt.pem b/t/server_crt.pem new file mode 100644 index 0000000..6b7a315 --- /dev/null +++ b/t/server_crt.pem @@ -0,0 +1,67 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha1WithRSAEncryption + Issuer: CN=Test Root Certificate Authority, ST=CA, C=US/emailAddress=root@test.com, O=Test, OU=Test Department + Validity + Not Before: Jan 7 18:25:01 2019 GMT + Not After : Jan 6 18:25:01 2024 GMT + Subject: CN=test.com, ST=CA, C=US/emailAddress=root@test.com, O=Test, OU=Subunit of Test Organization + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (1024 bit) + Modulus: + 00:ca:76:35:79:91:e8:a2:ee:ef:f4:35:7e:29:85: + 75:90:65:5a:60:f8:a0:d9:ef:68:d1:61:79:69:d7: + e2:7b:f1:67:71:65:50:31:4e:9a:f8:6f:27:e9:05: + 0b:0e:76:95:24:9b:c2:bf:90:e5:6b:45:fd:e3:54: + ac:d5:62:90:4e:37:de:8f:ae:96:f6:b3:57:eb:ad: + b8:44:13:5d:a7:34:76:c1:26:49:91:67:3e:5e:52: + 68:c1:1c:7f:91:c7:9e:01:e2:be:a7:a8:eb:3f:44: + 6d:c7:c5:82:4b:97:d9:3f:c0:51:99:1b:20:df:12: + a3:e6:bb:66:45:6d:b4:e1:07 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Key Usage: + Digital Signature, Non Repudiation, Key Encipherment + X509v3 Subject Alternative Name: + DNS:test.com, DNS:alt.test.com + Signature Algorithm: sha1WithRSAEncryption + 61:c1:18:b4:04:79:05:0e:b9:79:2f:0e:3f:aa:f1:36:e3:90: + b9:c6:99:25:53:4a:06:64:52:92:29:c5:09:41:a6:16:74:1b: + 3c:4e:81:32:c2:d8:54:e0:1e:08:45:cf:f1:d0:ad:ea:11:1a: + b5:cf:7c:98:8a:dd:c1:01:e4:d0:f5:8e:60:fa:7f:e7:74:2d: + 91:43:81:bd:95:92:41:66:84:8b:8c:70:d7:2f:d4:2f:37:82: + 8f:9a:ef:c0:7d:c5:56:56:92:7a:00:b6:30:65:37:4c:6c:7a: + ba:cc:e2:dc:73:e9:f5:2c:3c:3e:31:67:ee:3d:b7:78:96:89: + ba:be:4f:85:a2:a8:83:3e:53:20:f0:bf:29:50:dc:23:38:58: + d8:33:f5:7b:4a:12:df:2b:34:4c:1c:f1:76:6b:86:95:74:43: + 29:f7:68:f0:ca:04:08:89:ac:97:7d:05:14:a0:ca:81:56:5c: + dd:c3:56:a3:53:01:0a:01:5c:55:b4:39:10:1b:be:47:19:58: + a1:f2:e6:0c:08:95:b0:35:4e:6c:81:6d:b9:cf:0e:5c:70:ff: + f3:b4:a7:95:69:1a:58:b7:ac:cc:2c:79:47:7e:20:17:cc:36: + be:2c:10:11:31:28:63:dd:6f:8d:8d:e9:11:ea:ca:fc:10:0a: + e0:ae:53:db +-----BEGIN CERTIFICATE----- +MIIDQjCCAiqgAwIBAgIBATANBgkqhkiG9w0BAQUFADCBizEoMCYGA1UEAwwfVGVz +dCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eTELMAkGA1UECAwCQ0ExCzAJBgNV +BAYTAlVTMRwwGgYJKoZIhvcNAQkBFg1yb290QHRlc3QuY29tMQ0wCwYDVQQKDARU +ZXN0MRgwFgYDVQQLDA9UZXN0IERlcGFydG1lbnQwHhcNMTkwMTA3MTgyNTAxWhcN +MjQwMTA2MTgyNTAxWjCBgTERMA8GA1UEAwwIdGVzdC5jb20xCzAJBgNVBAgMAkNB +MQswCQYDVQQGEwJVUzEcMBoGCSqGSIb3DQEJARYNcm9vdEB0ZXN0LmNvbTENMAsG +A1UECgwEVGVzdDElMCMGA1UECwwcU3VidW5pdCBvZiBUZXN0IE9yZ2FuaXphdGlv +bjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAynY1eZHoou7v9DV+KYV1kGVa +YPig2e9o0WF5adfie/FncWVQMU6a+G8n6QULDnaVJJvCv5Dla0X941Ss1WKQTjfe +j66W9rNX6624RBNdpzR2wSZJkWc+XlJowRx/kceeAeK+p6jrP0Rtx8WCS5fZP8BR +mRsg3xKj5rtmRW204QcCAwEAAaM9MDswCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAw +IQYDVR0RBBowGIIIdGVzdC5jb22CDGFsdC50ZXN0LmNvbTANBgkqhkiG9w0BAQUF +AAOCAQEAYcEYtAR5BQ65eS8OP6rxNuOQucaZJVNKBmRSkinFCUGmFnQbPE6BMsLY +VOAeCEXP8dCt6hEatc98mIrdwQHk0PWOYPp/53QtkUOBvZWSQWaEi4xw1y/ULzeC +j5rvwH3FVlaSegC2MGU3TGx6uszi3HPp9Sw8PjFn7j23eJaJur5PhaKogz5TIPC/ +KVDcIzhY2DP1e0oS3ys0TBzxdmuGlXRDKfdo8MoECImsl30FFKDKgVZc3cNWo1MB +CgFcVbQ5EBu+RxlYofLmDAiVsDVObIFtuc8OXHD/87SnlWkaWLeszCx5R34gF8w2 +viwQETEoY91vjY3pEerK/BAK4K5T2w== +-----END CERTIFICATE----- diff --git a/t/server_key.pem b/t/server_key.pem new file mode 100644 index 0000000..9e02ce4 --- /dev/null +++ b/t/server_key.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDKdjV5keii7u/0NX4phXWQZVpg+KDZ72jRYXlp1+J78WdxZVAx +Tpr4byfpBQsOdpUkm8K/kOVrRf3jVKzVYpBON96Prpb2s1frrbhEE12nNHbBJkmR +Zz5eUmjBHH+Rx54B4r6nqOs/RG3HxYJLl9k/wFGZGyDfEqPmu2ZFbbThBwIDAQAB +AoGBAItN/ItSVrRNHTN13wHovzSEWERiriJl9UQkAmtOTJqlRlyiriPPBxgrO1W8 +z5e7BfGzbrNqmkBOX1uctnL4J3tA7xFO6OnquDCLXhLc49mw+zxkcDP5ta1ROeiV +kIm8rSsvh8ks7StA17m5910rNfn5/IRHK6dC+G4FK7qLfCiRAkEA5Szy22sFeE4D +OM/wmCHpoOc0qA5k0FeyvnfGFKcAsVZ2+b/MPI2/KbB0uY/PWj43H6h9fEqMGVf6 +J7Ukas1N2QJBAOIozZjzfs3PQvKHMzOG3mQt+qSjsDKiEq/cPMIRPXYX5pS0+2Gz +mtUaikI+Nk5hekXqA2i+4uis9UhCzm+W+d8CQQCEo/ZPrmp1DdnpiNh8hKw+l3Kv +jd0lhIyMlrALhfjtqtijhjHEHlo0289DEwv09CtdZFx0koTxqiy7zKiuM/NJAkBZ +S2sB/QIQGMmCIMeijJm6TD0uTEMBeuSN8xM6PLxbqEwuYtbuWI/FnFkClrWydOJm +QGNgNB47aC7gfSAtBxtZAkBiCJcoVmXkm3rS7scUgCHrNhKH0G0nJbqjgfj57I31 +rCw34N6L+fyUozJQCBYdECyI1xG2eajqrMmnVZ046cjF +-----END RSA PRIVATE KEY----- diff --git a/t/ssl_cert_refresh.t b/t/ssl_cert_refresh.t new file mode 100644 index 0000000..6fdb37a --- /dev/null +++ b/t/ssl_cert_refresh.t @@ -0,0 +1,76 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use File::Copy; +use Test::More; +use FindBin qw($Bin); +use lib "$Bin/lib"; +use MemcachedTest; + +if (!enabled_tls_testing()) { + plan skip_all => 'SSL testing is not enabled'; + exit 0; +} + +my $cert = "t/". MemcachedTest::SRV_CRT; +my $key = "t/". MemcachedTest::SRV_KEY; +my $cert_back = "t/cert_back"; +my $key_back = "t/pkey_back"; +my $new_cert_key = "t/server.pem"; +my $default_crt_ou = "OU=Subunit of Test Organization"; + +my $server = new_memcached(); +my $stats = mem_stats($server->sock); +my $pid = $stats->{pid}; +my $sock = $server->sock; + +# This connection should return the default server certificate +# memcached was started with. +my $cert_details =$sock->dump_peer_certificate(); +$cert_details =~ m/(OU=([^\/\n]*))/; +is($1, $default_crt_ou, 'Got the default cert'); + +# Swap a new certificate with a key +copy($cert, $cert_back) or die "Cert backup failed: $!"; +copy($key, $key_back) or die "Key backup failed: $!"; +copy($new_cert_key, $cert) or die "New Cert copy failed: $!"; +copy($new_cert_key, $key) or die "New key copy failed: $!"; + +# Ask server to refresh certificates +print $sock "refresh_certs\r\n"; +is(scalar <$sock>, "OK\r\n", "refreshed certificates"); + +# New connections should use the new certificate +$cert_details = $server->new_sock->dump_peer_certificate(); +$cert_details =~ m/(OU=([^\/]*))/; +is($1, 'OU=FOR TESTING PURPOSES ONLY','Got the new cert'); +# Old connection should use the previous certificate +$cert_details =$sock->dump_peer_certificate(); +$cert_details =~ m/(OU=([^\/\n]*))/; +is($1, $default_crt_ou, 'Old connection still has the old cert'); + +# Just sleep a while to test the time_since_server_cert_refresh as it's counted +# in seconds. +sleep 2; +$stats = mem_stats($sock); + +# Restore and ensure previous certificate is back for new connections. +move($cert_back, $cert) or die "Cert restore failed: $!"; +move($key_back, $key) or die "Key restore failed: $!"; +print $sock "refresh_certs\r\n"; +is(scalar <$sock>, "OK\r\n", "refreshed certificates"); + + +$cert_details = $server->new_sock->dump_peer_certificate(); +$cert_details =~ m/(OU=([^\/\n]*))/; +is($1, $default_crt_ou, 'Got the old cert back'); + +my $stats_after = mem_stats($sock); + +# We should see last refresh time is reset; hence the new +# time_since_server_cert_refresh should be less. +cmp_ok($stats_after->{time_since_server_cert_refresh}, '<', + $stats->{time_since_server_cert_refresh}, 'Certs refreshed'); + +done_testing(); diff --git a/t/ssl_ports.t b/t/ssl_ports.t new file mode 100644 index 0000000..d717f63 --- /dev/null +++ b/t/ssl_ports.t @@ -0,0 +1,31 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Test::More; +use FindBin qw($Bin); +use lib "$Bin/lib"; +use MemcachedTest; + + +if (!enabled_tls_testing()) { + plan skip_all => 'SSL testing is not enabled'; + exit 0; +} + +my $tcp_port = free_port(); +my $ssl_port = free_port(); + +my $server = new_memcached("-l notls:127.0.0.1:$tcp_port,127.0.0.1:$ssl_port", $ssl_port); +my $sock = $server->sock; + +# Make sure we can talk over SSL +print $sock "set foo:123 0 0 16\r\nfoo set over SSL\r\n"; +is(scalar <$sock>, "STORED\r\n", "stored foo"); + + +#.. and TCP +my $tcp_sock = IO::Socket::INET->new(PeerAddr => "127.0.0.1:$tcp_port"); +mem_get_is($tcp_sock, "foo:123", "foo set over SSL"); + +done_testing() diff --git a/t/ssl_settings.t b/t/ssl_settings.t new file mode 100644 index 0000000..57f9668 --- /dev/null +++ b/t/ssl_settings.t @@ -0,0 +1,36 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Test::More; +use FindBin qw($Bin); +use lib "$Bin/lib"; +use MemcachedTest; +use Cwd; + +if (!enabled_tls_testing()) { + plan skip_all => 'SSL testing is not enabled'; + exit 0; +} + +my $server = new_memcached(); +my $settings = mem_stats($server->sock, ' settings'); + +my $cert = getcwd ."/t/". MemcachedTest::SRV_CRT; +my $key = getcwd ."/t/". MemcachedTest::SRV_KEY; + +is($settings->{'ssl_enabled'}, 'yes'); +is($settings->{'ssl_chain_cert'}, $cert); +is($settings->{'ssl_key'}, $key); +is($settings->{'ssl_verify_mode'}, 0); +is($settings->{'ssl_keyformat'}, 1); +is($settings->{'ssl_ciphers'}, 'NULL'); +is($settings->{'ssl_ca_cert'}, 'NULL'); +is($settings->{'ssl_wbuf_size'}, 16384); + +$server->DESTROY(); +$server = new_memcached("-o ssl_wbuf_size=64"); +$settings = mem_stats($server->sock, ' settings'); +is($settings->{'ssl_wbuf_size'},65536); + +done_testing(); diff --git a/t/ssl_verify_modes.t b/t/ssl_verify_modes.t new file mode 100644 index 0000000..4d6445c --- /dev/null +++ b/t/ssl_verify_modes.t @@ -0,0 +1,22 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Test::More; +use FindBin qw($Bin); +use lib "$Bin/lib"; +use MemcachedTest; +use Cwd; + +if (!enabled_tls_testing()) { + plan skip_all => 'SSL testing is not enabled'; + exit 0; +} + +my $ca_crt = getcwd() . "/t/" . MemcachedTest::CA_CRT; +my $server = new_memcached("-o ssl_verify_mode=2 -o ssl_ca_cert=$ca_crt"); +# just using stats to make sure everything is working fine. +my $stats = mem_stats($server->sock); +is($stats->{accepting_conns}, 1, "client cert is verified"); + +done_testing(); @@ -24,7 +24,12 @@ my $sock = $server->sock; my $stats = mem_stats($sock); # Test number of keys -is(scalar(keys(%$stats)), 70, "expected count of stats values"); +if (MemcachedTest::enabled_tls_testing()) { + # when TLS is enabled, stats contains time_since_server_cert_refresh + is(scalar(keys(%$stats)), 71, "expected count of stats values"); +} else { + is(scalar(keys(%$stats)), 70, "expected count of stats values"); +} # Test initial state foreach my $key (qw(curr_items total_items bytes cmd_get cmd_set get_hits evictions get_misses get_expired @@ -130,7 +135,13 @@ is('z', $v, 'got the expected value'); my $settings = mem_stats($sock, ' settings'); is(1024, $settings->{'maxconns'}); -isnt('NULL', $settings->{'domain_socket'}); +# we run SSL tests over TCP; hence the domain_socket +# is expected to be NULL. +if (enabled_tls_testing()) { + is('NULL', $settings->{'domain_socket'}); +} else { + isnt('NULL', $settings->{'domain_socket'}); +} is('on', $settings->{'evictions'}); is('yes', $settings->{'cas_enabled'}); is('no', $settings->{'auth_enabled_sasl'}); @@ -22,15 +22,73 @@ #include "cache.h" #include "util.h" #include "protocol_binary.h" +#ifdef TLS +#include <openssl/ssl.h> +#endif #define TMP_TEMPLATE "/tmp/test_file.XXXXXXX" enum test_return { TEST_SKIP, TEST_PASS, TEST_FAIL }; +struct conn { + int sock; +#ifdef TLS + SSL_CTX *ssl_ctx; + SSL *ssl; +#endif + ssize_t (*read)(struct conn *c, void *buf, size_t count); + ssize_t (*write)(struct conn *c, const void *buf, size_t count); +}; + + +static ssize_t tcp_read(struct conn *c, void *buf, size_t count); +static ssize_t tcp_write(struct conn *c, const void *buf, size_t count); +#ifdef TLS +static ssize_t ssl_read(struct conn *c, void *buf, size_t count); +static ssize_t ssl_write(struct conn *c, const void *buf, size_t count); +#endif + +ssize_t tcp_read(struct conn *c, void *buf, size_t count) { + assert(c != NULL); + return read(c->sock, buf, count); +} + +ssize_t tcp_write(struct conn *c, const void *buf, size_t count) { + assert(c != NULL); + return write(c->sock, buf, count); +} +#ifdef TLS +ssize_t ssl_read(struct conn *c, void *buf, size_t count) { + assert(c != NULL); + return SSL_read(c->ssl, buf, count); +} + +ssize_t ssl_write(struct conn *c, const void *buf, size_t count) { + assert(c != NULL); + return SSL_write(c->ssl, buf, count); +} +#endif + static pid_t server_pid; static in_port_t port; -static int sock; +static struct conn *con = NULL; static bool allow_closed_read = false; +static bool enable_ssl = false; + +static void close_conn() { + if (con == NULL) return; +#ifdef TLS + if (con->ssl) { + SSL_shutdown(con->ssl); + SSL_free(con->ssl); + } + if (con->ssl_ctx) + SSL_CTX_free(con->ssl_ctx); +#endif + if (con->sock > 0) close(con->sock); + free(con); + con = NULL; +} static enum test_return cache_create_test(void) { @@ -318,7 +376,7 @@ static pid_t start_server(in_port_t *port_out, bool daemon, int timeout) { if (pid == 0) { /* Child */ - char *argv[20]; + char *argv[24]; int arg = 0; char tmo[24]; snprintf(tmo, sizeof(tmo), "%u", timeout); @@ -339,6 +397,15 @@ static pid_t start_server(in_port_t *port_out, bool daemon, int timeout) { argv[arg++] = "-1"; argv[arg++] = "-U"; argv[arg++] = "0"; +#ifdef TLS + if (enable_ssl) { + argv[arg++] = "-Z"; + argv[arg++] = "-o"; + argv[arg++] = "ssl_chain_cert=t/server_crt.pem"; + argv[arg++] = "-o"; + argv[arg++] = "ssl_key=t/server_key.pem"; + } +#endif /* Handle rpmbuild and the like doing this as root */ if (getuid() == 0) { argv[arg++] = "-u"; @@ -445,8 +512,15 @@ static struct addrinfo *lookuphost(const char *hostname, in_port_t port) return ai; } -static int connect_server(const char *hostname, in_port_t port, bool nonblock) +static struct conn *connect_server(const char *hostname, in_port_t port, + bool nonblock, const bool ssl) { + struct conn *c; + if (!(c = (struct conn *)calloc(1, sizeof(struct conn)))) { + fprintf(stderr, "Failed to allocate the client connection: %s\n", + strerror(errno)); + } + struct addrinfo *ai = lookuphost(hostname, port); int sock = -1; if (ai != NULL) { @@ -472,7 +546,43 @@ static int connect_server(const char *hostname, in_port_t port, bool nonblock) freeaddrinfo(ai); } - return sock; + c->sock = sock; +#ifdef TLS + if (sock > 0 && ssl) { + c->ssl_ctx = SSL_CTX_new(SSLv23_client_method()); + if (c->ssl_ctx == NULL) { + fprintf(stderr, "Failed to create the SSL context: %s\n", + strerror(errno)); + close(sock); + sock = -1; + } + c->ssl = SSL_new(c->ssl_ctx); + if (c->ssl == NULL) { + fprintf(stderr, "Failed to create the SSL object: %s\n", + strerror(errno)); + close(sock); + sock = -1; + } + SSL_set_fd (c->ssl, c->sock); + int ret = SSL_connect(c->ssl); + if (ret < 0) { + int err = SSL_get_error(c->ssl, ret); + if (err == SSL_ERROR_SYSCALL || err == SSL_ERROR_SSL) { + fprintf(stderr, "SSL connection failed with error code : %s\n", + strerror(errno)); + close(sock); + sock = -1; + } + } + c->read = ssl_read; + c->write = ssl_write; + } else +#endif + { + c->read = tcp_read; + c->write = tcp_write; + } + return c; } static enum test_return test_vperror(void) { @@ -525,7 +635,7 @@ static void send_ascii_command(const char *buf) { size_t len = strlen(buf); do { - ssize_t nw = write(sock, ptr + offset, len - offset); + ssize_t nw = con->write((void*)con, ptr + offset, len - offset); if (nw == -1) { if (errno != EINTR) { fprintf(stderr, "Failed to write: %s\n", strerror(errno)); @@ -547,7 +657,7 @@ static void read_ascii_response(char *buffer, size_t size) { off_t offset = 0; bool need_more = true; do { - ssize_t nr = read(sock, buffer + offset, 1); + ssize_t nr = con->read(con, buffer + offset, 1); if (nr == -1) { if (errno != EINTR) { fprintf(stderr, "Failed to read: %s\n", strerror(errno)); @@ -568,10 +678,11 @@ static void read_ascii_response(char *buffer, size_t size) { static enum test_return test_issue_92(void) { char buffer[1024]; - close(sock); - sock = connect_server("127.0.0.1", port, false); + close_conn(); + con = connect_server("127.0.0.1", port, false, enable_ssl); send_ascii_command("stats cachedump 1 0 0\r\n"); + read_ascii_response(buffer, sizeof(buffer)); assert(strncmp(buffer, "END", strlen("END")) == 0); @@ -579,8 +690,8 @@ static enum test_return test_issue_92(void) { read_ascii_response(buffer, sizeof(buffer)); assert(strncmp(buffer, "CLIENT_ERROR", strlen("CLIENT_ERROR")) == 0); - close(sock); - sock = connect_server("127.0.0.1", port, false); + close_conn(); + con = connect_server("127.0.0.1", port, false, enable_ssl); return TEST_PASS; } @@ -589,14 +700,14 @@ static enum test_return test_issue_102(void) { memset(buffer, ' ', sizeof(buffer)); buffer[sizeof(buffer) - 1] = '\0'; - close(sock); - sock = connect_server("127.0.0.1", port, false); + close_conn(); + con = connect_server("127.0.0.1", port, false, enable_ssl); send_ascii_command(buffer); /* verify that the server closed the connection */ - assert(read(sock, buffer, sizeof(buffer)) == 0); - close(sock); - sock = connect_server("127.0.0.1", port, false); + assert(con->read(con, buffer, sizeof(buffer)) == 0); + + con = connect_server("127.0.0.1", port, false, enable_ssl); snprintf(buffer, sizeof(buffer), "gets "); size_t offset = 5; @@ -625,22 +736,23 @@ static enum test_return test_issue_102(void) { buffer[sizeof(buffer) - 1] = '\0'; send_ascii_command(buffer); /* verify that the server closed the connection */ - assert(read(sock, buffer, sizeof(buffer)) == 0); + assert(con->read(con, buffer, sizeof(buffer)) == 0); - close(sock); - sock = connect_server("127.0.0.1", port, false); + close_conn(); + con = connect_server("127.0.0.1", port, false, enable_ssl); return TEST_PASS; } static enum test_return start_memcached_server(void) { server_pid = start_server(&port, false, 600); - sock = connect_server("127.0.0.1", port, false); + close_conn(); + con = connect_server("127.0.0.1", port, false, enable_ssl); return TEST_PASS; } static enum test_return stop_memcached_server(void) { - close(sock); + close_conn(); if (server_pid != -1) { assert(kill(server_pid, SIGTERM) == 0); } @@ -651,14 +763,14 @@ static enum test_return stop_memcached_server(void) { static enum test_return shutdown_memcached_server(void) { char buffer[1024]; - close(sock); - sock = connect_server("127.0.0.1", port, false); + close_conn(); + con = connect_server("127.0.0.1", port, false, enable_ssl); send_ascii_command("shutdown\r\n"); /* verify that the server closed the connection */ - assert(read(sock, buffer, sizeof(buffer)) == 0); + assert(con->read(con, buffer, sizeof(buffer)) == 0); - close(sock); + close_conn(); /* We set server_pid to -1 so that we don't later call kill() */ if (kill(server_pid, 0) == 0) { @@ -694,8 +806,7 @@ static void safe_send(const void* buf, size_t len, bool hickup) num_bytes = (rand() % 1023) + 1; } } - - ssize_t nw = write(sock, ptr + offset, num_bytes); + ssize_t nw = con->write(con, ptr + offset, num_bytes); if (nw == -1) { if (errno != EINTR) { fprintf(stderr, "Failed to write: %s\n", strerror(errno)); @@ -716,7 +827,7 @@ static bool safe_recv(void *buf, size_t len) { } off_t offset = 0; do { - ssize_t nr = read(sock, ((char*)buf) + offset, len - offset); + ssize_t nr = con->read(con, ((char*)buf) + offset, len - offset); if (nr == -1) { if (errno != EINTR) { fprintf(stderr, "Failed to read: %s\n", strerror(errno)); @@ -1064,9 +1175,9 @@ static enum test_return test_binary_quit_impl(uint8_t cmd) { } /* Socket should be closed now, read should return 0 */ - assert(read(sock, buffer.bytes, sizeof(buffer.bytes)) == 0); - close(sock); - sock = connect_server("127.0.0.1", port, false); + assert(con->read(con, buffer.bytes, sizeof(buffer.bytes)) == 0); + close_conn(); + con = connect_server("127.0.0.1", port, false, enable_ssl); return TEST_PASS; } @@ -1867,7 +1978,7 @@ static enum test_return test_binary_pipeline_hickup(void) static enum test_return test_issue_101(void) { enum { max = 2 }; enum test_return ret = TEST_PASS; - int fds[max]; + struct conn *conns[max]; int ii = 0; pid_t child = 0; @@ -1881,15 +1992,16 @@ static enum test_return test_issue_101(void) { server_pid = start_server(&port, false, 1000); for (ii = 0; ii < max; ++ii) { - fds[ii] = connect_server("127.0.0.1", port, true); - assert(fds[ii] > 0); + conns[ii] = NULL; + conns[ii] = connect_server("127.0.0.1", port, true, enable_ssl); + assert(conns[ii]->sock > 0); } /* Send command on the connection until it blocks */ for (ii = 0; ii < max; ++ii) { bool more = true; do { - ssize_t err = write(fds[ii], command, cmdlen); + ssize_t err = conns[ii]->write(conns[ii], command, cmdlen); if (err == -1) { switch (errno) { case EINTR: @@ -1916,16 +2028,27 @@ static enum test_return test_issue_101(void) { assert(c == child); assert(stat == 0); } else { - sock = connect_server("127.0.0.1", port, false); + con = connect_server("127.0.0.1", port, false, enable_ssl); ret = test_binary_noop(); - close(sock); exit(0); } cleanup: /* close all connections */ for (ii = 0; ii < max; ++ii) { - close(fds[ii]); + struct conn *c = conns[ii]; + if (c == NULL) continue; +#ifdef TLS + if (c->ssl) { + SSL_shutdown(c->ssl); + SSL_free(c->ssl); + } + if (c->ssl_ctx) + SSL_CTX_free(c->ssl_ctx); +#endif + if (c->sock > 0) close(c->sock); + free(conns[ii]); + conns[ii] = NULL; } assert(kill(server_pid, SIGTERM) == 0); @@ -2000,6 +2123,13 @@ int main(int argc, char **argv) { int exitcode = 0; int ii = 0, num_cases = 0; +#ifdef TLS + if (getenv("SSL_TEST") != NULL) { + SSLeay_add_ssl_algorithms(); + SSL_load_error_strings(); + enable_ssl = true; + } +#endif for (num_cases = 0; testcases[num_cases].description; num_cases++) { /* Just counting */ @@ -17,6 +17,10 @@ #include <atomic.h> #endif +#ifdef TLS +#include <openssl/ssl.h> +#endif + #define ITEMS_PER_ALLOC 64 /* An item in the connection queue. */ @@ -33,6 +37,7 @@ struct conn_queue_item { enum network_transport transport; enum conn_queue_item_modes mode; conn *c; + void *ssl; CQ_ITEM *next; }; @@ -361,6 +366,15 @@ static void setup_thread(LIBEVENT_THREAD *me) { exit(EXIT_FAILURE); } #endif +#ifdef TLS + if (settings.ssl_enabled) { + me->ssl_wbuf = (char *)malloc((size_t)settings.ssl_wbuf_size); + if (me->ssl_wbuf == NULL) { + fprintf(stderr, "Failed to allocate the SSL write buffer\n"); + exit(EXIT_FAILURE); + } + } +#endif } /* @@ -419,7 +433,7 @@ static void thread_libevent_process(int fd, short which, void *arg) { case queue_new_conn: c = conn_new(item->sfd, item->init_state, item->event_flags, item->read_buffer_size, item->transport, - me->base); + me->base, item->ssl); if (c == NULL) { if (IS_UDP(item->transport)) { fprintf(stderr, "Can't listen for events on UDP socket\n"); @@ -433,6 +447,12 @@ static void thread_libevent_process(int fd, short which, void *arg) { } } else { c->thread = me; +#ifdef TLS + if (settings.ssl_enabled && c->ssl != NULL) { + assert(c->thread && c->thread->ssl_wbuf); + c->ssl_wbuf = c->thread->ssl_wbuf; + } +#endif } break; @@ -467,7 +487,7 @@ static int last_thread = -1; * of an incoming connection. */ void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags, - int read_buffer_size, enum network_transport transport) { + int read_buffer_size, enum network_transport transport, void *ssl) { CQ_ITEM *item = cqi_new(); char buf[1]; if (item == NULL) { @@ -489,6 +509,7 @@ void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags, item->read_buffer_size = read_buffer_size; item->transport = transport; item->mode = queue_new_conn; + item->ssl = ssl; cq_push(thread->new_conn_queue, item); @@ -531,6 +552,13 @@ void sidethread_conn_close(conn *c) { c->state = conn_closed; if (settings.verbose > 1) fprintf(stderr, "<%d connection closed from side thread.\n", c->sfd); +#ifdef TLS + if (c->ssl) { + c->ssl_wbuf = NULL; + SSL_shutdown(c->ssl); + SSL_free(c->ssl); + } +#endif close(c->sfd); STATS_LOCK(); @@ -0,0 +1,195 @@ +#include "memcached.h" + +#ifdef TLS + +#include "tls.h" +#include <string.h> +#include <sysexits.h> +#include <sys/param.h> + +#ifndef MAXPATHLEN +#define MAXPATHLEN 4096 +#endif + +static pthread_mutex_t ssl_ctx_lock = PTHREAD_MUTEX_INITIALIZER; + +const unsigned MAX_ERROR_MSG_SIZE = 128; + +void SSL_LOCK() { + pthread_mutex_lock(&(ssl_ctx_lock)); +} + +void SSL_UNLOCK(void) { + pthread_mutex_unlock(&(ssl_ctx_lock)); +} + +/* + * Reads decrypted data from the underlying BIO read buffers, + * which reads from the socket. + */ +ssize_t ssl_read(conn *c, void *buf, size_t count) { + assert (c != NULL); + /* TODO : document the state machine interactions for SSL_read with + non-blocking sockets/ SSL re-negotiations + */ + return SSL_read(c->ssl, buf, count); +} + +/* + * SSL sendmsg implementation. Perform a SSL_write. + */ +ssize_t ssl_sendmsg(conn *c, struct msghdr *msg, int flags) { + assert (c != NULL); + size_t buf_remain = settings.ssl_wbuf_size; + size_t bytes = 0; + size_t to_copy; + int i; + + // ssl_wbuf is pointing to the buffer allocated in the worker thread. + assert(c->ssl_wbuf); + // TODO: allocate a fix buffer in crawler/logger if they start using + // the sendmsg method. Also, set c->ssl_wbuf when the side thread + // start owning the connection and reset the pointer in + // conn_worker_readd. + // Currntly this connection would not be served by a different thread + // than the one it's assigned. + assert(c->thread->thread_id == (unsigned long)pthread_self()); + + char *bp = c->ssl_wbuf; + for (i = 0; i < msg->msg_iovlen; i++) { + size_t len = msg->msg_iov[i].iov_len; + to_copy = len < buf_remain ? len : buf_remain; + + memcpy(bp + bytes, (void*)msg->msg_iov[i].iov_base, to_copy); + buf_remain -= to_copy; + bytes += to_copy; + if (buf_remain == 0) + break; + } + /* TODO : document the state machine interactions for SSL_write with + non-blocking sockets/ SSL re-negotiations + */ + return SSL_write(c->ssl, c->ssl_wbuf, bytes); +} + +/* + * Writes data to the underlying BIO write buffers, + * which encrypt and write them to the socket. + */ +ssize_t ssl_write(conn *c, void *buf, size_t count) { + assert (c != NULL); + return SSL_write(c->ssl, buf, count); +} + +/* + * Loads server certificates to the SSL context and validate them. + * @return whether certificates are successfully loaded and verified or not. + * @param error_msg contains the error when unsuccessful. + */ +static bool load_server_certificates(char **errmsg) { + bool success = true; + char *error_msg = malloc(MAXPATHLEN + MAX_ERROR_MSG_SIZE); + size_t errmax = MAXPATHLEN + MAX_ERROR_MSG_SIZE - 1; + if (error_msg == NULL) { + *errmsg = NULL; + return false; + } + SSL_LOCK(); + if (!SSL_CTX_use_certificate_chain_file(settings.ssl_ctx, + settings.ssl_chain_cert)) { + snprintf(error_msg, errmax, "Error loading the certificate chain: %s\r\n", + settings.ssl_chain_cert); + success = false; + } else if (!SSL_CTX_use_PrivateKey_file(settings.ssl_ctx, settings.ssl_key, + settings.ssl_keyformat)) { + snprintf(error_msg, errmax, "Error loading the key: %s\r\n", settings.ssl_key); + success = false; + } else if (!SSL_CTX_check_private_key(settings.ssl_ctx)) { + snprintf(error_msg, errmax, "Error validating the certificate\r\n"); + success = false; + } else { + settings.ssl_last_cert_refresh_time = current_time; + } + SSL_UNLOCK(); + if (success) { + free(error_msg); + } else { + *errmsg = error_msg; + } + return success; +} + +/* + * Verify SSL settings and initiates the SSL context. + */ +int ssl_init(void) { + assert(settings.ssl_enabled); + // SSL context for the process. All connections will share one + // process level context. + settings.ssl_ctx = SSL_CTX_new(TLS_server_method()); + // Clients should use at least TLSv1.2 + int flags = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | + SSL_OP_NO_TLSv1 |SSL_OP_NO_TLSv1_1; + SSL_CTX_set_options(settings.ssl_ctx, flags); + + // The server certificate, private key and validations. + char *error_msg; + if (!load_server_certificates(&error_msg)) { + if (settings.verbose) { + fprintf(stderr, "%s", error_msg); + } + free(error_msg); + exit(EX_USAGE); + } + + // The verification mode of client certificate, default is SSL_VERIFY_PEER. + SSL_CTX_set_verify(settings.ssl_ctx, settings.ssl_verify_mode, NULL); + if (settings.ssl_ciphers && !SSL_CTX_set_cipher_list(settings.ssl_ctx, + settings.ssl_ciphers)) { + if (settings.verbose) { + fprintf(stderr, "Error setting the provided cipher(s): %s\n", + settings.ssl_ciphers); + } + exit(EX_USAGE); + } + // List of acceptable CAs for client certificates. + if (settings.ssl_ca_cert) + { + SSL_CTX_set_client_CA_list(settings.ssl_ctx, + SSL_load_client_CA_file(settings.ssl_ca_cert)); + if (!SSL_CTX_load_verify_locations(settings.ssl_ctx, + settings.ssl_ca_cert, NULL)) { + if (settings.verbose) { + fprintf(stderr, "Error loading the client CA cert (%s)\n", + settings.ssl_ca_cert); + } + exit(EX_USAGE); + } + } + settings.ssl_last_cert_refresh_time = current_time; + return 0; +} + +/* + * This method is registered with each SSL connection and abort the SSL session + * if a client initiates a renegotiation. + * TODO : Proper way to do this is to set SSL_OP_NO_RENEGOTIATION + * using the SSL_CTX_set_options but that option only available in + * openssl 1.1.0h or above. + */ +void ssl_callback(const SSL *s, int where, int ret) { + SSL* ssl = (SSL*)s; + if (SSL_in_before(ssl)) { + if (settings.verbose) { + fprintf(stderr, "%d: SSL renegotiation is not supported, " + "closing the connection\n", SSL_get_fd(ssl)); + } + SSL_set_shutdown(ssl, SSL_SENT_SHUTDOWN | SSL_RECEIVED_SHUTDOWN); + return; + } +} + +bool refresh_certs(char **errmsg) { + return load_server_certificates(errmsg); +} +#endif @@ -0,0 +1,14 @@ +#ifndef TLS_H +#define TLS_H + +void SSL_LOCK(void); +void SSL_UNLOCK(void); +ssize_t ssl_read(conn *c, void *buf, size_t count); +ssize_t ssl_sendmsg(conn *c, struct msghdr *msg, int flags); +ssize_t ssl_write(conn *c, void *buf, size_t count); + +int ssl_init(void); +bool refresh_certs(char **errmsg); +void ssl_callback(const SSL *s, int where, int ret); + +#endif |