Repository: mesos Updated Branches: refs/heads/master d6685e492 -> 067034047
SSL: Made SSLTest fixture publicly accessible. Refactored libprocess SSL tests to be available for reuse by other projects. Review: https://reviews.apache.org/r/37871 Project: http://git-wip-us.apache.org/repos/asf/mesos/repo Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/06703404 Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/06703404 Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/06703404 Branch: refs/heads/master Commit: 067034047317bb42ad41d2d93851b373108987d3 Parents: d6685e4 Author: Jojy Varghese <[email protected]> Authored: Mon Aug 31 09:37:48 2015 -0400 Committer: Joris Van Remoortere <[email protected]> Committed: Mon Aug 31 10:38:23 2015 -0400 ---------------------------------------------------------------------- 3rdparty/libprocess/Makefile.am | 3 +- 3rdparty/libprocess/include/Makefile.am | 2 + .../libprocess/include/process/ssl/gtest.hpp | 349 +++++++++++++++++++ .../include/process/ssl/utilities.hpp | 115 ++++++ 3rdparty/libprocess/src/openssl_util.cpp | 262 -------------- 3rdparty/libprocess/src/openssl_util.hpp | 111 ------ 3rdparty/libprocess/src/ssl/utilities.cpp | 262 ++++++++++++++ 3rdparty/libprocess/src/tests/ssl_tests.cpp | 313 +---------------- 8 files changed, 732 insertions(+), 685 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mesos/blob/06703404/3rdparty/libprocess/Makefile.am ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/Makefile.am b/3rdparty/libprocess/Makefile.am index 7ef5158..6361ac6 100644 --- a/3rdparty/libprocess/Makefile.am +++ b/3rdparty/libprocess/Makefile.am @@ -83,8 +83,7 @@ libprocess_la_SOURCES += \ src/libevent_ssl_socket.hpp \ src/openssl.cpp \ src/openssl.hpp \ - src/openssl_util.cpp \ - src/openssl_util.hpp + src/ssl/utilities.cpp endif libprocess_la_CPPFLAGS = \ http://git-wip-us.apache.org/repos/asf/mesos/blob/06703404/3rdparty/libprocess/include/Makefile.am ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/include/Makefile.am b/3rdparty/libprocess/include/Makefile.am index 3b6108d..fcc62e9 100644 --- a/3rdparty/libprocess/include/Makefile.am +++ b/3rdparty/libprocess/include/Makefile.am @@ -57,6 +57,8 @@ nobase_include_HEADERS = \ process/sequence.hpp \ process/shared.hpp \ process/socket.hpp \ + process/ssl/gtest.hpp \ + process/ssl/utilities.hpp \ process/statistics.hpp \ process/system.hpp \ process/subprocess.hpp \ http://git-wip-us.apache.org/repos/asf/mesos/blob/06703404/3rdparty/libprocess/include/process/ssl/gtest.hpp ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/include/process/ssl/gtest.hpp b/3rdparty/libprocess/include/process/ssl/gtest.hpp new file mode 100644 index 0000000..a6051b7 --- /dev/null +++ b/3rdparty/libprocess/include/process/ssl/gtest.hpp @@ -0,0 +1,349 @@ +/** + * 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 <openssl/rsa.h> +#include <openssl/bio.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> + +#include <process/io.hpp> +#include <process/socket.hpp> + +#include <process/ssl/utilities.hpp> + +using std::map; +using std::string; +using std::vector; + +using namespace process; +using namespace process::network; + +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 { + + +/** + * 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 cleanup_directories() + { + os::rm(key_path().value); + os::rm(certificate_path().value); + os::rm(scrap_key_path().value); + os::rm(scrap_certificate_path().value); + } + + 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) { + cleanup_directories(); + } + }; + + // Generate the authority key. + private_key = openssl::generate_private_rsa_key(); + if (private_key.isError()) { + ABORT("Could not generate private key: " + private_key.error()); + } + + // Figure out the hostname that 'INADDR_LOOPBACK' will bind to. + // Set the hostname of the certificate to this hostname so that + // hostname verification of the certificate will pass. + Try<string> hostname = net::getHostname(net::IP(INADDR_LOOPBACK)); + if (hostname.isError()) { + cleanup(); + ABORT("Could not determine hostname of 'INADDR_LOOPBACK': " + + hostname.error()); + } + + // Generate an authorized certificate. + certificate = openssl::generate_x509( + private_key.get(), + private_key.get(), + None(), + 1, + 365, + hostname.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. + cleanup_directories(); + } + + 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_SUPPORT_DOWNGRADE"); + 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_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(); + + // We need to explicitly bind to INADDR_LOOPBACK so the + // certificate we create in this test fixture can be verified. + Try<Address> bind = server.bind(Address(net::IP(INADDR_LOOPBACK), 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. + * + * @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; +}; + +#endif // USE_SSL_SOCKET + +#endif // __PROCESS_SSL_TEST_HPP__ http://git-wip-us.apache.org/repos/asf/mesos/blob/06703404/3rdparty/libprocess/include/process/ssl/utilities.hpp ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/include/process/ssl/utilities.hpp b/3rdparty/libprocess/include/process/ssl/utilities.hpp new file mode 100644 index 0000000..25d3a38 --- /dev/null +++ b/3rdparty/libprocess/include/process/ssl/utilities.hpp @@ -0,0 +1,115 @@ +/** +* Licensed 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 __OPENSSL_UTIL_HPP__ +#define __OPENSSL_UTIL_HPP__ + +#ifdef USE_SSL_SOCKET + +#include <openssl/ssl.h> +#include <openssl/x509.h> + +#include <stout/nothing.hpp> +#include <stout/path.hpp> +#include <stout/try.hpp> + +namespace process { +namespace network { +namespace openssl { + +/** + * Generates an RSA key. + * + * The caller is responsible for calling `EVP_PKEY_free` on the + * returned EVP_PKEY. + * @see <a href="https://www.openssl.org/docs/crypto/RSA_generate_key.html">RSA_generate_key_ex</a> // NOLINT + * + * @param bits The modulus size used to generate the key. + * @param exponent The public exponent, an odd number. + * + * @return A pointer to an EVP_PKEY if successful otherwise an Error. + */ +Try<EVP_PKEY*> generate_private_rsa_key( + int bits = 2048, + unsigned long exponent = RSA_F4); + + +/** + * Generates an X509 certificate. + * + * The X509 certificate is generated for the @param subject_key and + * signed by the @param sign_key. The caller is responsible for + * calling `X509_free` on the returned X509 object. + * The common name of the certificate will be set to @param hostname + * or that of the localhost if @param hostname is not provided. + * If a @param parent_certificate is provided, then the issuer name of + * the certificate will be set to the subject name of the + * @param parent_certificate. Otherwise, it is assumed this is a + * self-signed certificate in which case the @param subject_key must + * be the same as the @param sign_key, and the issuer name will be the + * same as the subject name. + * + * @param subject_key The key that will be made public by the + * certificate. + * @param sign_key The private key used to sign the certificate. + * @param parent_certificate An optional parent certificate that will + * be used to set the issuer name. + * @param serial The serial number of the certificate. + * @param days The number of days from the current time that the + * certificate will be valid. + * @param hostname An optional hostname used to set the common name of + * the certificate. + * + * @return A pointer to an X509 certificate if successful otherwise an + * Error. + */ +Try<X509*> generate_x509( + EVP_PKEY* subject_key, + EVP_PKEY* sign_key, + const Option<X509*>& parent_certificate = None(), + int serial = 1, + int days = 365, + Option<std::string> hostname = None()); + + +/** + * Writes a private key (EVP_PKEY) to a file on disk. + * @see <a href="https://www.openssl.org/docs/crypto/pem.html">PEM_write_PrivateKey</a> // NOLINT + * + * @param private_key The private key to write. + * @param path The file location to create the file. + * + * @return Nothing if successful otherwise an Error. + */ +Try<Nothing> write_key_file(EVP_PKEY* private_key, const Path& path); + + +/** + * Writes an X509 certificate (X509) to a file on disk. + * @see <a href="https://www.openssl.org/docs/crypto/pem.html">PEM_write_X509</a> // NOLINT + * + * @param x509 The certificate to write. + * @param path The file location to create the file. + * + * @return Nothing if successful otherwise an Error. + */ +Try<Nothing> write_certificate_file(X509* x509, const Path& path); + +} // namespace openssl { +} // namespace network { +} // namespace process { + +#endif // USE_SSL_SOCKET + +#endif // __OPENSSL_UTIL_HPP__ http://git-wip-us.apache.org/repos/asf/mesos/blob/06703404/3rdparty/libprocess/src/openssl_util.cpp ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/src/openssl_util.cpp b/3rdparty/libprocess/src/openssl_util.cpp deleted file mode 100644 index 42b2860..0000000 --- a/3rdparty/libprocess/src/openssl_util.cpp +++ /dev/null @@ -1,262 +0,0 @@ -/** -* Licensed 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 -*/ - -#include "openssl_util.hpp" - -#include <openssl/err.h> -#include <openssl/rsa.h> -#include <openssl/x509.h> -#include <openssl/x509v3.h> - -#include <stout/net.hpp> -#include <stout/stringify.hpp> - -// TODO(jmlvanre): Add higher level abstractions for key and -// certificate generation. - -namespace process { -namespace network { -namespace openssl { - -Try<EVP_PKEY*> generate_private_rsa_key(int bits, unsigned long _exponent) -{ - // Allocate the in-memory structure for the private key. - EVP_PKEY* private_key = EVP_PKEY_new(); - if (private_key == NULL) { - return Error("Failed to allocate key: EVP_PKEY_new"); - } - - // Allocate space for the exponent. - BIGNUM* exponent = BN_new(); - if (exponent == NULL) { - EVP_PKEY_free(private_key); - return Error("Failed to allocate exponent: BN_new"); - } - - // Assign the exponent. - if (BN_set_word(exponent, _exponent) != 1) { - BN_free(exponent); - EVP_PKEY_free(private_key); - return Error("Failed to set exponent: BN_set_word"); - } - - // Allocate the in-memory structure for the key pair. - RSA* rsa = RSA_new(); - if (rsa == NULL) { - BN_free(exponent); - EVP_PKEY_free(private_key); - return Error("Failed to allocate RSA: RSA_new"); - } - - // Generate the RSA key pair. - if (RSA_generate_key_ex(rsa, bits, exponent, NULL) != 1) { - RSA_free(rsa); - BN_free(exponent); - EVP_PKEY_free(private_key); - return Error(ERR_error_string(ERR_get_error(), NULL)); - } - - // We no longer need the exponent, so let's free it. - BN_free(exponent); - - // Associate the RSA key with the private key. If this association - // is successful, then the RSA key will be freed when the private - // key is freed. - if (EVP_PKEY_assign_RSA(private_key, rsa) != 1) { - RSA_free(rsa); - EVP_PKEY_free(private_key); - return Error("Failed to assign RSA key: EVP_PKEY_assign_RSA"); - } - - return private_key; -} - - -Try<X509*> generate_x509( - EVP_PKEY* subject_key, - EVP_PKEY* sign_key, - const Option<X509*>& parent_certificate, - int serial, - int days, - Option<std::string> hostname) -{ - Option<X509_NAME*> issuer_name = None(); - if (parent_certificate.isNone()) { - // If there is no parent certificate, then the subject and - // signing key must be the same. - if (subject_key != sign_key) { - return Error("Subject vs signing key mismatch"); - } - } else { - // If there is a parent certificate, then set the issuer name to - // be that of the parent. - issuer_name = X509_get_subject_name(parent_certificate.get()); - - if (issuer_name.get() == NULL) { - return Error("Failed to get subject name of parent certificate: " - "X509_get_subject_name"); - } - } - - // Allocate the in-memory structure for the certificate. - X509* x509 = X509_new(); - if (x509 == NULL) { - return Error("Failed to allocate certification: X509_new"); - } - - // Set the version to V3. - if (X509_set_version(x509, 2) != 1) { - X509_free(x509); - return Error("Failed to set version: X509_set_version"); - } - - // Set the serial number. - if (ASN1_INTEGER_set(X509_get_serialNumber(x509), serial) != 1) { - X509_free(x509); - return Error("Failed to set serial number: ASN1_INTEGER_set"); - } - - // Make this certificate valid for 'days' number of days from now. - if (X509_gmtime_adj(X509_get_notBefore(x509), 0) == NULL || - X509_gmtime_adj(X509_get_notAfter(x509), - 60L * 60L * 24L * days) == NULL) { - X509_free(x509); - return Error("Failed to set valid days of certificate: X509_gmtime_adj"); - } - - // Set the public key for our certificate based on the subject key. - if (X509_set_pubkey(x509, subject_key) != 1) { - X509_free(x509); - return Error("Failed to set public key: X509_set_pubkey"); - } - - // Figure out our hostname if one was not provided. - if (hostname.isNone()) { - const Try<std::string> _hostname = net::hostname(); - if (_hostname.isError()) { - X509_free(x509); - return Error("Failed to determine hostname"); - } - - hostname = _hostname.get(); - } - - // Grab the subject name of the new certificate. - X509_NAME* name = X509_get_subject_name(x509); - if (name == NULL) { - X509_free(x509); - return Error("Failed to get subject name: X509_get_subject_name"); - } - - // Set the country code, organization, and common name. - if (X509_NAME_add_entry_by_txt( - name, - "C", - MBSTRING_ASC, - reinterpret_cast<const unsigned char*>("US"), - -1, - -1, - 0) != 1) { - X509_free(x509); - return Error("Failed to set country code: X509_NAME_add_entry_by_txt"); - } - - if (X509_NAME_add_entry_by_txt( - name, - "O", - MBSTRING_ASC, - reinterpret_cast<const unsigned char*>("Test"), - -1, - -1, - 0) != 1) { - X509_free(x509); - return Error("Failed to set organization name: X509_NAME_add_entry_by_txt"); - } - - if (X509_NAME_add_entry_by_txt( - name, - "CN", - MBSTRING_ASC, - reinterpret_cast<const unsigned char*>(hostname.get().c_str()), - -1, - -1, - 0) != 1) { - X509_free(x509); - return Error("Failed to set common name: X509_NAME_add_entry_by_txt"); - } - - // Set the issuer name to be the same as the subject if it is not - // already set (this is a self-signed certificate). - if (issuer_name.isNone()) { - issuer_name = name; - } - - CHECK_SOME(issuer_name); - if (X509_set_issuer_name(x509, issuer_name.get()) != 1) { - X509_free(x509); - return Error("Failed to set issuer name: X509_set_issuer_name"); - } - - // Sign the certificate with the sign key. - if (X509_sign(x509, sign_key, EVP_sha1()) == 0) { - X509_free(x509); - return Error("Failed to sign certificate: X509_sign"); - } - - return x509; -} - - -Try<Nothing> write_key_file(EVP_PKEY* private_key, const Path& path) -{ - // We use 'FILE*' here because it is an API requirement by openssl. - FILE* file = fopen(path.value.c_str(), "wb"); - if (file == NULL) { - return Error("Failed to open file '" + stringify(path) + "' for writing"); - } - - if (PEM_write_PrivateKey(file, private_key, NULL, NULL, 0, NULL, NULL) != 1) { - fclose(file); - return Error("Failed to write private key to file '" + stringify(path) + - "': PEM_write_PrivateKey"); - } - - fclose(file); - - return Nothing(); -} - - -Try<Nothing> write_certificate_file(X509* x509, const Path& path) -{ - // We use 'FILE*' here because it is an API requirement by openssl. - FILE* file = fopen(path.value.c_str(), "wb"); - if (file == NULL) { - return Error("Failed to open file '" + stringify(path) + "' for writing"); - } - - if (PEM_write_X509(file, x509) != 1) { - fclose(file); - return Error("Failed to write certificate to file '" + stringify(path) + - "': PEM_write_X509"); - } - - fclose(file); - - return Nothing(); -} - -} // namespace openssl { -} // namespace network { -} // namespace process { http://git-wip-us.apache.org/repos/asf/mesos/blob/06703404/3rdparty/libprocess/src/openssl_util.hpp ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/src/openssl_util.hpp b/3rdparty/libprocess/src/openssl_util.hpp deleted file mode 100644 index 0890b63..0000000 --- a/3rdparty/libprocess/src/openssl_util.hpp +++ /dev/null @@ -1,111 +0,0 @@ -/** -* Licensed 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 __OPENSSL_UTIL_HPP__ -#define __OPENSSL_UTIL_HPP__ - -#include <openssl/ssl.h> -#include <openssl/x509.h> - -#include <stout/nothing.hpp> -#include <stout/path.hpp> -#include <stout/try.hpp> - -namespace process { -namespace network { -namespace openssl { - -/** - * Generates an RSA key. - * - * The caller is responsible for calling `EVP_PKEY_free` on the - * returned EVP_PKEY. - * @see <a href="https://www.openssl.org/docs/crypto/RSA_generate_key.html">RSA_generate_key_ex</a> // NOLINT - * - * @param bits The modulus size used to generate the key. - * @param exponent The public exponent, an odd number. - * - * @return A pointer to an EVP_PKEY if successful otherwise an Error. - */ -Try<EVP_PKEY*> generate_private_rsa_key( - int bits = 2048, - unsigned long exponent = RSA_F4); - - -/** - * Generates an X509 certificate. - * - * The X509 certificate is generated for the @param subject_key and - * signed by the @param sign_key. The caller is responsible for - * calling `X509_free` on the returned X509 object. - * The common name of the certificate will be set to @param hostname - * or that of the localhost if @param hostname is not provided. - * If a @param parent_certificate is provided, then the issuer name of - * the certificate will be set to the subject name of the - * @param parent_certificate. Otherwise, it is assumed this is a - * self-signed certificate in which case the @param subject_key must - * be the same as the @param sign_key, and the issuer name will be the - * same as the subject name. - * - * @param subject_key The key that will be made public by the - * certificate. - * @param sign_key The private key used to sign the certificate. - * @param parent_certificate An optional parent certificate that will - * be used to set the issuer name. - * @param serial The serial number of the certificate. - * @param days The number of days from the current time that the - * certificate will be valid. - * @param hostname An optional hostname used to set the common name of - * the certificate. - * - * @return A pointer to an X509 certificate if successful otherwise an - * Error. - */ -Try<X509*> generate_x509( - EVP_PKEY* subject_key, - EVP_PKEY* sign_key, - const Option<X509*>& parent_certificate = None(), - int serial = 1, - int days = 365, - Option<std::string> hostname = None()); - - -/** - * Writes a private key (EVP_PKEY) to a file on disk. - * @see <a href="https://www.openssl.org/docs/crypto/pem.html">PEM_write_PrivateKey</a> // NOLINT - * - * @param private_key The private key to write. - * @param path The file location to create the file. - * - * @return Nothing if successful otherwise an Error. - */ -Try<Nothing> write_key_file(EVP_PKEY* private_key, const Path& path); - - -/** - * Writes an X509 certificate (X509) to a file on disk. - * @see <a href="https://www.openssl.org/docs/crypto/pem.html">PEM_write_X509</a> // NOLINT - * - * @param x509 The certificate to write. - * @param path The file location to create the file. - * - * @return Nothing if successful otherwise an Error. - */ -Try<Nothing> write_certificate_file(X509* x509, const Path& path); - -} // namespace openssl { -} // namespace network { -} // namespace process { - -#endif // __OPENSSL_UTIL_HPP__ http://git-wip-us.apache.org/repos/asf/mesos/blob/06703404/3rdparty/libprocess/src/ssl/utilities.cpp ---------------------------------------------------------------------- diff --git a/3rdparty/libprocess/src/ssl/utilities.cpp b/3rdparty/libprocess/src/ssl/utilities.cpp new file mode 100644 index 0000000..b244964 --- /dev/null +++ b/3rdparty/libprocess/src/ssl/utilities.cpp @@ -0,0 +1,262 @@ +/** +* Licensed 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 +*/ + +#include <process/ssl/utilities.hpp> + +#include <openssl/err.h> +#include <openssl/rsa.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> + +#include <stout/net.hpp> +#include <stout/stringify.hpp> + +// TODO(jmlvanre): Add higher level abstractions for key and +// certificate generation. + +namespace process { +namespace network { +namespace openssl { + +Try<EVP_PKEY*> generate_private_rsa_key(int bits, unsigned long _exponent) +{ + // Allocate the in-memory structure for the private key. + EVP_PKEY* private_key = EVP_PKEY_new(); + if (private_key == NULL) { + return Error("Failed to allocate key: EVP_PKEY_new"); + } + + // Allocate space for the exponent. + BIGNUM* exponent = BN_new(); + if (exponent == NULL) { + EVP_PKEY_free(private_key); + return Error("Failed to allocate exponent: BN_new"); + } + + // Assign the exponent. + if (BN_set_word(exponent, _exponent) != 1) { + BN_free(exponent); + EVP_PKEY_free(private_key); + return Error("Failed to set exponent: BN_set_word"); + } + + // Allocate the in-memory structure for the key pair. + RSA* rsa = RSA_new(); + if (rsa == NULL) { + BN_free(exponent); + EVP_PKEY_free(private_key); + return Error("Failed to allocate RSA: RSA_new"); + } + + // Generate the RSA key pair. + if (RSA_generate_key_ex(rsa, bits, exponent, NULL) != 1) { + RSA_free(rsa); + BN_free(exponent); + EVP_PKEY_free(private_key); + return Error(ERR_error_string(ERR_get_error(), NULL)); + } + + // We no longer need the exponent, so let's free it. + BN_free(exponent); + + // Associate the RSA key with the private key. If this association + // is successful, then the RSA key will be freed when the private + // key is freed. + if (EVP_PKEY_assign_RSA(private_key, rsa) != 1) { + RSA_free(rsa); + EVP_PKEY_free(private_key); + return Error("Failed to assign RSA key: EVP_PKEY_assign_RSA"); + } + + return private_key; +} + + +Try<X509*> generate_x509( + EVP_PKEY* subject_key, + EVP_PKEY* sign_key, + const Option<X509*>& parent_certificate, + int serial, + int days, + Option<std::string> hostname) +{ + Option<X509_NAME*> issuer_name = None(); + if (parent_certificate.isNone()) { + // If there is no parent certificate, then the subject and + // signing key must be the same. + if (subject_key != sign_key) { + return Error("Subject vs signing key mismatch"); + } + } else { + // If there is a parent certificate, then set the issuer name to + // be that of the parent. + issuer_name = X509_get_subject_name(parent_certificate.get()); + + if (issuer_name.get() == NULL) { + return Error("Failed to get subject name of parent certificate: " + "X509_get_subject_name"); + } + } + + // Allocate the in-memory structure for the certificate. + X509* x509 = X509_new(); + if (x509 == NULL) { + return Error("Failed to allocate certification: X509_new"); + } + + // Set the version to V3. + if (X509_set_version(x509, 2) != 1) { + X509_free(x509); + return Error("Failed to set version: X509_set_version"); + } + + // Set the serial number. + if (ASN1_INTEGER_set(X509_get_serialNumber(x509), serial) != 1) { + X509_free(x509); + return Error("Failed to set serial number: ASN1_INTEGER_set"); + } + + // Make this certificate valid for 'days' number of days from now. + if (X509_gmtime_adj(X509_get_notBefore(x509), 0) == NULL || + X509_gmtime_adj(X509_get_notAfter(x509), + 60L * 60L * 24L * days) == NULL) { + X509_free(x509); + return Error("Failed to set valid days of certificate: X509_gmtime_adj"); + } + + // Set the public key for our certificate based on the subject key. + if (X509_set_pubkey(x509, subject_key) != 1) { + X509_free(x509); + return Error("Failed to set public key: X509_set_pubkey"); + } + + // Figure out our hostname if one was not provided. + if (hostname.isNone()) { + const Try<std::string> _hostname = net::hostname(); + if (_hostname.isError()) { + X509_free(x509); + return Error("Failed to determine hostname"); + } + + hostname = _hostname.get(); + } + + // Grab the subject name of the new certificate. + X509_NAME* name = X509_get_subject_name(x509); + if (name == NULL) { + X509_free(x509); + return Error("Failed to get subject name: X509_get_subject_name"); + } + + // Set the country code, organization, and common name. + if (X509_NAME_add_entry_by_txt( + name, + "C", + MBSTRING_ASC, + reinterpret_cast<const unsigned char*>("US"), + -1, + -1, + 0) != 1) { + X509_free(x509); + return Error("Failed to set country code: X509_NAME_add_entry_by_txt"); + } + + if (X509_NAME_add_entry_by_txt( + name, + "O", + MBSTRING_ASC, + reinterpret_cast<const unsigned char*>("Test"), + -1, + -1, + 0) != 1) { + X509_free(x509); + return Error("Failed to set organization name: X509_NAME_add_entry_by_txt"); + } + + if (X509_NAME_add_entry_by_txt( + name, + "CN", + MBSTRING_ASC, + reinterpret_cast<const unsigned char*>(hostname.get().c_str()), + -1, + -1, + 0) != 1) { + X509_free(x509); + return Error("Failed to set common name: X509_NAME_add_entry_by_txt"); + } + + // Set the issuer name to be the same as the subject if it is not + // already set (this is a self-signed certificate). + if (issuer_name.isNone()) { + issuer_name = name; + } + + CHECK_SOME(issuer_name); + if (X509_set_issuer_name(x509, issuer_name.get()) != 1) { + X509_free(x509); + return Error("Failed to set issuer name: X509_set_issuer_name"); + } + + // Sign the certificate with the sign key. + if (X509_sign(x509, sign_key, EVP_sha1()) == 0) { + X509_free(x509); + return Error("Failed to sign certificate: X509_sign"); + } + + return x509; +} + + +Try<Nothing> write_key_file(EVP_PKEY* private_key, const Path& path) +{ + // We use 'FILE*' here because it is an API requirement by openssl. + FILE* file = fopen(path.value.c_str(), "wb"); + if (file == NULL) { + return Error("Failed to open file '" + stringify(path) + "' for writing"); + } + + if (PEM_write_PrivateKey(file, private_key, NULL, NULL, 0, NULL, NULL) != 1) { + fclose(file); + return Error("Failed to write private key to file '" + stringify(path) + + "': PEM_write_PrivateKey"); + } + + fclose(file); + + return Nothing(); +} + + +Try<Nothing> write_certificate_file(X509* x509, const Path& path) +{ + // We use 'FILE*' here because it is an API requirement by openssl. + FILE* file = fopen(path.value.c_str(), "wb"); + if (file == NULL) { + return Error("Failed to open file '" + stringify(path) + "' for writing"); + } + + if (PEM_write_X509(file, x509) != 1) { + fclose(file); + return Error("Failed to write certificate to file '" + stringify(path) + + "': PEM_write_X509"); + } + + fclose(file); + + return Nothing(); +} + +} // namespace openssl { +} // namespace network { +} // namespace process { http://git-wip-us.apache.org/repos/asf/mesos/blob/06703404/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 index 7a316bc..ee30a02 100644 --- a/3rdparty/libprocess/src/tests/ssl_tests.cpp +++ b/3rdparty/libprocess/src/tests/ssl_tests.cpp @@ -14,22 +14,19 @@ #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 <process/ssl/gtest.hpp> +#include <process/ssl/utilities.hpp> + #include <stout/gtest.hpp> #include <stout/os.hpp> #include "openssl.hpp" -#include "openssl_util.hpp" using std::map; using std::string; @@ -38,18 +35,6 @@ 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; @@ -98,298 +83,6 @@ Future<Nothing> await_subprocess( } -/** - * 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()); - } - - // Figure out the hostname that 'INADDR_LOOPBACK' will bind to. - // Set the hostname of the certificate to this hostname so that - // hostname verification of the certificate will pass. - Try<string> hostname = net::getHostname(net::IP(INADDR_LOOPBACK)); - if (hostname.isError()) { - cleanup(); - ABORT("Could not determine hostname of 'INADDR_LOOPBACK': " + - hostname.error()); - } - - // Generate an authorized certificate. - certificate = openssl::generate_x509( - private_key.get(), - private_key.get(), - None(), - 1, - 365, - hostname.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_SUPPORT_DOWNGRADE"); - 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_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(); - - // We need to explicitly bind to INADDR_LOOPBACK so the - // certificate we create in this test fixture can be verified. - Try<Address> bind = server.bind(Address(net::IP(INADDR_LOOPBACK), 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. - * - * @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) {
