diff --git a/config/http_config.go b/config/http_config.go index da5d5901..ba7798ac 100644 --- a/config/http_config.go +++ b/config/http_config.go @@ -14,6 +14,8 @@ package config import ( + "bytes" + "crypto/md5" "crypto/tls" "crypto/x509" "fmt" @@ -21,6 +23,7 @@ import ( "net/http" "net/url" "strings" + "sync" "time" "github.com/mwitkow/go-conntrack" @@ -124,42 +127,51 @@ func NewClientFromConfig(cfg HTTPClientConfig, name string) (*http.Client, error // NewRoundTripperFromConfig returns a new HTTP RoundTripper configured for the // given config.HTTPClientConfig. The name is used as go-conntrack metric label. func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string) (http.RoundTripper, error) { + newRT := func(tlsConfig *tls.Config) (http.RoundTripper, error) { + // The only timeout we care about is the configured scrape timeout. + // It is applied on request. So we leave out any timings here. + var rt http.RoundTripper = &http.Transport{ + Proxy: http.ProxyURL(cfg.ProxyURL.URL), + MaxIdleConns: 20000, + MaxIdleConnsPerHost: 1000, // see https://github.com/golang/go/issues/13801 + DisableKeepAlives: false, + TLSClientConfig: tlsConfig, + DisableCompression: true, + // 5 minutes is typically above the maximum sane scrape interval. So we can + // use keepalive for all configurations. + IdleConnTimeout: 5 * time.Minute, + DialContext: conntrack.NewDialContextFunc( + conntrack.DialWithTracing(), + conntrack.DialWithName(name), + ), + } + + // If a bearer token is provided, create a round tripper that will set the + // Authorization header correctly on each request. + if len(cfg.BearerToken) > 0 { + rt = NewBearerAuthRoundTripper(cfg.BearerToken, rt) + } else if len(cfg.BearerTokenFile) > 0 { + rt = NewBearerAuthFileRoundTripper(cfg.BearerTokenFile, rt) + } + + if cfg.BasicAuth != nil { + rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, cfg.BasicAuth.PasswordFile, rt) + } + // Return a new configured RoundTripper. + return rt, nil + } + tlsConfig, err := NewTLSConfig(&cfg.TLSConfig) if err != nil { return nil, err } - // The only timeout we care about is the configured scrape timeout. - // It is applied on request. So we leave out any timings here. - var rt http.RoundTripper = &http.Transport{ - Proxy: http.ProxyURL(cfg.ProxyURL.URL), - MaxIdleConns: 20000, - MaxIdleConnsPerHost: 1000, // see https://github.com/golang/go/issues/13801 - DisableKeepAlives: false, - TLSClientConfig: tlsConfig, - DisableCompression: true, - // 5 minutes is typically above the maximum sane scrape interval. So we can - // use keepalive for all configurations. - IdleConnTimeout: 5 * time.Minute, - DialContext: conntrack.NewDialContextFunc( - conntrack.DialWithTracing(), - conntrack.DialWithName(name), - ), - } - // If a bearer token is provided, create a round tripper that will set the - // Authorization header correctly on each request. - if len(cfg.BearerToken) > 0 { - rt = NewBearerAuthRoundTripper(cfg.BearerToken, rt) - } else if len(cfg.BearerTokenFile) > 0 { - rt = NewBearerAuthFileRoundTripper(cfg.BearerTokenFile, rt) + if len(cfg.TLSConfig.CAFile) == 0 { + // No need for a RoundTripper that reloads the CA file automatically. + return newRT(tlsConfig) } - if cfg.BasicAuth != nil { - rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, cfg.BasicAuth.PasswordFile, rt) - } - - // Return a new configured RoundTripper. - return rt, nil + return newTLSRoundTripper(tlsConfig, cfg.TLSConfig.CAFile, newRT) } type bearerAuthRoundTripper struct { @@ -258,14 +270,13 @@ func NewTLSConfig(cfg *TLSConfig) (*tls.Config, error) { // If a CA cert is provided then let's read it in so we can validate the // scrape target's certificate properly. if len(cfg.CAFile) > 0 { - caCertPool := x509.NewCertPool() - // Load CA cert. - caCert, err := ioutil.ReadFile(cfg.CAFile) + b, err := readCAFile(cfg.CAFile) if err != nil { - return nil, fmt.Errorf("unable to use specified CA cert %s: %s", cfg.CAFile, err) + return nil, err + } + if !updateRootCA(tlsConfig, b) { + return nil, fmt.Errorf("unable to use specified CA cert %s", cfg.CAFile) } - caCertPool.AppendCertsFromPEM(caCert) - tlsConfig.RootCAs = caCertPool } if len(cfg.ServerName) > 0 { @@ -277,13 +288,12 @@ func NewTLSConfig(cfg *TLSConfig) (*tls.Config, error) { } else if len(cfg.KeyFile) > 0 && len(cfg.CertFile) == 0 { return nil, fmt.Errorf("client key file %q specified without client cert file", cfg.KeyFile) } else if len(cfg.CertFile) > 0 && len(cfg.KeyFile) > 0 { - cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile) - if err != nil { - return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %s", cfg.CertFile, cfg.KeyFile, err) + // Verify that client cert and key are valid. + if _, err := cfg.getClientCertificate(nil); err != nil { + return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.GetClientCertificate = cfg.getClientCertificate } - tlsConfig.BuildNameToCertificate() return tlsConfig, nil } @@ -308,6 +318,116 @@ func (c *TLSConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return unmarshal((*plain)(c)) } +// getClientCertificate reads the pair of client cert and key from disk and returns a tls.Certificate. +func (c *TLSConfig) getClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile) + if err != nil { + return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %s", c.CertFile, c.KeyFile, err) + } + return &cert, nil +} + +// readCAFile reads the CA cert file from disk. +func readCAFile(f string) ([]byte, error) { + data, err := ioutil.ReadFile(f) + if err != nil { + return nil, fmt.Errorf("unable to load specified CA cert %s: %s", f, err) + } + return data, nil +} + +// updateRootCA parses the given byte slice as a series of PEM encoded certificates and updates tls.Config.RootCAs. +func updateRootCA(cfg *tls.Config, b []byte) bool { + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(b) { + return false + } + cfg.RootCAs = caCertPool + return true +} + +// tlsRoundTripper is a RoundTripper that updates automatically its TLS +// configuration whenever the content of the CA file changes. +type tlsRoundTripper struct { + caFile string + // newRT returns a new RoundTripper. + newRT func(*tls.Config) (http.RoundTripper, error) + + mtx sync.RWMutex + rt http.RoundTripper + hashCAFile []byte + tlsConfig *tls.Config +} + +func newTLSRoundTripper( + cfg *tls.Config, + caFile string, + newRT func(*tls.Config) (http.RoundTripper, error), +) (http.RoundTripper, error) { + t := &tlsRoundTripper{ + caFile: caFile, + newRT: newRT, + tlsConfig: cfg, + } + + rt, err := t.newRT(t.tlsConfig) + if err != nil { + return nil, err + } + t.rt = rt + + _, t.hashCAFile, err = t.getCAWithHash() + if err != nil { + return nil, err + } + + return t, nil +} + +func (t *tlsRoundTripper) getCAWithHash() ([]byte, []byte, error) { + b, err := readCAFile(t.caFile) + if err != nil { + return nil, nil, err + } + h := md5.Sum(b) + return b, h[:], nil + +} + +// RoundTrip implements the http.RoundTrip interface. +func (t *tlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + b, h, err := t.getCAWithHash() + if err != nil { + return nil, err + } + + t.mtx.RLock() + equal := bytes.Equal(h[:], t.hashCAFile) + rt := t.rt + t.mtx.RUnlock() + if equal { + // The CA cert hasn't changed, use the existing RoundTripper. + return rt.RoundTrip(req) + } + + // Create a new RoundTripper. + tlsConfig := t.tlsConfig.Clone() + if !updateRootCA(tlsConfig, b) { + return nil, fmt.Errorf("unable to use specified CA cert %s", t.caFile) + } + rt, err = t.newRT(tlsConfig) + if err != nil { + return nil, err + } + + t.mtx.Lock() + t.rt = rt + t.hashCAFile = h[:] + t.mtx.Unlock() + + return rt.RoundTrip(req) +} + func (c HTTPClientConfig) String() string { b, err := yaml.Marshal(c) if err != nil { diff --git a/config/http_config_test.go b/config/http_config_test.go index 698ea38f..840fd520 100644 --- a/config/http_config_test.go +++ b/config/http_config_test.go @@ -20,11 +20,17 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "os" + "path/filepath" "reflect" + "strconv" "strings" + "sync" + "sync/atomic" "testing" + "time" - "gopkg.in/yaml.v2" + yaml "gopkg.in/yaml.v2" ) const ( @@ -33,6 +39,10 @@ const ( ServerKeyPath = "testdata/server.key" BarneyCertificatePath = "testdata/barney.crt" BarneyKeyNoPassPath = "testdata/barney-no-pass.key" + InvalidCA = "testdata/barney-no-pass.key" + WrongClientCertPath = "testdata/self-signed-client.crt" + WrongClientKeyPath = "testdata/self-signed-client.key" + EmptyFile = "testdata/empty" MissingCA = "missing/ca.crt" MissingCert = "missing/cert.crt" MissingKey = "missing/secret.key" @@ -229,12 +239,17 @@ func TestNewClientFromInvalidConfig(t *testing.T) { clientConfig: HTTPClientConfig{ TLSConfig: TLSConfig{ CAFile: MissingCA, - CertFile: "", - KeyFile: "", - ServerName: "", InsecureSkipVerify: true}, }, - errorMsg: fmt.Sprintf("unable to use specified CA cert %s:", MissingCA), + errorMsg: fmt.Sprintf("unable to load specified CA cert %s:", MissingCA), + }, + { + clientConfig: HTTPClientConfig{ + TLSConfig: TLSConfig{ + CAFile: InvalidCA, + InsecureSkipVerify: true}, + }, + errorMsg: fmt.Sprintf("unable to use specified CA cert %s", InvalidCA), }, } @@ -247,7 +262,7 @@ func TestNewClientFromInvalidConfig(t *testing.T) { t.Errorf("No error was returned using this config: %+v", invalidConfig.clientConfig) } if !strings.Contains(err.Error(), invalidConfig.errorMsg) { - t.Errorf("Expected error %s does not contain %s", err.Error(), invalidConfig.errorMsg) + t.Errorf("Expected error %q does not contain %q", err.Error(), invalidConfig.errorMsg) } } } @@ -357,24 +372,31 @@ func TestTLSConfig(t *testing.T) { rootCAs := x509.NewCertPool() rootCAs.AppendCertsFromPEM(tlsCAChain) - barneyCertificate, err := tls.LoadX509KeyPair(BarneyCertificatePath, BarneyKeyNoPassPath) - if err != nil { - t.Fatalf("Can't load the client key pair ('%s' and '%s'). Reason: %s", - BarneyCertificatePath, BarneyKeyNoPassPath, err) - } - expectedTLSConfig := &tls.Config{ RootCAs: rootCAs, - Certificates: []tls.Certificate{barneyCertificate}, ServerName: configTLSConfig.ServerName, InsecureSkipVerify: configTLSConfig.InsecureSkipVerify} - expectedTLSConfig.BuildNameToCertificate() tlsConfig, err := NewTLSConfig(&configTLSConfig) if err != nil { t.Fatalf("Can't create a new TLS Config from a configuration (%s).", err) } + barneyCertificate, err := tls.LoadX509KeyPair(BarneyCertificatePath, BarneyKeyNoPassPath) + if err != nil { + t.Fatalf("Can't load the client key pair ('%s' and '%s'). Reason: %s", + BarneyCertificatePath, BarneyKeyNoPassPath, err) + } + cert, err := tlsConfig.GetClientCertificate(nil) + if err != nil { + t.Fatalf("unexpected error returned by tlsConfig.GetClientCertificate(): %s", err) + } + if !reflect.DeepEqual(cert, &barneyCertificate) { + t.Fatalf("Unexpected client certificate result: \n\n%+v\n expected\n\n%+v", cert, barneyCertificate) + } + + // non-nil functions are never equal. + tlsConfig.GetClientCertificate = nil if !reflect.DeepEqual(tlsConfig, expectedTLSConfig) { t.Fatalf("Unexpected TLS Config result: \n\n%+v\n expected\n\n%+v", tlsConfig, expectedTLSConfig) } @@ -382,15 +404,12 @@ func TestTLSConfig(t *testing.T) { func TestTLSConfigEmpty(t *testing.T) { configTLSConfig := TLSConfig{ - CAFile: "", - CertFile: "", - KeyFile: "", - ServerName: "", - InsecureSkipVerify: true} + InsecureSkipVerify: true, + } expectedTLSConfig := &tls.Config{ - InsecureSkipVerify: configTLSConfig.InsecureSkipVerify} - expectedTLSConfig.BuildNameToCertificate() + InsecureSkipVerify: configTLSConfig.InsecureSkipVerify, + } tlsConfig, err := NewTLSConfig(&configTLSConfig) if err != nil { @@ -414,7 +433,7 @@ func TestTLSConfigInvalidCA(t *testing.T) { KeyFile: "", ServerName: "", InsecureSkipVerify: false}, - errorMessage: fmt.Sprintf("unable to use specified CA cert %s:", MissingCA), + errorMessage: fmt.Sprintf("unable to load specified CA cert %s:", MissingCA), }, { configTLSConfig: TLSConfig{ CAFile: "", @@ -449,11 +468,11 @@ func TestTLSConfigInvalidCA(t *testing.T) { func TestBasicAuthNoPassword(t *testing.T) { cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.basic-auth.no-password.yaml") if err != nil { - t.Errorf("Error loading HTTP client config: %v", err) + t.Fatalf("Error loading HTTP client config: %v", err) } client, err := NewClientFromConfig(*cfg, "test") if err != nil { - t.Errorf("Error creating HTTP Client: %v", err) + t.Fatalf("Error creating HTTP Client: %v", err) } rt, ok := client.Transport.(*basicAuthRoundTripper) @@ -475,11 +494,11 @@ func TestBasicAuthNoPassword(t *testing.T) { func TestBasicAuthNoUsername(t *testing.T) { cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.basic-auth.no-username.yaml") if err != nil { - t.Errorf("Error loading HTTP client config: %v", err) + t.Fatalf("Error loading HTTP client config: %v", err) } client, err := NewClientFromConfig(*cfg, "test") if err != nil { - t.Errorf("Error creating HTTP Client: %v", err) + t.Fatalf("Error creating HTTP Client: %v", err) } rt, ok := client.Transport.(*basicAuthRoundTripper) @@ -501,16 +520,16 @@ func TestBasicAuthNoUsername(t *testing.T) { func TestBasicAuthPasswordFile(t *testing.T) { cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.basic-auth.good.yaml") if err != nil { - t.Errorf("Error loading HTTP client config: %v", err) + t.Fatalf("Error loading HTTP client config: %v", err) } client, err := NewClientFromConfig(*cfg, "test") if err != nil { - t.Errorf("Error creating HTTP Client: %v", err) + t.Fatalf("Error creating HTTP Client: %v", err) } rt, ok := client.Transport.(*basicAuthRoundTripper) if !ok { - t.Errorf("Error casting to basic auth transport, %v", client.Transport) + t.Fatalf("Error casting to basic auth transport, %v", client.Transport) } if rt.username != "user" { @@ -524,6 +543,265 @@ func TestBasicAuthPasswordFile(t *testing.T) { } } +func getCertificateBlobs(t *testing.T) map[string][]byte { + t.Helper() + files := []string{ + TLSCAChainPath, + BarneyCertificatePath, + BarneyKeyNoPassPath, + ServerCertificatePath, + ServerKeyPath, + WrongClientCertPath, + WrongClientKeyPath, + EmptyFile, + } + bs := make(map[string][]byte, len(files)+1) + for _, f := range files { + b, err := ioutil.ReadFile(f) + if err != nil { + t.Fatal(err) + } + bs[f] = b + } + + return bs +} + +func writeCertificate(bs map[string][]byte, src string, dst string) { + b, ok := bs[src] + if !ok { + panic(fmt.Sprintf("Couldn't find %q in bs", src)) + } + if err := ioutil.WriteFile(dst, b, 0664); err != nil { + panic(err) + } +} + +func TestTLSRoundTripper(t *testing.T) { + bs := getCertificateBlobs(t) + + tmpDir, err := ioutil.TempDir("", "tlsroundtripper") + if err != nil { + t.Fatal("Failed to create tmp dir", err) + } + defer os.RemoveAll(tmpDir) + + ca, cert, key := filepath.Join(tmpDir, "ca"), filepath.Join(tmpDir, "cert"), filepath.Join(tmpDir, "key") + + handler := func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, ExpectedMessage) + } + testServer, err := newTestServer(handler) + if err != nil { + t.Fatal(err.Error()) + } + defer testServer.Close() + + testCases := []struct { + ca string + cert string + key string + + errMsg string + }{ + { + // Valid certs. + ca: TLSCAChainPath, + cert: BarneyCertificatePath, + key: BarneyKeyNoPassPath, + }, + { + // CA not matching. + ca: BarneyCertificatePath, + cert: BarneyCertificatePath, + key: BarneyKeyNoPassPath, + + errMsg: "certificate signed by unknown authority", + }, + { + // Invalid client cert+key. + ca: TLSCAChainPath, + cert: WrongClientCertPath, + key: WrongClientKeyPath, + + errMsg: "remote error: tls", + }, + { + // CA file empty + ca: EmptyFile, + cert: BarneyCertificatePath, + key: BarneyKeyNoPassPath, + + errMsg: "unable to use specified CA cert", + }, + { + // cert file empty + ca: TLSCAChainPath, + cert: EmptyFile, + key: BarneyKeyNoPassPath, + + errMsg: "failed to find any PEM data in certificate input", + }, + { + // key file empty + ca: TLSCAChainPath, + cert: BarneyCertificatePath, + key: EmptyFile, + + errMsg: "failed to find any PEM data in key input", + }, + { + // Valid certs again. + ca: TLSCAChainPath, + cert: BarneyCertificatePath, + key: BarneyKeyNoPassPath, + }, + } + + cfg := HTTPClientConfig{ + TLSConfig: TLSConfig{ + CAFile: ca, + CertFile: cert, + KeyFile: key, + InsecureSkipVerify: false}, + } + + var c *http.Client + for i, tc := range testCases { + tc := tc + t.Run(strconv.Itoa(i), func(t *testing.T) { + writeCertificate(bs, tc.ca, ca) + writeCertificate(bs, tc.cert, cert) + writeCertificate(bs, tc.key, key) + if c == nil { + c, err = NewClientFromConfig(cfg, "test") + if err != nil { + t.Fatalf("Error creating HTTP Client: %v", err) + } + } + + req, err := http.NewRequest(http.MethodGet, testServer.URL, nil) + if err != nil { + t.Fatalf("Error creating HTTP request: %v", err) + } + r, err := c.Do(req) + if len(tc.errMsg) > 0 { + if err == nil { + r.Body.Close() + t.Fatalf("Could connect to the test server.") + } + if !strings.Contains(err.Error(), tc.errMsg) { + t.Fatalf("Expected error message to contain %q, got %q", tc.errMsg, err) + } + return + } + + if err != nil { + t.Fatalf("Can't connect to the test server") + } + + b, err := ioutil.ReadAll(r.Body) + r.Body.Close() + if err != nil { + t.Errorf("Can't read the server response body") + } + + got := strings.TrimSpace(string(b)) + if ExpectedMessage != got { + t.Errorf("The expected message %q differs from the obtained message %q", ExpectedMessage, got) + } + }) + } +} + +func TestTLSRoundTripperRaces(t *testing.T) { + bs := getCertificateBlobs(t) + + tmpDir, err := ioutil.TempDir("", "tlsroundtripper") + if err != nil { + t.Fatal("Failed to create tmp dir", err) + } + defer os.RemoveAll(tmpDir) + + ca, cert, key := filepath.Join(tmpDir, "ca"), filepath.Join(tmpDir, "cert"), filepath.Join(tmpDir, "key") + + handler := func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, ExpectedMessage) + } + testServer, err := newTestServer(handler) + if err != nil { + t.Fatal(err.Error()) + } + defer testServer.Close() + + cfg := HTTPClientConfig{ + TLSConfig: TLSConfig{ + CAFile: ca, + CertFile: cert, + KeyFile: key, + InsecureSkipVerify: false}, + } + + var c *http.Client + writeCertificate(bs, TLSCAChainPath, ca) + writeCertificate(bs, BarneyCertificatePath, cert) + writeCertificate(bs, BarneyKeyNoPassPath, key) + c, err = NewClientFromConfig(cfg, "test") + if err != nil { + t.Fatalf("Error creating HTTP Client: %v", err) + } + + var wg sync.WaitGroup + ch := make(chan struct{}) + var total, ok int64 + // Spawn 10 Go routines polling the server concurrently. + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-ch: + return + default: + atomic.AddInt64(&total, 1) + r, err := c.Get(testServer.URL) + if err == nil { + r.Body.Close() + atomic.AddInt64(&ok, 1) + } + } + } + }() + } + + // Change the CA file every 10ms for 1 second. + wg.Add(1) + go func() { + defer wg.Done() + i := 0 + for { + tick := time.NewTicker(10 * time.Millisecond) + <-tick.C + if i%2 == 0 { + writeCertificate(bs, BarneyCertificatePath, ca) + } else { + writeCertificate(bs, TLSCAChainPath, ca) + } + i++ + if i > 100 { + close(ch) + return + } + } + }() + + wg.Wait() + if ok == total { + t.Fatalf("Expecting some requests to fail but got %d/%d successful requests", ok, total) + } +} + func TestHideHTTPClientConfigSecrets(t *testing.T) { c, _, err := LoadHTTPConfigFile("testdata/http.conf.good.yml") if err != nil { diff --git a/config/testdata/empty b/config/testdata/empty new file mode 100644 index 00000000..e69de29b diff --git a/config/testdata/self-signed-client.crt b/config/testdata/self-signed-client.crt new file mode 100644 index 00000000..fe2973ab --- /dev/null +++ b/config/testdata/self-signed-client.crt @@ -0,0 +1,121 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 0e:47:ce:db:33:a0:10:93:9b:b1:ac:66:7c:16:2d:89:d0:b7:ea:1d + Signature Algorithm: sha256WithRSAEncryption + Issuer: C = US, ST = Denial, L = Springfield, O = Dis, CN = www.example.com + Validity + Not Before: Mar 1 16:51:42 2019 GMT + Not After : Jul 17 16:51:42 2046 GMT + Subject: C = US, ST = Denial, L = Springfield, O = Dis, CN = www.example.com + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (4096 bit) + Modulus: + 00:ce:c6:ab:fd:9c:d2:da:55:f9:3d:5f:c0:0d:1a: + a6:1c:d1:7f:01:f4:0d:9c:ce:85:8b:01:8f:06:73: + 0a:b6:92:e1:6e:63:7d:e4:83:ca:c0:11:67:70:d9: + 89:0c:a9:62:0a:c3:cc:00:53:6f:b6:1b:0b:e1:eb: + 62:00:e8:ed:14:16:c6:29:45:0c:ee:25:40:21:10: + c2:3d:9a:3b:5c:27:54:bb:e4:9c:f6:e3:b4:dc:f1: + 0e:ba:c5:6f:60:94:45:b8:8d:f6:a4:1a:b4:fa:82: + 7b:5a:55:a6:11:c1:d4:e6:41:dc:c7:41:8e:db:46: + 6b:a2:0a:c1:13:96:47:12:4b:27:2e:d5:45:d4:51: + c9:b6:28:f8:0d:24:44:42:12:b8:b4:cd:ab:4a:67: + ba:8c:ff:34:92:38:b4:e5:4a:53:fe:33:72:55:df: + 27:d9:70:0f:47:cc:7c:d5:b2:52:bf:80:c0:a7:15: + b0:25:c8:d9:a1:41:e2:ee:e9:f5:0f:9f:27:ea:7c: + dc:ec:19:48:73:74:48:47:13:59:ea:89:e0:61:50: + 08:95:fc:32:9d:73:21:8e:b2:75:95:41:62:0c:61: + c7:b9:59:e2:51:a2:4f:bd:74:1b:0d:26:3c:c8:a6: + 1a:cb:db:10:cc:33:dd:2a:0b:38:55:60:85:f8:25: + 74:1f:0d:26:4e:db:2d:03:12:d5:85:00:cf:51:01: + 95:94:c8:85:cc:0e:5a:05:aa:3e:7a:34:e2:17:8b: + 3b:c5:21:a2:da:56:0a:ed:de:6c:2c:40:10:85:25: + 5d:df:39:e9:45:0e:10:82:bf:34:5c:64:52:35:4b: + aa:1a:56:37:ab:1f:7f:b5:07:5f:8a:22:45:4d:96: + 21:6c:a2:eb:47:39:bf:38:de:b5:4c:99:af:bf:de: + f8:7c:54:8b:40:2e:1f:80:1b:97:6a:fe:2c:05:6a: + 1b:9c:cb:a1:1c:f9:9e:36:ef:d9:a2:1d:d4:61:d0: + 6d:d1:b6:00:f8:e7:7f:74:f8:c0:81:95:7d:68:dc: + f3:93:7d:49:33:99:15:d5:49:d6:6d:69:82:c1:9f: + f2:3e:c2:db:0b:b1:e6:7c:e5:98:f4:9f:01:7d:57: + ac:36:78:15:a9:54:6f:e6:3e:52:54:68:a3:bc:8f: + 99:3f:02:02:1f:d2:21:b1:39:70:61:4c:2f:71:e5: + 27:d3:d0:75:46:d7:5e:78:ee:82:a5:bd:6d:12:2d: + 0b:40:92:61:c0:9e:8c:71:be:d1:bb:4f:23:fe:4e: + f2:79:a0:bd:60:f8:62:e4:9a:5b:1d:e0:a7:99:bd: + 32:b2:29:7b:ca:8c:6b:1a:80:c8:6f:b3:aa:a0:9e: + 1b:03:ab + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + D3:CB:AC:FB:69:9C:D3:14:67:44:9F:FA:0F:B9:02:60:64:95:4E:17 + X509v3 Authority Key Identifier: + keyid:D3:CB:AC:FB:69:9C:D3:14:67:44:9F:FA:0F:B9:02:60:64:95:4E:17 + + X509v3 Basic Constraints: critical + CA:TRUE + Signature Algorithm: sha256WithRSAEncryption + 57:d6:69:ed:9e:05:ea:4d:64:3b:88:98:26:6c:00:6e:e7:b7: + cb:ff:48:a2:c1:50:03:39:28:46:94:c0:19:7d:ff:10:7b:11: + 6e:88:6d:fe:d8:62:3a:ce:28:33:64:86:85:0f:9f:bf:13:23: + 48:11:b0:86:fa:7a:1d:6b:8a:e7:8c:76:fb:1b:a8:a9:d5:b3: + b8:f0:b4:08:27:a4:91:14:1a:e3:1c:11:83:39:2c:20:f1:19: + 21:35:9e:af:69:eb:52:ec:eb:c8:63:e2:bd:76:46:c5:4b:0c: + c2:f7:b9:c3:2a:db:31:4a:b9:ea:a5:04:c4:e7:b6:cf:fc:7c: + 8b:8a:88:39:ad:f9:06:e1:c6:63:47:6c:47:5c:e9:0b:24:b5: + c1:eb:5d:67:ee:07:ac:42:5b:d4:cb:00:eb:ec:c5:2f:3a:d0: + 76:f1:2a:9c:b9:44:3e:ed:71:40:02:4d:68:b5:b4:09:de:4d: + ba:1c:87:86:2d:3c:b7:2c:e5:87:aa:ff:e2:5e:ad:0b:8c:bb: + 39:9a:13:26:e3:c4:34:00:48:06:14:8f:ec:4b:cb:e7:be:80: + bd:c7:6c:b0:75:88:4e:cd:b7:b1:7e:bf:92:85:c7:a0:45:4f: + 73:ba:a7:27:86:8f:12:cd:35:f7:8c:34:3f:66:1a:7f:53:1d: + 21:8c:90:22:ff:e7:d9:95:aa:15:c2:28:d0:c5:9b:6c:61:e9: + 15:ff:63:9f:8e:d8:b4:a2:d5:06:38:1a:cc:5f:89:2a:23:70: + a3:32:22:cd:00:20:c7:65:60:17:5e:8a:cc:dc:96:08:38:a5: + 7d:65:46:79:79:02:11:04:4b:86:9d:f3:b3:2c:c6:2d:18:b4: + 31:e1:86:aa:4c:0c:93:c3:fb:7a:5a:63:c2:6f:68:d3:86:2c: + 6d:cd:ab:6d:41:d2:36:32:c1:52:25:d0:68:bc:ac:ca:f3:41: + f6:5a:46:83:15:bd:e6:aa:3b:dc:6b:44:1f:6c:02:e9:ed:b5: + 91:28:8d:af:6f:27:1b:71:83:61:a8:8e:15:36:01:92:42:32: + 61:62:43:04:31:f7:f3:f3:c9:c0:93:19:c9:dd:4d:51:3b:64: + 3b:06:90:4f:93:22:15:6e:8b:5f:2e:4e:11:a7:b9:a3:f2:fe: + 45:c9:ea:4b:58:57:95:b3:77:29:9f:7d:bc:1d:a2:3d:5a:38: + b3:72:b2:c7:8b:12:a9:39:4f:4f:2e:bb:7e:ce:91:bb:82:c0: + 67:37:79:f6:9c:75:3b:39:6c:82:ac:6a:06:09:70:99:10:76: + a4:38:46:50:7d:8e:d0:24:fb:dd:32:8f:40:00:d9:d1:50:20: + 69:bd:86:b9:9e:89:23:60 +-----BEGIN CERTIFICATE----- +MIIFmTCCA4GgAwIBAgIUDkfO2zOgEJObsaxmfBYtidC36h0wDQYJKoZIhvcNAQEL +BQAwXDELMAkGA1UEBhMCVVMxDzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3By +aW5nZmllbGQxDDAKBgNVBAoMA0RpczEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29t +MB4XDTE5MDMwMTE2NTE0MloXDTQ2MDcxNzE2NTE0MlowXDELMAkGA1UEBhMCVVMx +DzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3ByaW5nZmllbGQxDDAKBgNVBAoM +A0RpczEYMBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAzsar/ZzS2lX5PV/ADRqmHNF/AfQNnM6FiwGPBnMKtpLh +bmN95IPKwBFncNmJDKliCsPMAFNvthsL4etiAOjtFBbGKUUM7iVAIRDCPZo7XCdU +u+Sc9uO03PEOusVvYJRFuI32pBq0+oJ7WlWmEcHU5kHcx0GO20ZrogrBE5ZHEksn +LtVF1FHJtij4DSREQhK4tM2rSme6jP80kji05UpT/jNyVd8n2XAPR8x81bJSv4DA +pxWwJcjZoUHi7un1D58n6nzc7BlIc3RIRxNZ6ongYVAIlfwynXMhjrJ1lUFiDGHH +uVniUaJPvXQbDSY8yKYay9sQzDPdKgs4VWCF+CV0Hw0mTtstAxLVhQDPUQGVlMiF +zA5aBao+ejTiF4s7xSGi2lYK7d5sLEAQhSVd3znpRQ4Qgr80XGRSNUuqGlY3qx9/ +tQdfiiJFTZYhbKLrRzm/ON61TJmvv974fFSLQC4fgBuXav4sBWobnMuhHPmeNu/Z +oh3UYdBt0bYA+Od/dPjAgZV9aNzzk31JM5kV1UnWbWmCwZ/yPsLbC7HmfOWY9J8B +fVesNngVqVRv5j5SVGijvI+ZPwICH9IhsTlwYUwvceUn09B1RtdeeO6Cpb1tEi0L +QJJhwJ6Mcb7Ru08j/k7yeaC9YPhi5JpbHeCnmb0ysil7yoxrGoDIb7OqoJ4bA6sC +AwEAAaNTMFEwHQYDVR0OBBYEFNPLrPtpnNMUZ0Sf+g+5AmBklU4XMB8GA1UdIwQY +MBaAFNPLrPtpnNMUZ0Sf+g+5AmBklU4XMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBAFfWae2eBepNZDuImCZsAG7nt8v/SKLBUAM5KEaUwBl9/xB7 +EW6Ibf7YYjrOKDNkhoUPn78TI0gRsIb6eh1riueMdvsbqKnVs7jwtAgnpJEUGuMc +EYM5LCDxGSE1nq9p61Ls68hj4r12RsVLDML3ucMq2zFKueqlBMTnts/8fIuKiDmt ++QbhxmNHbEdc6QsktcHrXWfuB6xCW9TLAOvsxS860HbxKpy5RD7tcUACTWi1tAne +Tboch4YtPLcs5Yeq/+JerQuMuzmaEybjxDQASAYUj+xLy+e+gL3HbLB1iE7Nt7F+ +v5KFx6BFT3O6pyeGjxLNNfeMND9mGn9THSGMkCL/59mVqhXCKNDFm2xh6RX/Y5+O +2LSi1QY4GsxfiSojcKMyIs0AIMdlYBdeiszclgg4pX1lRnl5AhEES4ad87Msxi0Y +tDHhhqpMDJPD+3paY8JvaNOGLG3Nq21B0jYywVIl0Gi8rMrzQfZaRoMVveaqO9xr +RB9sAunttZEoja9vJxtxg2GojhU2AZJCMmFiQwQx9/PzycCTGcndTVE7ZDsGkE+T +IhVui18uThGnuaPy/kXJ6ktYV5Wzdymffbwdoj1aOLNysseLEqk5T08uu37OkbuC +wGc3efacdTs5bIKsagYJcJkQdqQ4RlB9jtAk+90yj0AA2dFQIGm9hrmeiSNg +-----END CERTIFICATE----- diff --git a/config/testdata/self-signed-client.key b/config/testdata/self-signed-client.key new file mode 100644 index 00000000..f7089513 --- /dev/null +++ b/config/testdata/self-signed-client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +XM9QqgBE9ayYiJyup/gvGszbv/43ZOuHPg== +-----END PRIVATE KEY----- diff --git a/config/tls_config_test.go b/config/tls_config_test.go index 31ddb6e9..a33b072a 100644 --- a/config/tls_config_test.go +++ b/config/tls_config_test.go @@ -50,11 +50,12 @@ var expectedTLSConfigs = []struct { func TestValidTLSConfig(t *testing.T) { for _, cfg := range expectedTLSConfigs { - cfg.config.BuildNameToCertificate() got, err := LoadTLSConfig("testdata/" + cfg.filename) if err != nil { t.Errorf("Error parsing %s: %s", cfg.filename, err) } + // non-nil functions are never equal. + got.GetClientCertificate = nil if !reflect.DeepEqual(*got, *cfg.config) { t.Fatalf("%v: unexpected config result: \n\n%v\n expected\n\n%v", cfg.filename, got, cfg.config) }