summaryrefslogtreecommitdiffstats
path: root/vespalib
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@oath.com>2018-11-13 14:58:27 +0100
committerGitHub <noreply@github.com>2018-11-13 14:58:27 +0100
commit8d811ba1508f0c16dc9b82eaef4e53627dc5048c (patch)
tree48d0d32915a4ca8c308cf23b86e8185ea4a37c32 /vespalib
parent03c01d5476f9884b31328e8301e5e6e7bf7752ab (diff)
parent9e5a54463dd08d861e068ec5ea3bb9b194a0e481 (diff)
Merge pull request #7608 from vespa-engine/vekterli/add-support-for-basic-certificate-verification-policies
Add support for basic certificate verification policies in C++
Diffstat (limited to 'vespalib')
-rw-r--r--vespalib/CMakeLists.txt1
-rw-r--r--vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp76
-rw-r--r--vespalib/src/tests/net/tls/policy_checking_certificate_verifier/CMakeLists.txt10
-rw-r--r--vespalib/src/tests/net/tls/policy_checking_certificate_verifier/policy_checking_certificate_verifier_test.cpp187
-rw-r--r--vespalib/src/tests/net/tls/transport_options/transport_options_reading_test.cpp70
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt2
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp13
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h9
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/peer_credentials.cpp14
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/peer_credentials.h3
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/peer_policies.cpp113
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/peer_policies.h97
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.cpp74
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/policy_checking_certificate_verifier.h11
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/tls_context.cpp11
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/tls_context.h10
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp23
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/transport_security_options.h29
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/transport_security_options_reading.cpp74
-rw-r--r--vespalib/src/vespa/vespalib/test/CMakeLists.txt1
-rw-r--r--vespalib/src/vespa/vespalib/test/peer_policy_utils.cpp23
-rw-r--r--vespalib/src/vespa/vespalib/test/peer_policy_utils.h13
22 files changed, 827 insertions, 37 deletions
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 <vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h>
#include <vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h>
#include <vespa/vespalib/test/make_tls_options_for_testing.h>
+#include <vespa/vespalib/test/peer_policy_utils.h>
#include <iostream>
#include <stdexcept>
#include <stdlib.h>
@@ -83,6 +84,14 @@ struct Fixture {
}
static std::unique_ptr<CryptoCodec> create_openssl_codec(
+ const TransportSecurityOptions& opts,
+ std::shared_ptr<CertificateVerificationCallback> 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<CryptoCodec> create_openssl_codec(
const std::shared_ptr<TlsContext>& ctx, CryptoCodec::Mode mode) {
auto ctx_impl = std::dynamic_pointer_cast<impl::OpenSslTlsContextImpl>(ctx);
return std::make_unique<impl::OpenSslCryptoCodecImpl>(std::move(ctx_impl), mode);
@@ -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<CertificateVerificationCallback> 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<CertificateVerificationCallback> cert_cb) {
+ void reset_client_with_cert_opts(const CertKeyWrapper& ck, std::shared_ptr<CertificateVerificationCallback> 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<CertificateVerificationCallback> 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..6e9e0304c89
--- /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 <vespa/vespalib/io/fileutil.h>
+#include <vespa/vespalib/net/tls/transport_security_options.h>
+#include <vespa/vespalib/net/tls/transport_security_options_reading.h>
+#include <vespa/vespalib/net/tls/policy_checking_certificate_verifier.h>
+#include <vespa/vespalib/test/peer_policy_utils.h>
+#include <vespa/vespalib/testkit/test_kit.h>
+#include <vespa/vespalib/util/exceptions.h>
+
+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<vespalib::string> 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 authenticated 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 <vespa/vespalib/io/fileutil.h>
#include <vespa/vespalib/net/tls/transport_security_options.h>
#include <vespa/vespalib/net/tls/transport_security_options_reading.h>
+#include <vespa/vespalib/test/peer_policy_utils.h>
#include <vespa/vespalib/testkit/test_kit.h>
#include <vespa/vespalib/util/exceptions.h>
@@ -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<CertificateVerificationCallback> 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<CertificateVerificationCallback> _cert_verify_callback;
public:
- explicit OpenSslTlsContextImpl(const TransportSecurityOptions&);
+ OpenSslTlsContextImpl(const TransportSecurityOptions& ts_opts,
+ std::shared_ptr<CertificateVerificationCallback> 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 <iostream>
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 <vespa/vespalib/stllike/string.h>
#include <vector>
+#include <iosfwd>
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 <iostream>
+#include <regex>
+
+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<const HostGlobPattern> HostGlobPattern::create_from_glob(vespalib::stringref glob_pattern) {
+ return std::make_shared<const RegexHostMatchPattern>(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 <typename Collection>
+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 <vespa/vespalib/stllike/string.h>
+#include <memory>
+#include <vector>
+#include <iosfwd>
+
+namespace vespalib::net::tls {
+
+struct HostGlobPattern {
+ virtual ~HostGlobPattern() = default;
+ virtual bool matches(vespalib::stringref str) const = 0;
+
+ static std::shared_ptr<const HostGlobPattern> 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<const HostGlobPattern> _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<RequiredPeerCredential> _required_peer_credentials;
+public:
+ PeerPolicy() = default;
+ explicit PeerPolicy(std::vector<RequiredPeerCredential> 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<RequiredPeerCredential>& 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<PeerPolicy> _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<PeerPolicy> 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<PeerPolicy>& 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..b3746182205
--- /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 (const 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 (const 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 (const auto& policy : _allowed_peers.peer_policies()) {
+ if (matches_all_policy_requirements(peer_creds, policy)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+std::shared_ptr<CertificateVerificationCallback> create_verify_callback_from(AllowedPeers allowed_peers) {
+ return std::make_shared<PolicyConfiguredCertificateVerifier>(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<CertificateVerificationCallback> 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 <vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h>
+#include <vespa/vespalib/net/tls/policy_checking_certificate_verifier.h>
+#include <vespa/vespalib/net/tls/transport_security_options.h>
namespace vespalib::net::tls {
std::shared_ptr<TlsContext> TlsContext::create_default_context(const TransportSecurityOptions& opts) {
- return std::make_shared<impl::OpenSslTlsContextImpl>(opts);
+ auto verifier = create_verify_callback_from(opts.allowed_peers());
+ return std::make_shared<impl::OpenSslTlsContextImpl>(opts, std::move(verifier));
+}
+
+std::shared_ptr<TlsContext> TlsContext::create_default_context(
+ const TransportSecurityOptions& opts,
+ std::shared_ptr<CertificateVerificationCallback> cert_verify_callback) {
+ return std::make_shared<impl::OpenSslTlsContextImpl>(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<TlsContext> 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<TlsContext> create_default_context(
+ const TransportSecurityOptions&,
+ std::shared_ptr<CertificateVerificationCallback> 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<AcceptAllPreVerifiedCertificates>())
+ _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<CertificateVerificationCallback> 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 <vespa/vespalib/stllike/string.h>
+#include "peer_policies.h"
#include <memory>
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<CertificateVerificationCallback> _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<CertificateVerificationCallback> 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<CertificateVerificationCallback>& 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..f14ad78dc91 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,18 +55,13 @@ 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()));
}
}
-vespalib::string load_file_referenced_by_field(const Cursor& cursor, const char* field) {
+vespalib::string load_file_referenced_by_field(const Inspector& cursor, const char* field) {
auto file_path = cursor[field].asString().make_string();
if (file_path.empty()) {
throw IllegalArgumentException(make_string("TLS config field '%s' has not been set", field));
@@ -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 Inspector& 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 Inspector& 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<RequiredPeerCredential> 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 Inspector& 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<PeerPolicy> 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<TransportSecurityOptions> 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<TransportSecurityOptions>(std::move(ca_certs), std::move(certs), std::move(priv_key));
+ return std::make_unique<TransportSecurityOptions>(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<RequiredPeerCredential> creds) {
+ return PeerPolicy(std::move(creds));
+}
+
+AllowedPeers allowed_peers(std::vector<PeerPolicy> 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 <vespa/vespalib/net/tls/peer_policies.h>
+
+namespace vespalib::net::tls {
+
+RequiredPeerCredential required_cn(vespalib::stringref pattern);
+RequiredPeerCredential required_san_dns(vespalib::stringref pattern);
+PeerPolicy policy_with(std::vector<RequiredPeerCredential> creds);
+AllowedPeers allowed_peers(std::vector<PeerPolicy> peer_policies);
+
+}