Skip to content

Commit 54f7515

Browse files
committed
Add path exclusion support to mTLS authentication
Signed-off-by: Kacper Rzetelski <[email protected]>
1 parent c184361 commit 54f7515

File tree

8 files changed

+567
-36
lines changed

8 files changed

+567
-36
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIB3DCCAWGgAwIBAgIUJVN8KehL1MmccvLb/mHthSMfnnswCgYIKoZIzj0EAwIw
3+
EDEOMAwGA1UEAwwFdGVzdDMwIBcNMjMwMTEwMTgxMTAwWhgPMjEyMjEyMTcxODEx
4+
MDBaMBAxDjAMBgNVBAMMBXRlc3QzMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf8wC
5+
qU9e4lPZZqJMA4nJ84rLPdfryoUI8tquBAHtae4yfXP3z6Hz92XdPaS4ZAFDjTLt
6+
Jsl45KYixNb7y9dtbVoNxNxdDC4ywaoklqkpBGY0I9GEpNzaBll/4DIJvGcgo3ow
7+
eDAdBgNVHQ4EFgQUvyvu/TnJyRS7OGdujTbWM/W07yMwHwYDVR0jBBgwFoAUvyvu
8+
/TnJyRS7OGdujTbWM/W07yMwDwYDVR0TAQH/BAUwAwEB/zAQBgNVHREECTAHggV0
9+
ZXN0MzATBgNVHSUEDDAKBggrBgEFBQcDAjAKBggqhkjOPQQDAgNpADBmAjEAt7HK
10+
knE2MzwZ2B2dgn1/q3ikWDiO20Hbd97jo3tmv87FcF2vMqqJpHjcldJqplfsAjEA
11+
sfAz49y6Sf6LNlNS+Fc/lbOOwcrlzC+J5GJ8OmNoQPsvvDvhzGbwFiVw1M2uMqtG
12+
-----END CERTIFICATE-----
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIBxzCCAU2gAwIBAgIUGCNnsX0qd0HD7UaQsx67ze0UaNowCgYIKoZIzj0EAwIw
3+
DzENMAsGA1UEAwwEdGVzdDAgFw0yMTA4MjAxNDQ5MTRaGA8yMTIxMDcyNzE0NDkx
4+
NFowDzENMAsGA1UEAwwEdGVzdDB2MBAGByqGSM49AgEGBSuBBAAiA2IABLFRLjQB
5+
XViHUAEIsKglwb0HxPC/+CDa1TTOp1b0WErYW7Xcx5mRNEksVWAXOWYKPej10hfy
6+
JSJE/2NiRAbrAcPjiRv01DgDt+OzwM4A0ZYqBj/3qWJKH/Kc8oKhY41bzKNoMGYw
7+
HQYDVR0OBBYEFPRbKtRBgw+AZ0b6T8oWw/+QoyjaMB8GA1UdIwQYMBaAFPRbKtRB
8+
gw+AZ0b6T8oWw/+QoyjaMA8GA1UdEwEB/wQFMAMBAf8wEwYDVR0lBAwwCgYIKwYB
9+
BQUHAwIwCgYIKoZIzj0EAwIDaAAwZQIwZqwXMJiTycZdmLN+Pwk/8Sb7wQazbocb
10+
16Zw5mZXqFJ4K+74OQMZ33i82hYohtE/AjEAn0a8q8QupgiXpr0I/PvGTRKqLQRM
11+
0mptBvpn/DcB2p3Hi80GJhtchz9Z0OqbMX4S
12+
-----END CERTIFICATE-----

web/authentication/x509/x509.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package x509
15+
16+
import (
17+
"crypto/x509"
18+
"encoding/hex"
19+
"fmt"
20+
"net/http"
21+
"strings"
22+
23+
"github.com/prometheus/exporter-toolkit/web/authentication"
24+
)
25+
26+
type RequireClientCertsFunc func() bool
27+
28+
type VerifyOptionsFunc func() x509.VerifyOptions
29+
30+
type VerifyPeerCertificateFunc func([][]byte, [][]*x509.Certificate) error
31+
32+
type X509Authenticator struct {
33+
requireClientCertsFn RequireClientCertsFunc
34+
verifyOptionsFn VerifyOptionsFunc
35+
verifyPeerCertificateFn VerifyPeerCertificateFunc
36+
}
37+
38+
func columnSeparatedHex(d []byte) string {
39+
h := strings.ToUpper(hex.EncodeToString(d))
40+
var sb strings.Builder
41+
for i, r := range h {
42+
sb.WriteRune(r)
43+
if i%2 == 1 && i != len(h)-1 {
44+
sb.WriteRune(':')
45+
}
46+
}
47+
return sb.String()
48+
}
49+
50+
func certificateIdentifier(c *x509.Certificate) string {
51+
return fmt.Sprintf(
52+
"SN=%d, SKID=%s, AKID=%s",
53+
c.SerialNumber,
54+
columnSeparatedHex(c.SubjectKeyId),
55+
columnSeparatedHex(c.AuthorityKeyId),
56+
)
57+
}
58+
59+
func (x *X509Authenticator) Authenticate(r *http.Request) (bool, string, error) {
60+
if r.TLS == nil {
61+
return false, "No TLS connection state in request", nil
62+
}
63+
64+
if len(r.TLS.PeerCertificates) == 0 && x.requireClientCertsFn() {
65+
return false, "A certificate is required to be sent by the client.", nil
66+
}
67+
68+
var verifiedChains [][]*x509.Certificate
69+
if len(r.TLS.PeerCertificates) > 0 && x.verifyOptionsFn != nil {
70+
opts := x.verifyOptionsFn()
71+
if opts.Intermediates == nil && len(r.TLS.PeerCertificates) > 1 {
72+
opts.Intermediates = x509.NewCertPool()
73+
for _, cert := range r.TLS.PeerCertificates[1:] {
74+
opts.Intermediates.AddCert(cert)
75+
}
76+
}
77+
78+
chains, err := r.TLS.PeerCertificates[0].Verify(opts)
79+
if err != nil {
80+
return false, fmt.Sprintf("verifying certificate %s failed: %v", certificateIdentifier(r.TLS.PeerCertificates[0]), err), nil
81+
}
82+
83+
verifiedChains = chains
84+
}
85+
86+
if x.verifyPeerCertificateFn != nil {
87+
rawCerts := make([][]byte, 0, len(r.TLS.PeerCertificates))
88+
for _, c := range r.TLS.PeerCertificates {
89+
rawCerts = append(rawCerts, c.Raw)
90+
}
91+
92+
err := x.verifyPeerCertificateFn(rawCerts, verifiedChains)
93+
if err != nil {
94+
return false, fmt.Sprintf("verifying peer certificate failed: %v", err), nil
95+
}
96+
}
97+
98+
return true, "", nil
99+
}
100+
101+
func NewX509Authenticator(requireClientCertsFn RequireClientCertsFunc, verifyOptionsFn VerifyOptionsFunc, verifyPeerCertificateFn VerifyPeerCertificateFunc) authentication.Authenticator {
102+
return &X509Authenticator{
103+
requireClientCertsFn: requireClientCertsFn,
104+
verifyOptionsFn: verifyOptionsFn,
105+
verifyPeerCertificateFn: verifyPeerCertificateFn,
106+
}
107+
}
108+
109+
var _ authentication.Authenticator = &X509Authenticator{}
110+
111+
// DefaultVerifyOptions returns VerifyOptions that use the system root certificates, current time,
112+
// and requires certificates to be valid for client auth (x509.ExtKeyUsageClientAuth)
113+
func DefaultVerifyOptions() x509.VerifyOptions {
114+
return x509.VerifyOptions{
115+
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
116+
}
117+
}
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// Copyright 2023 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package x509
15+
16+
import (
17+
"crypto/tls"
18+
"crypto/x509"
19+
_ "embed"
20+
"encoding/pem"
21+
"errors"
22+
"testing"
23+
24+
"github.com/prometheus/exporter-toolkit/web/authentication/testhelpers"
25+
)
26+
27+
//go:embed testdata/client_selfsigned.pem
28+
var clientSelfsignedPEM []byte
29+
30+
//go:embed testdata/client2_selfsigned.pem
31+
var client2SelfsignedPEM []byte
32+
33+
func TestX509Authenticator_Authenticate(t *testing.T) {
34+
t.Parallel()
35+
36+
tt := []struct {
37+
Name string
38+
39+
RequireClientCertsFn RequireClientCertsFunc
40+
VerifyOptionsFn VerifyOptionsFunc
41+
VerifyPeerCertificateFn VerifyPeerCertificateFunc
42+
43+
Certs []*x509.Certificate
44+
45+
ExpectAuthenticated bool
46+
ExpectedReason string
47+
ExpectedError error
48+
}{
49+
{
50+
Name: "Certs not required, certs not provided",
51+
RequireClientCertsFn: func() bool {
52+
return false
53+
},
54+
ExpectAuthenticated: true,
55+
ExpectedError: nil,
56+
},
57+
{
58+
Name: "Certs required, certs not provided",
59+
RequireClientCertsFn: func() bool {
60+
return true
61+
},
62+
ExpectAuthenticated: false,
63+
ExpectedReason: "A certificate is required to be sent by the client.",
64+
ExpectedError: nil,
65+
},
66+
{
67+
Name: "Certs not required, no verify, selfsigned cert provided",
68+
RequireClientCertsFn: func() bool {
69+
return false
70+
},
71+
Certs: getCerts(t, clientSelfsignedPEM),
72+
ExpectAuthenticated: true,
73+
ExpectedError: nil,
74+
},
75+
{
76+
Name: "Certs required, no verify, selfsigned cert provided",
77+
RequireClientCertsFn: func() bool {
78+
return true
79+
},
80+
Certs: getCerts(t, clientSelfsignedPEM),
81+
ExpectAuthenticated: true,
82+
ExpectedError: nil,
83+
},
84+
{
85+
Name: "Certs not required, verify, selfsigned cert provided",
86+
RequireClientCertsFn: func() bool {
87+
return false
88+
},
89+
VerifyOptionsFn: func() x509.VerifyOptions {
90+
opts := DefaultVerifyOptions()
91+
opts.Roots = getCertPool(t, clientSelfsignedPEM)
92+
return opts
93+
},
94+
Certs: getCerts(t, clientSelfsignedPEM),
95+
ExpectAuthenticated: true,
96+
ExpectedError: nil,
97+
},
98+
{
99+
Name: "Certs not required, verify, no certs provided",
100+
RequireClientCertsFn: func() bool {
101+
return false
102+
},
103+
VerifyOptionsFn: func() x509.VerifyOptions {
104+
opts := DefaultVerifyOptions()
105+
opts.Roots = getCertPool(t, clientSelfsignedPEM)
106+
return opts
107+
},
108+
ExpectAuthenticated: true,
109+
ExpectedError: nil,
110+
},
111+
{
112+
Name: "Certs required, verify, selfsigned cert provided",
113+
RequireClientCertsFn: func() bool {
114+
return true
115+
},
116+
VerifyOptionsFn: func() x509.VerifyOptions {
117+
opts := DefaultVerifyOptions()
118+
opts.Roots = getCertPool(t, clientSelfsignedPEM)
119+
return opts
120+
},
121+
Certs: getCerts(t, clientSelfsignedPEM),
122+
ExpectAuthenticated: true,
123+
ExpectedError: nil,
124+
},
125+
{
126+
Name: "Certs required, verify, invalid selfsigned cert provided",
127+
RequireClientCertsFn: func() bool {
128+
return true
129+
},
130+
VerifyOptionsFn: func() x509.VerifyOptions {
131+
opts := DefaultVerifyOptions()
132+
opts.Roots = getCertPool(t, clientSelfsignedPEM)
133+
return opts
134+
},
135+
Certs: getCerts(t, client2SelfsignedPEM),
136+
ExpectAuthenticated: false,
137+
ExpectedReason: "verifying certificate SN=213094436555767319277040831510558969429548310139," +
138+
" SKID=BF:2B:EE:FD:39:C9:C9:14:BB:38:67:6E:8D:36:D6:33:F5:B4:EF:23," +
139+
" AKID=BF:2B:EE:FD:39:C9:C9:14:BB:38:67:6E:8D:36:D6:33:F5:B4:EF:23" +
140+
" failed: x509: certificate signed by unknown authority",
141+
ExpectedError: nil,
142+
},
143+
{
144+
Name: "Certs required, verify, selfsigned cert provided, invalid peer certificate",
145+
RequireClientCertsFn: func() bool {
146+
return true
147+
},
148+
VerifyOptionsFn: func() x509.VerifyOptions {
149+
opts := DefaultVerifyOptions()
150+
opts.Roots = getCertPool(t, clientSelfsignedPEM)
151+
return opts
152+
},
153+
VerifyPeerCertificateFn: func(_ [][]byte, _ [][]*x509.Certificate) error {
154+
return errors.New("invalid peer certificate")
155+
},
156+
Certs: getCerts(t, clientSelfsignedPEM),
157+
ExpectAuthenticated: false,
158+
ExpectedReason: "verifying peer certificate failed: invalid peer certificate",
159+
ExpectedError: nil,
160+
},
161+
{
162+
Name: "RequireAndVerifyClientCert, selfsigned certs, valid peer certificate",
163+
RequireClientCertsFn: func() bool {
164+
return true
165+
},
166+
VerifyOptionsFn: func() x509.VerifyOptions {
167+
opts := DefaultVerifyOptions()
168+
opts.Roots = getCertPool(t, clientSelfsignedPEM)
169+
return opts
170+
},
171+
VerifyPeerCertificateFn: func(_ [][]byte, _ [][]*x509.Certificate) error {
172+
return nil
173+
},
174+
Certs: getCerts(t, clientSelfsignedPEM),
175+
ExpectAuthenticated: true,
176+
ExpectedError: nil,
177+
},
178+
}
179+
180+
for _, tc := range tt {
181+
t.Run(tc.Name, func(t *testing.T) {
182+
t.Parallel()
183+
184+
req := testhelpers.MakeDefaultRequest(t)
185+
req.TLS = &tls.ConnectionState{
186+
PeerCertificates: tc.Certs,
187+
}
188+
189+
a := NewX509Authenticator(tc.RequireClientCertsFn, tc.VerifyOptionsFn, tc.VerifyPeerCertificateFn)
190+
authenticated, reason, err := a.Authenticate(req)
191+
192+
if err != nil && tc.ExpectedError == nil {
193+
t.Errorf("Got unexpected error: %v", err)
194+
}
195+
196+
if err == nil && tc.ExpectedError != nil {
197+
t.Errorf("Expected error %v, got none", tc.ExpectedError)
198+
}
199+
200+
if err != nil && tc.ExpectedError != nil && !errors.Is(err, tc.ExpectedError) {
201+
t.Errorf("Expected error %v, got %v", tc.ExpectedError, err)
202+
}
203+
204+
if tc.ExpectedReason != reason {
205+
t.Errorf("Expected reason %v, got %v", tc.ExpectedReason, reason)
206+
}
207+
208+
if tc.ExpectAuthenticated != authenticated {
209+
t.Errorf("Expected authenticated %v, got %v", tc.ExpectAuthenticated, authenticated)
210+
}
211+
})
212+
}
213+
}
214+
215+
func getCertPool(t *testing.T, pemData ...[]byte) *x509.CertPool {
216+
t.Helper()
217+
218+
pool := x509.NewCertPool()
219+
certs := getCerts(t, pemData...)
220+
for _, c := range certs {
221+
pool.AddCert(c)
222+
}
223+
224+
return pool
225+
}
226+
227+
func getCerts(t *testing.T, pemData ...[]byte) []*x509.Certificate {
228+
t.Helper()
229+
230+
certs := make([]*x509.Certificate, 0)
231+
for _, pd := range pemData {
232+
pemBlock, _ := pem.Decode(pd)
233+
cert, err := x509.ParseCertificate(pemBlock.Bytes)
234+
if err != nil {
235+
t.Fatalf("Error parsing cert: %v", err)
236+
}
237+
certs = append(certs, cert)
238+
}
239+
240+
return certs
241+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
tls_server_config:
2+
cert_file: "server.crt"
3+
key_file: "server.key"
4+
client_auth_type: "RequireAndVerifyClientCert"
5+
client_ca_file: "client_selfsigned.pem"
6+
7+
auth_excluded_paths:
8+
- "/somepath"

0 commit comments

Comments
 (0)