aboutsummaryrefslogtreecommitdiffstats
path: root/vespalib/src/tests/net/tls/policy_checking_certificate_verifier/policy_checking_certificate_verifier_test.cpp
blob: 26db06e35f1f7cc7da59aa8ce55128b7a89e8395 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
#include <vespa/vespalib/net/tls/transport_security_options.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>

using namespace vespalib;
using namespace vespalib::net::tls;

bool dns_glob_matches(vespalib::stringref pattern, vespalib::stringref string_to_check) {
    auto glob = CredentialMatchPattern::create_from_dns_glob(pattern);
    return glob->matches(string_to_check);
}

bool uri_glob_matches(vespalib::stringref pattern, vespalib::stringref string_to_check) {
    auto glob = CredentialMatchPattern::create_from_uri_glob(pattern);
    return glob->matches(string_to_check);
}

void verify_all_glob_types_match(vespalib::stringref pattern, vespalib::stringref string_to_check) {
    EXPECT_TRUE(dns_glob_matches(pattern, string_to_check));
    EXPECT_TRUE(uri_glob_matches(pattern, string_to_check));
}

void verify_all_glob_types_mismatch(vespalib::stringref pattern, vespalib::stringref string_to_check) {
    EXPECT_FALSE(dns_glob_matches(pattern, string_to_check));
    EXPECT_FALSE(uri_glob_matches(pattern, string_to_check));
}

TEST("glob without wildcards matches entire string") {
    verify_all_glob_types_match("foo", "foo");
    verify_all_glob_types_mismatch("foo", "fooo");
    verify_all_glob_types_mismatch("foo", "ffoo");
}

TEST("wildcard glob can match prefix") {
    verify_all_glob_types_match("foo*", "foo");
    verify_all_glob_types_match("foo*", "foobar");
    verify_all_glob_types_mismatch("foo*", "ffoo");
}

TEST("wildcard glob can match suffix") {
    verify_all_glob_types_match("*foo", "foo");
    verify_all_glob_types_match("*foo", "ffoo");
    verify_all_glob_types_mismatch("*foo", "fooo");
}

TEST("wildcard glob can match substring") {
    verify_all_glob_types_match("f*o", "fo");
    verify_all_glob_types_match("f*o", "foo");
    verify_all_glob_types_match("f*o", "ffoo");
    verify_all_glob_types_mismatch("f*o", "boo");
}

TEST("single char DNS glob matches single character") {
    EXPECT_TRUE(dns_glob_matches("f?o", "foo"));
    EXPECT_FALSE(dns_glob_matches("f?o", "fooo"));
    EXPECT_FALSE(dns_glob_matches("f?o", "ffoo"));
}

// Due to URIs being able to contain '?' characters as a query separator, don't use it for wildcarding.
TEST("URI glob matching treats question mark character as literal match") {
    EXPECT_TRUE(uri_glob_matches("f?o", "f?o"));
    EXPECT_FALSE(uri_glob_matches("f?o", "foo"));
    EXPECT_FALSE(uri_glob_matches("f?o", "f?oo"));
}

TEST("wildcard DNS glob does not cross multiple dot delimiter boundaries") {
    EXPECT_TRUE(dns_glob_matches("*.bar.baz", "foo.bar.baz"));
    EXPECT_TRUE(dns_glob_matches("*.bar.baz", ".bar.baz"));
    EXPECT_FALSE(dns_glob_matches("*.bar.baz", "zoid.foo.bar.baz"));
    EXPECT_TRUE(dns_glob_matches("foo.*.baz", "foo.bar.baz"));
    EXPECT_FALSE(dns_glob_matches("foo.*.baz", "foo.bar.zoid.baz"));
}

TEST("wildcard URI glob does not cross multiple fwd slash delimiter boundaries") {
    EXPECT_TRUE(uri_glob_matches("*/bar/baz", "foo/bar/baz"));
    EXPECT_TRUE(uri_glob_matches("*/bar/baz", "/bar/baz"));
    EXPECT_FALSE(uri_glob_matches("*/bar/baz", "bar/baz"));
    EXPECT_FALSE(uri_glob_matches("*/bar/baz", "/bar/baz/"));
    EXPECT_FALSE(uri_glob_matches("*/bar/baz", "zoid/foo/bar/baz"));
    EXPECT_TRUE(uri_glob_matches("foo/*/baz", "foo/bar/baz"));
    EXPECT_FALSE(uri_glob_matches("foo/*/baz", "foo/bar/zoid/baz"));
    EXPECT_TRUE(uri_glob_matches("foo/*/baz", "foo/bar.zoid/baz")); // No special handling of dots
}

TEST("single char DNS glob matches non dot characters only") {
    EXPECT_FALSE(dns_glob_matches("f?o", "f.o"));
}

TEST("special basic regex characters are escaped") {
    verify_all_glob_types_match("$[.\\^", "$[.\\^");
}

TEST("special extended regex characters are ignored") {
    verify_all_glob_types_match("{)(+|]}", "{)(+|]}");
}

// TODO CN + SANs
PeerCredentials creds_with_sans(std::vector<vespalib::string> dns_sans, std::vector<vespalib::string> 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<vespalib::string> dns_sans) {
    PeerCredentials creds;
    creds.dns_sans = std::move(dns_sans);
    return creds;
}

PeerCredentials creds_with_uri_sans(std::vector<vespalib::string> 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;
    return creds;
}

bool verify(AuthorizedPeers authorized_peers, const PeerCredentials& peer_creds) {
    auto verifier = create_verify_callback_from(std::move(authorized_peers));
    return verifier->verify(peer_creds).success();
}

CapabilitySet verify_capabilities(AuthorizedPeers authorized_peers, const PeerCredentials& peer_creds) {
    auto verifier = create_verify_callback_from(std::move(authorized_peers));
    return verifier->verify(peer_creds).granted_capabilities();
}

TEST("Default-constructed AuthorizedPeers does not allow all authenticated peers") {
    EXPECT_FALSE(AuthorizedPeers().allows_all_authenticated());
}

TEST("Specially constructed set of policies allows all authenticated peers") {
    auto allow_all = AuthorizedPeers::allow_all_authenticated();
    EXPECT_TRUE(allow_all.allows_all_authenticated());
    EXPECT_TRUE(verify(allow_all, creds_with_dns_sans({{"anything.goes"}})));
}

TEST("specially constructed set of policies returns full capability set") {
    auto allow_all = AuthorizedPeers::allow_all_authenticated();
    EXPECT_EQUAL(verify_capabilities(allow_all, creds_with_dns_sans({{"anything.goes"}})),
                 CapabilitySet::make_with_all_capabilities());
}

TEST("policy without explicit capability set implicitly returns full capability set") {
    auto authorized = authorized_peers({policy_with({required_san_dns("yolo.swag")})});
    EXPECT_EQUAL(verify_capabilities(authorized, creds_with_dns_sans({{"yolo.swag"}})),
                 CapabilitySet::make_with_all_capabilities());
}

TEST("Non-empty policies do not allow all authenticated peers") {
    auto allow_not_all = authorized_peers({policy_with({required_san_dns("hello.world")})});
    EXPECT_FALSE(allow_not_all.allows_all_authenticated());
}

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"}})));
    EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"hello.worlds"}})));
    EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"hhello.world"}})));
    EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"foo.hello.world"}})));
    EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"hello.world.bar"}})));
}

TEST("DNS SAN requirement can include glob wildcards, delimited by dot character") {
    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"}})));
    EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"hello.wrld"}})));
    EXPECT_FALSE(verify(authorized, creds_with_dns_sans({{"world"}})));
}

// TODO consider making this RFC 2459-compliant with case insensitivity for scheme and host
TEST("URI SAN requirement without glob pattern is matched as exact string") {
    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"}})));
}

// TODO consider making this RFC 2459-compliant with case insensitivity for scheme and host
TEST("URI SAN requirement can include glob wildcards, delimited by fwd slash character") {
    auto authorized = authorized_peers({policy_with({required_san_uri("myscheme://my/*/uri")})});
    EXPECT_TRUE(verify(authorized,  creds_with_uri_sans({{"myscheme://my/cool/uri"}})));
    EXPECT_TRUE(verify(authorized,  creds_with_uri_sans({{"myscheme://my/really.cool/uri"}}))); // Not delimited by dots
    EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"theirscheme://my/cool/uri"}})));
    EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"myscheme://their/cool/uri"}})));
    EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"myscheme://my/cool/uris"}})));
    EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"myscheme://my/swag/uri/"}})));
    EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"myscheme://my/uri"}})));
}

TEST("URI SAN requirement can include query part even though it's rather silly to do so") {
    auto authorized = authorized_peers({policy_with({required_san_uri("myscheme://my/fancy/*?magic")})});
    EXPECT_TRUE(verify(authorized,  creds_with_uri_sans({{"myscheme://my/fancy/uri?magic"}})));
    EXPECT_TRUE(verify(authorized,  creds_with_uri_sans({{"myscheme://my/fancy/?magic"}})));
    EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"myscheme://my/fancy/urimagic"}})));
}

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"),
                                                     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_sans({{"hello.world"}, {"foo.bar"}, {"baz.blorg"}},
                                                    {{"foo://bar/baz"}, {"hello://world/"}})));
}

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 URI SAN in certificate is not treated as a wildcard match by policy") {
    auto authorized = authorized_peers({policy_with({required_san_uri("hello://world")})});
    EXPECT_FALSE(verify(authorized, creds_with_uri_sans({{"hello://*"}})));
}

// TODO this is just by coincidence since we match '*' as any other character, not because we interpret
//  the wildcard in the SAN as anything special during matching. Consider if we need/want to handle explicitly.
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"}})));
}

struct MultiPolicyMatchFixture {
    AuthorizedPeers authorized;
    MultiPolicyMatchFixture();
    ~MultiPolicyMatchFixture();
};

MultiPolicyMatchFixture::MultiPolicyMatchFixture()
    : authorized(authorized_peers({policy_with({required_san_dns("hello.world")},   CapabilitySet::of({cap_1()})),
                                   policy_with({required_san_dns("foo.bar")},       CapabilitySet::of({cap_2()})),
                                   policy_with({required_san_dns("zoid.berg")},     CapabilitySet::of({cap_2(), cap_3()})),
                                   policy_with({required_san_dns("secret.sauce")},  CapabilitySet::make_with_all_capabilities()),
                                   policy_with({required_san_uri("zoid://be.rg/")}, CapabilitySet::of({cap_4()}))}))
{}

MultiPolicyMatchFixture::~MultiPolicyMatchFixture() = default;

TEST_F("peer verifies if it matches at least 1 policy of multiple", MultiPolicyMatchFixture) {
    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("capability set is returned for single matched policy", MultiPolicyMatchFixture) {
    EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"hello.world"}})),
                 CapabilitySet::of({cap_1()}));
    EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"foo.bar"}})),
                 CapabilitySet::of({cap_2()}));
    EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"zoid.berg"}})),
                 CapabilitySet::of({cap_2(), cap_3()}));
    EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"secret.sauce"}})),
                 CapabilitySet::make_with_all_capabilities());
    EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_uri_sans({{"zoid://be.rg/"}})),
                 CapabilitySet::of({cap_4()}));
}

TEST_F("peer verifies if it matches multiple policies", MultiPolicyMatchFixture) {
    EXPECT_TRUE(verify(f.authorized, creds_with_dns_sans({{"hello.world"}, {"zoid.berg"}})));
}

TEST_F("union capability set is returned if multiple policies match", MultiPolicyMatchFixture) {
    EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"hello.world"}, {"foo.bar"}, {"zoid.berg"}})),
                 CapabilitySet::of({cap_1(), cap_2(), cap_3()}));
    EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"hello.world"}, {"foo.bar"}, {"secret.sauce"}})),
                 CapabilitySet::make_with_all_capabilities());
}

TEST_F("peer must match at least 1 of multiple policies", MultiPolicyMatchFixture) {
    EXPECT_FALSE(verify(f.authorized, creds_with_dns_sans({{"does.not.exist"}})));
}

TEST_F("empty capability set is returned if no policies match", MultiPolicyMatchFixture) {
    EXPECT_EQUAL(verify_capabilities(f.authorized, creds_with_dns_sans({{"does.not.exist"}})),
                 CapabilitySet::make_empty());
}

TEST("CN requirement without glob pattern is matched as exact string") {
    auto authorized = authorized_peers({policy_with({required_cn("hello.world")})});
    EXPECT_TRUE(verify(authorized,  creds_with_cn("hello.world")));
    EXPECT_FALSE(verify(authorized, creds_with_cn("foo.bar")));
    EXPECT_FALSE(verify(authorized, creds_with_cn("hello.worlds")));
    EXPECT_FALSE(verify(authorized, creds_with_cn("hhello.world")));
    EXPECT_FALSE(verify(authorized, creds_with_cn("foo.hello.world")));
    EXPECT_FALSE(verify(authorized, creds_with_cn("hello.world.bar")));
}

TEST("CN requirement can include glob wildcards") {
    auto authorized = authorized_peers({policy_with({required_cn("*.w?rld")})});
    EXPECT_TRUE(verify(authorized,  creds_with_cn("hello.world")));
    EXPECT_TRUE(verify(authorized,  creds_with_cn("greetings.w0rld")));
    EXPECT_FALSE(verify(authorized, creds_with_cn("hello.wrld")));
    EXPECT_FALSE(verify(authorized, creds_with_cn("world")));
}

TEST("VerificationResult is not authorized by default") {
    VerificationResult result;
    EXPECT_FALSE(result.success());
    EXPECT_TRUE(result.granted_capabilities().empty());
}

TEST("VerificationResult can be explicitly created as not authorized") {
    auto result = VerificationResult::make_not_authorized();
    EXPECT_FALSE(result.success());
    EXPECT_TRUE(result.granted_capabilities().empty());
}

TEST("VerificationResult can be pre-authorized with all capabilities") {
    auto result = VerificationResult::make_authorized_with_all_capabilities();
    EXPECT_TRUE(result.success());
    EXPECT_FALSE(result.granted_capabilities().empty());
    EXPECT_EQUAL(result.granted_capabilities(), CapabilitySet::make_with_all_capabilities());
}

TEST("VerificationResult can be pre-authorized for an explicit set of capabilities") {
    auto result = VerificationResult::make_authorized_with_capabilities(CapabilitySet::of({cap_2(), cap_3()}));
    EXPECT_TRUE(result.success());
    EXPECT_FALSE(result.granted_capabilities().empty());
    EXPECT_TRUE(result.granted_capabilities().contains(cap_2()));
    EXPECT_TRUE(result.granted_capabilities().contains(cap_3()));
    EXPECT_FALSE(result.granted_capabilities().contains(cap_1()));
}

// TODO test CN _and_ SAN

TEST_MAIN() { TEST_RUN_ALL(); }