| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #ifndef __PROCESS_SSL_TEST_HPP__ |
| #define __PROCESS_SSL_TEST_HPP__ |
| |
| #ifdef USE_SSL_SOCKET |
| #include <string> |
| |
| #ifdef __WINDOWS__ |
| // NOTE: This must be included before the OpenSSL headers as it includes |
| // `WinSock2.h` and `Windows.h` in the correct order. |
| #include <stout/windows.hpp> |
| #endif // __WINDOWS__ |
| |
| #include <openssl/rsa.h> |
| #include <openssl/bio.h> |
| #include <openssl/x509.h> |
| #include <openssl/x509v3.h> |
| |
| #include <process/io.hpp> |
| #include <process/process.hpp> |
| #include <process/socket.hpp> |
| #include <process/subprocess.hpp> |
| |
| #include <process/ssl/utilities.hpp> |
| |
| #include <stout/none.hpp> |
| #include <stout/option.hpp> |
| #include <stout/try.hpp> |
| #include <stout/result.hpp> |
| |
| #include <stout/os/realpath.hpp> |
| #endif // USE_SSL_SOCKET |
| |
| #include <stout/tests/utils.hpp> |
| |
| #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 { |
| #endif // USE_SSL_SOCKET |
| |
| // When SSL is not compiled in, we want the `SSLTemporaryDirectoryTest` class |
| // to exist, so that other tests can inherit it; this class is equivalent |
| // to the `TemporaryDirectoryTest` under that condition. |
| #ifndef USE_SSL_SOCKET |
| class SSLTemporaryDirectoryTest : public TemporaryDirectoryTest {}; |
| #else |
| /** |
| * A Test fixture that contains helpers for setting up SSL keys |
| * and certificates, as well as cleaning them up afterwards. |
| */ |
| class SSLTemporaryDirectoryTest : public TemporaryDirectoryTest |
| { |
| public: |
| static void TearDownTestCase() |
| { |
| // Clear and reset any environment variables. |
| set_environment_variables({}); |
| } |
| |
| protected: |
| /** |
| * @return The path to the authorized private key. |
| */ |
| Path key_path() |
| { |
| return Path(path::join(os::getcwd(), "key.pem")); |
| } |
| |
| /** |
| * @return The path to the authorized certificate. |
| */ |
| Path certificate_path() |
| { |
| return Path(path::join(os::getcwd(), "cert.pem")); |
| } |
| |
| /** |
| * @return The path to the unauthorized private key. |
| */ |
| Path scrap_key_path() |
| { |
| return Path(path::join(os::getcwd(), "scrap_key.pem")); |
| } |
| |
| /** |
| * @return The path to the unauthorized certificate. |
| */ |
| Path scrap_certificate_path() |
| { |
| return Path(path::join(os::getcwd(), "scrap_cert.pem")); |
| } |
| |
| /** |
| * Wipes out existing SSL environment variables and replaces them |
| * with the given map. The SSL library is reinitialized afterwards. |
| */ |
| static void set_environment_variables( |
| const std::map<std::string, std::string>& environment) |
| { |
| // This unsets all the SSL environment variables. Necessary for |
| // ensuring a clean starting slate between tests. |
| os::unsetenv("LIBPROCESS_SSL_ENABLED"); |
| os::unsetenv("LIBPROCESS_SSL_SUPPORT_DOWNGRADE"); |
| os::unsetenv("LIBPROCESS_SSL_CERT_FILE"); |
| os::unsetenv("LIBPROCESS_SSL_KEY_FILE"); |
| os::unsetenv("LIBPROCESS_SSL_VERIFY_CERT"); |
| os::unsetenv("LIBPROCESS_SSL_VERIFY_SERVER_CERT"); |
| os::unsetenv("LIBPROCESS_SSL_REQUIRE_CERT"); |
| os::unsetenv("LIBPROCESS_SSL_REQUIRE_CLIENT_CERT"); |
| os::unsetenv("LIBPROCESS_SSL_VERIFY_DEPTH"); |
| os::unsetenv("LIBPROCESS_SSL_CA_DIR"); |
| os::unsetenv("LIBPROCESS_SSL_CA_FILE"); |
| os::unsetenv("LIBPROCESS_SSL_CIPHERS"); |
| os::unsetenv("LIBPROCESS_SSL_ENABLE_SSL_V3"); |
| os::unsetenv("LIBPROCESS_SSL_ENABLE_TLS_V1_0"); |
| os::unsetenv("LIBPROCESS_SSL_ENABLE_TLS_V1_1"); |
| os::unsetenv("LIBPROCESS_SSL_ENABLE_TLS_V1_2"); |
| os::unsetenv("LIBPROCESS_SSL_ENABLE_TLS_V1_3"); |
| |
| // Copy the given map into the clean slate. |
| foreachpair ( |
| const std::string& name, const std::string& value, environment) { |
| os::setenv(name, value); |
| } |
| |
| // Make sure the library internally reflects the new environment variables. |
| process::network::openssl::reinitialize(); |
| } |
| |
| /** |
| * Sets up a private key and certificate pair at SSLTest::key_path |
| * and SSLTest::certificate_path. 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. |
| */ |
| void generate_keys_and_certs() { |
| // 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]( |
| const Option<std::string> abort_message = None()) { |
| 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()); } |
| |
| // We abort here because failure during setup indicates that something |
| // is horribly and irrecoverably wrong. |
| if (abort_message.isSome()) { |
| ABORT(abort_message.get()); |
| } |
| }; |
| |
| // Generate the authority key. |
| private_key = process::network::openssl::generate_private_rsa_key(); |
| if (private_key.isError()) { |
| cleanup("Could not generate private key: " + private_key.error()); |
| } |
| |
| // Figure out the hostname that libprocess is advertising. |
| // Set the hostname of the certificate to this hostname so that |
| // hostname verification of the certificate will pass. |
| Try<std::string> hostname = net::getHostname(process::address().ip); |
| if (hostname.isError()) { |
| cleanup("Could not determine hostname of libprocess: " + |
| hostname.error()); |
| } |
| |
| // Generate an authorized certificate. |
| certificate = process::network::openssl::generate_x509( |
| private_key.get(), |
| private_key.get(), |
| None(), |
| 1, |
| 365, |
| hostname.get(), |
| net::IP(process::address().ip)); |
| |
| if (certificate.isError()) { |
| cleanup("Could not generate certificate: " + certificate.error()); |
| } |
| |
| // Write the authority key to disk. |
| Try<Nothing> key_write = |
| process::network::openssl::write_key_file(private_key.get(), key_path()); |
| |
| if (key_write.isError()) { |
| cleanup("Could not write private key to disk: " + key_write.error()); |
| } |
| |
| // Write the authorized certificate to disk. |
| Try<Nothing> certificate_write = |
| process::network::openssl::write_certificate_file( |
| certificate.get(), |
| certificate_path()); |
| |
| if (certificate_write.isError()) { |
| cleanup("Could not write certificate to disk: " + |
| certificate_write.error()); |
| } |
| |
| // Generate a scrap key. |
| scrap_key = process::network::openssl::generate_private_rsa_key(); |
| if (scrap_key.isError()) { |
| cleanup("Could not generate a scrap private key: " + scrap_key.error()); |
| } |
| |
| // Write the scrap key to disk. |
| key_write = process::network::openssl::write_key_file( |
| scrap_key.get(), |
| scrap_key_path()); |
| |
| if (key_write.isError()) { |
| cleanup("Could not write scrap key to disk: " + key_write.error()); |
| } |
| |
| // Generate a scrap certificate. |
| scrap_certificate = process::network::openssl::generate_x509( |
| scrap_key.get(), |
| scrap_key.get()); |
| |
| if (scrap_certificate.isError()) { |
| cleanup("Could not generate a scrap certificate: " + |
| scrap_certificate.error()); |
| } |
| |
| // Write the scrap certificate to disk. |
| certificate_write = process::network::openssl::write_certificate_file( |
| scrap_certificate.get(), |
| scrap_certificate_path()); |
| |
| if (certificate_write.isError()) { |
| cleanup("Could not write scrap certificate to disk: " + |
| certificate_write.error()); |
| } |
| |
| // Since we successfully set up all our state, we call cleanup |
| // without an abort message (so as not to abort). |
| cleanup(); |
| } |
| }; |
| |
| |
| /** |
| * A Test fixture that sets up SSL keys and certificates. |
| * |
| * There are some helper functions like SSLTest::setup_server and |
| * SSLTest::launch_client that factor out common behavior used in |
| * tests. |
| */ |
| class SSLTest : public SSLTemporaryDirectoryTest |
| { |
| protected: |
| SSLTest() : data("Hello World!") {} |
| |
| void SetUp() override |
| { |
| SSLTemporaryDirectoryTest::SetUp(); |
| generate_keys_and_certs(); |
| } |
| |
| /** |
| * Initializes a listening server. |
| * |
| * @param environment The SSL environment variables to launch the |
| * server socket with. |
| * |
| * @return Socket if successful otherwise an Error. |
| */ |
| Try<process::network::inet::Socket> setup_server( |
| const std::map<std::string, std::string>& environment) |
| { |
| set_environment_variables(environment); |
| |
| const Try<process::network::inet::Socket> create = |
| process::network::inet::Socket::create( |
| process::network::internal::SocketImpl::Kind::SSL); |
| |
| if (create.isError()) { |
| return Error(create.error()); |
| } |
| |
| process::network::inet::Socket server = create.get(); |
| |
| // We need to explicitly bind to the address advertised by libprocess so the |
| // certificate we create in this test fixture can be verified. |
| Try<process::network::inet::Address> bind = |
| server.bind( |
| process::network::inet::Address(net::IP(process::address().ip), 0)); |
| |
| if (bind.isError()) { |
| return Error(bind.error()); |
| } |
| |
| 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. |
| * @param hostname The hostname to use for TLS certificate validation. |
| * It is passed separately because some tests want to provide the |
| * "wrong" hostname to test error conditions. |
| * |
| * @return Subprocess if successful otherwise an Error. |
| */ |
| Try<process::Subprocess> launch_client( |
| const std::map<std::string, std::string>& environment, |
| const Option<std::string>& hostname, |
| const net::IP& ip, |
| uint16_t port, |
| bool use_ssl_socket) |
| { |
| // Set up arguments to be passed to the 'client-ssl' binary. |
| std::vector<std::string> argv = { |
| "ssl-client", |
| "--use_ssl=" + stringify(use_ssl_socket), |
| "--server=" + stringify(ip), |
| "--port=" + stringify(port), |
| "--data=" + data}; |
| |
| if (hostname.isSome()) { |
| argv.push_back("--server_hostname=" + hostname.get()); |
| } |
| |
| Result<std::string> path = os::realpath(BUILD_DIR); |
| if (!path.isSome()) { |
| return Error("Could not establish build directory path"); |
| } |
| |
| // Explicitly set `LIBPROCESS_IP` in the subprocess to the same IP that was |
| // used to generate the hostname for SSL certificates. This ensures that |
| // certificate verification can succeed. |
| std::map<std::string, std::string> full_environment(environment); |
| full_environment["LIBPROCESS_IP"] = stringify(process::address().ip); |
| |
| return process::subprocess( |
| path::join(path.get(), "ssl-client"), |
| argv, |
| process::Subprocess::PIPE(), |
| process::Subprocess::PIPE(), |
| process::Subprocess::FD(STDERR_FILENO), |
| nullptr, |
| full_environment); |
| } |
| |
| /** |
| * Launches a test SSL client as a subprocess connecting to the |
| * server. This is a convenience overload for `launch_client()` |
| * that uses the IP address from the passed Socket as the server |
| * hostname. |
| * |
| * @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<process::Subprocess> launch_client( |
| const std::map<std::string, std::string>& environment, |
| const process::network::inet::Socket& server, |
| bool use_ssl_socket) |
| { |
| const Try<process::network::inet::Address> address = server.address(); |
| if (address.isError()) { |
| return Error(address.error()); |
| } |
| |
| return launch_client( |
| environment, |
| None(), |
| address->ip, |
| address->port, |
| use_ssl_socket); |
| } |
| |
| static constexpr size_t BACKLOG = 5; |
| |
| const std::string data; |
| }; |
| |
| #endif // USE_SSL_SOCKET |
| |
| #endif // __PROCESS_SSL_TEST_HPP__ |