From 45babbf6d16022768fb19ac5ed7f13b707763b50 Mon Sep 17 00:00:00 2001 From: Damien Mathieu <42@dmathieu.com> Date: Fri, 28 Feb 2025 12:56:42 +0100 Subject: [PATCH] Run ci with fips check (#1691) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * run ci with fips check * disable custom certificate path in fips mode * prevent disabling certificate verification in fips mode * fix goimports * remove fips disabled checks from non-fips tests * Update Makefile Co-authored-by: Tim Rühsen * Revert "Update Makefile" This reverts commit e585f0060521fe4b6421f8b7f7d2e3c86aa7c51b. * Update Makefile Co-authored-by: Tim Rühsen --------- Co-authored-by: Tim Rühsen --- .github/workflows/ci.yml | 15 +++ Makefile | 6 +- transport/crypto.go | 97 ++++++++++++++++++ transport/crypto_fips.go | 30 ++++++ transport/crypto_test.go | 216 +++++++++++++++++++++++++++++++++++++++ transport/http.go | 86 ++++------------ transport/http_test.go | 178 -------------------------------- 7 files changed, 384 insertions(+), 244 deletions(-) create mode 100644 transport/crypto.go create mode 100644 transport/crypto_fips.go create mode 100644 transport/crypto_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f924dde27..af6633348 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,21 @@ jobs: - name: Unit tests run: make test + test-fips: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + # TODO switch to go.mod once we update to 1.24+ + go-version: 1.24 + cache: true + - env: + GOFIPS140: "latest" + GODEBUG: "fips141=only" + run: make test-fips + check-update-modules: runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/Makefile b/Makefile index f2a6a734b..8b33251c4 100644 --- a/Makefile +++ b/Makefile @@ -36,9 +36,11 @@ check-vet: docker-test: scripts/docker-compose-testing run -T --rm go-agent-tests make test -.PHONY: test +.PHONY: test-fips test +test-fips: ARGS=-tags=requirefips +test-fips: test test: - @for dir in $(shell scripts/moduledirs.sh); do (cd $$dir && go test -race -v -timeout=$(TEST_TIMEOUT) ./...) || exit $$?; done + @for dir in $(shell scripts/moduledirs.sh); do (cd "$$dir" && go test -race -v -timeout=$(TEST_TIMEOUT) $(ARGS) ./...) || exit $$?; done .PHONY: coverage coverage: diff --git a/transport/crypto.go b/transport/crypto.go new file mode 100644 index 000000000..3682ac31a --- /dev/null +++ b/transport/crypto.go @@ -0,0 +1,97 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +//go:build !requirefips + +package transport // import "go.elastic.co/apm/v2/transport" + +import ( + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + + "github.com/pkg/errors" + + "go.elastic.co/apm/v2/internal/configutil" +) + +const envVerifyServerCert = "ELASTIC_APM_VERIFY_SERVER_CERT" + +func checkVerifyServerCert() (bool, error) { + return configutil.ParseBoolEnv(envVerifyServerCert, true) +} + +func addCertPath(tlsClientConfig *tls.Config) error { + if serverCertPath := os.Getenv(envServerCert); serverCertPath != "" { + serverCert, err := loadCertificate(serverCertPath) + if err != nil { + return errors.Wrapf(err, "failed to load certificate from %s", serverCertPath) + } + // Disable standard verification, we'll check that the + // server supplies the exact certificate provided. + tlsClientConfig.InsecureSkipVerify = true + tlsClientConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return verifyPeerCertificate(rawCerts, serverCert) + } + } + if serverCACertPath := os.Getenv(envServerCACert); serverCACertPath != "" { + rootCAs := x509.NewCertPool() + additionalCerts, err := os.ReadFile(serverCACertPath) + if err != nil { + return errors.Wrapf(err, "failed to load root CA file from %s", serverCACertPath) + } + if !rootCAs.AppendCertsFromPEM(additionalCerts) { + return fmt.Errorf("failed to load CA certs from %s", serverCACertPath) + } + tlsClientConfig.RootCAs = rootCAs + } + + return nil +} + +func loadCertificate(path string) (*x509.Certificate, error) { + pemBytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + for { + var certBlock *pem.Block + certBlock, pemBytes = pem.Decode(pemBytes) + if certBlock == nil { + return nil, errors.New("missing or invalid certificate") + } + if certBlock.Type == "CERTIFICATE" { + return x509.ParseCertificate(certBlock.Bytes) + } + } +} + +func verifyPeerCertificate(rawCerts [][]byte, trusted *x509.Certificate) error { + if len(rawCerts) == 0 { + return errors.New("missing leaf certificate") + } + cert, err := x509.ParseCertificate(rawCerts[0]) + if err != nil { + return errors.Wrap(err, "failed to parse certificate from server") + } + if !cert.Equal(trusted) { + return errors.New("failed to verify server certificate") + } + return nil +} diff --git a/transport/crypto_fips.go b/transport/crypto_fips.go new file mode 100644 index 000000000..197be4593 --- /dev/null +++ b/transport/crypto_fips.go @@ -0,0 +1,30 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +//go:build requirefips + +package transport // import "go.elastic.co/apm/v2/transport" + +import "crypto/tls" + +func checkVerifyServerCert() (bool, error) { + return true, nil +} + +func addCertPath(tlsClientConfig *tls.Config) error { + return nil +} diff --git a/transport/crypto_test.go b/transport/crypto_test.go new file mode 100644 index 000000000..0cbadd4f1 --- /dev/null +++ b/transport/crypto_test.go @@ -0,0 +1,216 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +//go:build !requirefips + +package transport_test + +import ( + "context" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.elastic.co/apm/v2/transport" +) + +func TestHTTPTransportEnvVerifyServerCert(t *testing.T) { + var h recordingHandler + server := httptest.NewTLSServer(&h) + defer server.Close() + defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() + defer patchEnv("ELASTIC_APM_VERIFY_SERVER_CERT", "false")() + + transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) + assert.NoError(t, err) + + assert.NotNil(t, transport.Client) + assert.IsType(t, &http.Transport{}, transport.Client.Transport) + httpTransport := transport.Client.Transport.(*http.Transport) + assert.NotNil(t, httpTransport.TLSClientConfig) + assert.True(t, httpTransport.TLSClientConfig.InsecureSkipVerify) + + err = transport.SendStream(context.Background(), strings.NewReader("")) + assert.NoError(t, err) +} + +func TestHTTPTransportServerFailover(t *testing.T) { + defer patchEnv("ELASTIC_APM_VERIFY_SERVER_CERT", "false")() + + var hosts []string + errorHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + hosts = append(hosts, req.Host) + http.Error(w, "error-message", http.StatusInternalServerError) + }) + server1 := httptest.NewServer(errorHandler) + defer server1.Close() + server2 := httptest.NewTLSServer(errorHandler) + defer server2.Close() + + transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) + require.NoError(t, err) + err = transport.SetServerURL(mustParseURL(server1.URL), mustParseURL(server2.URL)) + require.NoError(t, err) + + for i := 0; i < 4; i++ { + err := transport.SendStream(context.Background(), strings.NewReader("")) + assert.EqualError(t, err, "request failed with 500 Internal Server Error: error-message") + } + assert.Len(t, hosts, 4) + + // Each time SendStream returns an error, the transport should switch + // to the next URL in the list. The list is shuffled so we only compare + // the output values to each other, rather than to the original input. + assert.NotEqual(t, hosts[0], hosts[1]) + assert.Equal(t, hosts[0], hosts[2]) + assert.Equal(t, hosts[1], hosts[3]) +} + +func TestHTTPTransportServerCert(t *testing.T) { + var h recordingHandler + server := httptest.NewUnstartedServer(&h) + server.Config.ErrorLog = log.New(ioutil.Discard, "", 0) + server.StartTLS() + defer server.Close() + defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() + + p := strings.NewReader("") + + newTransport := func() *transport.HTTPTransport { + transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) + require.NoError(t, err) + return transport + } + + // SendStream should fail, because we haven't told the client about + // the server certificate, nor disabled certificate verification. + transport := newTransport() + err := transport.SendStream(context.Background(), p) + assert.Error(t, err) + + // Set a certificate that doesn't match, SendStream should still fail. + defer patchEnv("ELASTIC_APM_SERVER_CERT", "./testdata/cert.pem")() + transport = newTransport() + err = transport.SendStream(context.Background(), p) + assert.Error(t, err) + + f, err := ioutil.TempFile("", "apm-test") + require.NoError(t, err) + defer os.Remove(f.Name()) + defer f.Close() + defer patchEnv("ELASTIC_APM_SERVER_CERT", f.Name())() + + // Reconfigure the transport so that it knows about the + // server certificate. We avoid using server.Client here, as + // it is not available in older versions of Go. + err = pem.Encode(f, &pem.Block{ + Type: "CERTIFICATE", + Bytes: server.TLS.Certificates[0].Certificate[0], + }) + require.NoError(t, err) + + transport = newTransport() + err = transport.SendStream(context.Background(), p) + assert.NoError(t, err) +} + +func TestHTTPTransportServerCertInvalid(t *testing.T) { + f, err := ioutil.TempFile("", "apm-test") + require.NoError(t, err) + defer os.Remove(f.Name()) + defer f.Close() + defer patchEnv("ELASTIC_APM_SERVER_CERT", f.Name())() + + fmt.Fprintln(f, ` +-----BEGIN GARBAGE----- +garbage +-----END GARBAGE----- +`[1:]) + + _, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) + assert.EqualError(t, err, fmt.Sprintf("failed to load certificate from %s: missing or invalid certificate", f.Name())) +} + +func TestHTTPTransportCACert(t *testing.T) { + var h recordingHandler + server := httptest.NewUnstartedServer(&h) + server.Config.ErrorLog = log.New(ioutil.Discard, "", 0) + server.StartTLS() + defer server.Close() + defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() + + p := strings.NewReader("") + + // SendStream should fail, because we haven't told the client about + // the server certificate, nor disabled certificate verification. + trans, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) + assert.NoError(t, err) + assert.NotNil(t, trans) + err = trans.SendStream(context.Background(), p) + assert.Error(t, err) + + // Set the env var to a file that doesn't exist, should get an error + defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", "./testdata/file_that_doesnt_exist.pem")() + trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) + assert.Error(t, err) + assert.Nil(t, trans) + + // Set the env var to a file that has no cert, should get an error + f, err := ioutil.TempFile("", "apm-test-1") + require.NoError(t, err) + defer os.Remove(f.Name()) + defer f.Close() + defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", f.Name())() + trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) + assert.Error(t, err) + assert.Nil(t, trans) + + // Set a certificate that doesn't match, SendStream should still fail + defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", "./testdata/cert.pem")() + trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) + assert.NoError(t, err) + assert.NotNil(t, trans) + err = trans.SendStream(context.Background(), p) + assert.Error(t, err) + + f, err = ioutil.TempFile("", "apm-test-2") + require.NoError(t, err) + defer os.Remove(f.Name()) + defer f.Close() + defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", f.Name())() + + err = pem.Encode(f, &pem.Block{ + Type: "CERTIFICATE", + Bytes: server.TLS.Certificates[0].Certificate[0], + }) + require.NoError(t, err) + + trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) + assert.NoError(t, err) + assert.NotNil(t, trans) + err = trans.SendStream(context.Background(), p) + assert.NoError(t, err) +} diff --git a/transport/http.go b/transport/http.go index 51b0110a9..cff480dfd 100644 --- a/transport/http.go +++ b/transport/http.go @@ -21,9 +21,7 @@ import ( "bytes" "context" "crypto/tls" - "crypto/x509" "encoding/json" - "encoding/pem" "fmt" "io" "io/ioutil" @@ -51,14 +49,13 @@ const ( profilePath = "/intake/v2/profile" configPath = "/config/v1/agents" - envAPIKey = "ELASTIC_APM_API_KEY" - envSecretToken = "ELASTIC_APM_SECRET_TOKEN" - envServerURLs = "ELASTIC_APM_SERVER_URLS" - envServerURL = "ELASTIC_APM_SERVER_URL" - envServerTimeout = "ELASTIC_APM_SERVER_TIMEOUT" - envServerCert = "ELASTIC_APM_SERVER_CERT" - envVerifyServerCert = "ELASTIC_APM_VERIFY_SERVER_CERT" - envServerCACert = "ELASTIC_APM_SERVER_CA_CERT_FILE" + envAPIKey = "ELASTIC_APM_API_KEY" + envSecretToken = "ELASTIC_APM_SECRET_TOKEN" + envServerURLs = "ELASTIC_APM_SERVER_URLS" + envServerURL = "ELASTIC_APM_SERVER_URL" + envServerTimeout = "ELASTIC_APM_SERVER_TIMEOUT" + envServerCert = "ELASTIC_APM_SERVER_CERT" + envServerCACert = "ELASTIC_APM_SERVER_CA_CERT_FILE" ) var ( @@ -250,33 +247,25 @@ func newHTTPTransportOptions(opts HTTPTransportOptions) (*HTTPTransport, error) } func newEnvTLSClientConfig() (*tls.Config, error) { - verifyServerCert, err := configutil.ParseBoolEnv(envVerifyServerCert, true) + // + // Certificate verification can be disabled, except if the client has FIPs + // compliance enabled. + // So we conditionnally make this customization using the `requirefips` build flag + // + verifyServerCert, err := checkVerifyServerCert() if err != nil { return nil, err } tlsClientConfig := &tls.Config{InsecureSkipVerify: !verifyServerCert} - if serverCertPath := os.Getenv(envServerCert); serverCertPath != "" { - serverCert, err := loadCertificate(serverCertPath) - if err != nil { - return nil, errors.Wrapf(err, "failed to load certificate from %s", serverCertPath) - } - // Disable standard verification, we'll check that the - // server supplies the exact certificate provided. - tlsClientConfig.InsecureSkipVerify = true - tlsClientConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - return verifyPeerCertificate(rawCerts, serverCert) - } - } - if serverCACertPath := os.Getenv(envServerCACert); serverCACertPath != "" { - rootCAs := x509.NewCertPool() - additionalCerts, err := ioutil.ReadFile(serverCACertPath) - if err != nil { - return nil, errors.Wrapf(err, "failed to load root CA file from %s", serverCACertPath) - } - if !rootCAs.AppendCertsFromPEM(additionalCerts) { - return nil, fmt.Errorf("failed to load CA certs from %s", serverCACertPath) - } - tlsClientConfig.RootCAs = rootCAs + + // + // Cert path and CA cert path can be customized, except if the client has + // FIPs compliance enabled. + // So we conditionnally make this customization using the `requirefips` build flag + // + err = addCertPath(tlsClientConfig) + if err != nil { + return nil, err } return tlsClientConfig, nil } @@ -712,37 +701,6 @@ func requestWithContext(ctx context.Context, req *http.Request) *http.Request { return reqCopy } -func loadCertificate(path string) (*x509.Certificate, error) { - pemBytes, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - for { - var certBlock *pem.Block - certBlock, pemBytes = pem.Decode(pemBytes) - if certBlock == nil { - return nil, errors.New("missing or invalid certificate") - } - if certBlock.Type == "CERTIFICATE" { - return x509.ParseCertificate(certBlock.Bytes) - } - } -} - -func verifyPeerCertificate(rawCerts [][]byte, trusted *x509.Certificate) error { - if len(rawCerts) == 0 { - return errors.New("missing leaf certificate") - } - cert, err := x509.ParseCertificate(rawCerts[0]) - if err != nil { - return errors.Wrap(err, "failed to parse certificate from server") - } - if !cert.Equal(trusted) { - return errors.New("failed to verify server certificate") - } - return nil -} - // DefaultUserAgent returns the default value to use for the User-Agent header: // apm-agent-go/. func DefaultUserAgent() string { diff --git a/transport/http_test.go b/transport/http_test.go index 1b554ca05..bf4adde85 100644 --- a/transport/http_test.go +++ b/transport/http_test.go @@ -21,7 +21,6 @@ import ( "context" "crypto/tls" "crypto/x509" - "encoding/pem" "fmt" "io" "io/ioutil" @@ -204,26 +203,6 @@ func TestHTTPTransportTLS(t *testing.T) { assert.NoError(t, err) } -func TestHTTPTransportEnvVerifyServerCert(t *testing.T) { - var h recordingHandler - server := httptest.NewTLSServer(&h) - defer server.Close() - defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() - defer patchEnv("ELASTIC_APM_VERIFY_SERVER_CERT", "false")() - - transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) - assert.NoError(t, err) - - assert.NotNil(t, transport.Client) - assert.IsType(t, &http.Transport{}, transport.Client.Transport) - httpTransport := transport.Client.Transport.(*http.Transport) - assert.NotNil(t, httpTransport.TLSClientConfig) - assert.True(t, httpTransport.TLSClientConfig.InsecureSkipVerify) - - err = transport.SendStream(context.Background(), strings.NewReader("")) - assert.NoError(t, err) -} - func TestHTTPError(t *testing.T) { h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { http.Error(w, "error-message", http.StatusInternalServerError) @@ -273,37 +252,6 @@ func TestHTTPTransportServerTimeout(t *testing.T) { }) } -func TestHTTPTransportServerFailover(t *testing.T) { - defer patchEnv("ELASTIC_APM_VERIFY_SERVER_CERT", "false")() - - var hosts []string - errorHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - hosts = append(hosts, req.Host) - http.Error(w, "error-message", http.StatusInternalServerError) - }) - server1 := httptest.NewServer(errorHandler) - defer server1.Close() - server2 := httptest.NewTLSServer(errorHandler) - defer server2.Close() - - transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) - require.NoError(t, err) - transport.SetServerURL(mustParseURL(server1.URL), mustParseURL(server2.URL)) - - for i := 0; i < 4; i++ { - err := transport.SendStream(context.Background(), strings.NewReader("")) - assert.EqualError(t, err, "request failed with 500 Internal Server Error: error-message") - } - assert.Len(t, hosts, 4) - - // Each time SendStream returns an error, the transport should switch - // to the next URL in the list. The list is shuffled so we only compare - // the output values to each other, rather than to the original input. - assert.NotEqual(t, hosts[0], hosts[1]) - assert.Equal(t, hosts[0], hosts[2]) - assert.Equal(t, hosts[1], hosts[3]) -} - func TestHTTPTransportV2NotFound(t *testing.T) { server := httptest.NewServer(http.NotFoundHandler()) defer server.Close() @@ -316,132 +264,6 @@ func TestHTTPTransportV2NotFound(t *testing.T) { assert.EqualError(t, err, fmt.Sprintf("request failed with 404 Not Found: %s/intake/v2/events not found (requires APM Server 6.5.0 or newer)", server.URL)) } -func TestHTTPTransportCACert(t *testing.T) { - var h recordingHandler - server := httptest.NewUnstartedServer(&h) - server.Config.ErrorLog = log.New(ioutil.Discard, "", 0) - server.StartTLS() - defer server.Close() - defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() - - p := strings.NewReader("") - - // SendStream should fail, because we haven't told the client about - // the server certificate, nor disabled certificate verification. - trans, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) - assert.NoError(t, err) - assert.NotNil(t, trans) - err = trans.SendStream(context.Background(), p) - assert.Error(t, err) - - // Set the env var to a file that doesn't exist, should get an error - defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", "./testdata/file_that_doesnt_exist.pem")() - trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) - assert.Error(t, err) - assert.Nil(t, trans) - - // Set the env var to a file that has no cert, should get an error - f, err := ioutil.TempFile("", "apm-test-1") - require.NoError(t, err) - defer os.Remove(f.Name()) - defer f.Close() - defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", f.Name())() - trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) - assert.Error(t, err) - assert.Nil(t, trans) - - // Set a certificate that doesn't match, SendStream should still fail - defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", "./testdata/cert.pem")() - trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) - assert.NoError(t, err) - assert.NotNil(t, trans) - err = trans.SendStream(context.Background(), p) - assert.Error(t, err) - - f, err = ioutil.TempFile("", "apm-test-2") - require.NoError(t, err) - defer os.Remove(f.Name()) - defer f.Close() - defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", f.Name())() - - err = pem.Encode(f, &pem.Block{ - Type: "CERTIFICATE", - Bytes: server.TLS.Certificates[0].Certificate[0], - }) - require.NoError(t, err) - - trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) - assert.NoError(t, err) - assert.NotNil(t, trans) - err = trans.SendStream(context.Background(), p) - assert.NoError(t, err) -} - -func TestHTTPTransportServerCert(t *testing.T) { - var h recordingHandler - server := httptest.NewUnstartedServer(&h) - server.Config.ErrorLog = log.New(ioutil.Discard, "", 0) - server.StartTLS() - defer server.Close() - defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() - - p := strings.NewReader("") - - newTransport := func() *transport.HTTPTransport { - transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) - require.NoError(t, err) - return transport - } - - // SendStream should fail, because we haven't told the client about - // the server certificate, nor disabled certificate verification. - transport := newTransport() - err := transport.SendStream(context.Background(), p) - assert.Error(t, err) - - // Set a certificate that doesn't match, SendStream should still fail. - defer patchEnv("ELASTIC_APM_SERVER_CERT", "./testdata/cert.pem")() - transport = newTransport() - err = transport.SendStream(context.Background(), p) - assert.Error(t, err) - - f, err := ioutil.TempFile("", "apm-test") - require.NoError(t, err) - defer os.Remove(f.Name()) - defer f.Close() - defer patchEnv("ELASTIC_APM_SERVER_CERT", f.Name())() - - // Reconfigure the transport so that it knows about the - // server certificate. We avoid using server.Client here, as - // it is not available in older versions of Go. - err = pem.Encode(f, &pem.Block{ - Type: "CERTIFICATE", - Bytes: server.TLS.Certificates[0].Certificate[0], - }) - require.NoError(t, err) - - transport = newTransport() - err = transport.SendStream(context.Background(), p) - assert.NoError(t, err) -} - -func TestHTTPTransportServerCertInvalid(t *testing.T) { - f, err := ioutil.TempFile("", "apm-test") - require.NoError(t, err) - defer os.Remove(f.Name()) - defer f.Close() - defer patchEnv("ELASTIC_APM_SERVER_CERT", f.Name())() - - fmt.Fprintln(f, ` ------BEGIN GARBAGE----- -garbage ------END GARBAGE----- -`[1:]) - - _, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) - assert.EqualError(t, err, fmt.Sprintf("failed to load certificate from %s: missing or invalid certificate", f.Name())) -} - func TestHTTPTransportWatchConfig(t *testing.T) { type response struct { code int