summaryrefslogtreecommitdiffstats
path: root/vespalib
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@oath.com>2018-09-05 11:10:30 +0200
committerTor Brede Vekterli <vekterli@oath.com>2018-09-05 12:07:40 +0000
commit690060d5c6be51d6a1ca43fd1c30d241722c7b4c (patch)
treea97d5b26ac01da46279c786ac2056034dade7e5b /vespalib
parent28d30783fd56eff725e457de13f2d4ef350b6f44 (diff)
The current implementation is known to be sub-optimal due to requiring
memory copies in and out of OpenSSL's working BIOs for every encode and decode. Codec design is also up for change, depending on how well it fits with crypto socket integration.
Diffstat (limited to 'vespalib')
-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.txt11
-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.txt9
-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, 1229 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..6f0e88d3e39
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt
@@ -0,0 +1,11 @@
+# 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
+)
+vespa_add_target_package_dependency(vespalib_vespalib_net_tls OpenSSL)
+
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..8b4398679db
--- /dev/null
+++ b/vespalib/src/vespa/vespalib/net/tls/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_library(vespalib_vespalib_net_tls_impl OBJECT
+ SOURCES
+ openssl_tls_context_impl.cpp
+ openssl_crypto_codec_impl.cpp
+ DEPENDS
+)
+vespa_add_target_package_dependency(vespalib_vespalib_net_tls_impl OpenSSL)
+
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; }
+};
+
+}