diff --git a/docs/web-configuration.md b/docs/web-configuration.md index e461ffd8..4042fd0a 100644 --- a/docs/web-configuration.md +++ b/docs/web-configuration.md @@ -37,7 +37,14 @@ tls_server_config: # CA certificate for client certificate authentication to the server. [ client_ca_file: ] - + + # Verify that the client certificate has a Subject Alternate Name (SAN) + # which is an exact match to an entry in this list, else terminate the + # connection. SAN match can be one or multiple of the following: DNS, + # IP, e-mail, or URI address from https://pkg.go.dev/crypto/x509#Certificate. + [ client_allowed_sans: + [ - ] ] + # Minimum TLS version that is acceptable. [ min_version: | default = "TLS12" ] diff --git a/web/testdata/client2_selfsigned.key b/web/testdata/client2_selfsigned.key index d4dad255..885a41a0 100644 --- a/web/testdata/client2_selfsigned.key +++ b/web/testdata/client2_selfsigned.key @@ -1,6 +1,6 @@ -----BEGIN PRIVATE KEY----- -MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDC8CYtAwKp1uLWXLXFE -Ue2Bz6PijwHZcL7jAxtlk2dbW0GlRQ+rcalHCcnExIIKAAehZANiAATlPRxDnbJb -Zq9u+jh7DyEJumQZFqjIDFdFxfHtI6hwyMtlL6FIwpqn3z4uXs2wx6/NsD4XOChy -j/tXXKCHS/22+51TivjGA53c9bLgc4dK/uJJNSivp0kymbtA5vgKzJE= +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCgZCrQEAUVqznwKRvu +dwwi8wutaRaHHHWDd/IpjJopLhcdvONT7Fv57X0foCvmYFOhZANiAAR/zAKpT17i +U9lmokwDicnziss91+vKhQjy2q4EAe1p7jJ9c/fPofP3Zd09pLhkAUONMu0myXjk +piLE1vvL121tWg3E3F0MLjLBqiSWqSkEZjQj0YSk3NoGWX/gMgm8ZyA= -----END PRIVATE KEY----- diff --git a/web/testdata/client2_selfsigned.pem b/web/testdata/client2_selfsigned.pem index 2bd61a7a..be1426c4 100644 --- a/web/testdata/client2_selfsigned.pem +++ b/web/testdata/client2_selfsigned.pem @@ -1,12 +1,12 @@ -----BEGIN CERTIFICATE----- -MIIByjCCAU+gAwIBAgIUYcG9p4RzCRdvUGa9BWvc6rB/wMYwCgYIKoZIzj0EAwIw -EDEOMAwGA1UEAwwFdGVzdDIwIBcNMjEwODIwMTUzMjE4WhgPMjEyMTA3MjcxNTMy -MThaMBAxDjAMBgNVBAMMBXRlc3QyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE5T0c -Q52yW2avbvo4ew8hCbpkGRaoyAxXRcXx7SOocMjLZS+hSMKap98+Ll7NsMevzbA+ -Fzgoco/7V1ygh0v9tvudU4r4xgOd3PWy4HOHSv7iSTUor6dJMpm7QOb4CsyRo2gw -ZjAdBgNVHQ4EFgQUWpsZ2aWo6WEI2LiNQXoWKYr0rlkwHwYDVR0jBBgwFoAUWpsZ -2aWo6WEI2LiNQXoWKYr0rlkwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUEDDAKBggr -BgEFBQcDAjAKBggqhkjOPQQDAgNpADBmAjEA/Mv4OjCqVw8PzxQW4FJmZNyJB4ps -xkAUBRpDy75n64ICsWKX/Mille0bo+C8d63JAjEA3IH/y1O4oyCaawNpibfcwSZK -7ND9Z+WTJi50EumXUWKirmb/V59ToH5nc10x7NDX +MIIB3DCCAWGgAwIBAgIUJVN8KehL1MmccvLb/mHthSMfnnswCgYIKoZIzj0EAwIw +EDEOMAwGA1UEAwwFdGVzdDMwIBcNMjMwMTEwMTgxMTAwWhgPMjEyMjEyMTcxODEx +MDBaMBAxDjAMBgNVBAMMBXRlc3QzMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEf8wC +qU9e4lPZZqJMA4nJ84rLPdfryoUI8tquBAHtae4yfXP3z6Hz92XdPaS4ZAFDjTLt +Jsl45KYixNb7y9dtbVoNxNxdDC4ywaoklqkpBGY0I9GEpNzaBll/4DIJvGcgo3ow +eDAdBgNVHQ4EFgQUvyvu/TnJyRS7OGdujTbWM/W07yMwHwYDVR0jBBgwFoAUvyvu +/TnJyRS7OGdujTbWM/W07yMwDwYDVR0TAQH/BAUwAwEB/zAQBgNVHREECTAHggV0 +ZXN0MzATBgNVHSUEDDAKBggrBgEFBQcDAjAKBggqhkjOPQQDAgNpADBmAjEAt7HK +knE2MzwZ2B2dgn1/q3ikWDiO20Hbd97jo3tmv87FcF2vMqqJpHjcldJqplfsAjEA +sfAz49y6Sf6LNlNS+Fc/lbOOwcrlzC+J5GJ8OmNoQPsvvDvhzGbwFiVw1M2uMqtG -----END CERTIFICATE----- diff --git a/web/testdata/web_config_auth_client_san.bad.yaml b/web/testdata/web_config_auth_client_san.bad.yaml new file mode 100644 index 00000000..3b0d9189 --- /dev/null +++ b/web/testdata/web_config_auth_client_san.bad.yaml @@ -0,0 +1,7 @@ +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" diff --git a/web/testdata/web_config_auth_client_san.good.yaml b/web/testdata/web_config_auth_client_san.good.yaml new file mode 100644 index 00000000..88eaad1e --- /dev/null +++ b/web/testdata/web_config_auth_client_san.good.yaml @@ -0,0 +1,9 @@ +tls_server_config: + cert_file: "server.crt" + key_file: "server.key" + client_auth_type: "RequireAndVerifyClientCert" + client_ca_file: "client2_selfsigned.pem" + client_allowed_sans: + - "one" + - "test3" + - "two" diff --git a/web/tls_config.go b/web/tls_config.go index 47bbca17..29d16371 100644 --- a/web/tls_config.go +++ b/web/tls_config.go @@ -51,6 +51,7 @@ type TLSConfig struct { MinVersion TLSVersion `yaml:"min_version"` MaxVersion TLSVersion `yaml:"max_version"` PreferServerCipherSuites bool `yaml:"prefer_server_cipher_suites"` + ClientAllowedSans []string `yaml:"client_allowed_sans"` } type FlagConfig struct { @@ -66,6 +67,36 @@ func (t *TLSConfig) SetDirectory(dir string) { t.ClientCAs = config_util.JoinDir(dir, t.ClientCAs) } +// VerifyPeerCertificate will check the SAN entries of the client cert if there is configuration for it +func (t *TLSConfig) VerifyPeerCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error { + // sender cert comes first, see https://www.rfc-editor.org/rfc/rfc5246#section-7.4.2 + cert, err := x509.ParseCertificate(rawCerts[0]) + if err != nil { + return fmt.Errorf("error parsing client certificate: %s", err) + } + + // Build up a slice of strings with all Subject Alternate Name values + sanValues := append(cert.DNSNames, cert.EmailAddresses...) + + for _, ip := range cert.IPAddresses { + sanValues = append(sanValues, ip.String()) + } + + for _, uri := range cert.URIs { + sanValues = append(sanValues, uri.String()) + } + + for _, sanValue := range sanValues { + for _, allowedSan := range t.ClientAllowedSans { + if sanValue == allowedSan { + return nil + } + } + } + + return fmt.Errorf("could not find allowed SANs in client cert, found: %v", t.ClientAllowedSans) +} + type HTTPConfig struct { HTTP2 bool `yaml:"http2"` Header map[string]string `yaml:"headers,omitempty"` @@ -163,6 +194,11 @@ func ConfigToTLSConfig(c *TLSConfig) (*tls.Config, error) { cfg.ClientCAs = clientCAPool } + if c.ClientAllowedSans != nil { + // verify that the client cert contains an allowed SAN + cfg.VerifyPeerCertificate = c.VerifyPeerCertificate + } + switch c.ClientAuth { case "RequestClientCert": cfg.ClientAuth = tls.RequestClientCert diff --git a/web/tls_config_test.go b/web/tls_config_test.go index ceb7f9b2..b2479338 100644 --- a/web/tls_config_test.go +++ b/web/tls_config_test.go @@ -67,6 +67,7 @@ var ( "Bad certificate": regexp.MustCompile(`bad certificate`), "Invalid value": regexp.MustCompile(`invalid value for`), "Invalid header": regexp.MustCompile(`HTTP header ".*" can not be configured`), + "Invalid client cert": regexp.MustCompile(`bad certificate`), } ) @@ -347,6 +348,20 @@ func TestServerBehaviour(t *testing.T) { ClientCertificate: "client2_selfsigned", ExpectedError: ErrorMap["Bad certificate"], }, + { + Name: `valid tls config yml and tls client with VerifyPeerCertificate (present good SAN DNS entry)`, + YAMLConfigPath: "testdata/web_config_auth_client_san.good.yaml", + UseTLSClient: true, + ClientCertificate: "client2_selfsigned", + ExpectedError: nil, + }, + { + Name: `valid tls config yml and tls client with VerifyPeerCertificate (present invalid SAN DNS entries)`, + YAMLConfigPath: "testdata/web_config_auth_client_san.bad.yaml", + UseTLSClient: true, + ClientCertificate: "client2_selfsigned", + ExpectedError: ErrorMap["Invalid client cert"], + }, } for _, testInputs := range testTables { t.Run(testInputs.Name, testInputs.Test)