From 8444c245b06837d0b5dd3a1fa2d7b724d283a282 Mon Sep 17 00:00:00 2001 From: Tor Brede Vekterli Date: Wed, 31 Oct 2018 16:04:24 +0000 Subject: Add support for basic certificate verification policies in C++ Extends TLS config JSON file with an `allowed-peers` object, which if non-empty specifies a set of policies that a peer may match. If at least one policy exists a peer must match all requirements in any single policy to be allowed to connect. I.e. it's sufficient to match 1 policy out of many. --- vespalib/CMakeLists.txt | 1 + .../net/tls/openssl_impl/openssl_impl_test.cpp | 76 ++++++++- .../CMakeLists.txt | 10 ++ .../policy_checking_certificate_verifier_test.cpp | 187 +++++++++++++++++++++ .../transport_options_reading_test.cpp | 70 ++++++++ vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt | 2 + .../net/tls/impl/openssl_tls_context_impl.cpp | 13 +- .../net/tls/impl/openssl_tls_context_impl.h | 9 +- .../vespa/vespalib/net/tls/peer_credentials.cpp | 14 ++ .../src/vespa/vespalib/net/tls/peer_credentials.h | 3 + .../src/vespa/vespalib/net/tls/peer_policies.cpp | 113 +++++++++++++ .../src/vespa/vespalib/net/tls/peer_policies.h | 97 +++++++++++ .../tls/policy_checking_certificate_verifier.cpp | 74 ++++++++ .../net/tls/policy_checking_certificate_verifier.h | 11 ++ .../src/vespa/vespalib/net/tls/tls_context.cpp | 11 +- vespalib/src/vespa/vespalib/net/tls/tls_context.h | 10 ++ .../net/tls/transport_security_options.cpp | 23 ++- .../vespalib/net/tls/transport_security_options.h | 29 +++- .../net/tls/transport_security_options_reading.cpp | 72 ++++++-- vespalib/src/vespa/vespalib/test/CMakeLists.txt | 1 + .../src/vespa/vespalib/test/peer_policy_utils.cpp | 23 +++ .../src/vespa/vespalib/test/peer_policy_utils.h | 13 ++ 22 files changed, 826 insertions(+), 36 deletions(-) create mode 100644 vespalib/src/tests/net/tls/policy_checking_certificate_verifier/CMakeLists.txt create mode 100644 vespalib/src/tests/net/tls/policy_checking_certificate_verifier/policy_checking_certificate_verifier_test.cpp create mode 100644 vespalib/src/vespa/vespalib/net/tls/peer_policies.cpp create mode 100644 vespalib/src/vespa/vespalib/net/tls/peer_policies.h create mode 100644 vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.cpp create mode 100644 vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.h create mode 100644 vespalib/src/vespa/vespalib/test/peer_policy_utils.cpp create mode 100644 vespalib/src/vespa/vespalib/test/peer_policy_utils.h (limited to 'vespalib') diff --git a/vespalib/CMakeLists.txt b/vespalib/CMakeLists.txt index 08df6bb2c15..6491bdfb036 100644 --- a/vespalib/CMakeLists.txt +++ b/vespalib/CMakeLists.txt @@ -62,6 +62,7 @@ vespa_define_module( src/tests/net/sync_crypto_socket src/tests/net/tls/direct_buffer_bio src/tests/net/tls/openssl_impl + src/tests/net/tls/policy_checking_certificate_verifier src/tests/net/tls/protocol_snooping src/tests/net/tls/transport_options src/tests/objects/nbostream 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 index 844d9591a45..1ae4d622b4f 100644 --- a/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp +++ b/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -82,6 +83,14 @@ struct Fixture { return create_openssl_codec(ctx, mode); } + static std::unique_ptr create_openssl_codec( + const TransportSecurityOptions& opts, + std::shared_ptr cert_verify_callback, + CryptoCodec::Mode mode) { + auto ctx = TlsContext::create_default_context(opts, std::move(cert_verify_callback)); + return create_openssl_codec(ctx, mode); + } + static std::unique_ptr create_openssl_codec( const std::shared_ptr& ctx, CryptoCodec::Mode mode) { auto ctx_impl = std::dynamic_pointer_cast(ctx); @@ -409,17 +418,27 @@ struct CertFixture : Fixture { return {std::move(cert), std::move(key)}; } - void reset_client_with_cert_opts(const CertKeyWrapper& ck, std::shared_ptr cert_cb) { + void reset_client_with_cert_opts(const CertKeyWrapper& ck, AllowedPeers allowed) { TransportSecurityOptions client_opts(root_ca.cert->to_pem(), ck.cert->to_pem(), - ck.key->private_to_pem(), std::move(cert_cb)); + ck.key->private_to_pem(), std::move(allowed)); client = create_openssl_codec(client_opts, CryptoCodec::Mode::Client); } - void reset_server_with_cert_opts(const CertKeyWrapper& ck, std::shared_ptr cert_cb) { + void reset_client_with_cert_opts(const CertKeyWrapper& ck, std::shared_ptr cert_cb) { + TransportSecurityOptions client_opts(root_ca.cert->to_pem(), ck.cert->to_pem(), ck.key->private_to_pem()); + client = create_openssl_codec(client_opts, std::move(cert_cb), CryptoCodec::Mode::Client); + } + + void reset_server_with_cert_opts(const CertKeyWrapper& ck, AllowedPeers allowed) { TransportSecurityOptions server_opts(root_ca.cert->to_pem(), ck.cert->to_pem(), - ck.key->private_to_pem(), std::move(cert_cb)); + ck.key->private_to_pem(), std::move(allowed)); server = create_openssl_codec(server_opts, CryptoCodec::Mode::Server); } + + void reset_server_with_cert_opts(const CertKeyWrapper& ck, std::shared_ptr cert_cb) { + TransportSecurityOptions server_opts(root_ca.cert->to_pem(), ck.cert->to_pem(), ck.key->private_to_pem()); + server = create_openssl_codec(server_opts, std::move(cert_cb), CryptoCodec::Mode::Server); + } }; CertFixture::~CertFixture() = default; @@ -517,6 +536,55 @@ TEST_F("Only DNS SANs are enumerated", CertFixture) { EXPECT_EQUAL(0u, server_cb->creds.dns_sans.size()); } +// We don't test too many combinations of peer policies here, only that +// the wiring is set up. Verification logic is tested elsewhere. + +TEST_F("Client rejects server with certificate that DOES NOT match peer policy", CertFixture) { + auto client_ck = f.create_ca_issued_peer_cert({"hello.world.example.com"}, {}); + auto allowed = allowed_peers({policy_with({required_san_dns("crash.wile.example.com")})}); + f.reset_client_with_cert_opts(client_ck, std::move(allowed)); + // crash.wile.example.com not present in certificate + auto server_ck = f.create_ca_issued_peer_cert( + {}, {{"DNS:birdseed.wile.example.com"}, {"DNS:roadrunner.wile.example.com"}}); + f.reset_server_with_cert_opts(server_ck, AllowedPeers::allow_all_authenticated()); + + EXPECT_FALSE(f.handshake()); +} + +TEST_F("Client allows server with certificate that DOES match peer policy", CertFixture) { + auto client_ck = f.create_ca_issued_peer_cert({"hello.world.example.com"}, {}); + auto allowed = allowed_peers({policy_with({required_san_dns("crash.wile.example.com")})}); + f.reset_client_with_cert_opts(client_ck, std::move(allowed)); + auto server_ck = f.create_ca_issued_peer_cert( + {}, {{"DNS:birdseed.wile.example.com"}, {"DNS:crash.wile.example.com"}}); + f.reset_server_with_cert_opts(server_ck, AllowedPeers::allow_all_authenticated()); + + EXPECT_TRUE(f.handshake()); +} + +TEST_F("Server rejects client with certificate that DOES NOT match peer policy", CertFixture) { + auto server_ck = f.create_ca_issued_peer_cert({"hello.world.example.com"}, {}); + auto allowed = allowed_peers({policy_with({required_san_dns("crash.wile.example.com")})}); + f.reset_server_with_cert_opts(server_ck, std::move(allowed)); + // crash.wile.example.com not present in certificate + auto client_ck = f.create_ca_issued_peer_cert( + {}, {{"DNS:birdseed.wile.example.com"}, {"DNS:roadrunner.wile.example.com"}}); + f.reset_client_with_cert_opts(client_ck, AllowedPeers::allow_all_authenticated()); + + EXPECT_FALSE(f.handshake()); +} + +TEST_F("Server allows client with certificate that DOES match peer policy", CertFixture) { + auto server_ck = f.create_ca_issued_peer_cert({"hello.world.example.com"}, {}); + auto allowed = allowed_peers({policy_with({required_san_dns("crash.wile.example.com")})}); + f.reset_server_with_cert_opts(server_ck, std::move(allowed)); + auto client_ck = f.create_ca_issued_peer_cert( + {}, {{"DNS:birdseed.wile.example.com"}, {"DNS:crash.wile.example.com"}}); + f.reset_client_with_cert_opts(client_ck, AllowedPeers::allow_all_authenticated()); + + EXPECT_TRUE(f.handshake()); +} + // TODO we can't test embedded nulls since the OpenSSL v3 extension APIs // take in null terminated strings as arguments... :I diff --git a/vespalib/src/tests/net/tls/policy_checking_certificate_verifier/CMakeLists.txt b/vespalib/src/tests/net/tls/policy_checking_certificate_verifier/CMakeLists.txt new file mode 100644 index 00000000000..5e11a93ac09 --- /dev/null +++ b/vespalib/src/tests/net/tls/policy_checking_certificate_verifier/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_executable(vespalib_net_tls_policy_checking_certificate_verifier_test_app TEST + SOURCES + policy_checking_certificate_verifier_test.cpp + DEPENDS + vespalib +) +vespa_add_test(NAME vespalib_net_tls_policy_checking_certificate_verifier_test_app + COMMAND vespalib_net_tls_policy_checking_certificate_verifier_test_app) + diff --git a/vespalib/src/tests/net/tls/policy_checking_certificate_verifier/policy_checking_certificate_verifier_test.cpp b/vespalib/src/tests/net/tls/policy_checking_certificate_verifier/policy_checking_certificate_verifier_test.cpp new file mode 100644 index 00000000000..6eb19fec3cd --- /dev/null +++ b/vespalib/src/tests/net/tls/policy_checking_certificate_verifier/policy_checking_certificate_verifier_test.cpp @@ -0,0 +1,187 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include +#include +#include +#include +#include +#include +#include + +using namespace vespalib; +using namespace vespalib::net::tls; + +bool glob_matches(vespalib::stringref pattern, vespalib::stringref string_to_check) { + auto glob = HostGlobPattern::create_from_glob(pattern); + return glob->matches(string_to_check); +} + +TEST("glob without wildcards matches entire string") { + EXPECT_TRUE(glob_matches("foo", "foo")); + EXPECT_FALSE(glob_matches("foo", "fooo")); + EXPECT_FALSE(glob_matches("foo", "ffoo")); +} + +TEST("wildcard glob can match prefix") { + EXPECT_TRUE(glob_matches("foo*", "foo")); + EXPECT_TRUE(glob_matches("foo*", "foobar")); + EXPECT_FALSE(glob_matches("foo*", "ffoo")); +} + +TEST("wildcard glob can match suffix") { + EXPECT_TRUE(glob_matches("*foo", "foo")); + EXPECT_TRUE(glob_matches("*foo", "ffoo")); + EXPECT_FALSE(glob_matches("*foo", "fooo")); +} + +TEST("wildcard glob can match substring") { + EXPECT_TRUE(glob_matches("f*o", "fo")); + EXPECT_TRUE(glob_matches("f*o", "foo")); + EXPECT_TRUE(glob_matches("f*o", "ffoo")); + EXPECT_FALSE(glob_matches("f*o", "boo")); +} + +TEST("wildcard glob does not cross multiple dot delimiter boundaries") { + EXPECT_TRUE(glob_matches("*.bar.baz", "foo.bar.baz")); + EXPECT_TRUE(glob_matches("*.bar.baz", ".bar.baz")); + EXPECT_FALSE(glob_matches("*.bar.baz", "zoid.foo.bar.baz")); + EXPECT_TRUE(glob_matches("foo.*.baz", "foo.bar.baz")); + EXPECT_FALSE(glob_matches("foo.*.baz", "foo.bar.zoid.baz")); +} + +TEST("single char glob matches non dot characters") { + EXPECT_TRUE(glob_matches("f?o", "foo")); + EXPECT_FALSE(glob_matches("f?o", "fooo")); + EXPECT_FALSE(glob_matches("f?o", "ffoo")); + EXPECT_FALSE(glob_matches("f?o", "f.o")); +} + +TEST("special basic regex characters are escaped") { + EXPECT_TRUE(glob_matches("$[.\\^", "$[.\\^")); +} + +TEST("special extended regex characters are ignored") { + EXPECT_TRUE(glob_matches("{)(+|]}", "{)(+|]}")); +} + +// TODO CN + SANs +PeerCredentials creds_with_dns_sans(std::vector dns_sans) { + PeerCredentials creds; + creds.dns_sans = std::move(dns_sans); + return creds; +} + +PeerCredentials creds_with_cn(vespalib::stringref cn) { + PeerCredentials creds; + creds.common_name = cn; + return creds; +} + +bool verify(AllowedPeers allowed_peers, const PeerCredentials& peer_creds) { + auto verifier = create_verify_callback_from(std::move(allowed_peers)); + return verifier->verify(peer_creds); +} + +TEST("Default-constructed AllowedPeers does not allow all authenticated peers") { + EXPECT_FALSE(AllowedPeers().allows_all_authenticated()); +} + +TEST("Specially constructed set of policies allows all authenticated peers") { + auto allow_all = AllowedPeers::allow_all_authenticated(); + EXPECT_TRUE(allow_all.allows_all_authenticated()); + EXPECT_TRUE(verify(allow_all, creds_with_dns_sans({{"anything.goes"}}))); +} + +TEST("Non-empty policies do not allow all unauthenticated peers") { + auto allow_not_all = allowed_peers({policy_with({required_san_dns("hello.world")})}); + EXPECT_FALSE(allow_not_all.allows_all_authenticated()); +} + +TEST("SAN requirement without glob pattern is matched as exact string") { + auto allowed = allowed_peers({policy_with({required_san_dns("hello.world")})}); + EXPECT_TRUE(verify(allowed, creds_with_dns_sans({{"hello.world"}}))); + EXPECT_FALSE(verify(allowed, creds_with_dns_sans({{"foo.bar"}}))); + EXPECT_FALSE(verify(allowed, creds_with_dns_sans({{"hello.worlds"}}))); + EXPECT_FALSE(verify(allowed, creds_with_dns_sans({{"hhello.world"}}))); + EXPECT_FALSE(verify(allowed, creds_with_dns_sans({{"foo.hello.world"}}))); + EXPECT_FALSE(verify(allowed, creds_with_dns_sans({{"hello.world.bar"}}))); +} + +TEST("SAN requirement can include glob wildcards") { + auto allowed = allowed_peers({policy_with({required_san_dns("*.w?rld")})}); + EXPECT_TRUE(verify(allowed, creds_with_dns_sans({{"hello.world"}}))); + EXPECT_TRUE(verify(allowed, creds_with_dns_sans({{"greetings.w0rld"}}))); + EXPECT_FALSE(verify(allowed, creds_with_dns_sans({{"hello.wrld"}}))); + EXPECT_FALSE(verify(allowed, creds_with_dns_sans({{"world"}}))); +} + +TEST("multi-SAN policy requires all SANs to be present in certificate") { + auto allowed = allowed_peers({policy_with({required_san_dns("hello.world"), + required_san_dns("foo.bar")})}); + EXPECT_TRUE(verify(allowed, creds_with_dns_sans({{"hello.world"}, {"foo.bar"}}))); + // Need both + EXPECT_FALSE(verify(allowed, creds_with_dns_sans({{"hello.world"}}))); + EXPECT_FALSE(verify(allowed, creds_with_dns_sans({{"foo.bar"}}))); + // OK with more SANs that strictly required + EXPECT_TRUE(verify(allowed, creds_with_dns_sans({{"hello.world"}, {"foo.bar"}, {"baz.blorg"}}))); +} + +TEST("wildcard SAN in certificate is not treated as a wildcard match by policy") { + auto allowed = allowed_peers({policy_with({required_san_dns("hello.world")})}); + EXPECT_FALSE(verify(allowed, creds_with_dns_sans({{"*.world"}}))); +} + +TEST("wildcard SAN in certificate is still matched by wildcard policy SAN") { + auto allowed = allowed_peers({policy_with({required_san_dns("*.world")})}); + EXPECT_TRUE(verify(allowed, creds_with_dns_sans({{"*.world"}}))); +} + +struct MultiPolicyMatchFixture { + AllowedPeers allowed; + MultiPolicyMatchFixture(); + ~MultiPolicyMatchFixture(); +}; + +MultiPolicyMatchFixture::MultiPolicyMatchFixture() + : allowed(allowed_peers({policy_with({required_san_dns("hello.world")}), + policy_with({required_san_dns("foo.bar")}), + policy_with({required_san_dns("zoid.berg")})})) +{} + +MultiPolicyMatchFixture::~MultiPolicyMatchFixture() = default; + +TEST_F("peer verifies if it matches at least 1 policy of multiple", MultiPolicyMatchFixture) { + EXPECT_TRUE(verify(f.allowed, creds_with_dns_sans({{"hello.world"}}))); + EXPECT_TRUE(verify(f.allowed, creds_with_dns_sans({{"foo.bar"}}))); + EXPECT_TRUE(verify(f.allowed, creds_with_dns_sans({{"zoid.berg"}}))); +} + +TEST_F("peer verifies if it matches multiple policies", MultiPolicyMatchFixture) { + EXPECT_TRUE(verify(f.allowed, creds_with_dns_sans({{"hello.world"}, {"zoid.berg"}}))); +} + +TEST_F("peer must match at least 1 of multiple policies", MultiPolicyMatchFixture) { + EXPECT_FALSE(verify(f.allowed, creds_with_dns_sans({{"does.not.exist"}}))); +} + +TEST("CN requirement without glob pattern is matched as exact string") { + auto allowed = allowed_peers({policy_with({required_cn("hello.world")})}); + EXPECT_TRUE(verify(allowed, creds_with_cn("hello.world"))); + EXPECT_FALSE(verify(allowed, creds_with_cn("foo.bar"))); + EXPECT_FALSE(verify(allowed, creds_with_cn("hello.worlds"))); + EXPECT_FALSE(verify(allowed, creds_with_cn("hhello.world"))); + EXPECT_FALSE(verify(allowed, creds_with_cn("foo.hello.world"))); + EXPECT_FALSE(verify(allowed, creds_with_cn("hello.world.bar"))); +} + +TEST("CN requirement can include glob wildcards") { + auto allowed = allowed_peers({policy_with({required_cn("*.w?rld")})}); + EXPECT_TRUE(verify(allowed, creds_with_cn("hello.world"))); + EXPECT_TRUE(verify(allowed, creds_with_cn("greetings.w0rld"))); + EXPECT_FALSE(verify(allowed, creds_with_cn("hello.wrld"))); + EXPECT_FALSE(verify(allowed, creds_with_cn("world"))); +} + +// TODO test CN _and_ SAN + +TEST_MAIN() { TEST_RUN_ALL(); } + 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 1ce4a4353d0..380bb0a3d71 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 @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -61,5 +62,74 @@ TEST("missing file referenced by field throws exception") { "File 'missing_privkey.txt' referenced by TLS config does not exist"); } +vespalib::string json_with_policies(const vespalib::string& policies) { + const char* fmt = R"({"files":{"private-key":"dummy_privkey.txt", + "certificates":"dummy_certs.txt", + "ca-certificates":"dummy_ca_certs.txt"}, + "allowed-peers":[%s]})"; + return vespalib::make_string(fmt, policies.c_str()); +} + +TransportSecurityOptions parse_policies(const vespalib::string& policies) { + return *read_options_from_json_string(json_with_policies(policies)); +} + +TEST("config file without allowed-peers accepts all pre-verified certificates") { + 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)->allowed_peers().allows_all_authenticated()); +} + +// Instead of contemplating what the semantics of an empty allow list should be, +// we do the easy way out and just say it's not allowed in the first place. +TEST("empty policy array throws exception") { + EXPECT_EXCEPTION(parse_policies(""), vespalib::IllegalArgumentException, + "\"allowed-peers\" must either be not present (allows " + "all peers with valid certificates) or a non-empty array"); +} + +TEST("can parse single peer policy with single requirement") { + const char* json = R"({ + "required-credentials":[ + {"field": "SAN_DNS", "must-match": "hello.world"} + ] + })"; + EXPECT_EQUAL(allowed_peers({policy_with({required_san_dns("hello.world")})}), + parse_policies(json).allowed_peers()); +} + +TEST("can parse single peer policy with multiple requirements") { + const char* json = R"({ + "required-credentials":[ + {"field": "SAN_DNS", "must-match": "hello.world"}, + {"field": "CN", "must-match": "goodbye.moon"} + ] + })"; + EXPECT_EQUAL(allowed_peers({policy_with({required_san_dns("hello.world"), + required_cn("goodbye.moon")})}), + parse_policies(json).allowed_peers()); +} + +TEST("unknown field type throws exception") { + const char* json = R"({ + "required-credentials":[ + {"field": "winnie the pooh", "must-match": "piglet"} + ] + })"; + EXPECT_EXCEPTION(parse_policies(json), vespalib::IllegalArgumentException, + "Unsupported credential field type: 'winnie the pooh'. Supported are: CN, SAN_DNS"); +} + +TEST("empty required-credentials array throws exception") { + const char* json = R"({ + "required-credentials":[] + })"; + EXPECT_EXCEPTION(parse_policies(json), vespalib::IllegalArgumentException, + "\"required-credentials\" array can't be empty (would allow all peers)"); +} + +// TODO test parsing of multiple policies + TEST_MAIN() { TEST_RUN_ALL(); } diff --git a/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt index 8fd92220abb..170d2148cfa 100644 --- a/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt +++ b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt @@ -7,6 +7,8 @@ vespa_add_library(vespalib_vespalib_net_tls OBJECT maybe_tls_crypto_engine.cpp maybe_tls_crypto_socket.cpp peer_credentials.cpp + peer_policies.cpp + policy_checking_certificate_verifier.cpp protocol_snooping.cpp tls_context.cpp tls_crypto_engine.cpp 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 bc981bccb96..9ebe8c540f1 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 @@ -185,9 +185,11 @@ SslCtxPtr new_tls_ctx_with_auto_init() { } // anon ns -OpenSslTlsContextImpl::OpenSslTlsContextImpl(const TransportSecurityOptions& ts_opts) +OpenSslTlsContextImpl::OpenSslTlsContextImpl( + const TransportSecurityOptions& ts_opts, + std::shared_ptr cert_verify_callback) : _ctx(new_tls_ctx_with_auto_init()), - _cert_verify_callback(ts_opts.cert_verify_callback()) + _cert_verify_callback(std::move(cert_verify_callback)) { if (!_ctx) { throw CryptoException("Failed to create new TLS context"); @@ -201,6 +203,7 @@ OpenSslTlsContextImpl::OpenSslTlsContextImpl(const TransportSecurityOptions& ts_ } enable_ephemeral_key_exchange(); disable_compression(); + disable_renegotiation(); enforce_peer_certificate_verification(); set_provided_certificate_verification_callback(); // TODO set accepted cipher suites! @@ -302,6 +305,12 @@ void OpenSslTlsContextImpl::disable_compression() { SSL_CTX_set_options(_ctx.get(), SSL_OP_NO_COMPRESSION); } +void OpenSslTlsContextImpl::disable_renegotiation() { +#if (OPENSSL_VERSION_NUMBER >= 0x10100080L) // v1.1.0h and beyond + SSL_CTX_set_options(_ctx.get(), SSL_OP_NO_RENEGOTIATION); +#endif +} + namespace { // There's no good reason for entries to contain embedded nulls, aside from 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 e6de28043d6..0ff8dd5932e 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 @@ -10,10 +10,10 @@ namespace vespalib::net::tls::impl { class OpenSslTlsContextImpl : public TlsContext { SslCtxPtr _ctx; - // Callback provided by options std::shared_ptr _cert_verify_callback; public: - explicit OpenSslTlsContextImpl(const TransportSecurityOptions&); + OpenSslTlsContextImpl(const TransportSecurityOptions& ts_opts, + std::shared_ptr cert_verify_callback); ~OpenSslTlsContextImpl() override; ::SSL_CTX* native_context() const noexcept { return _ctx.get(); } @@ -26,6 +26,11 @@ private: // Enable use of ephemeral key exchange (ECDHE), allowing forward secrecy. void enable_ephemeral_key_exchange(); void disable_compression(); + // Explicitly disable TLS renegotiation for <= TLSv1.2 on OpenSSL versions + // that support this. We don't support renegotiation in general (and will break + // the connection if it's attempted by the peer), but this should signal + // explicitly to the peer that it's not a supported action. + void disable_renegotiation(); void enforce_peer_certificate_verification(); void set_provided_certificate_verification_callback(); }; diff --git a/vespalib/src/vespa/vespalib/net/tls/peer_credentials.cpp b/vespalib/src/vespa/vespalib/net/tls/peer_credentials.cpp index ec8bb7ef9cd..e8351aa38f3 100644 --- a/vespalib/src/vespa/vespalib/net/tls/peer_credentials.cpp +++ b/vespalib/src/vespa/vespalib/net/tls/peer_credentials.cpp @@ -1,10 +1,24 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "peer_credentials.h" +#include namespace vespalib::net::tls { PeerCredentials::PeerCredentials() = default; PeerCredentials::~PeerCredentials() = default; +std::ostream& operator<<(std::ostream& os, const PeerCredentials& creds) { + os << "PeerCredentials(CN '" << creds.common_name + << "', DNS SANs {"; + for (size_t i = 0; i < creds.dns_sans.size(); ++i) { + if (i != 0) { + os << ", "; + } + os << '\'' << creds.dns_sans[i] << '\''; + } + os << "})"; + return os; +} + } diff --git a/vespalib/src/vespa/vespalib/net/tls/peer_credentials.h b/vespalib/src/vespa/vespalib/net/tls/peer_credentials.h index 802d5c0bd27..4402244cc18 100644 --- a/vespalib/src/vespa/vespalib/net/tls/peer_credentials.h +++ b/vespalib/src/vespa/vespalib/net/tls/peer_credentials.h @@ -3,6 +3,7 @@ #include #include +#include namespace vespalib::net::tls { @@ -18,4 +19,6 @@ struct PeerCredentials { ~PeerCredentials(); }; +std::ostream& operator<<(std::ostream&, const PeerCredentials&); + } diff --git a/vespalib/src/vespa/vespalib/net/tls/peer_policies.cpp b/vespalib/src/vespa/vespalib/net/tls/peer_policies.cpp new file mode 100644 index 00000000000..d6ae16011a7 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/peer_policies.cpp @@ -0,0 +1,113 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "peer_policies.h" +#include +#include + +namespace vespalib::net::tls { + +namespace { + +// Note: this is for basix regexp only, _not_ extended regexp +bool is_basic_regex_special_char(char c) noexcept { + switch (c) { + case '^': + case '$': + case '.': + case '[': + case '\\': + return true; + default: + return false; + } +} + +std::string glob_to_basic_regex(vespalib::stringref glob) { + std::string ret = "^"; + ret.reserve(glob.size() + 2); + for (auto c : glob) { + if (c == '*') { + // Note: we explicitly stop matching at a dot separator boundary. + // This is to make host name matching less vulnerable to dirty tricks. + ret += "[^.]*"; + } else if (c == '?') { + // Same applies for single chars; they should only match _within_ a dot boundary. + ret += "[^.]"; + } else { + if (is_basic_regex_special_char(c)) { + ret += '\\'; + } + ret += c; + } + } + ret += '$'; + return ret; +} + +class RegexHostMatchPattern : public HostGlobPattern { + std::regex _pattern_as_regex; +public: + explicit RegexHostMatchPattern(vespalib::stringref glob_pattern) + : _pattern_as_regex(glob_to_basic_regex(glob_pattern), std::regex_constants::basic) + { + } + ~RegexHostMatchPattern() override = default; + + bool matches(vespalib::stringref str) const override { + return std::regex_match(str.begin(), str.end(), _pattern_as_regex); + } +}; + +} // anon ns + +std::shared_ptr HostGlobPattern::create_from_glob(vespalib::stringref glob_pattern) { + return std::make_shared(glob_pattern); +} + +RequiredPeerCredential::RequiredPeerCredential(Field field, vespalib::string must_match_pattern) + : _field(field), + _original_pattern(std::move(must_match_pattern)), + _match_pattern(HostGlobPattern::create_from_glob(_original_pattern)) +{ +} + +RequiredPeerCredential::~RequiredPeerCredential() = default; + +namespace { +template +void print_joined(std::ostream& os, const Collection& coll, const char* sep) { + bool first = true; + for (const auto& e : coll) { + if (!first) { + os << sep; + } + first = false; + os << e; + } +} +} + +std::ostream& operator<<(std::ostream& os, const RequiredPeerCredential& cred) { + os << "RequiredPeerCredential(" + << (cred.field() == RequiredPeerCredential::Field::CN ? "CN" : "SAN_DNS") + << " matches '" + << cred.original_pattern() + << "')"; + return os; +} + +std::ostream& operator<<(std::ostream& os, const PeerPolicy& policy) { + os << "PeerPolicy("; + print_joined(os, policy.required_peer_credentials(), ", "); + os << ")"; + return os; +} + +std::ostream& operator<<(std::ostream& os, const AllowedPeers& allowed){ + os << "AllowedPeers("; + print_joined(os, allowed.peer_policies(), ", "); + os << ")"; + return os; +} + +} // vespalib::net::tls diff --git a/vespalib/src/vespa/vespalib/net/tls/peer_policies.h b/vespalib/src/vespa/vespalib/net/tls/peer_policies.h new file mode 100644 index 00000000000..c445698c75e --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/peer_policies.h @@ -0,0 +1,97 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include +#include +#include +#include + +namespace vespalib::net::tls { + +struct HostGlobPattern { + virtual ~HostGlobPattern() = default; + virtual bool matches(vespalib::stringref str) const = 0; + + static std::shared_ptr create_from_glob(vespalib::stringref pattern); +}; + +class RequiredPeerCredential { +public: + enum class Field { + CN, SAN_DNS + }; +private: + Field _field; + vespalib::string _original_pattern; + std::shared_ptr _match_pattern; +public: + RequiredPeerCredential() = default; + RequiredPeerCredential(Field field, vespalib::string must_match_pattern); + ~RequiredPeerCredential(); + + bool operator==(const RequiredPeerCredential& rhs) const { + // We assume (opaque) _match_pattern matches rhs._match_pattern if the pattern + // strings they were created from are equal. This should be fully deterministic. + return ((_field == rhs._field) + && (_original_pattern == rhs._original_pattern)); + } + + bool matches(vespalib::stringref str) const { + return (_match_pattern && _match_pattern->matches(str)); + } + + Field field() const noexcept { return _field; } + const vespalib::string& original_pattern() const noexcept { return _original_pattern; } +}; + +class PeerPolicy { + // _All_ credentials must match for the policy itself to match. + std::vector _required_peer_credentials; +public: + PeerPolicy() = default; + explicit PeerPolicy(std::vector required_peer_credentials_) + : _required_peer_credentials(std::move(required_peer_credentials_)) + {} + + bool operator==(const PeerPolicy& rhs) const { + return (_required_peer_credentials == rhs._required_peer_credentials); + } + const std::vector& required_peer_credentials() const noexcept { + return _required_peer_credentials; + } +}; + +class AllowedPeers { + // A peer will be allowed iff it matches _one or more_ policies. + std::vector _peer_policies; + bool _allow_all_if_empty = false; + + AllowedPeers(bool allow_all_if_empty) + : _peer_policies(), + _allow_all_if_empty(allow_all_if_empty) + {} +public: + AllowedPeers() = default; + explicit AllowedPeers(std::vector peer_policies_) + : _peer_policies(std::move(peer_policies_)), + _allow_all_if_empty(false) + {} + + static AllowedPeers allow_all_authenticated() { + return AllowedPeers(true); + } + + bool operator==(const AllowedPeers& rhs) const { + return (_peer_policies == rhs._peer_policies); + } + bool allows_all_authenticated() const noexcept { + return _allow_all_if_empty; + } + const std::vector& peer_policies() const noexcept { return _peer_policies; } +}; + +std::ostream& operator<<(std::ostream&, const RequiredPeerCredential&); +std::ostream& operator<<(std::ostream&, const PeerPolicy&); +std::ostream& operator<<(std::ostream&, const AllowedPeers&); + +} // vespalib::net::tls diff --git a/vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.cpp b/vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.cpp new file mode 100644 index 00000000000..7d8cf7cf899 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.cpp @@ -0,0 +1,74 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "policy_checking_certificate_verifier.h" + +namespace vespalib::net::tls { + +namespace { + +bool matches_single_san_requirement(const PeerCredentials& peer_creds, const RequiredPeerCredential& requirement) { + for (auto& provided_cred : peer_creds.dns_sans) { + if (requirement.matches(provided_cred)) { + return true; + } + } + return false; +} + +bool matches_cn_requirement(const PeerCredentials& peer_creds, const RequiredPeerCredential& requirement) { + return requirement.matches(peer_creds.common_name); +} + +bool matches_all_policy_requirements(const PeerCredentials& peer_creds, const PeerPolicy& policy) { + for (auto& required_cred : policy.required_peer_credentials()) { + switch (required_cred.field()) { + case RequiredPeerCredential::Field::SAN_DNS: + if (!matches_single_san_requirement(peer_creds, required_cred)) { + return false; + } + continue; + case RequiredPeerCredential::Field::CN: + if (!matches_cn_requirement(peer_creds, required_cred)) { + return false; + } + continue; + } + abort(); + } + return true; +} + +} // anon ns + +class PolicyConfiguredCertificateVerifier : public CertificateVerificationCallback { + AllowedPeers _allowed_peers; +public: + explicit PolicyConfiguredCertificateVerifier(AllowedPeers allowed_peers); + + ~PolicyConfiguredCertificateVerifier() override; + + bool verify(const PeerCredentials& peer_creds) const override; +}; + +PolicyConfiguredCertificateVerifier::PolicyConfiguredCertificateVerifier(AllowedPeers allowed_peers) + : _allowed_peers(std::move(allowed_peers)) {} + +PolicyConfiguredCertificateVerifier::~PolicyConfiguredCertificateVerifier() = default; + +bool PolicyConfiguredCertificateVerifier::verify(const PeerCredentials& peer_creds) const { + if (_allowed_peers.allows_all_authenticated()) { + return true; + } + for (auto& policy : _allowed_peers.peer_policies()) { + if (matches_all_policy_requirements(peer_creds, policy)) { + return true; + } + } + return false; +} + +std::shared_ptr create_verify_callback_from(AllowedPeers allowed_peers) { + return std::make_shared(std::move(allowed_peers)); +} + +} // vespalib::net::tls diff --git a/vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.h b/vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.h new file mode 100644 index 00000000000..6eac4e8c2ab --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.h @@ -0,0 +1,11 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "certificate_verification_callback.h" +#include "peer_policies.h" + +namespace vespalib::net::tls { + +std::shared_ptr create_verify_callback_from(AllowedPeers allowed_peers); + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/tls_context.cpp b/vespalib/src/vespa/vespalib/net/tls/tls_context.cpp index cafa61898d7..8b62e5ce280 100644 --- a/vespalib/src/vespa/vespalib/net/tls/tls_context.cpp +++ b/vespalib/src/vespa/vespalib/net/tls/tls_context.cpp @@ -1,11 +1,20 @@ // 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 +#include +#include namespace vespalib::net::tls { std::shared_ptr TlsContext::create_default_context(const TransportSecurityOptions& opts) { - return std::make_shared(opts); + auto verifier = create_verify_callback_from(opts.allowed_peers()); + return std::make_shared(opts, std::move(verifier)); +} + +std::shared_ptr TlsContext::create_default_context( + const TransportSecurityOptions& opts, + std::shared_ptr cert_verify_callback) { + return std::make_shared(opts, std::move(cert_verify_callback)); } } diff --git a/vespalib/src/vespa/vespalib/net/tls/tls_context.h b/vespalib/src/vespa/vespalib/net/tls/tls_context.h index ce71d3e2ddb..90cc67f716b 100644 --- a/vespalib/src/vespa/vespalib/net/tls/tls_context.h +++ b/vespalib/src/vespa/vespalib/net/tls/tls_context.h @@ -6,11 +6,21 @@ namespace vespalib::net::tls { class TransportSecurityOptions; +class CertificateVerificationCallback; struct TlsContext { virtual ~TlsContext() = default; + // Create a TLS context which verifies certificates according to the provided options' + // CA trust roots AND allowed peer policies static std::shared_ptr create_default_context(const TransportSecurityOptions&); + // Create a TLS context where the certificate verification callback is explicitly provided. + // IMPORTANT: This does NOT verify that the peer satisfies the allowed peer policies! + // It only verifies that a peer is signed by a trusted CA. This function should + // therefore only be used in very special circumstances, such as unit tests. + static std::shared_ptr create_default_context( + const TransportSecurityOptions&, + std::shared_ptr cert_verify_callback); }; } 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 e828010019c..829da94a448 100644 --- a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp +++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp @@ -6,30 +6,37 @@ namespace vespalib::net::tls { +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)), + _allowed_peers(std::move(builder._allowed_peers)) +{ +} + TransportSecurityOptions::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)), - _cert_verify_callback(std::make_shared()) + _allowed_peers(AllowedPeers::allow_all_authenticated()) { } TransportSecurityOptions::TransportSecurityOptions(vespalib::string ca_certs_pem, vespalib::string cert_chain_pem, vespalib::string private_key_pem, - std::shared_ptr cert_verify_callback) - : _ca_certs_pem(std::move(ca_certs_pem)), - _cert_chain_pem(std::move(cert_chain_pem)), - _private_key_pem(std::move(private_key_pem)), - _cert_verify_callback(std::move(cert_verify_callback)) + AllowedPeers allowed_peers) + : _ca_certs_pem(std::move(ca_certs_pem)), + _cert_chain_pem(std::move(cert_chain_pem)), + _private_key_pem(std::move(private_key_pem)), + _allowed_peers(std::move(allowed_peers)) { - assert(_cert_verify_callback.get() != nullptr); } TransportSecurityOptions::~TransportSecurityOptions() { OPENSSL_cleanse(&_private_key_pem[0], _private_key_pem.size()); } -} +} // vespalib::net::tls 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 2cc3e701724..1463bfba248 100644 --- a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h +++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h @@ -3,7 +3,7 @@ #pragma once #include "certificate_verification_callback.h" -#include +#include "peer_policies.h" #include namespace vespalib::net::tls { @@ -12,12 +12,24 @@ class TransportSecurityOptions { vespalib::string _ca_certs_pem; vespalib::string _cert_chain_pem; vespalib::string _private_key_pem; - std::shared_ptr _cert_verify_callback; + AllowedPeers _allowed_peers; public: TransportSecurityOptions() = default; - // Construct transport options with a default certificate verification callback - // which accepts all certificates correctly signed by the given CA(s). + struct Builder { + vespalib::string _ca_certs_pem; + vespalib::string _cert_chain_pem; + vespalib::string _private_key_pem; + AllowedPeers _allowed_peers; + + 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& allowed_peers(AllowedPeers allowed) { _allowed_peers = std::move(allowed); return *this; } + }; + + explicit TransportSecurityOptions(Builder builder); + TransportSecurityOptions(vespalib::string ca_certs_pem, vespalib::string cert_chain_pem, vespalib::string private_key_pem); @@ -25,15 +37,14 @@ public: TransportSecurityOptions(vespalib::string ca_certs_pem, vespalib::string cert_chain_pem, vespalib::string private_key_pem, - std::shared_ptr cert_verify_callback); + AllowedPeers allowed_peers); + ~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; } - const std::shared_ptr& cert_verify_callback() const noexcept { - return _cert_verify_callback; - } + const AllowedPeers& allowed_peers() const noexcept { return _allowed_peers; } }; -} +} // 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 05cfc797e51..afc0982973e 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 @@ -27,6 +27,7 @@ namespace vespalib::net::tls { "must-match": "DNS:foo.bar.baz.*" } ], + // TODO skip tags for now? just binary decision? "tags": ["cluster-peers", "config-server"] // or "roles"? Avoid ambiguities with Athenz concepts }, { @@ -36,6 +37,16 @@ namespace vespalib::net::tls { "tags": ["config-server"] } ] + // alternative 2: + "allowed-peers": [ + { + "required-credentials":[ + { "field":"CN", "must-match": "*.config.blarg"}, + { "field":"SAN_DNS", "must-match": "*.fancy.config.blarg"} + ], + "name": "funky config servers" + } + ] } */ @@ -44,11 +55,6 @@ using namespace slime::convenience; namespace { -constexpr const char* files_field = "files"; -constexpr const char* private_key_field = "private-key"; -constexpr const char* ca_certs_field = "ca-certificates"; -constexpr const char* certs_field = "certificates"; - void verify_referenced_file_exists(const vespalib::string& file_path) { if (!fileExists(file_path)) { throw IllegalArgumentException(make_string("File '%s' referenced by TLS config does not exist", file_path.c_str())); @@ -64,24 +70,70 @@ vespalib::string load_file_referenced_by_field(const Cursor& cursor, const char* return File::readAll(file_path); } +RequiredPeerCredential parse_peer_credential(const Cursor& req_entry) { + auto field_string = req_entry["field"].asString().make_string(); + RequiredPeerCredential::Field field; + if (field_string == "CN") { + field = RequiredPeerCredential::Field::CN; + } else if (field_string == "SAN_DNS") { + field = RequiredPeerCredential::Field::SAN_DNS; + } else { + throw IllegalArgumentException(make_string( + "Unsupported credential field type: '%s'. Supported are: CN, SAN_DNS", + field_string.c_str())); + } + auto match = req_entry["must-match"].asString().make_string(); + return RequiredPeerCredential(field, std::move(match)); +} + +PeerPolicy parse_peer_policy(const Cursor& peer_entry) { + auto& creds = peer_entry["required-credentials"]; + if (creds.children() == 0) { + throw IllegalArgumentException("\"required-credentials\" array can't be empty (would allow all peers)"); + } + std::vector required_creds; + for (size_t i = 0; i < creds.children(); ++i) { + required_creds.emplace_back(parse_peer_credential(creds[i])); + } + return PeerPolicy(std::move(required_creds)); +} + +AllowedPeers parse_allowed_peers(const Cursor& allowed_peers) { + if (!allowed_peers.valid()) { + // If there's no "allowed-peers" object, valid CA signing is sufficient. + return AllowedPeers::allow_all_authenticated(); + } + if (allowed_peers.children() == 0) { + throw IllegalArgumentException("\"allowed-peers\" must either be not present (allows " + "all peers with valid certificates) or a non-empty array"); + } + std::vector policies; + for (size_t i = 0; i < allowed_peers.children(); ++i) { + policies.emplace_back(parse_peer_policy(allowed_peers[i])); + } + return AllowedPeers(std::move(policies)); +} + std::unique_ptr load_from_input(Input& input) { Slime root; auto parsed = slime::JsonFormat::decode(input, root); if (parsed == 0) { throw IllegalArgumentException("Provided TLS config file is not valid JSON"); } - auto& files = root[files_field]; + auto& files = root["files"]; if (files.fields() == 0) { throw IllegalArgumentException("TLS config root field 'files' is missing or empty"); } // Note: we do no look at the _contents_ of the files; this is deferred to the // TLS context code which actually tries to extract key and certificate material // from them. - auto ca_certs = load_file_referenced_by_field(files, ca_certs_field); - auto certs = load_file_referenced_by_field(files, certs_field); - auto priv_key = load_file_referenced_by_field(files, private_key_field); + auto ca_certs = load_file_referenced_by_field(files, "ca-certificates"); + auto certs = load_file_referenced_by_field(files, "certificates"); + auto priv_key = load_file_referenced_by_field(files, "private-key"); + auto allowed_peers = parse_allowed_peers(root["allowed-peers"]); - return std::make_unique(std::move(ca_certs), std::move(certs), std::move(priv_key)); + return std::make_unique(std::move(ca_certs), std::move(certs), + std::move(priv_key), std::move(allowed_peers)); } } // anon ns diff --git a/vespalib/src/vespa/vespalib/test/CMakeLists.txt b/vespalib/src/vespa/vespalib/test/CMakeLists.txt index 4eb47735ca7..73d31208721 100644 --- a/vespalib/src/vespa/vespalib/test/CMakeLists.txt +++ b/vespalib/src/vespa/vespalib/test/CMakeLists.txt @@ -2,5 +2,6 @@ vespa_add_library(vespalib_vespalib_test OBJECT SOURCES make_tls_options_for_testing.cpp + peer_policy_utils.cpp DEPENDS ) diff --git a/vespalib/src/vespa/vespalib/test/peer_policy_utils.cpp b/vespalib/src/vespa/vespalib/test/peer_policy_utils.cpp new file mode 100644 index 00000000000..7d116b8893c --- /dev/null +++ b/vespalib/src/vespa/vespalib/test/peer_policy_utils.cpp @@ -0,0 +1,23 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "peer_policy_utils.h" + +namespace vespalib::net::tls { + +RequiredPeerCredential required_cn(vespalib::stringref pattern) { + return {RequiredPeerCredential::Field::CN, pattern}; +} + +RequiredPeerCredential required_san_dns(vespalib::stringref pattern) { + return {RequiredPeerCredential::Field::SAN_DNS, pattern}; +} + +PeerPolicy policy_with(std::vector creds) { + return PeerPolicy(std::move(creds)); +} + +AllowedPeers allowed_peers(std::vector peer_policies) { + return AllowedPeers(std::move(peer_policies)); +} + +} diff --git a/vespalib/src/vespa/vespalib/test/peer_policy_utils.h b/vespalib/src/vespa/vespalib/test/peer_policy_utils.h new file mode 100644 index 00000000000..f5adf31d08e --- /dev/null +++ b/vespalib/src/vespa/vespalib/test/peer_policy_utils.h @@ -0,0 +1,13 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include + +namespace vespalib::net::tls { + +RequiredPeerCredential required_cn(vespalib::stringref pattern); +RequiredPeerCredential required_san_dns(vespalib::stringref pattern); +PeerPolicy policy_with(std::vector creds); +AllowedPeers allowed_peers(std::vector peer_policies); + +} -- cgit v1.2.3