Skip to content

Commit

Permalink
Add path exclusion support to mTLS authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
rzetelskik committed May 11, 2023
1 parent d217ea9 commit 20ed165
Show file tree
Hide file tree
Showing 7 changed files with 540 additions and 29 deletions.
12 changes: 12 additions & 0 deletions web/authentication/x509/testdata/selfsigned.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBxzCCAU2gAwIBAgIUGCNnsX0qd0HD7UaQsx67ze0UaNowCgYIKoZIzj0EAwIw
DzENMAsGA1UEAwwEdGVzdDAgFw0yMTA4MjAxNDQ5MTRaGA8yMTIxMDcyNzE0NDkx
NFowDzENMAsGA1UEAwwEdGVzdDB2MBAGByqGSM49AgEGBSuBBAAiA2IABLFRLjQB
XViHUAEIsKglwb0HxPC/+CDa1TTOp1b0WErYW7Xcx5mRNEksVWAXOWYKPej10hfy
JSJE/2NiRAbrAcPjiRv01DgDt+OzwM4A0ZYqBj/3qWJKH/Kc8oKhY41bzKNoMGYw
HQYDVR0OBBYEFPRbKtRBgw+AZ0b6T8oWw/+QoyjaMB8GA1UdIwQYMBaAFPRbKtRB
gw+AZ0b6T8oWw/+QoyjaMA8GA1UdEwEB/wQFMAMBAf8wEwYDVR0lBAwwCgYIKwYB
BQUHAwIwCgYIKoZIzj0EAwIDaAAwZQIwZqwXMJiTycZdmLN+Pwk/8Sb7wQazbocb
16Zw5mZXqFJ4K+74OQMZ33i82hYohtE/AjEAn0a8q8QupgiXpr0I/PvGTRKqLQRM
0mptBvpn/DcB2p3Hi80GJhtchz9Z0OqbMX4S
-----END CERTIFICATE-----
117 changes: 117 additions & 0 deletions web/authentication/x509/x509.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package x509

import (
"crypto/x509"
"encoding/hex"
"fmt"
"net/http"
"strings"

"github.com/prometheus/exporter-toolkit/web/authentication"
)

type RequireClientCertsFunc func() bool

type VerifyOptionsFunc func() x509.VerifyOptions

type VerifyPeerCertificateFunc func([][]byte, [][]*x509.Certificate) error

type X509Authenticator struct {
requireClientCertsFn RequireClientCertsFunc
verifyOptionsFn VerifyOptionsFunc
verifyPeerCertificateFn VerifyPeerCertificateFunc
}

func columnSeparatedHex(d []byte) string {
h := strings.ToUpper(hex.EncodeToString(d))
var sb strings.Builder
for i, r := range h {
sb.WriteRune(r)
if i%2 == 1 && i != len(h)-1 {
sb.WriteRune(':')
}
}
return sb.String()
}

func certificateIdentifier(c *x509.Certificate) string {
return fmt.Sprintf(
"SN=%d, SKID=%s, AKID=%s",
c.SerialNumber,
columnSeparatedHex(c.SubjectKeyId),
columnSeparatedHex(c.AuthorityKeyId),
)
}

func (x *X509Authenticator) Authenticate(r *http.Request) (bool, string, error) {
if r.TLS == nil {
return false, "No TLS connection state in request", nil
}

if len(r.TLS.PeerCertificates) == 0 && x.requireClientCertsFn() {
return false, "A certificate is required to be sent by the client.", nil
}

var verifiedChains [][]*x509.Certificate
if len(r.TLS.PeerCertificates) > 0 && x.verifyOptionsFn != nil {
opts := x.verifyOptionsFn()
if opts.Intermediates == nil && len(r.TLS.PeerCertificates) > 1 {
opts.Intermediates = x509.NewCertPool()
for _, cert := range r.TLS.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}
}

chains, err := r.TLS.PeerCertificates[0].Verify(opts)
if err != nil {
return false, fmt.Sprintf("verifying certificate %s failed: %v", certificateIdentifier(r.TLS.PeerCertificates[0]), err), nil
}

verifiedChains = chains
}

if x.verifyPeerCertificateFn != nil {
rawCerts := make([][]byte, 0, len(r.TLS.PeerCertificates))
for _, c := range r.TLS.PeerCertificates {
rawCerts = append(rawCerts, c.Raw)
}

err := x.verifyPeerCertificateFn(rawCerts, verifiedChains)
if err != nil {
return false, fmt.Sprintf("verifying peer certificate failed: %v", err), nil
}
}

return true, "", nil
}

func NewX509Authenticator(requireClientCertsFn RequireClientCertsFunc, verifyOptionsFn VerifyOptionsFunc, verifyPeerCertificateFn VerifyPeerCertificateFunc) authentication.Authenticator {
return &X509Authenticator{
requireClientCertsFn: requireClientCertsFn,
verifyOptionsFn: verifyOptionsFn,
verifyPeerCertificateFn: verifyPeerCertificateFn,
}
}

var _ authentication.Authenticator = &X509Authenticator{}

// DefaultVerifyOptions returns VerifyOptions that use the system root certificates, current time,
// and requires certificates to be valid for client auth (x509.ExtKeyUsageClientAuth)
func DefaultVerifyOptions() x509.VerifyOptions {
return x509.VerifyOptions{
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
}
235 changes: 235 additions & 0 deletions web/authentication/x509/x509_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package x509

import (
"crypto/tls"
"crypto/x509"
_ "embed"
"encoding/pem"
"errors"
"net/http"
"testing"

"github.com/go-kit/log"
)

//go:embed testdata/selfsigned.pem
var selfsignedPEM []byte

func TestX509Authenticator_Authenticate(t *testing.T) {
ts := []struct {
Name string

RequireClientCertsFn RequireClientCertsFunc
VerifyOptionsFn VerifyOptionsFunc
VerifyPeerCertificateFn VerifyPeerCertificateFunc

Certs []*x509.Certificate

ExpectAuthenticated bool
ExpectedResponse string
ExpectedError error
}{
{
Name: "Certs not required, certs not provided",
RequireClientCertsFn: func() bool {
return false
},
ExpectAuthenticated: true,
ExpectedError: nil,
},
{
Name: "Certs required, certs not provided",
RequireClientCertsFn: func() bool {
return true
},
ExpectAuthenticated: false,
ExpectedResponse: "A certificate is required to be sent by the client.",
ExpectedError: nil,
},
{
Name: "Certs not required, no verify, selfsigned cert provided",
RequireClientCertsFn: func() bool {
return false
},
Certs: getCerts(t, selfsignedPEM),
ExpectAuthenticated: true,
ExpectedError: nil,
},
{
Name: "Certs required, no verify, selfsigned cert provided",
RequireClientCertsFn: func() bool {
return true
},
Certs: getCerts(t, selfsignedPEM),
ExpectAuthenticated: true,
ExpectedError: nil,
},
{
Name: "Certs not required, verify, selfsigned cert provided",
RequireClientCertsFn: func() bool {
return false
},
VerifyOptionsFn: func() x509.VerifyOptions {
opts := DefaultVerifyOptions()
opts.Roots = getCertPool(t, selfsignedPEM)
return opts
},
Certs: getCerts(t, selfsignedPEM),
ExpectAuthenticated: true,
ExpectedError: nil,
},
{
Name: "Certs not required, verify, no certs provided",
RequireClientCertsFn: func() bool {
return false
},
VerifyOptionsFn: func() x509.VerifyOptions {
opts := DefaultVerifyOptions()
opts.Roots = getCertPool(t, selfsignedPEM)
return opts
},
ExpectAuthenticated: true,
ExpectedError: nil,
},
{
Name: "Certs required, verify, selfsigned cert provided",
RequireClientCertsFn: func() bool {
return true
},
VerifyOptionsFn: func() x509.VerifyOptions {
opts := DefaultVerifyOptions()
opts.Roots = getCertPool(t, selfsignedPEM)
return opts
},
Certs: getCerts(t, selfsignedPEM),
ExpectAuthenticated: true,
ExpectedError: nil,
},
{
Name: "Certs required, verify, selfsigned cert provided, invalid peer certificate",
RequireClientCertsFn: func() bool {
return true
},
VerifyOptionsFn: func() x509.VerifyOptions {
opts := DefaultVerifyOptions()
opts.Roots = getCertPool(t, selfsignedPEM)
return opts
},
VerifyPeerCertificateFn: func(_ [][]byte, _ [][]*x509.Certificate) error {
return errors.New("invalid peer certificate")
},
Certs: getCerts(t, selfsignedPEM),
ExpectAuthenticated: false,
ExpectedResponse: "verifying peer certificate failed: invalid peer certificate",
ExpectedError: nil,
},
{
Name: "RequireAndVerifyClientCert, selfsigned certs, valid peer certificate",
RequireClientCertsFn: func() bool {
return true
},
VerifyOptionsFn: func() x509.VerifyOptions {
opts := DefaultVerifyOptions()
opts.Roots = getCertPool(t, selfsignedPEM)
return opts
},
VerifyPeerCertificateFn: func(_ [][]byte, _ [][]*x509.Certificate) error {
return nil
},
Certs: getCerts(t, selfsignedPEM),
ExpectAuthenticated: true,
ExpectedError: nil,
},
}

for _, tt := range ts {
t.Run(tt.Name, func(t *testing.T) {
req := makeDefaultRequest(t)
req.TLS = &tls.ConnectionState{
PeerCertificates: tt.Certs,
}

a := NewX509Authenticator(tt.RequireClientCertsFn, tt.VerifyOptionsFn, tt.VerifyPeerCertificateFn)
authenticated, response, err := a.Authenticate(req)

if err != nil && tt.ExpectedError == nil {
t.Errorf("Got unexpected error: %v", err)
}

if err == nil && tt.ExpectedError != nil {
t.Errorf("Expected error %v, got none", tt.ExpectedError)
}

if err != nil && tt.ExpectedError != nil && !errors.Is(err, tt.ExpectedError) {
t.Errorf("Expected error %v, got %v", tt.ExpectedError, err)
}

if tt.ExpectedResponse != response {
t.Errorf("Expected response %v, got %v", tt.ExpectedResponse, response)
}

if tt.ExpectAuthenticated != authenticated {
t.Errorf("Expected authenticated %v, got %v", tt.ExpectAuthenticated, authenticated)
}
})
}
}

type noOpLogger struct{}

func (noOpLogger) Log(...interface{}) error {
return nil
}

var _ log.Logger = &noOpLogger{}

func makeDefaultRequest(t *testing.T) *http.Request {
t.Helper()

req, err := http.NewRequest(http.MethodGet, "/", nil)
if err != nil {
t.Fatalf("Error creating request: %v", err)
}
return req
}

func getCertPool(t *testing.T, pemData ...[]byte) *x509.CertPool {
t.Helper()

pool := x509.NewCertPool()
certs := getCerts(t, pemData...)
for _, c := range certs {
pool.AddCert(c)
}

return pool
}

func getCerts(t *testing.T, pemData ...[]byte) []*x509.Certificate {
t.Helper()

certs := make([]*x509.Certificate, 0)
for _, pd := range pemData {
pemBlock, _ := pem.Decode(pd)
cert, err := x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
t.Fatalf("Error parsing cert: %v", err)
}
certs = append(certs, cert)
}

return certs
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
tls_server_config:
cert_file: "server.crt"
key_file: "server.key"
client_auth_type: "RequireAndVerifyClientCert"
client_ca_file: "client_selfsigned.pem"

auth_excluded_paths:
- "/somepath"
10 changes: 10 additions & 0 deletions web/testdata/web_config_auth_client_san.authexcludedpaths.bad.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
tls_server_config:
cert_file: "server.crt"
key_file: "server.key"
client_auth_type: "RequireAndVerifyClientCert"
client_ca_file: "client2_selfsigned.pem"
client_allowed_sans:
- "bad"

auth_excluded_paths:
- "/somepath"
Loading

0 comments on commit 20ed165

Please sign in to comment.