diff --git a/config/configtls/README.md b/config/configtls/README.md index b7ea4e84e76..4a751591628 100644 --- a/config/configtls/README.md +++ b/config/configtls/README.md @@ -72,6 +72,14 @@ Additionally certificates may be reloaded by setting the below configuration. Accepts a [duration string](https://pkg.go.dev/time#ParseDuration), valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". +- `trusted_keys` (optional) : TrustedKeys allows specifying a list of SHA256 hashes to trust when verifying client certificates. This allows only trusting a specific set of keys, rather than all the keys signed by a given CA. Note: The client certificate still has to pass standard TLS verification, so you must still set the required CA values. + +Example: +``` + trusted_keys: + - "29:9D:39:8E:26:E7:72:88:D7:5D:2D:34:FE:8C:41:FA:9F:F1:C3:D7" +``` + How TLS/mTLS is configured depends on whether configuring the client or server. See below for examples. diff --git a/config/configtls/configtls.go b/config/configtls/configtls.go index 2ce0490b16c..38207661afb 100644 --- a/config/configtls/configtls.go +++ b/config/configtls/configtls.go @@ -5,12 +5,14 @@ package configtls // import "go.opentelemetry.io/collector/config/configtls" import ( "context" + "crypto/sha256" "crypto/tls" "crypto/x509" "errors" "fmt" "os" "path/filepath" + "strings" "sync" "time" @@ -38,6 +40,9 @@ type Config struct { // In memory PEM encoded cert. (optional) CAPem configopaque.String `mapstructure:"ca_pem"` + // A set of SHA256 fingerprints that represent the client certificates we should trust. + TrustedKeys []string `mapstructure:"trusted_keys"` + // If true, load system CA certificates pool in addition to the certificates // configured in this struct. IncludeSystemCACertsPool bool `mapstructure:"include_system_ca_certs_pool"` @@ -177,6 +182,23 @@ func (r *certReloader) GetCertificate() (*tls.Certificate, error) { return r.cert, nil } +func (c Config) validatePeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + if len(c.TrustedKeys) == 0 { + return nil + } + + for _, chain := range verifiedChains { + hash := fingerprintCertificate(chain[0]) + for _, trusted := range c.TrustedKeys { + if hash == trusted { + return nil + } + } + } + + return fmt.Errorf("key not in trusted_keys") +} + func (c Config) Validate() error { if c.hasCAFile() && c.hasCAPem() { return fmt.Errorf("provide either a CA file or the PEM-encoded string, but not both") @@ -233,12 +255,13 @@ func (c Config) loadTLSConfig() (*tls.Config, error) { } return &tls.Config{ - RootCAs: certPool, - GetCertificate: getCertificate, - GetClientCertificate: getClientCertificate, - MinVersion: minTLS, - MaxVersion: maxTLS, - CipherSuites: cipherSuites, + RootCAs: certPool, + GetCertificate: getCertificate, + GetClientCertificate: getClientCertificate, + MinVersion: minTLS, + MaxVersion: maxTLS, + CipherSuites: cipherSuites, + VerifyPeerCertificate: c.validatePeerCertificate, }, nil } @@ -448,3 +471,20 @@ var tlsVersions = map[string]uint16{ "1.2": tls.VersionTLS12, "1.3": tls.VersionTLS13, } + +// Returns a SHA256 hash of the given certificate. +func fingerprintCertificate(cert *x509.Certificate) string { + sha := sha256.New() + sha.Write(cert.Raw) + fingerprint := sha.Sum(nil) + + var buf strings.Builder + for i, f := range fingerprint { + if i > 0 { + fmt.Fprintf(&buf, ":") + } + fmt.Fprintf(&buf, "%02X", f) + } + + return buf.String() +} diff --git a/config/configtls/configtls_test.go b/config/configtls/configtls_test.go index 91c0e871055..0df7f209ac1 100644 --- a/config/configtls/configtls_test.go +++ b/config/configtls/configtls_test.go @@ -7,9 +7,12 @@ import ( "context" "crypto/tls" "crypto/x509" + "encoding/pem" "errors" "fmt" "io" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" @@ -877,3 +880,78 @@ func TestSystemCertPool_loadCert(t *testing.T) { }) } } + +func TestTrustedKeys(t *testing.T) { + trustedClientCertPem := readFilePanics("testdata/client-1.crt") + block, _ := pem.Decode([]byte(trustedClientCertPem)) + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err) + + tests := []struct { + Name string + ClientCertPath string + ClientKeyPath string + CACertPath string + ExpectSuccess bool + }{ + { + Name: "trusted cert", + ClientCertPath: "testdata/client-1.crt", + ClientKeyPath: "testdata/client-1.key", + CACertPath: "testdata/ca-1.crt", + ExpectSuccess: true, + }, + { + Name: "untrusted cert", + ClientCertPath: "testdata/client-2.crt", + ClientKeyPath: "testdata/client-2.key", + CACertPath: "testdata/ca-2.crt", + ExpectSuccess: false, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + config := NewDefaultServerConfig() + config.TrustedKeys = []string{fingerprintCertificate(cert)} + config.IncludeSystemCACertsPool = true + config.CertFile = "testdata/server-1.crt" + config.KeyFile = "testdata/server-1.key" + config.ClientCAFile = tt.CACertPath + tlsConf, err := config.LoadTLSConfig(context.Background()) + require.NoError(t, err) + + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + + server.TLS = tlsConf + server.StartTLS() + defer server.Close() + + clientCert, err := tls.LoadX509KeyPair(tt.ClientCertPath, tt.ClientKeyPath) + require.NoError(t, err) + + client := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + Certificates: []tls.Certificate{clientCert}, + InsecureSkipVerify: true, + }, + }, + } + + request, err := http.NewRequest(http.MethodGet, server.URL, nil) + require.NoError(t, err) + + resp, err := client.Do(request) + if tt.ExpectSuccess { + require.NoError(t, err) + require.NotNil(t, resp) + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + } else { + assert.Error(t, err) + } + }) + } +}