Skip to content

Commit

Permalink
Add TrustedKeys to TLS Config
Browse files Browse the repository at this point in the history
When using client certificates, it's sometimes useful
to use publically signed certificates to avoid
having to run your own CA infrastructure. This leaves
you open though, because if you trust a public cert
then _any_ public cert will work.

This alleviates this by allowing specifying a list
of "Trusted Keys". This is an array of SHA256
fingerprints of certs that are trusted, allowing
us to reject any certs that are not the exact one we expect.

Signed-off-by: sinkingpoint <[email protected]>
  • Loading branch information
sinkingpoint committed Aug 6, 2024
1 parent 93cbae4 commit 17aa0b2
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 6 deletions.
8 changes: 8 additions & 0 deletions config/configtls/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
52 changes: 46 additions & 6 deletions config/configtls/configtls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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

Check warning on line 187 in config/configtls/configtls.go

View check run for this annotation

Codecov / codecov/patch

config/configtls/configtls.go#L187

Added line #L187 was not covered by tests
}

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")
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
}
78 changes: 78 additions & 0 deletions config/configtls/configtls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -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)
}
})
}
}

0 comments on commit 17aa0b2

Please sign in to comment.