diff options
author | Tor Brede Vekterli <vekterli@oath.com> | 2018-11-13 12:48:56 +0000 |
---|---|---|
committer | Tor Brede Vekterli <vekterli@oath.com> | 2019-01-24 12:29:20 +0000 |
commit | 04f493deab394c70d57472f7971a10e4a6a4e85b (patch) | |
tree | c9a1d6f37c15c865f45438c8e10834e20466c6bb /vespalib | |
parent | 2a9c88419abf98fb0cb67f13aa5da1c44cf5d99a (diff) |
Add support for default cipher suite and `accepted-ciphers` config in C++
Since the TLS config file uses IANA cipher names but OpenSSL uses
its own cipher spec format internally, we explicitly remap the
provided names. We only support a modern subset of ciphers.
The default cipher suite contains ciphers that work across both
TLSv1.2 and TLSv1.3.
Diffstat (limited to 'vespalib')
9 files changed, 168 insertions, 6 deletions
diff --git a/vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp b/vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp index c1e2c0b5f49..a54e2f29aa1 100644 --- a/vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp +++ b/vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp @@ -137,6 +137,24 @@ TEST("empty required-credentials array throws exception") { "\"required-credentials\" array can't be empty (would allow all peers)"); } +TEST("accepted cipher list is empty if not specified") { + const char* json = R"({"files":{"private-key":"dummy_privkey.txt", + "certificates":"dummy_certs.txt", + "ca-certificates":"dummy_ca_certs.txt"}})"; + EXPECT_TRUE(read_options_from_json_string(json)->accepted_ciphers().empty()); +} + +TEST("accepted cipher list is populated if specified") { + const char* json = R"({"files":{"private-key":"dummy_privkey.txt", + "certificates":"dummy_certs.txt", + "ca-certificates":"dummy_ca_certs.txt"}, + "accepted-ciphers":["foo", "bar"]})"; + auto ciphers = read_options_from_json_string(json)->accepted_ciphers(); + ASSERT_EQUAL(2u, ciphers.size()); + EXPECT_EQUAL("foo", ciphers[0]); + EXPECT_EQUAL("bar", ciphers[1]); +} + // TODO test parsing of multiple policies TEST_MAIN() { TEST_RUN_ALL(); } diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/CMakeLists.txt b/vespalib/src/vespa/vespalib/net/tls/impl/CMakeLists.txt index 6129fde3c6c..d69da590311 100644 --- a/vespalib/src/vespa/vespalib/net/tls/impl/CMakeLists.txt +++ b/vespalib/src/vespa/vespalib/net/tls/impl/CMakeLists.txt @@ -2,6 +2,7 @@ vespa_add_library(vespalib_vespalib_net_tls_impl OBJECT SOURCES direct_buffer_bio.cpp + iana_cipher_map.cpp openssl_tls_context_impl.cpp openssl_crypto_codec_impl.cpp DEPENDS diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/iana_cipher_map.cpp b/vespalib/src/vespa/vespalib/net/tls/impl/iana_cipher_map.cpp new file mode 100644 index 00000000000..5e60714efea --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/impl/iana_cipher_map.cpp @@ -0,0 +1,53 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "iana_cipher_map.h" +#include <vespa/vespalib/stllike/hash_fun.h> +#include <utility> +#include <unordered_map> + +namespace vespalib::net::tls { + +using vespalib::stringref; +using CipherMapType = std::unordered_map<stringref, stringref, vespalib::hash<stringref>>; + +namespace { + +const CipherMapType& modern_cipher_suites_iana_to_openssl() { + // Handpicked subset of supported ciphers from https://www.openssl.org/docs/manmaster/man1/ciphers.html + // based on Modern spec from https://wiki.mozilla.org/Security/Server_Side_TLS + // For TLSv1.2 we only allow RSA and ECDSA with ephemeral key exchange and GCM. + // For TLSv1.3 we allow the DEFAULT group ciphers. + // Note that we _only_ allow AEAD ciphers for either TLS version. + static CipherMapType ciphers({ + {"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "ECDHE-RSA-AES128-GCM-SHA256"}, + {"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "ECDHE-RSA-AES256-GCM-SHA384"}, + {"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "ECDHE-ECDSA-AES128-GCM-SHA256"}, + {"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "ECDHE-ECDSA-AES256-GCM-SHA384"}, + {"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", "ECDHE-RSA-CHACHA20-POLY1305"}, + {"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "ECDHE-ECDSA-CHACHA20-POLY1305"}, + {"TLS_AES_128_GCM_SHA256", "TLS13-AES-128-GCM-SHA256"}, + {"TLS_AES_256_GCM_SHA384", "TLS13-AES-256-GCM-SHA384"}, + {"TLS_CHACHA20_POLY1305_SHA256", "TLS13-CHACHA20-POLY1305-SHA256"} + }); + return ciphers; +} + +} // anon ns + +const char* iana_cipher_to_openssl_cipher(vespalib::stringref iana_name) { + const auto& ciphers = modern_cipher_suites_iana_to_openssl(); + auto iter = ciphers.find(iana_name); + return ((iter != ciphers.end()) ? iter->second.data() : nullptr); +} + +std::vector<vespalib::string> modern_iana_cipher_suite() { + const auto& ciphers = modern_cipher_suites_iana_to_openssl(); + std::vector<vespalib::string> iana_cipher_names; + iana_cipher_names.reserve(ciphers.size()); + for (auto& cipher : ciphers) { + iana_cipher_names.emplace_back(cipher.first); + } + return iana_cipher_names; +} + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/iana_cipher_map.h b/vespalib/src/vespa/vespalib/net/tls/impl/iana_cipher_map.h new file mode 100644 index 00000000000..b41977f4e29 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/impl/iana_cipher_map.h @@ -0,0 +1,25 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/vespalib/stllike/string.h> + +#include <vector> + +namespace vespalib::net::tls { + +/** + * Returns the OpenSSL cipher name for a given IANA cipher name, or nullptr if + * there is no known mapping. + * + * Note that this only covers a very restricted subset of the existing IANA ciphers. + */ +const char* iana_cipher_to_openssl_cipher(vespalib::stringref iana_name); + +/** + * Returns a vector of all IANA cipher names that we support internally. + * It is guaranteed that any cipher name returned from this function will + * have a non-nullptr return value from iana_cipher_to_openssl_cipher(name). + */ +std::vector<vespalib::string> modern_iana_cipher_suite(); + +} 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 index 130847b49a7..b835584a2b3 100644 --- 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 @@ -1,4 +1,5 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include "iana_cipher_map.h" #include "openssl_typedefs.h" #include "openssl_tls_context_impl.h" #include <vespa/vespalib/net/tls/crypto_exception.h> @@ -210,8 +211,13 @@ OpenSslTlsContextImpl::OpenSslTlsContextImpl( disable_renegotiation(); enforce_peer_certificate_verification(); set_ssl_ctx_self_reference(); - // TODO set accepted cipher suites! - // TODO `--> If not set in options, use Modern spec from https://wiki.mozilla.org/Security/Server_Side_TLS + if (!ts_opts.accepted_ciphers().empty()) { + // Due to how we resolve provided ciphers, this implicitly provides an + // _intersection_ between our default cipher suite and the configured one. + set_accepted_cipher_suite(ts_opts.accepted_ciphers()); + } else { + set_accepted_cipher_suite(modern_iana_cipher_suite()); + } } OpenSslTlsContextImpl::~OpenSslTlsContextImpl() { @@ -469,4 +475,31 @@ void OpenSslTlsContextImpl::set_ssl_ctx_self_reference() { SSL_CTX_set_app_data(_ctx.get(), this); } +void OpenSslTlsContextImpl::set_accepted_cipher_suite(const std::vector<vespalib::string>& ciphers) { + vespalib::string openssl_ciphers; + size_t bad_ciphers = 0; + for (auto& iana_cipher : ciphers) { + auto our_cipher = iana_cipher_to_openssl_cipher(iana_cipher); + if (our_cipher) { + if (!openssl_ciphers.empty()) { + openssl_ciphers += ':'; + } + openssl_ciphers += our_cipher; + } else { + LOG(warning, "Unsupported cipher: '%s' (bad name or unknown IANA -> OpenSSL mapping)", iana_cipher.c_str()); + ++bad_ciphers; + } + } + if (bad_ciphers > 0) { + LOG(warning, "A total of %zu configured cipher names were not added to the set of allowed TLS ciphers. " + "Vespa only supports TLS ciphers with forward secrecy and AEAD properties", bad_ciphers); + } + if (openssl_ciphers.empty()) { + throw CryptoException("Configured cipher suite does not contain any supported ciphers"); + } + if (::SSL_CTX_set_cipher_list(_ctx.get(), openssl_ciphers.c_str()) != 1) { + throw CryptoException("SSL_CTX_set_cipher_list failed; no provided ciphers could be used"); + } +} + } 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 index 6b72dfe84ee..93976f37523 100644 --- 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 @@ -43,6 +43,7 @@ private: void disable_renegotiation(); void enforce_peer_certificate_verification(); void set_ssl_ctx_self_reference(); + void set_accepted_cipher_suite(const std::vector<vespalib::string>& ciphers); bool verify_trusted_certificate(::X509_STORE_CTX* store_ctx); diff --git a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp index e79fee6cabf..d4fa2ede559 100644 --- a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp +++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp @@ -10,7 +10,8 @@ TransportSecurityOptions::TransportSecurityOptions(Builder builder) : _ca_certs_pem(std::move(builder._ca_certs_pem)), _cert_chain_pem(std::move(builder._cert_chain_pem)), _private_key_pem(std::move(builder._private_key_pem)), - _authorized_peers(std::move(builder._authorized_peers)) + _authorized_peers(std::move(builder._authorized_peers)), + _accepted_ciphers(std::move(builder._accepted_ciphers)) { } @@ -35,6 +36,9 @@ TransportSecurityOptions::TransportSecurityOptions(vespalib::string ca_certs_pem { } +TransportSecurityOptions::Builder::Builder() = default; +TransportSecurityOptions::Builder::~Builder() = default; + 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 index 0502276f7a6..647e290d985 100644 --- a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h +++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h @@ -13,6 +13,7 @@ class TransportSecurityOptions { vespalib::string _cert_chain_pem; vespalib::string _private_key_pem; AuthorizedPeers _authorized_peers; + std::vector<vespalib::string> _accepted_ciphers; public: TransportSecurityOptions() = default; @@ -21,11 +22,19 @@ public: vespalib::string _cert_chain_pem; vespalib::string _private_key_pem; AuthorizedPeers _authorized_peers; + std::vector<vespalib::string> _accepted_ciphers; + + Builder(); + ~Builder(); Builder& ca_certs_pem(vespalib::stringref pem) { _ca_certs_pem = pem; return *this; } Builder& cert_chain_pem(vespalib::stringref pem) { _cert_chain_pem = pem; return *this; } Builder& private_key_pem(vespalib::stringref pem) { _private_key_pem = pem; return *this; } Builder& authorized_peers(AuthorizedPeers auth) { _authorized_peers = std::move(auth); return *this; } + Builder& accepted_ciphers(std::vector<vespalib::string> ciphers) { + _accepted_ciphers = std::move(ciphers); + return *this; + } }; explicit TransportSecurityOptions(Builder builder); @@ -49,6 +58,7 @@ public: TransportSecurityOptions copy_without_private_key() const { return TransportSecurityOptions(_ca_certs_pem, _cert_chain_pem, "", _authorized_peers); } + const std::vector<vespalib::string>& accepted_ciphers() const noexcept { return _accepted_ciphers; } }; } // vespalib::net::tls diff --git a/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp b/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp index 1b6398153ca..0c29932699e 100644 --- a/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp +++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp @@ -94,6 +94,17 @@ AuthorizedPeers parse_authorized_peers(const Inspector& authorized_peers) { return AuthorizedPeers(std::move(policies)); } +std::vector<vespalib::string> parse_accepted_ciphers(const Cursor& accepted_ciphers) { + if (!accepted_ciphers.valid()) { + return {}; + } + std::vector<vespalib::string> ciphers; + for (size_t i = 0; i < accepted_ciphers.children(); ++i) { + ciphers.emplace_back(accepted_ciphers[i].asString().make_string()); + } + return ciphers; +} + std::unique_ptr<TransportSecurityOptions> load_from_input(Input& input) { Slime root; auto parsed = slime::JsonFormat::decode(input, root); @@ -111,9 +122,15 @@ std::unique_ptr<TransportSecurityOptions> load_from_input(Input& input) { auto certs = load_file_referenced_by_field(files, "certificates"); auto priv_key = load_file_referenced_by_field(files, "private-key"); auto authorized_peers = parse_authorized_peers(root["authorized-peers"]); - - return std::make_unique<TransportSecurityOptions>(std::move(ca_certs), std::move(certs), - std::move(priv_key), std::move(authorized_peers)); + auto accepted_ciphers = parse_accepted_ciphers(root["accepted-ciphers"]); + + return std::make_unique<TransportSecurityOptions>( + TransportSecurityOptions::Builder() + .ca_certs_pem(ca_certs) + .cert_chain_pem(certs) + .private_key_pem(priv_key) + .authorized_peers(std::move(authorized_peers)) + .accepted_ciphers(std::move(accepted_ciphers))); } } // anon ns |