From aded443066847841b349afc7abea627b47778484 Mon Sep 17 00:00:00 2001 From: Tor Brede Vekterli Date: Tue, 10 Nov 2020 14:08:12 +0000 Subject: Add basic exact matching support for X509 URI SANs Adds extraction of X509 URI peer credentials during the handshake process as well as a new SAN_URI field to the transport security options peer policy section. This implementation is NOT conformant with RFC 2459 since we don't currently support case insensitive matching of scheme, host etc., but it's good enough for our purposes for now. --- .../net/tls/openssl_impl/openssl_impl_test.cpp | 10 ++-- .../policy_checking_certificate_verifier_test.cpp | 56 +++++++++++++++++----- .../transport_options_reading_test.cpp | 2 + 3 files changed, 53 insertions(+), 15 deletions(-) (limited to 'vespalib/src/tests/net') 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 4586beef910..7dacbd89503 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 @@ -583,10 +583,11 @@ TEST_F("Exception during verification callback processing breaks handshake", Cer EXPECT_FALSE(f.handshake()); } -TEST_F("Certificate verification callback observes CN and DNS SANs", CertFixture) { +TEST_F("Certificate verification callback observes CN, DNS SANs and URI SANs", CertFixture) { auto ck = f.create_ca_issued_peer_cert( {{"rockets.wile.example.com"}}, - {{"DNS:crash.wile.example.com"}, {"DNS:burn.wile.example.com"}}); + {{"DNS:crash.wile.example.com"}, {"DNS:burn.wile.example.com"}, + {"URI:foo://bar.baz/zoid"}}); fprintf(stderr, "certs:\n%s%s\n", f.root_ca.cert->to_pem().c_str(), ck.cert->to_pem().c_str()); @@ -600,6 +601,8 @@ TEST_F("Certificate verification callback observes CN and DNS SANs", CertFixture ASSERT_EQUAL(2u, creds.dns_sans.size()); EXPECT_EQUAL("crash.wile.example.com", creds.dns_sans[0]); EXPECT_EQUAL("burn.wile.example.com", creds.dns_sans[1]); + ASSERT_EQUAL(1u, server_cb->creds.uri_sans.size()); + EXPECT_EQUAL("foo://bar.baz/zoid", server_cb->creds.uri_sans[0]); } TEST_F("Last occurring CN is given to verification callback if multiple CNs are present", CertFixture) { @@ -616,7 +619,7 @@ TEST_F("Last occurring CN is given to verification callback if multiple CNs are } // TODO we are likely to want IPADDR SANs at some point -TEST_F("Only DNS SANs are enumerated", CertFixture) { +TEST_F("Only DNS and URI SANs are enumerated", CertFixture) { auto ck = f.create_ca_issued_peer_cert({}, {"IP:127.0.0.1"}); f.reset_client_with_cert_opts(ck, std::make_shared()); @@ -624,6 +627,7 @@ TEST_F("Only DNS SANs are enumerated", CertFixture) { f.reset_server_with_cert_opts(ck, server_cb); ASSERT_TRUE(f.handshake()); EXPECT_EQUAL(0u, server_cb->creds.dns_sans.size()); + EXPECT_EQUAL(0u, server_cb->creds.uri_sans.size()); } // We don't test too many combinations of peer policies here, only that 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 index a9e823bf3ab..9a7e1b1b585 100644 --- 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 @@ -8,7 +8,7 @@ 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); + auto glob = CredentialMatchPattern::create_from_glob(pattern); return glob->matches(string_to_check); } @@ -61,12 +61,25 @@ TEST("special extended regex characters are ignored") { } // TODO CN + SANs +PeerCredentials creds_with_sans(std::vector dns_sans, std::vector uri_sans) { + PeerCredentials creds; + creds.dns_sans = std::move(dns_sans); + creds.uri_sans = std::move(uri_sans); + return creds; +} + PeerCredentials creds_with_dns_sans(std::vector dns_sans) { PeerCredentials creds; creds.dns_sans = std::move(dns_sans); return creds; } +PeerCredentials creds_with_uri_sans(std::vector uri_sans) { + PeerCredentials creds; + creds.uri_sans = std::move(uri_sans); + return creds; +} + PeerCredentials creds_with_cn(vespalib::stringref cn) { PeerCredentials creds; creds.common_name = cn; @@ -93,7 +106,7 @@ TEST("Non-empty policies do not allow all authenticated peers") { EXPECT_FALSE(allow_not_all.allows_all_authenticated()); } -TEST("SAN requirement without glob pattern is matched as exact string") { +TEST("DNS SAN requirement without glob pattern is matched as exact string") { auto authorized = authorized_peers({policy_with({required_san_dns("hello.world")})}); EXPECT_TRUE(verify(authorized, creds_with_dns_sans({{"hello.world"}}))); EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"foo.bar"}}))); @@ -103,7 +116,7 @@ TEST("SAN requirement without glob pattern is matched as exact string") { EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"hello.world.bar"}}))); } -TEST("SAN requirement can include glob wildcards") { +TEST("DNS SAN requirement can include glob wildcards") { auto authorized = authorized_peers({policy_with({required_san_dns("*.w?rld")})}); EXPECT_TRUE(verify(authorized, creds_with_dns_sans({{"hello.world"}}))); EXPECT_TRUE(verify(authorized, creds_with_dns_sans({{"greetings.w0rld"}}))); @@ -111,23 +124,40 @@ TEST("SAN requirement can include glob wildcards") { EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"world"}}))); } +// FIXME make this RFC 2459-compliant with subdomain matching, case insensitity for host etc +TEST("URI SAN requirement is matched as exact string in cheeky, pragmatic violation of RFC 2459") { + auto authorized = authorized_peers({policy_with({required_san_uri("foo://bar.baz/zoid")})}); + EXPECT_TRUE(verify(authorized, creds_with_uri_sans({{"foo://bar.baz/zoid"}}))); + EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"foo://bar.baz/zoi"}}))); + EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"oo://bar.baz/zoid"}}))); + EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"bar://bar.baz/zoid"}}))); + EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"foo://bar.baz"}}))); + EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"foo://.baz/zoid"}}))); + EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"foo://BAR.baz/zoid"}}))); +} + TEST("multi-SAN policy requires all SANs to be present in certificate") { auto authorized = authorized_peers({policy_with({required_san_dns("hello.world"), - required_san_dns("foo.bar")})}); - EXPECT_TRUE(verify(authorized, creds_with_dns_sans({{"hello.world"}, {"foo.bar"}}))); - // Need both - EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"hello.world"}}))); - EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"foo.bar"}}))); + required_san_dns("foo.bar"), + required_san_uri("foo://bar/baz")})}); + EXPECT_TRUE(verify(authorized, creds_with_sans({{"hello.world"}, {"foo.bar"}}, {{"foo://bar/baz"}}))); + // Need all + EXPECT_FALSE(verify(authorized, creds_with_sans({{"hello.world"}, {"foo.bar"}}, {}))); + EXPECT_FALSE(verify(authorized, creds_with_sans({{"hello.world"}}, {{"foo://bar/baz"}}))); + EXPECT_FALSE(verify(authorized, creds_with_sans({{"hello.world"}}, {}))); + EXPECT_FALSE(verify(authorized, creds_with_sans({{"foo.bar"}}, {}))); + EXPECT_FALSE(verify(authorized, creds_with_sans({}, {{"foo://bar/baz"}}))); // OK with more SANs that strictly required - EXPECT_TRUE(verify(authorized, creds_with_dns_sans({{"hello.world"}, {"foo.bar"}, {"baz.blorg"}}))); + EXPECT_TRUE(verify(authorized, creds_with_sans({{"hello.world"}, {"foo.bar"}, {"baz.blorg"}}, + {{"foo://bar/baz"}, {"hello://world/"}}))); } -TEST("wildcard SAN in certificate is not treated as a wildcard match by policy") { +TEST("wildcard DNS SAN in certificate is not treated as a wildcard match by policy") { auto authorized = authorized_peers({policy_with({required_san_dns("hello.world")})}); EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"*.world"}}))); } -TEST("wildcard SAN in certificate is still matched by wildcard policy SAN") { +TEST("wildcard DNS SAN in certificate is still matched by wildcard policy SAN") { auto authorized = authorized_peers({policy_with({required_san_dns("*.world")})}); EXPECT_TRUE(verify(authorized, creds_with_dns_sans({{"*.world"}}))); } @@ -141,7 +171,8 @@ struct MultiPolicyMatchFixture { MultiPolicyMatchFixture::MultiPolicyMatchFixture() : authorized(authorized_peers({policy_with({required_san_dns("hello.world")}), policy_with({required_san_dns("foo.bar")}), - policy_with({required_san_dns("zoid.berg")})})) + policy_with({required_san_dns("zoid.berg")}), + policy_with({required_san_uri("zoid://be.rg/")})})) {} MultiPolicyMatchFixture::~MultiPolicyMatchFixture() = default; @@ -150,6 +181,7 @@ TEST_F("peer verifies if it matches at least 1 policy of multiple", MultiPolicyM EXPECT_TRUE(verify(f.authorized, creds_with_dns_sans({{"hello.world"}}))); EXPECT_TRUE(verify(f.authorized, creds_with_dns_sans({{"foo.bar"}}))); EXPECT_TRUE(verify(f.authorized, creds_with_dns_sans({{"zoid.berg"}}))); + EXPECT_TRUE(verify(f.authorized, creds_with_uri_sans({{"zoid://be.rg/"}}))); } TEST_F("peer verifies if it matches multiple policies", MultiPolicyMatchFixture) { 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 00459a4e69c..a2bced3f7b4 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 @@ -111,10 +111,12 @@ TEST("can parse single peer policy with multiple requirements") { const char* json = R"({ "required-credentials":[ {"field": "SAN_DNS", "must-match": "hello.world"}, + {"field": "SAN_URI", "must-match": "foo://bar/baz"}, {"field": "CN", "must-match": "goodbye.moon"} ] })"; EXPECT_EQUAL(authorized_peers({policy_with({required_san_dns("hello.world"), + required_san_uri("foo://bar/baz"), required_cn("goodbye.moon")})}), parse_policies(json).authorized_peers()); } -- cgit v1.2.3