Skip to content

Commit 5c8f3d7

Browse files
authored
Changing the wildcard matching logic for client cert common names (#178)
Microsoft ADO ID: 36341347 PR #168 added support for wildcard matching between client cert CNs and trusted CNs. This PR changes the wildcard matching logic based on a discussion with @qiluo-msft and @prsunny. Changes: 1. Wildcard patterns starting with `*` but not `*.` (e.g., `*example.sonic`) are no longer valid. These patterns are skipped during the matching. 2. `*.` is not a valid trusted CN and will be skipped if present. 3. `*` must match one or more characters. For example, `*.example.sonic` will not match `.example.sonic`. **Note 1:** Comparison is always case sensitive, but `*` can match both upper and lower case characters. For example, `*.example.sonic` matches `TEST.example.sonic`, but it does not match `TEST.EXAMPLE.SONIC`. **Note 2:** Multi-level subdomain matching is allowed for wildcard patterns. For example, `*.example.sonic` matches `test.example.sonic`, `sub.test.example.sonic`, `one.sub.test.example.sonic`, and so on.
1 parent efbffea commit 5c8f3d7

3 files changed

Lines changed: 113 additions & 30 deletions

File tree

go-server-server/go/auth.go

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,41 @@ import (
77
)
88

99
func CommonNameMatch(r *http.Request) bool {
10-
// During client cert authentication, after the certificate chain is validated by
11-
// TLS, here we will further check if at least one of the common names in the end-entity certificate
12-
// matches one of the trusted common names of the server config.
13-
for _, peercert := range r.TLS.PeerCertificates {
14-
commonName := peercert.Subject.CommonName
15-
log.Printf("info: CommonName in the client cert: %s", commonName)
16-
for _, name := range trustedCertCommonNames {
17-
if strings.HasPrefix(name, "*") {
10+
// During client cert authentication, after the certificate chain is validated by
11+
// TLS, here we will further check if at least one of the common names in the end-entity certificate
12+
// matches one of the trusted common names in the server config.
13+
14+
for _, name := range trustedCertCommonNames {
15+
is_wildcard := false
16+
domain := name
17+
if strings.HasPrefix(name, "*.") {
18+
if len(name) < 3 {
19+
log.Printf("warning: Skipping invalid trusted common name %s", name)
20+
continue;
21+
}
22+
is_wildcard = true
23+
domain = name[1:] //strip "*" but keep the "." at the beginning
24+
} else if strings.HasPrefix(name, "*") {
25+
log.Printf("warning: Skipping invalid trusted common name %s", name)
26+
continue;
27+
}
28+
for _, peercert := range r.TLS.PeerCertificates {
29+
commonName := peercert.Subject.CommonName
30+
if is_wildcard {
1831
// wildcard common name matching
19-
domain := name[1:] //strip "*"
20-
if strings.HasSuffix(commonName, domain) {
21-
log.Printf("info: CommonName %s in the client cert matches trusted wildcard common name %s", commonName, name)
32+
if len(commonName) > len(domain) && strings.HasSuffix(commonName, domain) {
33+
log.Printf("info: Wildcard match between common name %s in the client cert and trusted common name %s", commonName, name)
34+
return true;
35+
}
36+
} else {
37+
if commonName == name {
38+
log.Printf("info: Exact match with trusted common name %s", name)
2239
return true;
2340
}
24-
} else if commonName == name {
25-
return true;
2641
}
2742
}
2843
}
2944

30-
log.Printf("error: Authentication Fail! None of the CommonNames in the client cert match any of the trusted common names")
31-
return false;
45+
log.Printf("error: Authentication Fail! None of the common names in the client cert match any of the trusted common names")
46+
return false;
3247
}

supervisor/rest_api_test.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[program:rest-api]
2-
command=/usr/sbin/go-server-server.test -test.coverprofile=/coverage.cov -systemtest=true -enablehttps=true -clientcert=/usr/sbin/cert/client/selfsigned.crt -servercert=/usr/sbin/cert/server/selfsigned.crt -serverkey=/usr/sbin/cert/server/selfsigned.key -localapitestdocker=true -clientcertcommonname=test.client.restapi.sonic,*.example.sonic,*test.sonic -loglevel trace
2+
command=/usr/sbin/go-server-server.test -test.coverprofile=/coverage.cov -systemtest=true -enablehttps=true -clientcert=/usr/sbin/cert/client/selfsigned.crt -servercert=/usr/sbin/cert/server/selfsigned.crt -serverkey=/usr/sbin/cert/server/selfsigned.key -localapitestdocker=true -clientcertcommonname=test.client.restapi.sonic,*.example.sonic,*test.sonic,*. -loglevel trace
33
priority=1
44
autostart=false
55
autorestart=false

test/test_restapi.py

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,39 @@ def __exit__(self, exc_type, exc_value, traceback):
6161

6262
class TestClientCertAuth:
6363

64-
# Exact match tests for "test.client.restapi.sonic"
64+
# Exact matching tests for "test.client.restapi.sonic"
6565

6666
def test_exact_match_success(self, setup_restapi_client):
6767
_, _, _, restapi_client = setup_restapi_client
6868
with ClientCert("test.client.restapi.sonic") as client_cert:
6969
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
7070
assert r.status_code == 200
7171

72-
def test_exact_match_failure(self, setup_restapi_client):
72+
def test_exact_match_failure_1(self, setup_restapi_client):
7373
_, _, _, restapi_client = setup_restapi_client
7474
with ClientCert("client.restapi.sonic") as client_cert:
7575
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
7676
assert r.status_code == 401
7777

78-
# Wildcard match tests for "*.example.sonic"
78+
def test_exact_match_failure_2(self, setup_restapi_client):
79+
_, _, _, restapi_client = setup_restapi_client
80+
with ClientCert("test.client.restapi.com") as client_cert:
81+
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
82+
assert r.status_code == 401
83+
84+
def test_exact_match_failure_3(self, setup_restapi_client):
85+
_, _, _, restapi_client = setup_restapi_client
86+
with ClientCert("sub.test.client.restapi.sonic") as client_cert:
87+
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
88+
assert r.status_code == 401
89+
90+
def test_exact_match_failure_4(self, setup_restapi_client):
91+
_, _, _, restapi_client = setup_restapi_client
92+
with ClientCert("TEST.CLIENT.RESTAPI.SONIC") as client_cert:
93+
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
94+
assert r.status_code == 401
95+
96+
# Wildcard matching tests for "*.example.sonic"
7997

8098
def test_wildcard_match_success_1(self, setup_restapi_client):
8199
_, _, _, restapi_client = setup_restapi_client
@@ -85,7 +103,19 @@ def test_wildcard_match_success_1(self, setup_restapi_client):
85103

86104
def test_wildcard_match_success_2(self, setup_restapi_client):
87105
_, _, _, restapi_client = setup_restapi_client
88-
with ClientCert("another.test.example.sonic") as client_cert:
106+
with ClientCert("a.example.sonic") as client_cert:
107+
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
108+
assert r.status_code == 200
109+
110+
def test_wildcard_match_success_3(self, setup_restapi_client):
111+
_, _, _, restapi_client = setup_restapi_client
112+
with ClientCert("sub.test.example.sonic") as client_cert:
113+
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
114+
assert r.status_code == 200
115+
116+
def test_wildcard_match_success_4(self, setup_restapi_client):
117+
_, _, _, restapi_client = setup_restapi_client
118+
with ClientCert("TEST.example.sonic") as client_cert:
89119
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
90120
assert r.status_code == 200
91121

@@ -119,38 +149,76 @@ def test_wildcard_match_failure_5(self, setup_restapi_client):
119149
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
120150
assert r.status_code == 401
121151

122-
# Wildcard match tests for "*test.sonic"
152+
def test_wildcard_match_failure_6(self, setup_restapi_client):
153+
_, _, _, restapi_client = setup_restapi_client
154+
with ClientCert(".example.sonic") as client_cert:
155+
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
156+
assert r.status_code == 401
123157

124-
def test_wildcard_match_success_a(self, setup_restapi_client):
158+
def test_wildcard_match_failure_7(self, setup_restapi_client):
159+
_, _, _, restapi_client = setup_restapi_client
160+
with ClientCert("TEST.EXAMPLE.SONIC") as client_cert:
161+
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
162+
assert r.status_code == 401
163+
164+
# Matching tests for "*test.sonic" (invalid CN)
165+
166+
def test_invalid_match_failure_1(self, setup_restapi_client):
125167
_, _, _, restapi_client = setup_restapi_client
126168
with ClientCert("mytest.sonic") as client_cert:
127169
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
128-
assert r.status_code == 200
170+
assert r.status_code == 401
129171

130-
def test_wildcard_match_success_b(self, setup_restapi_client):
172+
def test_invalid_match_failure_2(self, setup_restapi_client):
131173
_, _, _, restapi_client = setup_restapi_client
132174
with ClientCert("test.sonic") as client_cert:
133175
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
134-
assert r.status_code == 200
176+
assert r.status_code == 401
135177

136-
def test_wildcard_match_success_c(self, setup_restapi_client):
178+
def test_invalid_match_failure_3(self, setup_restapi_client):
137179
_, _, _, restapi_client = setup_restapi_client
138-
with ClientCert("example.test.sonic") as client_cert:
180+
with ClientCert("sub.test.sonic") as client_cert:
139181
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
140-
assert r.status_code == 200
182+
assert r.status_code == 401
141183

142-
def test_wildcard_match_failure_a(self, setup_restapi_client):
184+
def test_invalid_match_failure_4(self, setup_restapi_client):
143185
_, _, _, restapi_client = setup_restapi_client
144186
with ClientCert("est.sonic") as client_cert:
145187
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
146188
assert r.status_code == 401
147189

148-
def test_wildcard_match_failure_b(self, setup_restapi_client):
190+
def test_invalid_match_failure_5(self, setup_restapi_client):
149191
_, _, _, restapi_client = setup_restapi_client
150192
with ClientCert("test.sonico") as client_cert:
151193
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
152194
assert r.status_code == 401
153195

196+
# Corner cases
197+
198+
def test_empty_cn(self, setup_restapi_client):
199+
_, _, _, restapi_client = setup_restapi_client
200+
with ClientCert("") as client_cert:
201+
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
202+
assert r.status_code == 401
203+
204+
def test_missing_tld(self, setup_restapi_client):
205+
_, _, _, restapi_client = setup_restapi_client
206+
with ClientCert("test.example.") as client_cert:
207+
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
208+
assert r.status_code == 401
209+
210+
def test_match_all(self, setup_restapi_client):
211+
_, _, _, restapi_client = setup_restapi_client
212+
with ClientCert("*") as client_cert:
213+
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
214+
assert r.status_code == 401
215+
216+
def test_ends_with_dot(self, setup_restapi_client):
217+
_, _, _, restapi_client = setup_restapi_client
218+
with ClientCert("*.") as client_cert:
219+
r = restapi_client.get_heartbeat(client_cert=(client_cert.cert, client_cert.key))
220+
assert r.status_code == 401
221+
154222

155223
class TestRestApiPositive:
156224
"""Normal behaviour tests"""

0 commit comments

Comments
 (0)