diff options
19 files changed, 1231 insertions, 0 deletions
diff --git a/vespalib/CMakeLists.txt b/vespalib/CMakeLists.txt index 33553da9422..a4b3f1e643c 100644 --- a/vespalib/CMakeLists.txt +++ b/vespalib/CMakeLists.txt @@ -56,6 +56,7 @@ vespa_define_module( src/tests/net/send_fd src/tests/net/socket src/tests/net/socket_spec + src/tests/net/tls/openssl_impl src/tests/objects/nbostream src/tests/optimized src/tests/printable @@ -118,6 +119,8 @@ vespa_define_module( src/vespa/vespalib/io src/vespa/vespalib/locale src/vespa/vespalib/net + src/vespa/vespalib/net/tls + src/vespa/vespalib/net/tls/impl src/vespa/vespalib/objects src/vespa/vespalib/stllike src/vespa/vespalib/test diff --git a/vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt b/vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt new file mode 100644 index 00000000000..799e2291d7c --- /dev/null +++ b/vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(vespalib_net_tls_openssl_impl_test_app TEST + SOURCES + openssl_impl_test.cpp + DEPENDS + vespalib +) +vespa_add_test(NAME vespalib_net_tls_openssl_impl_test_app COMMAND vespalib_net_tls_openssl_impl_test_app) + diff --git a/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp b/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp new file mode 100644 index 00000000000..cba88f2ba56 --- /dev/null +++ b/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp @@ -0,0 +1,199 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/vespalib/testkit/test_kit.h> +#include <vespa/vespalib/net/tls/tls_context.h> +#include <vespa/vespalib/net/tls/transport_security_options.h> +#include <vespa/vespalib/net/tls/crypto_codec.h> +#include <iostream> +#include <stdlib.h> + +using namespace vespalib; +using namespace vespalib::net::tls; + +/* + * Generated with the following commands: + * + * openssl ecparam -name prime256v1 -genkey -out ca.key + * + * openssl req -new -x509 -nodes -key ca.key \ + * -sha256 -out ca.pem \ + * -subj '/C=US/L=LooneyVille/O=ACME/OU=ACME test CA/CN=acme.example.com' \ + * -days 10000 + * + * openssl ecparam -name prime256v1 -genkey -out host.key + * + * openssl req -new -key host.key -out host.csr \ + * -subj '/C=US/L=LooneyVille/O=Wile. E. Coyote, Ltd./CN=wile.example.com' \ + * -sha256 + * + * openssl x509 -req -in host.csr \ + * -CA ca.pem \ + * -CAkey ca.key \ + * -CAcreateserial \ + * -out host.pem \ + * -days 10000 \ + * -sha256 + * + * TODO generate keypairs and certs at test-time to avoid any hard-coding + * There certs are valid until 2046, so that buys us some time..! + */ + +// ca.pem +constexpr const char* ca_pem = R"(-----BEGIN CERTIFICATE----- +MIIBuDCCAV4CCQDpVjQIixTxvDAKBggqhkjOPQQDAjBkMQswCQYDVQQGEwJVUzEU +MBIGA1UEBwwLTG9vbmV5VmlsbGUxDTALBgNVBAoMBEFDTUUxFTATBgNVBAsMDEFD +TUUgdGVzdCBDQTEZMBcGA1UEAwwQYWNtZS5leGFtcGxlLmNvbTAeFw0xODA4MzEx +MDU3NDVaFw00NjAxMTYxMDU3NDVaMGQxCzAJBgNVBAYTAlVTMRQwEgYDVQQHDAtM +b29uZXlWaWxsZTENMAsGA1UECgwEQUNNRTEVMBMGA1UECwwMQUNNRSB0ZXN0IENB +MRkwFwYDVQQDDBBhY21lLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAE1L7IzCN5pbyVnBATIHieuxq+hf9kWyn5yfjkXMhD52T5ITz1huq4nbiN +YtRoRP7XmipI60R/uiCHzERcsVz4rDAKBggqhkjOPQQDAgNIADBFAiEA6wmZDBca +y0aJ6ABtjbjx/vlmVDxdkaSZSgO8h2CkvIECIFktCkbZhDFfSvbqUScPOGuwkdGQ +L/EW2Bxp+1BPcYoZ +-----END CERTIFICATE-----)"; + +// host.pem +constexpr const char* cert_pem = R"(-----BEGIN CERTIFICATE----- +MIIBsTCCAVgCCQD6GfDh0ltpsjAKBggqhkjOPQQDAjBkMQswCQYDVQQGEwJVUzEU +MBIGA1UEBwwLTG9vbmV5VmlsbGUxDTALBgNVBAoMBEFDTUUxFTATBgNVBAsMDEFD +TUUgdGVzdCBDQTEZMBcGA1UEAwwQYWNtZS5leGFtcGxlLmNvbTAeFw0xODA4MzEx +MDU3NDVaFw00NjAxMTYxMDU3NDVaMF4xCzAJBgNVBAYTAlVTMRQwEgYDVQQHDAtM +b29uZXlWaWxsZTEeMBwGA1UECgwVV2lsZS4gRS4gQ295b3RlLCBMdGQuMRkwFwYD +VQQDDBB3aWxlLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +e+Y4hxt66em0STviGUj6ZDbxzoLoubXWRml8JDFrEc2S2433KWw2npxYSKVCyo3a +/Vo33V8/H0WgOXioKEZJxDAKBggqhkjOPQQDAgNHADBEAiAN+87hQuGv3z0Ja2BV +b8PHq2vp3BJHjeMuxWu4BFPn0QIgYlvIHikspgGatXRNMZ1gPC0oCccsJFcie+Cw +zL06UPI= +-----END CERTIFICATE-----)"; + +// host.key +constexpr const char* key_pem = R"(-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEID6di2PFYn8hPrxPbkFDGkSqF+K8L520In7nx3g0jwzOoAoGCCqGSM49 +AwEHoUQDQgAEe+Y4hxt66em0STviGUj6ZDbxzoLoubXWRml8JDFrEc2S2433KWw2 +npxYSKVCyo3a/Vo33V8/H0WgOXioKEZJxA== +-----END EC PRIVATE KEY-----)"; + +const char* decode_state_to_str(DecodeResult::State state) noexcept { + switch (state) { + case DecodeResult::State::Failed: return "Broken"; + case DecodeResult::State::OK: return "OK"; + case DecodeResult::State::NeedsMorePeerData: return "NeedsMorePeerData"; + default: + abort(); + } +} + +const char* hs_state_to_str(HandshakeResult::State state) noexcept { + switch (state) { + case HandshakeResult::State::Failed: return "Broken"; + case HandshakeResult::State::Done: return "Done"; + case HandshakeResult::State::NeedsMorePeerData: return "NeedsMorePeerData"; + default: + abort(); + } +} + +void log_handshake_result(const char* mode, const HandshakeResult& res) { + fprintf(stderr, "(handshake) %s consumed %zu peer bytes, wrote %zu peer bytes. State: %s\n", + mode, res.bytes_consumed, res.bytes_produced, + hs_state_to_str(res.state)); +} + +void log_encode_result(const char* mode, const EncodeResult& res) { + fprintf(stderr, "(encode) %s read %zu plaintext, wrote %zu cipher. State: %s\n", + mode, res.bytes_consumed, res.bytes_produced, + res.failed ? "Broken! D:" : "OK"); +} + +void log_decode_result(const char* mode, const DecodeResult& res) { + fprintf(stderr, "(decode) %s read %zu cipher, wrote %zu plaintext. State: %s\n", + mode, res.bytes_consumed, res.bytes_produced, + decode_state_to_str(res.state)); +} + +bool complete_handshake(CryptoCodec& client, CryptoCodec& server) { + // Not using vespalib::string here since it doesn't have erase(iter, length) implemented. + std::string client_to_server_buf; + std::string server_to_client_buf; + + HandshakeResult cli_res; + HandshakeResult serv_res; + while (!(cli_res.done() && serv_res.done())) { + client_to_server_buf.resize(client.min_encode_buffer_size()); + server_to_client_buf.resize(server.min_encode_buffer_size()); + + cli_res = client.handshake(server_to_client_buf.data(), serv_res.bytes_produced, + client_to_server_buf.data(), client_to_server_buf.size()); + log_handshake_result("client", cli_res); + server_to_client_buf.erase(server_to_client_buf.begin(), server_to_client_buf.begin() + cli_res.bytes_consumed); + + serv_res = server.handshake(client_to_server_buf.data(), cli_res.bytes_produced, + server_to_client_buf.data(), server_to_client_buf.size()); + log_handshake_result("server", serv_res); + client_to_server_buf.erase(client_to_server_buf.begin(), client_to_server_buf.begin() + serv_res.bytes_consumed); + + if (cli_res.failed() || serv_res.failed()) { + return false; + } + } + return true; +} + +TEST("client and server can complete handshake") { + // TODO move to fixture + auto tls_opts = TransportSecurityOptions(ca_pem, cert_pem, key_pem); + auto tls_ctx = TlsContext::create_default_context(tls_opts); + auto client = CryptoCodec::create_default_codec(*tls_ctx, CryptoCodec::Mode::Client); + auto server = CryptoCodec::create_default_codec(*tls_ctx, CryptoCodec::Mode::Server); + + EXPECT_TRUE(complete_handshake(*client, *server)); +} + +TEST("client can send single data frame to server after handshake") { + // TODO move to fixture + auto tls_opts = TransportSecurityOptions(ca_pem, cert_pem, key_pem); + auto tls_ctx = TlsContext::create_default_context(tls_opts); + auto client = CryptoCodec::create_default_codec(*tls_ctx, CryptoCodec::Mode::Client); + auto server = CryptoCodec::create_default_codec(*tls_ctx, CryptoCodec::Mode::Server); + + ASSERT_TRUE(complete_handshake(*client, *server)); + + std::string client_to_server_buf; + client_to_server_buf.resize(client->min_encode_buffer_size()); + + std::string client_plaintext = "Hellooo world! :D"; + auto cli_res = client->encode(client_plaintext.data(), client_plaintext.size(), + client_to_server_buf.data(), client_to_server_buf.size()); + log_encode_result("client", cli_res); + + std::string server_plaintext_out; + server_plaintext_out.resize(server->min_decode_buffer_size()); + auto serv_res = server->decode(client_to_server_buf.data(), cli_res.bytes_produced, + server_plaintext_out.data(), server_plaintext_out.size()); + log_decode_result("server", serv_res); + + ASSERT_FALSE(cli_res.failed); + ASSERT_FALSE(serv_res.failed()); + + ASSERT_TRUE(serv_res.state == DecodeResult::State::OK); + std::string data_received(server_plaintext_out.data(), serv_res.bytes_produced); + EXPECT_EQUAL(client_plaintext, data_received); +} + +/* + * TODO tests: + * - full duplex read/write + * - read and write of > frame size data + * - handshakes with multi frame writes + * - completed handshake with pipelined data frame + * - short ciphertext reads on decode + * - short plaintext writes on decode (.. if we even want to support this..) + * - short ciphertext write on encode + * - peer certificate validation on server + * - peer certificate validation on client + * - detection of peer shutdown session + */ + +TEST_MAIN() { TEST_RUN_ALL(); } diff --git a/vespalib/src/vespa/vespalib/CMakeLists.txt b/vespalib/src/vespa/vespalib/CMakeLists.txt index 480caf8f28d..dadfdec49d7 100644 --- a/vespalib/src/vespa/vespalib/CMakeLists.txt +++ b/vespalib/src/vespa/vespalib/CMakeLists.txt @@ -9,6 +9,8 @@ vespa_add_library(vespalib $<TARGET_OBJECTS:vespalib_vespalib_io> $<TARGET_OBJECTS:vespalib_vespalib_locale> $<TARGET_OBJECTS:vespalib_vespalib_net> + $<TARGET_OBJECTS:vespalib_vespalib_net_tls> + $<TARGET_OBJECTS:vespalib_vespalib_net_tls_impl> $<TARGET_OBJECTS:vespalib_vespalib_objects> $<TARGET_OBJECTS:vespalib_vespalib_stllike> $<TARGET_OBJECTS:vespalib_vespalib_testkit> @@ -23,3 +25,5 @@ vespa_add_library(vespalib vespalib_vespalib_test gcc ) + +vespa_add_target_package_dependency(vespalib OpenSSL) diff --git a/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt new file mode 100644 index 00000000000..938ae0896a2 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_library(vespalib_vespalib_net_tls OBJECT + SOURCES + crypto_codec.cpp + crypto_exception.cpp + tls_context.cpp + transport_security_options.cpp + DEPENDS +) +find_package(OpenSSL) +target_include_directories(vespalib_vespalib_net_tls PUBLIC ${OPENSSL_INCLUDE_DIR}) + diff --git a/vespalib/src/vespa/vespalib/net/tls/crypto_codec.cpp b/vespalib/src/vespa/vespalib/net/tls/crypto_codec.cpp new file mode 100644 index 00000000000..b36913d20e3 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/crypto_codec.cpp @@ -0,0 +1,15 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include "crypto_codec.h" +#include <vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h> +#include <vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h> +#include <cassert> + +namespace vespalib::net::tls { + +std::unique_ptr<CryptoCodec> CryptoCodec::create_default_codec(TlsContext& ctx, Mode mode) { + auto* ssl_ctx = dynamic_cast<impl::OpenSslTlsContextImpl*>(&ctx); + assert(ssl_ctx != nullptr); + return std::make_unique<impl::OpenSslCryptoCodecImpl>(*ssl_ctx->native_context(), mode); +} + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/crypto_codec.h b/vespalib/src/vespa/vespalib/net/tls/crypto_codec.h new file mode 100644 index 00000000000..6e690c809a5 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/crypto_codec.h @@ -0,0 +1,124 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <memory> + +namespace vespalib::net::tls { + +struct HandshakeResult { + // Handshake bytes consumed from peer. + size_t bytes_consumed = 0; + // Handshake bytes produced that must be sent to the peer. + size_t bytes_produced = 0; + enum class State { + Failed, + Done, + NeedsMorePeerData + }; + State state = State::Failed; + + bool failed() const noexcept { return (state == State::Failed); } + bool done() const noexcept { return (state == State::Done); } +}; + +struct EncodeResult { + // Plaintext bytes consumed + size_t bytes_consumed = 0; + // Ciphertext bytes produced that must be sent to the peer + size_t bytes_produced = 0; + bool failed = true; +}; + +struct DecodeResult { + // Ciphertext bytes consumed from peer + size_t bytes_consumed = 0; + // Plaintext bytes produced. + size_t bytes_produced = 0; + enum class State { + Failed, + OK, + NeedsMorePeerData + // TODO add Closed/Shutdown as own state? + }; + State state = State::Failed; + + bool failed() const noexcept { return (state == State::Failed); } +}; + +class TlsContext; + +// TODO move to different namespace, not dependent on TLS? + +/* + * A CryptoCodec provides a fully transport-independent way of negotiating + * a secure, authenticated session towards another peer. The codec requires + * the caller to handle any and all actual data transfer + */ +class CryptoCodec { +public: + enum class Mode { + Client, Server + }; + + virtual ~CryptoCodec() = default; + + /* + * Minimum buffer size required to represent one wire format frame + * of encrypted (ciphertext) data, including frame overhead. + */ + virtual size_t min_encode_buffer_size() const noexcept = 0; + /* + * Minimum buffer size required to represent the decoded (plaintext) + * output of a single frame of encrypted data. + */ + virtual size_t min_decode_buffer_size() const noexcept = 0; + + /* + * Precondition: to_peer_buf_size >= min_encode_buffer_size() + * Postcondition: if result.done(), the handshake process has completed + * and data may be passed through encode()/decode(). + */ + virtual HandshakeResult handshake(const char* from_peer, size_t from_peer_buf_size, + char* to_peer, size_t to_peer_buf_size) noexcept = 0; + + /* + * Encodes a single ciphertext frame into `ciphertext`. If plaintext_size + * is greater than can fit into a frame, the returned result's consumed_bytes + * field will be < plaintext_size. The number of actual ciphertext bytes produced + * is available in the returned result's produced_bytes field. + * + * Precondition: handshake must be completed + * Precondition: ciphertext_size >= min_encode_buffer_size(), i.e. it must be + * possible to encode at least 1 frame. + * Postcondition: if plaintext_size > 0 and result.failed == false, a single + * frame of ciphertext has been written into the to_peer buffer. + * Size of written frame is given by result.bytes_produced. This + * includes all protocol-specific frame overhead. + */ + virtual EncodeResult encode(const char* plaintext, size_t plaintext_size, + char* ciphertext, size_t ciphertext_size) noexcept = 0; + /* + * Attempt to decode ciphertext sent by the peer into plaintext. Since + * ciphertext is sent in frames, it's possible that invoking decode() + * may produce a CodecResult with a state of `NeedsMorePeerData` if a + * complete frame is not present in `ciphertext`. In this case, decode() + * must be called again once more data is available. + * + * Precondition: handshake must be completed + * Precondition: plaintext_size >= min_decode_buffer_size() + * Postcondition: if result.state == DecodeResult::State::OK, at least 1 + * complete frame has been written to the `plaintext` buffer + */ + virtual DecodeResult decode(const char* ciphertext, size_t ciphertext_size, + char* plaintext, size_t plaintext_size) noexcept = 0; + + /* + * Creates an implementation defined CryptoCodec that provides at least TLSv1.2 + * compliant handshaking and full duplex data transfer. + * + * Throws CryptoException if resources cannot be allocated for the codec. + */ + static std::unique_ptr<CryptoCodec> create_default_codec(TlsContext& ctx, Mode mode); +}; + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/crypto_exception.cpp b/vespalib/src/vespa/vespalib/net/tls/crypto_exception.cpp new file mode 100644 index 00000000000..41bb2060c04 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/crypto_exception.cpp @@ -0,0 +1,10 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "crypto_exception.h" + +namespace vespalib::net::tls { + +VESPA_IMPLEMENT_EXCEPTION(CryptoException, Exception); + +} + diff --git a/vespalib/src/vespa/vespalib/net/tls/crypto_exception.h b/vespalib/src/vespa/vespalib/net/tls/crypto_exception.h new file mode 100644 index 00000000000..696a158e058 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/crypto_exception.h @@ -0,0 +1,10 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/vespalib/util/exception.h> + +namespace vespalib::net::tls { + +VESPA_DEFINE_EXCEPTION(CryptoException, Exception); + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/CMakeLists.txt b/vespalib/src/vespa/vespalib/net/tls/impl/CMakeLists.txt new file mode 100644 index 00000000000..a5a8e8d3eb9 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/impl/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_library(vespalib_vespalib_net_tls_impl OBJECT + SOURCES + openssl_tls_context_impl.cpp + openssl_crypto_codec_impl.cpp + DEPENDS +) +find_package(OpenSSL) +target_include_directories(vespalib_vespalib_net_tls_impl PUBLIC ${OPENSSL_INCLUDE_DIR}) + diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.cpp b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.cpp new file mode 100644 index 00000000000..13e1be2ce34 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.cpp @@ -0,0 +1,382 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include "openssl_crypto_codec_impl.h" +#include "openssl_tls_context_impl.h" +#include <vespa/vespalib/net/tls/crypto_codec.h> +#include <vespa/vespalib/net/tls/crypto_exception.h> +#include <mutex> +#include <vector> +#include <memory> +#include <stdexcept> +#include <openssl/ssl.h> +#include <openssl/crypto.h> +#include <openssl/err.h> +#include <openssl/pem.h> + +#include <vespa/log/log.h> +LOG_SETUP(".vespalib.net.tls.openssl_crypto_codec_impl"); + +#if (OPENSSL_VERSION_NUMBER < 0x10000000L) +// < 1.0 requires explicit thread ID callback support. +# error "Provided OpenSSL version is too darn old, need at least 1.0" +#endif + +/* + * Beware all ye who dare enter, for this is OpenSSL integration territory. + * Dragons are known to roam the skies. Strange whispers are heard at night + * in the mist-covered lands where the forest meets the lake. Rumors of a + * tome that contains best practices and excellent documentation are heard + * at the local inn, but no one seems to know where it exists, or even if + * it ever existed. Be it best that people carry on with their lives and + * pretend to not know of the beasts that lurk beyond where the torch's + * light fades and turns to all-enveloping darkness. + */ + +namespace vespalib::net::tls::impl { + +namespace { + +const char* ssl_error_to_str(int ssl_error) noexcept { + // From https://www.openssl.org/docs/manmaster/man3/SSL_get_error.html + // Our code paths shouldn't trigger most of these, but included for completeness + switch (ssl_error) { + case SSL_ERROR_NONE: + return "SSL_ERROR_NONE"; + case SSL_ERROR_ZERO_RETURN: + return "SSL_ERROR_ZERO_RETURN"; + case SSL_ERROR_WANT_READ: + return "SSL_ERROR_WANT_READ"; + case SSL_ERROR_WANT_WRITE: + return "SSL_ERROR_WANT_WRITE"; + case SSL_ERROR_WANT_CONNECT: + return "SSL_ERROR_WANT_CONNECT"; + case SSL_ERROR_WANT_ACCEPT: + return "SSL_ERROR_WANT_ACCEPT"; + case SSL_ERROR_WANT_X509_LOOKUP: + return "SSL_ERROR_WANT_X509_LOOKUP"; +#if (OPENSSL_VERSION_NUMBER >= 0x10100000L) + case SSL_ERROR_WANT_ASYNC: + return "SSL_ERROR_WANT_ASYNC"; + case SSL_ERROR_WANT_ASYNC_JOB: + return "SSL_ERROR_WANT_ASYNC_JOB"; +#endif +#if (OPENSSL_VERSION_NUMBER >= 0x10101000L) + case SSL_ERROR_WANT_CLIENT_HELLO_CB: + return "SSL_ERROR_WANT_CLIENT_HELLO_CB"; +#endif + case SSL_ERROR_SYSCALL: + return "SSL_ERROR_SYSCALL"; + case SSL_ERROR_SSL: + return "SSL_ERROR_SSL"; + default: + return "Unknown SSL error code"; + } +} + +HandshakeResult handshake_consumed_bytes_and_needs_more_peer_data(size_t consumed) noexcept { + return {consumed, 0, HandshakeResult::State::NeedsMorePeerData}; +} + +HandshakeResult handshake_produced_bytes_and_needs_more_peer_data(size_t produced) noexcept { + return {0, produced, HandshakeResult::State::NeedsMorePeerData}; +} + +HandshakeResult handshake_consumed_bytes_and_is_complete(size_t consumed) noexcept { + return {consumed, 0, HandshakeResult::State::Done}; +} + +HandshakeResult handshaked_bytes(size_t consumed, size_t produced, HandshakeResult::State state) noexcept { + return {consumed, produced, state}; +} + +HandshakeResult handshake_completed() noexcept { + return {0, 0, HandshakeResult::State::Done}; +} + +HandshakeResult handshake_failed() noexcept { + return {0, 0, HandshakeResult::State::Failed}; +} + +EncodeResult encode_failed() noexcept { + return {0, 0, true}; +} + +EncodeResult encoded_bytes(size_t consumed, size_t produced) noexcept { + return {consumed, produced, false}; +} + +DecodeResult decode_failed() noexcept { + return {0, 0, DecodeResult::State::Failed}; +} + +DecodeResult decoded_frames_with_plaintext_bytes(size_t produced_bytes) noexcept { + return {0, produced_bytes, DecodeResult::State::OK}; +} + +DecodeResult decode_needs_more_peer_data() noexcept { + return {0, 0, DecodeResult::State::NeedsMorePeerData}; +} + +DecodeResult decoded_bytes(size_t consumed, size_t produced, DecodeResult::State state) noexcept { + return {consumed, produced, state}; +} + +BioPtr new_tls_frame_memory_bio() { + BioPtr bio(::BIO_new(BIO_s_mem())); + if (!bio) { + throw CryptoException("IO_new(BIO_s_mem()) failed; out of memory?"); + } + BIO_set_write_buf_size(bio.get(), 0); // 0 ==> default max frame size + return bio; +} + +} // anon ns + +OpenSslCryptoCodecImpl::OpenSslCryptoCodecImpl(::SSL_CTX& ctx, Mode mode) + : _ssl(::SSL_new(&ctx)), + _mode(mode) +{ + if (!_ssl) { + throw CryptoException("Failed to create new SSL from SSL_CTX"); + } + /* + * We use two separate memory BIOs rather than a BIO pair for writing and + * reading ciphertext, respectively. This is because it _seems_ quite + * a bit more straight forward to implement a full duplex API with two + * separate BIOs, but there is little available documentation as to the + * 'hows' and 'whys' around this. + * There are claims from core OpenSSL devs[0] that BIO pairs are more efficient, + * so we may reconsider the current approach (or just use the "OpenSSL controls + * the file descriptor" yolo approach for simplicity, assuming they do optimal + * stuff internally). + * + * Our BIOs are used as follows: + * + * Handshakes may use both BIOs opaquely: + * + * handshake() : SSL_do_handshake() --(_output_bio ciphertext)--> BIO_read --> [peer] + * : SSL_do_handshake() <--(_input_bio ciphertext)-- BIO_write <-- [peer] + * + * Once handshaking is complete, the input BIO is only used for decodes and the output + * BIO is only used for encodes. We explicitly disallow TLS renegotiation, both for + * the sake of simplicity and for added security (renegotiation is a bit of a rat's nest). + * + * encode() : SSL_write(plaintext) --(_output_bio ciphertext)--> BIO_read --> [peer] + * decode() : SSL_read(plaintext) <--(_input_bio ciphertext)-- BIO_write <-- [peer] + * + * To avoid blowing the sizes of BIOs out of the water, we do our best to encode and decode + * on a per-TLS frame granularity (16K) maximum. + */ + BioPtr tmp_input_bio = new_tls_frame_memory_bio(); + BioPtr tmp_output_bio = new_tls_frame_memory_bio(); + // Connect BIOs used internally by OpenSSL. This transfers ownership. No return value to check. + // TODO replace with explicit SSL_set0_rbio/SSL_set0_wbio on OpenSSL >= v1.1 + ::SSL_set_bio(_ssl.get(), tmp_input_bio.get(), tmp_output_bio.get()); + _input_bio = tmp_input_bio.release(); + _output_bio = tmp_output_bio.release(); + if (_mode == Mode::Client) { + ::SSL_set_connect_state(_ssl.get()); + } else { + ::SSL_set_accept_state(_ssl.get()); + } +} + +// TODO remove spammy logging once code is stable + +// Produces bytes previously written to _output_bio by SSL_do_handshake or SSL_write +int OpenSslCryptoCodecImpl::drain_outgoing_network_bytes_if_any( + char *to_peer, size_t to_peer_buf_size) noexcept { + int out_pending = BIO_pending(_output_bio); + if (out_pending > 0) { + int copied = ::BIO_read(_output_bio, to_peer, static_cast<int>(to_peer_buf_size)); + // TODO BIO_should_retry here? Semantics are unclear, especially for memory BIOs. + LOG(spam, "BIO_read copied out %d bytes of ciphertext from _output_bio", copied); + if (copied < 0) { + LOG(error, "Memory BIO_read() failed with BIO_pending() > 0"); + } + return copied; + } + return out_pending; +} + +HandshakeResult OpenSslCryptoCodecImpl::handshake(const char* from_peer, size_t from_peer_buf_size, + char* to_peer, size_t to_peer_buf_size) noexcept { + LOG_ASSERT(from_peer != nullptr && to_peer != nullptr + && from_peer_buf_size < INT32_MAX && to_peer_buf_size < INT32_MAX); + + if (SSL_is_init_finished(_ssl.get())) { + return handshake_completed(); + } + // Still ciphertext data left? If so, get rid of it before we start a new operation + // that wants to fill the output BIO. + int produced = drain_outgoing_network_bytes_if_any(to_peer, to_peer_buf_size); + if (produced > 0) { + // Handshake isn't complete yet and we've got stuff to send. Need to continue handshake + // once more data is available from the peer. + return handshake_produced_bytes_and_needs_more_peer_data(static_cast<size_t>(produced)); + } else if (produced < 0) { + return handshake_failed(); + } + const auto consume_res = do_handshake_and_consume_peer_input_bytes(from_peer, from_peer_buf_size); + LOG_ASSERT(consume_res.bytes_produced == 0); + if (consume_res.failed()) { + return consume_res; + } + // SSL_do_handshake() might have produced more data to send. Note: handshake may + // be complete at this point. + produced = drain_outgoing_network_bytes_if_any(to_peer, to_peer_buf_size); + if (produced < 0) { + return handshake_failed(); + } + return handshaked_bytes(consume_res.bytes_consumed, static_cast<size_t>(produced), consume_res.state); +} + +HandshakeResult OpenSslCryptoCodecImpl::do_handshake_and_consume_peer_input_bytes( + const char *from_peer, size_t from_peer_buf_size) noexcept { + // Feed the SSL session input in frame-sized chunks between each call to SSL_do_handshake(). + // This is primarily to ensure we don't shove unbounded amounts of data into the BIO + // in the case that someone naughty is sending us tons of garbage over the socket. + size_t consumed_total = 0; + while (true) { + // Assumption: SSL_do_handshake will place all required outgoing handshake + // data in the output memory BIO without requiring WANT_WRITE. Freestanding + // memory BIOs are _supposedly_ auto-resizing, so this should work transparently. + // At the very least, if this is not the case we'll auto-fail the connection + // and quickly find out..! + // TODO test multi-frame sized handshake + // TODO should we invoke ::ERR_clear_error() prior? + int ssl_result = ::SSL_do_handshake(_ssl.get()); + ssl_result = ::SSL_get_error(_ssl.get(), ssl_result); + + if (ssl_result == SSL_ERROR_WANT_READ) { + LOG(spam, "SSL_do_handshake() returned SSL_ERROR_WANT_READ"); + if (from_peer_buf_size - consumed_total > 0) { + int consumed = ::BIO_write(_input_bio, from_peer + consumed_total, + static_cast<int>(std::min(MaximumTlsFrameSize, from_peer_buf_size - consumed_total))); + LOG(spam, "BIO_write copied in %d bytes of ciphertext to _input_bio", consumed); + if (consumed < 0) { + LOG(error, "Memory BIO_write() returned %d", consumed); // TODO BIO_need_retry? + return handshake_failed(); + } + consumed_total += consumed; // TODO protect against consumed == 0? + continue; + } else { + return handshake_consumed_bytes_and_needs_more_peer_data(consumed_total); + } + } else if (ssl_result == SSL_ERROR_NONE) { + // At this point SSL_do_handshake has stated it does not need any more peer data, i.e. + // the handshake is complete. + if (!SSL_is_init_finished(_ssl.get())) { + LOG(error, "SSL handshake is not completed even though no more peer data is requested"); + return handshake_failed(); + } + return handshake_consumed_bytes_and_is_complete(consumed_total); + } else { + LOG(error, "SSL_do_handshake() returned unexpected error: %s", ssl_error_to_str(ssl_result)); + return handshake_failed(); + } + }; +} + +EncodeResult OpenSslCryptoCodecImpl::encode(const char* plaintext, size_t plaintext_size, + char* ciphertext, size_t ciphertext_size) noexcept { + LOG_ASSERT(plaintext != nullptr && ciphertext != nullptr + && plaintext_size < INT32_MAX && ciphertext_size < INT32_MAX); + + if (!SSL_is_init_finished(_ssl.get())) { + LOG(error, "OpenSslCryptoCodecImpl::encode() called before handshake completed"); + return encode_failed(); + } + size_t bytes_consumed = 0; + if (plaintext_size != 0) { + int to_consume = static_cast<int>(std::min(plaintext_size, MaximumFramePlaintextSize)); + // SSL_write encodes plaintext to ciphertext and writes to _output_bio + int consumed = ::SSL_write(_ssl.get(), plaintext, to_consume); + LOG(spam, "After SSL_write() -> %d, _input_bio pending=%d, _output_bio pending=%d", + consumed, BIO_pending(_input_bio), BIO_pending(_output_bio)); + if (consumed < 0) { + int ssl_error = ::SSL_get_error(_ssl.get(), consumed); + LOG(error, "SSL_write() failed to write frame, got error %s", ssl_error_to_str(ssl_error)); + // TODO explicitly detect and log TLS renegotiation error (SSL_ERROR_WANT_READ)? + return encode_failed(); + } else if (consumed != to_consume) { + LOG(error, "SSL_write() returned OK but did not consume all requested plaintext"); + return encode_failed(); + } + bytes_consumed = static_cast<size_t>(consumed); + } + + int produced = drain_outgoing_network_bytes_if_any(ciphertext, ciphertext_size); + if (produced < 0) { + return encode_failed(); + } + if (BIO_pending(_output_bio) != 0) { + LOG(error, "Residual data left in output BIO on encode(); provided buffer is too small"); + return encode_failed(); + } + return encoded_bytes(bytes_consumed, static_cast<size_t>(produced)); +} +DecodeResult OpenSslCryptoCodecImpl::decode(const char* ciphertext, size_t ciphertext_size, + char* plaintext, size_t plaintext_size) noexcept { + LOG_ASSERT(ciphertext != nullptr && plaintext != nullptr + && ciphertext_size < INT32_MAX && plaintext_size < INT32_MAX); + + if (!SSL_is_init_finished(_ssl.get())) { + LOG(error, "OpenSslCryptoCodecImpl::decode() called before handshake completed"); + return decode_failed(); + } + auto produce_res = drain_and_produce_plaintext_from_ssl(plaintext, static_cast<int>(plaintext_size)); + if ((produce_res.bytes_produced > 0) || produce_res.failed()) { + return produce_res; // TODO gRPC [1] handles this differently... allows fallthrough + } + int consumed = consume_peer_input_bytes(ciphertext, ciphertext_size); + if (consumed < 0) { + return decode_failed(); + } + produce_res = drain_and_produce_plaintext_from_ssl(plaintext, static_cast<int>(plaintext_size)); + return decoded_bytes(static_cast<size_t>(consumed), produce_res.bytes_produced, produce_res.state); +} + +DecodeResult OpenSslCryptoCodecImpl::drain_and_produce_plaintext_from_ssl( + char* plaintext, size_t plaintext_size) noexcept { + // SSL_read() is named a bit confusingly. We read _from_ the SSL-internal state + // via the input BIO _into_ to the receiving plaintext buffer. + // This may consume the entire, parts of, or none of the input BIO's data, + // depending on how much TLS frame data is available and its size relative + // to the receiving plaintext buffer. + int produced = ::SSL_read(_ssl.get(), plaintext, static_cast<int>(plaintext_size)); + LOG(spam, "After SSL_read() -> %d, _input_bio pending=%d, _output_bio pending=%d", + produced, BIO_pending(_input_bio), BIO_pending(_output_bio)); + if (produced > 0) { + // At least 1 frame decoded successfully. + return decoded_frames_with_plaintext_bytes(static_cast<size_t>(produced)); + } else { + int ssl_error = ::SSL_get_error(_ssl.get(), produced); + switch (ssl_error) { + case SSL_ERROR_WANT_READ: + // SSL_read() was not able to decode a full frame with the ciphertext that + // we've fed it thus far; caller must feed it some and then try again. + LOG(spam, "SSL_read() returned SSL_ERROR_WANT_READ, must get more ciphertext"); + return decode_needs_more_peer_data(); + default: + LOG(error, "SSL_read() returned unexpected error: %s", ssl_error_to_str(ssl_error)); + return decode_failed(); + } + } +} + +int OpenSslCryptoCodecImpl::consume_peer_input_bytes( + const char* ciphertext, size_t ciphertext_size) noexcept { + // TODO BIO_need_retry on failure? Can this even happen for memory BIOs? + int consumed = ::BIO_write(_input_bio, ciphertext, static_cast<int>(std::min(MaximumTlsFrameSize, ciphertext_size))); + LOG(spam, "BIO_write copied in %d bytes of ciphertext to _input_bio", consumed); + if (consumed < 0) { + LOG(error, "Memory BIO_write() returned %d", consumed); + } + return consumed; +} + +} + +// External references: +// [0] http://openssl.6102.n7.nabble.com/nonblocking-implementation-question-tp1728p1732.html +// [1] https://github.com/grpc/grpc/blob/master/src/core/tsi/ssl_transport_security.cc diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h new file mode 100644 index 00000000000..44ca8859596 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h @@ -0,0 +1,76 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "openssl_typedefs.h" +#include <vespa/vespalib/net/tls/transport_security_options.h> +#include <vespa/vespalib/net/tls/crypto_codec.h> +#include <memory> + +namespace vespalib::net::tls { class TlsContext; } + +namespace vespalib::net::tls::impl { + +/* + * Frame-level OpenSSL-backed TLSv1.2 crypto codec implementation. + * + * Currently has sub-optimal buffer management, and is mostly intended + * as a starting point. + * + * NOT thread safe per instance, but independent instances may be + * used by different threads safely. + */ +class OpenSslCryptoCodecImpl : public CryptoCodec { + SslPtr _ssl; + ::BIO* _input_bio; // Owned by _ssl + ::BIO* _output_bio; // Owned by _ssl + Mode _mode; +public: + OpenSslCryptoCodecImpl(::SSL_CTX& ctx, Mode mode); + + /* + * From RFC 8449 (Record Size Limit Extension for TLS), section 1: + * "TLS versions 1.2 [RFC5246] and earlier permit senders to + * generate records 16384 octets in size, plus any expansion + * from compression and protection up to 2048 octets (though + * typically this expansion is only 16 octets). TLS 1.3 reduces + * the allowance for expansion to 256 octets." + * + * We're on TLSv1.2, so make room for the worst case. + */ + static constexpr size_t MaximumTlsFrameSize = 16384 + 2048; + static constexpr size_t MaximumFramePlaintextSize = 16384; + + size_t min_encode_buffer_size() const noexcept override { + return MaximumTlsFrameSize; + } + size_t min_decode_buffer_size() const noexcept override { + return MaximumFramePlaintextSize; + } + + HandshakeResult handshake(const char* from_peer, size_t from_peer_buf_size, + char* to_peer, size_t to_peer_buf_size) noexcept override; + + EncodeResult encode(const char* plaintext, size_t plaintext_size, + char* ciphertext, size_t ciphertext_size) noexcept override; + DecodeResult decode(const char* ciphertext, size_t ciphertext_size, + char* plaintext, size_t plaintext_size) noexcept override; +private: + /* + * Returns + * n > 0 if n bytes written to `to_peer`. Always <= to_peer_buf_size + * n == 0 if no bytes pending in output BIO + * n < 0 on error + */ + int drain_outgoing_network_bytes_if_any(char *to_peer, size_t to_peer_buf_size) noexcept; + /* + * Returns + * n > 0 if n bytes written to `ciphertext`. Always <= ciphertext_size + * n == 0 if no bytes pending in input BIO + * n < 0 on error + */ + int consume_peer_input_bytes(const char* ciphertext, size_t ciphertext_size) noexcept; + HandshakeResult do_handshake_and_consume_peer_input_bytes(const char *from_peer, size_t from_peer_buf_size) noexcept; + DecodeResult drain_and_produce_plaintext_from_ssl(char* plaintext, size_t plaintext_size) noexcept; +}; + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp new file mode 100644 index 00000000000..c868f695b98 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp @@ -0,0 +1,241 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include "openssl_typedefs.h" +#include "openssl_tls_context_impl.h" +#include <vespa/vespalib/net/tls/crypto_exception.h> +#include <vespa/vespalib/net/tls/transport_security_options.h> +#include <mutex> +#include <vector> +#include <memory> +#include <stdexcept> +#include <openssl/ssl.h> +#include <openssl/crypto.h> +#include <openssl/err.h> +#include <openssl/pem.h> + +#include <vespa/log/log.h> +LOG_SETUP(".vespalib.net.tls.openssl_tls_context_impl"); + +#if (OPENSSL_VERSION_NUMBER < 0x10000000L) +// < 1.0 requires explicit thread ID callback support. +# error "Provided OpenSSL version is too darn old, need at least 1.0" +#endif + +namespace vespalib::net::tls::impl { + +namespace { + +#if (OPENSSL_VERSION_NUMBER < 0x10100000L) + +std::vector<std::unique_ptr<std::mutex>> _g_mutexes; + +// Some works on OpenSSL legacy locking: OpenSSL does not implement locking +// itself internally, deferring to user code callbacks that Do The Needful(tm). +// The `n` parameter refers to the nth mutex, which is always < CRYPTO_num_locks(). +void openssl_locking_cb(int mode, int n, [[maybe_unused]] const char *file, [[maybe_unused]] int line) { + if (mode & CRYPTO_LOCK) { + _g_mutexes[n]->lock(); + } else { + _g_mutexes[n]->unlock(); + } +} + +#endif + +struct OpenSslLibraryResources { + OpenSslLibraryResources(); + ~OpenSslLibraryResources(); +}; + +OpenSslLibraryResources::OpenSslLibraryResources() { + // Other implementations (Asio, gRPC) disagree on whether main library init + // itself should take place on >= v1.1. We always do it to be on the safe side..! + ::SSL_library_init(); + ::SSL_load_error_strings(); + ::OpenSSL_add_all_algorithms(); + // Luckily, the mutex callback madness is not present on >= v1.1 +#if (OPENSSL_VERSION_NUMBER < 0x10100000L) + // Since the init path should happen only once globally, but multiple libraries + // may use OpenSSL, make sure we don't step on any toes if locking callbacks are + // already set up. + if (!::CRYPTO_get_locking_callback()) { + const int num_locks = ::CRYPTO_num_locks(); + LOG_ASSERT(num_locks > 0); + _g_mutexes.reserve(num_locks); + for (int i = 0; i < num_locks; ++i) { + _g_mutexes.emplace_back(std::make_unique<std::mutex>()); + } + ::CRYPTO_set_locking_callback(openssl_locking_cb); + } +#endif +} + +OpenSslLibraryResources::~OpenSslLibraryResources() { +#if (OPENSSL_VERSION_NUMBER < 0x10100000L) + if (::CRYPTO_get_locking_callback() == openssl_locking_cb) { + ::CRYPTO_set_locking_callback(nullptr); + } +#endif + ::ERR_free_strings(); + ::EVP_cleanup(); + ::CRYPTO_cleanup_all_ex_data(); +} + +// TODO make global init instead..? +void ensure_openssl_initialized_once() { + static OpenSslLibraryResources openssl_resources; + (void) openssl_resources; +} + +BioPtr bio_from_string(vespalib::stringref str) { + LOG_ASSERT(str.size() <= INT_MAX); + BioPtr bio(::BIO_new_mem_buf(str.data(), static_cast<int>(str.size()))); + if (!bio) { + throw CryptoException("BIO_new_mem_buf"); + } + return bio; +} + +// Several OpenSSL functions take a magical user passphrase argument with +// potentially horrible default behavior for password protected input. +// +// From OpenSSL docs (https://www.openssl.org/docs/man1.1.0/crypto/PEM_read_bio_PrivateKey.html): +// +// "If the cb parameters is set to NULL and the u parameter is not NULL +// then the u parameter is interpreted as a null terminated string to use +// as the passphrase. If both cb and u are NULL then the default callback +// routine is used which will typically prompt for the passphrase on the +// current terminal with echoing turned off." +// +// Neat! +// +// Bonus points for being non-const as well. +constexpr inline void *empty_passphrase() { + return const_cast<void *>(static_cast<const void *>("")); +} + +// Attempt to read a PEM encoded (trusted) certificate from the given BIO. +// BIO might contain further certificates if function returns non-nullptr. +// Returns nullptr if no certificate could be loaded. This is usually an error, +// as this should be the first certificate in the chain. +X509Ptr read_trusted_x509_from_bio(::BIO& bio) { + // "_AUX" means the certificate is trusted. Why they couldn't name this function + // something with "trusted" instead is left as an exercise to the reader. + return X509Ptr(::PEM_read_bio_X509_AUX(&bio, nullptr, nullptr, empty_passphrase())); +} + +// Attempt to read a PEM encoded certificate from the given BIO. +// BIO might contain further certificates if function returns non-nullptr. +// Returns nullptr if no certificate could be loaded. This usually implies +// that there are no more certificates left in the chain. +X509Ptr read_untrusted_x509_from_bio(::BIO& bio) { + return X509Ptr(::PEM_read_bio_X509(&bio, nullptr, nullptr, empty_passphrase())); +} + +::SSL_CTX* new_tls1_2_ctx_with_auto_init() { + ensure_openssl_initialized_once(); + return ::SSL_CTX_new(::TLSv1_2_method()); +} + +} // anon ns + +OpenSslTlsContextImpl::OpenSslTlsContextImpl(const TransportSecurityOptions& ts_opts) + : _ctx(new_tls1_2_ctx_with_auto_init()) +{ + if (!_ctx) { + throw CryptoException("Failed to create new TLSv1.2 context"); + } + add_certificate_authorities(ts_opts.ca_certs_pem()); + add_certificate_chain(ts_opts.cert_chain_pem()); + use_private_key(ts_opts.private_key_pem()); + verify_private_key(); + enable_ephemeral_key_exchange(); + disable_compression(); + // TODO set accepted cipher suites! + // TODO `--> If not set in options, use Modern spec from https://wiki.mozilla.org/Security/Server_Side_TLS + // TODO set peer verification flags! +} + +OpenSslTlsContextImpl::~OpenSslTlsContextImpl() { + ::SSL_CTX_free(_ctx); +} + +void OpenSslTlsContextImpl::add_certificate_authorities(vespalib::stringref ca_pem) { + // TODO support empty CA set...? Ever useful? + auto bio = bio_from_string(ca_pem); + ::X509_STORE* cert_store = ::SSL_CTX_get_cert_store(_ctx); // Internal pointer, not owned by us. + while (true) { + auto ca_cert = read_untrusted_x509_from_bio(*bio); + if (!ca_cert) { + break; + } + if (::X509_STORE_add_cert(cert_store, ca_cert.get()) != 1) { // Does _not_ take ownership + throw CryptoException("X509_STORE_add_cert"); + } + } +} + +void OpenSslTlsContextImpl::add_certificate_chain(vespalib::stringref chain_pem) { + ::ERR_clear_error(); + auto bio = bio_from_string(chain_pem); + // First certificate in the chain is the node's own (trusted) certificate. + auto own_cert = read_trusted_x509_from_bio(*bio); + if (!own_cert) { + throw CryptoException("No X509 certificates could be found in provided chain"); + } + // Ownership of certificate is _not_ transferred, OpenSSL makes internal copy. + // This is not well documented, but is mentioned by other impls. + if (::SSL_CTX_use_certificate(_ctx, own_cert.get()) != 1) { + throw CryptoException("SSL_CTX_use_certificate"); + } + // After the node's own certificate comes any intermediate CA-provided certificates. + while (true) { + auto ca_cert = read_untrusted_x509_from_bio(*bio); + if (!ca_cert) { + // No more certificates in chain, hooray! + ::ERR_clear_error(); + break; + } + // Ownership of certificate _is_ transferred here! + if (!::SSL_CTX_add_extra_chain_cert(_ctx, ca_cert.release())) { + throw CryptoException("SSL_CTX_add_extra_chain_cert"); + } + } +} + +void OpenSslTlsContextImpl::use_private_key(vespalib::stringref key_pem) { + auto bio = bio_from_string(key_pem); + EvpPkeyPtr key(::PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, empty_passphrase())); + if (!key) { + throw CryptoException("Failed to read PEM private key data"); + } + // Ownership _not_ taken. + if (::SSL_CTX_use_PrivateKey(_ctx, key.get()) != 1) { + throw CryptoException("SSL_CTX_use_PrivateKey"); + } +} + +void OpenSslTlsContextImpl::verify_private_key() { + if (::SSL_CTX_check_private_key(_ctx) != 1) { + throw CryptoException("SSL_CTX_check_private_key failed; mismatch between public and private key?"); + } +} + +void OpenSslTlsContextImpl::enable_ephemeral_key_exchange() { + // Always enabled by default on higher versions. +#if (OPENSSL_VERSION_NUMBER < 0x10100000L) + // Auto curve selection is preferred over using SSL_CTX_set_ecdh_tmp + if (!::SSL_CTX_set_ecdh_auto(_ctx, 1)) { + throw CryptoException("SSL_CTX_set_ecdh_auto"); + } +#endif + // New ECDH key per connection. + ::SSL_CTX_set_options(_ctx, SSL_OP_SINGLE_ECDH_USE); +} + +void OpenSslTlsContextImpl::disable_compression() { + // TLS stream compression is vulnerable to a host of chosen plaintext + // attacks (CRIME, BREACH etc), so disable it. + ::SSL_CTX_set_options(_ctx, SSL_OP_NO_COMPRESSION); +} + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h new file mode 100644 index 00000000000..5fa982ee7ad --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h @@ -0,0 +1,28 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "openssl_typedefs.h" +#include <vespa/vespalib/net/tls/tls_context.h> +#include <vespa/vespalib/stllike/string.h> + +namespace vespalib::net::tls::impl { + +class OpenSslTlsContextImpl : public TlsContext { + ::SSL_CTX* _ctx; +public: + explicit OpenSslTlsContextImpl(const TransportSecurityOptions&); + ~OpenSslTlsContextImpl() override; + + ::SSL_CTX* native_context() const noexcept { return _ctx; } +private: + // Note: single use per instance; does _not_ clear existing chain! + void add_certificate_authorities(stringref ca_pem); + void add_certificate_chain(stringref chain_pem); + void use_private_key(stringref key_pem); + void verify_private_key(); + // Enable use of ephemeral key exchange (ECDHE), allowing forward secrecy. + void enable_ephemeral_key_exchange(); + void disable_compression(); +}; + +}
\ No newline at end of file diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_typedefs.h b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_typedefs.h new file mode 100644 index 00000000000..882ffde5897 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_typedefs.h @@ -0,0 +1,39 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <memory> +#include <openssl/ssl.h> +#include <openssl/crypto.h> +#include <openssl/x509.h> + +namespace vespalib::net::tls::impl { + +struct BioDeleter { + void operator()(::BIO* bio) const noexcept { + ::BIO_free(bio); + } +}; +using BioPtr = std::unique_ptr<::BIO, BioDeleter>; + +struct SslDeleter { + void operator()(::SSL* ssl) const noexcept { + ::SSL_free(ssl); + } +}; +using SslPtr = std::unique_ptr<::SSL, SslDeleter>; + +struct X509Deleter { + void operator()(::X509* cert) const noexcept { + ::X509_free(cert); + } +}; +using X509Ptr = std::unique_ptr<::X509, X509Deleter>; + +struct EvpPkeyDeleter { + void operator()(::EVP_PKEY* pkey) const noexcept { + ::EVP_PKEY_free(pkey); + } +}; +using EvpPkeyPtr = std::unique_ptr<::EVP_PKEY, EvpPkeyDeleter>; + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/tls_context.cpp b/vespalib/src/vespa/vespalib/net/tls/tls_context.cpp new file mode 100644 index 00000000000..467838975e7 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/tls_context.cpp @@ -0,0 +1,11 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include "tls_context.h" +#include <vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h> + +namespace vespalib::net::tls { + +std::unique_ptr<TlsContext> TlsContext::create_default_context(const TransportSecurityOptions& opts) { + return std::make_unique<impl::OpenSslTlsContextImpl>(opts); +} + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/tls_context.h b/vespalib/src/vespa/vespalib/net/tls/tls_context.h new file mode 100644 index 00000000000..7292f43f88c --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/tls_context.h @@ -0,0 +1,16 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <memory> + +namespace vespalib::net::tls { + +class TransportSecurityOptions; + +struct TlsContext { + virtual ~TlsContext() = default; + + static std::unique_ptr<TlsContext> create_default_context(const TransportSecurityOptions&); +}; + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp new file mode 100644 index 00000000000..4e39fe4d7fa --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp @@ -0,0 +1,12 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "transport_security_options.h" +#include <openssl/crypto.h> + +namespace vespalib::net::tls { + +TransportSecurityOptions::~TransportSecurityOptions() { + OPENSSL_cleanse(&_private_key_pem[0], _private_key_pem.size()); +} + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h new file mode 100644 index 00000000000..0a228388791 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h @@ -0,0 +1,30 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/vespalib/stllike/string.h> + +namespace vespalib::net::tls { + +class TransportSecurityOptions { + vespalib::string _ca_certs_pem; + vespalib::string _cert_chain_pem; + vespalib::string _private_key_pem; +public: + TransportSecurityOptions() = default; + + TransportSecurityOptions(vespalib::string ca_certs_pem, + vespalib::string cert_chain_pem, + vespalib::string private_key_pem) + : _ca_certs_pem(std::move(ca_certs_pem)), + _cert_chain_pem(std::move(cert_chain_pem)), + _private_key_pem(std::move(private_key_pem)) + {} + ~TransportSecurityOptions(); + + const vespalib::string& ca_certs_pem() const noexcept { return _ca_certs_pem; } + const vespalib::string& cert_chain_pem() const noexcept { return _cert_chain_pem; } + const vespalib::string& private_key_pem() const noexcept { return _private_key_pem; } +}; + +} |