Add SSL tests. Review: https://reviews.apache.org/r/35889
Project: http://git-wip-us.apache.org/repos/asf/mesos/repo Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/beac384c Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/beac384c Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/beac384c Branch: refs/heads/master Commit: beac384c77d4a9c235a813e9286716f4509bdd55 Parents: aadf4a7 Author: Joris Van Remoortere <[email protected]> Authored: Fri Jun 26 18:30:12 2015 -0700 Committer: Benjamin Hindman <[email protected]> Committed: Fri Jun 26 18:30:12 2015 -0700 ---------------------------------------------------------------------- 3rdparty/libprocess/Makefile.am | 24 +- 3rdparty/libprocess/src/libevent.hpp | 2 + 3rdparty/libprocess/src/openssl.cpp | 76 ++- 3rdparty/libprocess/src/tests/ssl_client.cpp | 132 ++++ 3rdparty/libprocess/src/tests/ssl_tests.cpp | 708 ++++++++++++++++++++++ 5 files changed, 909 insertions(+), 33 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mesos/blob/beac384c/3rdparty/libprocess/Makefile.am ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/Makefile.am b/3rdparty/libprocess/Makefile.am index 0a14eb9..358893c 100644 --- a/3rdparty/libprocess/Makefile.am +++ b/3rdparty/libprocess/Makefile.am @@ -4,6 +4,7 @@ ACLOCAL_AMFLAGS = -I m4 AUTOMAKE_OPTIONS = foreign +LIBPROCESS_BUILD_DIR=@abs_top_builddir@ # NOTE: The libprocess headers were moved to the Makefile in "include" # subdirectory to make it easy to preserve the directory structure of @@ -71,12 +72,13 @@ libprocess_la_SOURCES += \ src/openssl_util.hpp endif -libprocess_la_CPPFLAGS = \ - -I$(srcdir)/include \ - -I$(srcdir)/$(STOUT)/include \ - -I$(BOOST) \ - -I$(LIBEV) \ - -I$(PICOJSON) \ +libprocess_la_CPPFLAGS = \ + -DBUILD_DIR=\"$(LIBPROCESS_BUILD_DIR)\" \ + -I$(srcdir)/include \ + -I$(srcdir)/$(STOUT)/include \ + -I$(BOOST) \ + -I$(LIBEV) \ + -I$(PICOJSON) \ $(AM_CPPFLAGS) if ENABLE_LIBEVENT @@ -154,6 +156,16 @@ tests_LDADD = \ $(HTTP_PARSER_LIB) \ $(EVENT_LIB) +if ENABLE_SSL +check_PROGRAMS += ssl-client +ssl_client_SOURCES = src/tests/ssl_client.cpp +ssl_client_CPPFLAGS = $(tests_CPPFLAGS) +ssl_client_LDADD = $(tests_LDADD) + +tests_SOURCES += \ + src/tests/ssl_tests.cpp +endif + benchmarks_SOURCES = \ src/tests/benchmarks.cpp http://git-wip-us.apache.org/repos/asf/mesos/blob/beac384c/3rdparty/libprocess/src/libevent.hpp ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/src/libevent.hpp b/3rdparty/libprocess/src/libevent.hpp index a20f2c3..47b93f1 100644 --- a/3rdparty/libprocess/src/libevent.hpp +++ b/3rdparty/libprocess/src/libevent.hpp @@ -1,6 +1,8 @@ #ifndef __LIBEVENT_HPP__ #define __LIBEVENT_HPP__ +#include <event2/event.h> + #include <stout/lambda.hpp> #include <stout/thread.hpp> http://git-wip-us.apache.org/repos/asf/mesos/blob/beac384c/3rdparty/libprocess/src/openssl.cpp ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/src/openssl.cpp b/3rdparty/libprocess/src/openssl.cpp index 3c0fc4b..6ff4adb 100644 --- a/3rdparty/libprocess/src/openssl.cpp +++ b/3rdparty/libprocess/src/openssl.cpp @@ -250,14 +250,13 @@ string error_string(unsigned long code) } -void initialize() +// Tests can declare this function and use it to re-configure the SSL +// environment variables programatically. Without explicitly declaring +// this function, it is not visible. This is the preferred behavior as +// we do not want applications changing these settings while they are +// running (this would be undefined behavior). +void reinitialize() { - static Once* initialized = new Once(); - - if (initialized->once()) { - return; - } - // Load all the flags prefixed by SSL_ from the environment. See // comment at top of openssl.hpp for a full list. Try<Nothing> load = ssl_flags->load("SSL_"); @@ -269,33 +268,42 @@ void initialize() // Exit early if SSL is not enabled. if (!ssl_flags->enabled) { - initialized->done(); return; } - // We MUST have entropy, or else there's no point to crypto. - if (!RAND_poll()) { - EXIT(EXIT_FAILURE) << "SSL socket requires entropy"; - } + static Once* initialized_single_entry = new Once(); + + // We don't want to initialize everything multiple times, as we + // don't clean up some of these structures. The things we DO tend + // to re-initialize are things that are overwrites of settings, + // rather than allocations of new data structures. + if (!initialized_single_entry->once()) { + // We MUST have entropy, or else there's no point to crypto. + if (!RAND_poll()) { + EXIT(EXIT_FAILURE) << "SSL socket requires entropy"; + } + + // Initialize the OpenSSL library. + SSL_library_init(); + SSL_load_error_strings(); - // Initialize the OpenSSL library. - SSL_library_init(); - SSL_load_error_strings(); + // Prepare mutexes for threading callbacks. + mutexes = new std::mutex[CRYPTO_num_locks()]; - // Prepare mutexes for threading callbacks. - mutexes = new std::mutex[CRYPTO_num_locks()]; + // Install SSL threading callbacks. + // TODO(jmlvanre): the id mechanism is deprecated in OpenSSL. + CRYPTO_set_id_callback(&id_function); + CRYPTO_set_locking_callback(&locking_function); + CRYPTO_set_dynlock_create_callback(&dyn_create_function); + CRYPTO_set_dynlock_lock_callback(&dyn_lock_function); + CRYPTO_set_dynlock_destroy_callback(&dyn_destroy_function); - // Install SSL threading callbacks. - // TODO(jmlvanre): the id mechanism is deprecated in OpenSSL. - CRYPTO_set_id_callback(&id_function); - CRYPTO_set_locking_callback(&locking_function); - CRYPTO_set_dynlock_create_callback(&dyn_create_function); - CRYPTO_set_dynlock_lock_callback(&dyn_lock_function); - CRYPTO_set_dynlock_destroy_callback(&dyn_destroy_function); + ctx = SSL_CTX_new(SSLv23_method()); + CHECK(ctx) << "Failed to create SSL context: " + << ERR_error_string(ERR_get_error(), NULL); - ctx = SSL_CTX_new(SSLv23_method()); - CHECK(ctx) << "Failed to create SSL context: " - << ERR_error_string(ERR_get_error(), NULL); + initialized_single_entry->done(); + } // Disable SSL session caching. SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); @@ -459,6 +467,20 @@ void initialize() if (!ssl_flags->enable_tls_v1_2) { ssl_options |= SSL_OP_NO_TLSv1_2; } SSL_CTX_set_options(ctx, ssl_options); +} + + +void initialize() +{ + static Once* initialized = new Once(); + + if (initialized->once()) { + return; + } + + // We delegate to 'reinitialize()' so that tests can change the SSL + // configuration programatically. + reinitialize(); initialized->done(); } http://git-wip-us.apache.org/repos/asf/mesos/blob/beac384c/3rdparty/libprocess/src/tests/ssl_client.cpp ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/src/tests/ssl_client.cpp b/3rdparty/libprocess/src/tests/ssl_client.cpp new file mode 100644 index 0000000..4e91bda --- /dev/null +++ b/3rdparty/libprocess/src/tests/ssl_client.cpp @@ -0,0 +1,132 @@ +#include <iostream> + +#include <glog/logging.h> + +#include <gmock/gmock.h> + +#include <gtest/gtest.h> + +#include <process/gmock.hpp> +#include <process/gtest.hpp> +#include <process/socket.hpp> + +#include <stout/gtest.hpp> +#include <stout/try.hpp> + +#include "openssl.hpp" + +using namespace process; +using namespace process::network; + +using std::cout; +using std::endl; +using std::string; + +// We only run these tests if we have configured with '--enable-ssl'. +#ifdef USE_SSL_SOCKET + +/** + * The flags that control the test SSL client behavior. + * + * These flags augment the environment variables prefixed by 'SSL_' + * that are introduced by @see process::network::openssl::Flags. + */ +class Flags : public virtual flags::FlagsBase +{ +public: + Flags() + { + add(&Flags::use_ssl, + "use_ssl", + "Whether to try and connect using an SSL based socket. This is " + "separate from whether SSL_ENABLED is set. We use this for " + "testing failure cases.", + true); + + add(&Flags::data, + "data", + "The message to send as the client.", + "Hello World"); + + add(&Flags::server, "server", "IP address of server", "127.0.0.1"); + + add(&Flags::port, "port", "Port of server", 5050); + } + + bool use_ssl; + string data; + string server; + uint16_t port; +}; + + +class SSLClientTest : public ::testing::Test +{ +public: + static Flags flags; +}; + + +Flags SSLClientTest::flags; + + +int main(int argc, char** argv) +{ + // Load all the client flags. + Try<Nothing> load = SSLClientTest::flags.load("", argc, argv); + if (load.isError()) { + EXIT(EXIT_FAILURE) << "Failed to load flags: " << load.error(); + } + + if (SSLClientTest::flags.help) { + cout << SSLClientTest::flags.usage() << endl; + return EXIT_SUCCESS; + } + + process::initialize(); + + openssl::initialize(); + + // Initialize Google Mock/Test. + testing::InitGoogleMock(&argc, argv); + + // Add the libprocess test event listeners. + ::testing::TestEventListeners& listeners = + ::testing::UnitTest::GetInstance()->listeners(); + + listeners.Append(process::ClockTestEventListener::instance()); + listeners.Append(process::FilterTestEventListener::instance()); + + return RUN_ALL_TESTS(); +} + + +TEST_F(SSLClientTest, client) +{ + // Create the socket based on the 'use_ssl' flag. We use this to + // test whether a regular socket could connect to an SSL server + // socket. + const Try<Socket> create = + Socket::create(flags.use_ssl ? Socket::SSL : Socket::POLL); + ASSERT_SOME(create); + + Socket socket = create.get(); + + Try<net::IP> ip = net::IP::parse(flags.server, AF_INET); + EXPECT_SOME(ip); + + // Connect to the server socket located at `ip:port`. + const Future<Nothing> connect = + socket.connect(Address(ip.get(), flags.port)); + + // Verify that the client views the connection as established. + AWAIT_EXPECT_READY(connect); + + // Send 'data' from the client to the server. + AWAIT_EXPECT_READY(socket.send(flags.data)); + + // Verify the client received the message back from the server. + AWAIT_EXPECT_EQ(flags.data, socket.recv()); +} + +#endif // USE_SSL_SOCKET \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mesos/blob/beac384c/3rdparty/libprocess/src/tests/ssl_tests.cpp ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/src/tests/ssl_tests.cpp b/3rdparty/libprocess/src/tests/ssl_tests.cpp new file mode 100644 index 0000000..c077aae --- /dev/null +++ b/3rdparty/libprocess/src/tests/ssl_tests.cpp @@ -0,0 +1,708 @@ +#include <stdio.h> + +#include <openssl/rsa.h> +#include <openssl/bio.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> + +#include <process/future.hpp> +#include <process/gtest.hpp> +#include <process/io.hpp> +#include <process/socket.hpp> +#include <process/subprocess.hpp> + +#include <stout/gtest.hpp> +#include <stout/os.hpp> + +#include "openssl.hpp" +#include "openssl_util.hpp" + +using std::map; +using std::string; +using std::vector; + +// We only run these tests if we have configured with '--enable-ssl'. +#ifdef USE_SSL_SOCKET + +namespace process { +namespace network { +namespace openssl { + +// Forward declare the `reinitialize()` function since we want to +// programatically change SSL flags during tests. +void reinitialize(); + +} // namespace openssl { +} // namespace network { +} // namespace process { + +using namespace process; +using namespace process::network; + + +// Wait for a subprocess and test the status code for the following +// conditions of 'expected_status': +// 1. 'None' = Anything but '0'. +// 2. 'Some' = the value of 'expected_status'. +// Returns Nothing if the resulting status code matches the +// expectation otherwise a Failure with the output of the subprocess. +// TODO(jmlvanre): Turn this into a generally useful abstraction for +// gtest where we can have a more straigtforward 'expected_status'. +Future<Nothing> await_subprocess( + const Subprocess& subprocess, + const Option<int>& expected_status = None()) +{ + // Dup the pipe fd of the subprocess so we can read the output if + // needed. + int out = dup(subprocess.out().get()); + + // Once we get the status of the process. + return subprocess.status() + .then([=](const Option<int>& status) -> Future<Nothing> { + // If the status is not set, fail out. + if (status.isNone()) { + return Failure("Subprocess status is none"); + } + + // If the status is not what we expect then fail out with the + // output of the subprocess. The failure message will include + // the assertion failures of the subprocess. + if ((expected_status.isSome() && status.get() != expected_status.get()) || + (expected_status.isNone() && status.get() == 0)) { + return io::read(out) + .then([](const string& output) -> Future<Nothing> { + return Failure("\n[++++++++++] Subprocess output.\n" + output + + "[++++++++++]\n"); + }); + } + + // If the subprocess ran successfully then return nothing. + return Nothing(); + }).onAny([=]() { + os::close(out); + }); +} + + +/** + * A Test fixture that sets up SSL keys and certificates. + * + * This class sets up a private key and certificate pair at + * SSLTest::key_path and SSLTest::certificate_path. + * It also sets up an independent 'scrap' pair that can be used to + * test an invalid certificate authority chain. These can be found at + * SSLTest::scrap_key_path and SSLTest::scrap_certificate_path. + * + * There are some helper functions like SSLTest::setup_server and + * SSLTest::launch_client that factor out common behavior used in + * tests. + */ +class SSLTest : public ::testing::Test +{ +protected: + SSLTest() : data("Hello World!") {} + + /** + * @return The path to the authorized private key. + */ + static const Path& key_path() + { + static Path path(path::join(os::getcwd(), "key.pem")); + return path; + } + + /** + * @return The path to the authorized certificate. + */ + static const Path& certificate_path() + { + static Path path(path::join(os::getcwd(), "cert.pem")); + return path; + } + + /** + * @return The path to the unauthorized private key. + */ + static const Path& scrap_key_path() + { + static Path path(path::join(os::getcwd(), "scrap_key.pem")); + return path; + } + + /** + * @return The path to the unauthorized certificate. + */ + static const Path& scrap_certificate_path() + { + static Path path(path::join(os::getcwd(), "scrap_cert.pem")); + return path; + } + + static void SetUpTestCase() + { + // We store the allocated objects in these results so that we can + // have a consolidated 'cleanup()' function. This makes all the + // 'EXIT()' calls more readable and less error prone. + Result<EVP_PKEY*> private_key = None(); + Result<X509*> certificate = None(); + Result<EVP_PKEY*> scrap_key = None(); + Result<X509*> scrap_certificate = None(); + + auto cleanup = [&private_key, &certificate, &scrap_key, &scrap_certificate]( + bool failure = true) { + if (private_key.isSome()) { EVP_PKEY_free(private_key.get()); } + if (certificate.isSome()) { X509_free(certificate.get()); } + if (scrap_key.isSome()) { EVP_PKEY_free(scrap_key.get()); } + if (scrap_certificate.isSome()) { X509_free(scrap_certificate.get()); } + + // If we are under a failure condition, clean up any files we + // already generated. The expected behavior is that they will be + // cleaned up in 'TearDownTestCase()'; however, we call ABORT + // during 'SetUpTestCase()' failures. + if (failure) { + os::rm(key_path().value); + os::rm(certificate_path().value); + os::rm(scrap_key_path().value); + os::rm(scrap_certificate_path().value); + } + }; + + // Generate the authority key. + private_key = openssl::generate_private_rsa_key(); + if (private_key.isError()) { + ABORT("Could not generate private key: " + private_key.error()); + } + + // Generate an authorized certificate. + certificate = openssl::generate_x509(private_key.get(), private_key.get()); + + if (certificate.isError()) { + cleanup(); + ABORT("Could not generate certificate: " + certificate.error()); + } + + // Write the authority key to disk. + Try<Nothing> key_write = + openssl::write_key_file(private_key.get(), key_path()); + + if (key_write.isError()) { + cleanup(); + ABORT("Could not write private key to disk: " + key_write.error()); + } + + // Write the authorized certificate to disk. + Try<Nothing> certificate_write = + openssl::write_certificate_file(certificate.get(), certificate_path()); + + if (certificate_write.isError()) { + cleanup(); + ABORT("Could not write certificate to disk: " + + certificate_write.error()); + } + + // Generate a scrap key. + scrap_key = openssl::generate_private_rsa_key(); + if (scrap_key.isError()) { + cleanup(); + ABORT("Could not generate a scrap private key: " + scrap_key.error()); + } + + // Write the scrap key to disk. + key_write = openssl::write_key_file(scrap_key.get(), scrap_key_path()); + + if (key_write.isError()) { + cleanup(); + ABORT("Could not write scrap key to disk: " + key_write.error()); + } + + // Generate a scrap certificate. + scrap_certificate = + openssl::generate_x509(scrap_key.get(), scrap_key.get()); + + if (scrap_certificate.isError()) { + cleanup(); + ABORT("Could not generate a scrap certificate: " + + scrap_certificate.error()); + } + + // Write the scrap certificate to disk. + certificate_write = openssl::write_certificate_file( + scrap_certificate.get(), + scrap_certificate_path()); + + if (certificate_write.isError()) { + cleanup(); + ABORT("Could not write scrap certificate to disk: " + + certificate_write.error()); + } + + // Since we successfully set up all our state, we call cleanup + // with failure set to 'false'. + cleanup(false); + } + + static void TearDownTestCase() + { + // Clean up all the pem files we generated. + os::rm(key_path().value); + os::rm(certificate_path().value); + os::rm(scrap_key_path().value); + os::rm(scrap_certificate_path().value); + } + + virtual void SetUp() + { + // This unsets all the SSL environment variables. Necessary for + // ensuring a clean starting slate between tests. + os::unsetenv("SSL_ENABLED"); + os::unsetenv("SSL_CERT_FILE"); + os::unsetenv("SSL_KEY_FILE"); + os::unsetenv("SSL_VERIFY_CERT"); + os::unsetenv("SSL_REQUIRE_CERT"); + os::unsetenv("SSL_VERIFY_DEPTH"); + os::unsetenv("SSL_CA_DIR"); + os::unsetenv("SSL_CA_FILE"); + os::unsetenv("SSL_CIPHERS"); + os::unsetenv("SSL_ENABLE_SSL_V2"); + os::unsetenv("SSL_ENABLE_SSL_V3"); + os::unsetenv("SSL_ENABLE_TLS_V1_0"); + os::unsetenv("SSL_ENABLE_TLS_V1_1"); + os::unsetenv("SSL_ENABLE_TLS_V1_2"); + } + + /** + * Initializes a listening server. + * + * @param environment The SSL environment variables to launch the + * server socket with. + * + * @return Socket if successful otherwise an Error. + */ + Try<Socket> setup_server(const map<string, string>& environment) + { + foreachpair (const string& name, const string& value, environment) { + os::setenv(name, value); + } + openssl::reinitialize(); + + const Try<Socket> create = Socket::create(Socket::SSL); + if (create.isError()) { + return Error(create.error()); + } + + Socket server = create.get(); + + const Try<Nothing> listen = server.listen(BACKLOG); + if (listen.isError()) { + return Error(listen.error()); + } + + return server; + } + + /** + * Launches a test SSL client as a subprocess connecting to the + * server. + * + * The subprocess calls the 'ssl-client' binary with the provided + * environment. + * + * @param environment The SSL environment variables to launch the + * SSL client subprocess with. + * @param use_ssl_socket Whether the SSL client will try to connect + * using an SSL socket or a POLL socket. + * + * @return Subprocess if successful otherwise an Error. + */ + Try<Subprocess> launch_client( + const map<string, string>& environment, + const Socket& server, + bool use_ssl_socket) + { + const Try<Address> address = server.address(); + if (address.isError()) { + return Error(address.error()); + } + + // Set up arguments to be passed to the 'client-ssl' binary. + const vector<string> argv = { + "ssl-client", + "--use_ssl=" + stringify(use_ssl_socket), + "--server=127.0.0.1", + "--port=" + stringify(address.get().port), + "--data=" + data}; + + Result<string> path = os::realpath(BUILD_DIR); + if (!path.isSome()) { + return Error("Could not establish build directory path"); + } + + return subprocess( + path::join(path.get(), "ssl-client"), + argv, + Subprocess::PIPE(), + Subprocess::PIPE(), + Subprocess::FD(STDERR_FILENO), + None(), + environment); + } + + static constexpr size_t BACKLOG = 5; + + const string data; +}; + + +// Ensure that we can't create an SSL socket when SSL is not enabled. +TEST(SSL, Disabled) +{ + os::setenv("SSL_ENABLED", "false"); + openssl::reinitialize(); + EXPECT_ERROR(Socket::create(Socket::SSL)); +} + + +// Test a basic back-and-forth communication within the same OS +// process. +TEST_F(SSLTest, BasicSameProcess) +{ + os::setenv("SSL_ENABLED", "true"); + os::setenv("SSL_KEY_FILE", key_path().value); + os::setenv("SSL_CERT_FILE", certificate_path().value); + os::setenv("SSL_REQUIRE_CERT", "true"); + os::setenv("SSL_CA_DIR", os::getcwd()); + os::setenv("SSL_CA_FILE", certificate_path().value); + + openssl::reinitialize(); + + const Try<Socket> server_create = Socket::create(Socket::SSL); + ASSERT_SOME(server_create); + + const Try<Socket> client_create = Socket::create(Socket::SSL); + ASSERT_SOME(client_create); + + Socket server = server_create.get(); + Socket client = client_create.get(); + + const Try<Nothing> listen = server.listen(BACKLOG); + ASSERT_SOME(listen); + + const Try<Address> server_address = server.address(); + ASSERT_SOME(server_address); + + const Future<Socket> _socket = server.accept(); + + const Future<Nothing> connect = client.connect(server_address.get()); + + // Wait for the server to have accepted the client connection. + AWAIT_ASSERT_READY(_socket); + Socket socket = _socket.get(); // TODO(jmlvanre): Remove const copy. + + // Verify that the client also views the connection as established. + AWAIT_ASSERT_READY(connect); + + // Send a message from the client to the server. + const string data = "Hello World!"; + AWAIT_ASSERT_READY(client.send(data)); + + // Verify the server received the message. + AWAIT_ASSERT_EQ(data, socket.recv()); + + // Send the message back from the server to the client. + AWAIT_ASSERT_READY(socket.send(data)); + + // Verify the client received the message. + AWAIT_ASSERT_EQ(data, client.recv()); +} + + +// Test a basic back-and-forth communication using the 'ssl-client' +// subprocess. +TEST_F(SSLTest, SSLSocket) +{ + Try<Socket> server = setup_server({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}}); + ASSERT_SOME(server); + + Try<Subprocess> client = launch_client({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}}, + server.get(), + true); + ASSERT_SOME(client); + + Future<Socket> socket = server.get().accept(); + AWAIT_ASSERT_READY(socket); + + // TODO(jmlvanre): Remove const copy. + AWAIT_ASSERT_EQ(data, Socket(socket.get()).recv()); + AWAIT_ASSERT_READY(Socket(socket.get()).send(data)); + + AWAIT_ASSERT_READY(await_subprocess(client.get(), 0)); +} + + +// Ensure that a POLL based socket can't connect to an SSL based +// socket, even when SSL is enabled. +TEST_F(SSLTest, NonSSLSocket) +{ + Try<Socket> server = setup_server({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}}); + ASSERT_SOME(server); + + Try<Subprocess> client = launch_client({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}}, + server.get(), + false); + ASSERT_SOME(client); + + Future<Socket> socket = server.get().accept(); + AWAIT_ASSERT_FAILED(socket); + + AWAIT_ASSERT_READY(await_subprocess(client.get(), None())); +} + + +// Ensure that a certificate that was not generated using the +// certificate authority is still allowed to communicate as long as +// the SSL_VERIFY_CERT and SSL_REQUIRE_CERT flags are disabled. +TEST_F(SSLTest, NoVerifyBadCA) +{ + Try<Socket> server = setup_server({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}, + {"SSL_VERIFY_CERT", "false"}, + {"SSL_REQUIRE_CERT", "false"}}); + ASSERT_SOME(server); + + Try<Subprocess> client = launch_client({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", scrap_key_path().value}, + {"SSL_CERT_FILE", scrap_certificate_path().value}, + {"SSL_REQUIRE_CERT", "true"}, + {"SSL_CA_FILE", certificate_path().value}}, + server.get(), + true); + ASSERT_SOME(client); + + Future<Socket> socket = server.get().accept(); + AWAIT_ASSERT_READY(socket); + + // TODO(jmlvanre): Remove const copy. + AWAIT_ASSERT_EQ(data, Socket(socket.get()).recv()); + AWAIT_ASSERT_READY(Socket(socket.get()).send(data)); + + AWAIT_ASSERT_READY(await_subprocess(client.get(), 0)); +} + + +// Ensure that a certificate that was not generated using the +// certificate authority is NOT allowed to communicate when the +// SSL_REQUIRE_CERT flag is enabled. +TEST_F(SSLTest, RequireBadCA) +{ + Try<Socket> server = setup_server({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}, + {"SSL_REQUIRE_CERT", "true"}}); + ASSERT_SOME(server); + + Try<Subprocess> client = launch_client({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", scrap_key_path().value}, + {"SSL_CERT_FILE", scrap_certificate_path().value}, + {"SSL_REQUIRE_CERT", "false"}}, + server.get(), + true); + ASSERT_SOME(client); + + Future<Socket> socket = server.get().accept(); + AWAIT_ASSERT_FAILED(socket); + + AWAIT_ASSERT_READY(await_subprocess(client.get(), None())); +} + + +// Ensure that a certificate that was not generated using the +// certificate authority is NOT allowed to communicate when the +// SSL_VERIFY_CERT flag is enabled. +TEST_F(SSLTest, VerifyBadCA) +{ + Try<Socket> server = setup_server({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}, + {"SSL_VERIFY_CERT", "true"}}); + ASSERT_SOME(server); + + Try<Subprocess> client = launch_client({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", scrap_key_path().value}, + {"SSL_CERT_FILE", scrap_certificate_path().value}, + {"SSL_REQUIRE_CERT", "false"}}, + server.get(), + true); + ASSERT_SOME(client); + + Future<Socket> socket = server.get().accept(); + AWAIT_ASSERT_FAILED(socket); + + AWAIT_ASSERT_READY(await_subprocess(client.get(), None())); +} + + +// Ensure that a certificate that WAS generated using the certificate +// authority is NOT allowed to communicate when the SSL_VERIFY_CERT +// flag is enabled. +TEST_F(SSLTest, VerifyCertificate) +{ + Try<Socket> server = setup_server({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}, + {"SSL_VERIFY_CERT", "true"}}); + ASSERT_SOME(server); + + Try<Subprocess> client = launch_client({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}, + {"SSL_REQUIRE_CERT", "true"}}, + server.get(), + true); + ASSERT_SOME(client); + + Future<Socket> socket = server.get().accept(); + AWAIT_ASSERT_READY(socket); + + // TODO(jmlvanre): Remove const copy. + AWAIT_ASSERT_EQ(data, Socket(socket.get()).recv()); + AWAIT_ASSERT_READY(Socket(socket.get()).send(data)); + + AWAIT_ASSERT_READY(await_subprocess(client.get(), 0)); +} + + +// Ensure that a certificate that WAS generated using the certificate +// authority is NOT allowed to communicate when the SSL_REQUIRE_CERT +// flag is enabled. +TEST_F(SSLTest, RequireCertificate) +{ + Try<Socket> server = setup_server({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}, + {"SSL_REQUIRE_CERT", "true"}}); + ASSERT_SOME(server); + + Try<Subprocess> client = launch_client({ + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}, + {"SSL_REQUIRE_CERT", "true"}}, + server.get(), + true); + ASSERT_SOME(client); + + Future<Socket> socket = server.get().accept(); + AWAIT_ASSERT_READY(socket); + + // TODO(jmlvanre): Remove const copy. + AWAIT_ASSERT_EQ(data, Socket(socket.get()).recv()); + AWAIT_ASSERT_READY(Socket(socket.get()).send(data)); + + AWAIT_ASSERT_READY(await_subprocess(client.get(), 0)); +} + + +// Test all the combinations of protocols. Ensure that they can only +// communicate if the opposing end allows the given protocol, and not +// otherwise. +TEST_F(SSLTest, ProtocolMismatch) +{ + const vector<string> protocols = { + // Openssl can be compiled with SSLV2 and/or SSLV3 disabled + // completely, so we conditionally test these protocol. +#ifndef OPENSSL_NO_SSL2 + "SSL_ENABLE_SSL_V2", +#endif +#ifndef OPENSSL_NO_SSL3 + "SSL_ENABLE_SSL_V3", +#endif + "SSL_ENABLE_TLS_V1_0", + "SSL_ENABLE_TLS_V1_1", + "SSL_ENABLE_TLS_V1_2" + }; + + // For each server protocol. + foreach (const string& server_protocol, protocols) { + // For each client protocol. + foreach (const string& client_protocol, protocols) { + LOG(INFO) << "Testing server protocol '" << server_protocol + << "' with client protocol '" << client_protocol << "'\n"; + + // Set up the default server environment variables. + map<string, string> server_environment = { + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value} + }; + + // Set up the default client environment variables. + map<string, string> client_environment = { + {"SSL_ENABLED", "true"}, + {"SSL_KEY_FILE", key_path().value}, + {"SSL_CERT_FILE", certificate_path().value}, + }; + + // Disable all protocols except for the one we're testing. + foreach (const string& protocol, protocols) { + server_environment.emplace( + protocol, + stringify(protocol == server_protocol)); + + client_environment.emplace( + protocol, + stringify(protocol == client_protocol)); + } + + // Set up the server. + Try<Socket> server = setup_server(server_environment); + ASSERT_SOME(server); + + // Launch the client. + Try<Subprocess> client = + launch_client(client_environment, server.get(), true); + ASSERT_SOME(client); + + if (server_protocol == client_protocol) { + // If the protocols are the same, it is valid. + Future<Socket> socket = server.get().accept(); + AWAIT_ASSERT_READY(socket); + + // TODO(jmlvanre): Remove const copy. + AWAIT_ASSERT_EQ(data, Socket(socket.get()).recv()); + AWAIT_ASSERT_READY(Socket(socket.get()).send(data)); + + AWAIT_ASSERT_READY(await_subprocess(client.get(), 0)); + } else { + // If the protocols are NOT the same, it is invalid. + Future<Socket> socket = server.get().accept(); + AWAIT_ASSERT_FAILED(socket); + + AWAIT_ASSERT_READY(await_subprocess(client.get(), None())); + } + } + } +} + +#endif // USE_SSL_SOCKET
