aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--vespalib/CMakeLists.txt3
-rw-r--r--vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt9
-rw-r--r--vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp199
-rw-r--r--vespalib/src/vespa/vespalib/CMakeLists.txt4
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt12
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/crypto_codec.cpp15
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/crypto_codec.h124
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/crypto_exception.cpp10
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/crypto_exception.h10
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/CMakeLists.txt10
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.cpp382
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h76
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp241
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h28
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_typedefs.h39
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/tls_context.cpp11
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/tls_context.h16
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp12
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/transport_security_options.h30
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; }
+};
+
+}