diff --git a/libbeat/common/transport/httpcommon/httpcommon.go b/libbeat/common/transport/httpcommon/httpcommon.go index 8f79f3571831..6ba839b718c7 100644 --- a/libbeat/common/transport/httpcommon/httpcommon.go +++ b/libbeat/common/transport/httpcommon/httpcommon.go @@ -15,219 +15,17 @@ // specific language governing permissions and limitations // under the License. +// +build go1.15 + package httpcommon import ( "net/http" - "time" - - "go.elastic.co/apm/module/apmhttp" - "golang.org/x/net/http2" - "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/beats/v7/libbeat/common/transport" "github.com/elastic/beats/v7/libbeat/common/transport/tlscommon" - "github.com/elastic/beats/v7/libbeat/logp" ) -// HTTPTransportSettings provides common HTTP settings for HTTP clients. -type HTTPTransportSettings struct { - // TLS provides ssl/tls setup settings - TLS *tlscommon.Config `config:"ssl" yaml:"ssl,omitempty" json:"ssl,omitempty"` - - // Timeout configures the `(http.Transport).Timeout`. - Timeout time.Duration `config:"timeout" yaml:"timeout,omitempty" json:"timeout,omitempty"` - - Proxy HTTPClientProxySettings `config:",inline" yaml:",inline"` - - // TODO: Add more settings: - // - DisableKeepAlive - // - MaxIdleConns - // - IdleConnTimeout - // - ResponseHeaderTimeout - // - ConnectionTimeout (currently 'Timeout' is used for both) -} - -// WithKeepaliveSettings options can be used to modify the Keepalive -type WithKeepaliveSettings struct { - Disable bool - MaxIdleConns int - MaxIdleConnsPerHost int - IdleConnTimeout time.Duration -} - -var _ httpTransportOption = WithKeepaliveSettings{} - -const defaultHTTPTimeout = 90 * time.Second - -type ( - // TransportOption are applied to the http.RoundTripper to be build - // from HTTPTransportSettings. - TransportOption interface{ sealTransportOption() } - - extraSettings struct { - logger *logp.Logger - http2 bool - } - - dialerOption interface { - TransportOption - baseDialer() transport.Dialer - } - dialerModOption interface { - TransportOption - applyDialer(*HTTPTransportSettings, transport.Dialer) transport.Dialer - } - httpTransportOption interface { - TransportOption - applyTransport(*HTTPTransportSettings, *http.Transport) - } - roundTripperOption interface { - TransportOption - applyRoundTripper(*HTTPTransportSettings, http.RoundTripper) http.RoundTripper - } - extraOption interface { - TransportOption - applyExtra(*extraSettings) - } -) - -type baseDialerFunc func() transport.Dialer - -var _ dialerOption = baseDialerFunc(nil) - -func (baseDialerFunc) sealTransportOption() {} -func (fn baseDialerFunc) baseDialer() transport.Dialer { - return fn() -} - -type dialerOptFunc func(transport.Dialer) transport.Dialer - -var _ dialerModOption = dialerOptFunc(nil) - -func (dialerOptFunc) sealTransportOption() {} -func (fn dialerOptFunc) applyDialer(_ *HTTPTransportSettings, d transport.Dialer) transport.Dialer { - return fn(d) - -} - -type transportOptFunc func(*HTTPTransportSettings, *http.Transport) - -var _ httpTransportOption = transportOptFunc(nil) - -func (transportOptFunc) sealTransportOption() {} -func (fn transportOptFunc) applyTransport(s *HTTPTransportSettings, t *http.Transport) { - fn(s, t) -} - -type rtOptFunc func(http.RoundTripper) http.RoundTripper - -var _ roundTripperOption = rtOptFunc(nil) - -func (rtOptFunc) sealTransportOption() {} -func (fn rtOptFunc) applyRoundTripper(_ *HTTPTransportSettings, rt http.RoundTripper) http.RoundTripper { - return fn(rt) -} - -type extraOptionFunc func(*extraSettings) - -func (extraOptionFunc) sealTransportOption() {} -func (fn extraOptionFunc) applyExtra(s *extraSettings) { fn(s) } - -// DefaultHTTPTransportSettings returns the default HTTP transport setting. -func DefaultHTTPTransportSettings() HTTPTransportSettings { - return HTTPTransportSettings{ - Proxy: DefaultHTTPClientProxySettings(), - Timeout: defaultHTTPTimeout, - } -} - -// Unpack reads a config object into the settings. -func (settings *HTTPTransportSettings) Unpack(cfg *common.Config) error { - tmp := struct { - TLS *tlscommon.Config `config:"ssl"` - Timeout time.Duration `config:"timeout"` - }{Timeout: settings.Timeout} - - if err := cfg.Unpack(&tmp); err != nil { - return err - } - - var proxy HTTPClientProxySettings - if err := cfg.Unpack(&proxy); err != nil { - return err - } - - _, err := tlscommon.LoadTLSConfig(tmp.TLS) - if err != nil { - return err - } - - *settings = HTTPTransportSettings{ - TLS: tmp.TLS, - Timeout: tmp.Timeout, - Proxy: proxy, - } - return nil -} - -// RoundTripper creates a http.RoundTripper for use with http.Client. -// -// The dialers will registers with stats if given. Stats is used to collect metrics for io errors, -// bytes in, and bytes out. -func (settings *HTTPTransportSettings) RoundTripper(opts ...TransportOption) (http.RoundTripper, error) { - var dialer transport.Dialer - - var extra extraSettings - for _, opt := range opts { - if opt, ok := opt.(extraOption); ok { - opt.applyExtra(&extra) - } - } - - for _, opt := range opts { - if dialOpt, ok := opt.(dialerOption); ok { - dialer = dialOpt.baseDialer() - } - } - - if dialer == nil { - dialer = transport.NetDialer(settings.Timeout) - } - - tls, err := tlscommon.LoadTLSConfig(settings.TLS) - if err != nil { - return nil, err - } - - tlsDialer := transport.TLSDialer(dialer, tls, settings.Timeout) - for _, opt := range opts { - if dialOpt, ok := opt.(dialerModOption); ok { - dialer = dialOpt.applyDialer(settings, dialer) - tlsDialer = dialOpt.applyDialer(settings, tlsDialer) - } - } - - if logger := extra.logger; logger != nil { - dialer = transport.LoggingDialer(dialer, logger) - tlsDialer = transport.LoggingDialer(tlsDialer, logger) - } - - var rt http.RoundTripper - if extra.http2 { - rt, err = settings.http2RoundTripper(tls, dialer, tlsDialer, opts...) - } else { - rt, err = settings.httpRoundTripper(tls, dialer, tlsDialer, opts...) - } - - for _, opt := range opts { - if rtOpt, ok := opt.(roundTripperOption); ok { - rt = rtOpt.applyRoundTripper(settings, rt) - } - } - return rt, nil -} - func (settings *HTTPTransportSettings) httpRoundTripper( tls *tlscommon.TLSConfig, dialer, tlsDialer transport.Dialer, @@ -255,128 +53,3 @@ func (settings *HTTPTransportSettings) httpRoundTripper( return t, nil } - -func (settings *HTTPTransportSettings) http2RoundTripper( - tls *tlscommon.TLSConfig, - dialer, tlsDialer transport.Dialer, - opts ...TransportOption, -) (*http2.Transport, error) { - t1, err := settings.httpRoundTripper(tls, dialer, tlsDialer, opts...) - if err != nil { - return nil, err - } - - t2, err := http2.ConfigureTransports(t1) - if err != nil { - return nil, err - } - - t2.AllowHTTP = true - return t2, nil -} - -// Client creates a new http.Client with configured Transport. The transport is -// instrumented using apmhttp.WrapRoundTripper. -func (settings HTTPTransportSettings) Client(opts ...TransportOption) (*http.Client, error) { - rt, err := settings.RoundTripper(opts...) - if err != nil { - return nil, err - } - - return &http.Client{Transport: rt, Timeout: settings.Timeout}, nil -} - -func (opts WithKeepaliveSettings) sealTransportOption() {} -func (opts WithKeepaliveSettings) applyTransport(_ *HTTPTransportSettings, t *http.Transport) { - t.DisableKeepAlives = opts.Disable - if opts.IdleConnTimeout != 0 { - t.IdleConnTimeout = opts.IdleConnTimeout - } - if opts.MaxIdleConns != 0 { - t.MaxIdleConns = opts.MaxIdleConns - } - if opts.MaxIdleConnsPerHost != 0 { - t.MaxIdleConnsPerHost = opts.MaxIdleConnsPerHost - } -} - -// WithBaseDialer configures the dialer used for TCP and TLS connections. -func WithBaseDialer(d transport.Dialer) TransportOption { - return baseDialerFunc(func() transport.Dialer { - return d - }) -} - -// WithIOStats instruments the RoundTripper dialers with the given statser, such -// that bytes in, bytes out, and errors can be monitored. -func WithIOStats(stats transport.IOStatser) TransportOption { - return dialerOptFunc(func(d transport.Dialer) transport.Dialer { - if stats == nil { - return d - } - return transport.StatsDialer(d, stats) - }) -} - -// WithTransportFunc register a custom function that is used to apply -// custom changes to the net.Transport, when the Client is build. -func WithTransportFunc(fn func(*http.Transport)) TransportOption { - return transportOptFunc(func(_ *HTTPTransportSettings, t *http.Transport) { - fn(t) - }) -} - -// WithHTTP2Only will ensure that a HTTP 2 only roundtripper is created. -func WithHTTP2Only(b bool) TransportOption { - return extraOptionFunc(func(settings *extraSettings) { - settings.http2 = b - }) -} - -// WithForceAttemptHTTP2 sets the `http.Tansport.ForceAttemptHTTP2` field. -func WithForceAttemptHTTP2(b bool) TransportOption { - return transportOptFunc(func(settings *HTTPTransportSettings, t *http.Transport) { - t.ForceAttemptHTTP2 = b - }) -} - -// WithNOProxy disables the configured proxy. Proxy environment variables -// like HTTP_PROXY and HTTPS_PROXY will have no affect. -func WithNOProxy() TransportOption { - return transportOptFunc(func(s *HTTPTransportSettings, t *http.Transport) { - t.Proxy = nil - }) -} - -// WithoutProxyEnvironmentVariables disables support for the HTTP_PROXY, HTTPS_PROXY and -// NO_PROXY envionrment variables. Explicitely configured proxy URLs will still applied. -func WithoutProxyEnvironmentVariables() TransportOption { - return transportOptFunc(func(settings *HTTPTransportSettings, t *http.Transport) { - if settings.Proxy.Disable || settings.Proxy.URL == nil { - t.Proxy = nil - } - }) -} - -// WithModRoundtripper allows customization of the roundtipper. -func WithModRoundtripper(w func(http.RoundTripper) http.RoundTripper) TransportOption { - return rtOptFunc(w) -} - -var withAPMHTTPRountTripper = WithModRoundtripper(func(rt http.RoundTripper) http.RoundTripper { - return apmhttp.WrapRoundTripper(rt) -}) - -// WithAPMHTTPInstrumentation insruments the HTTP client via apmhttp.WrapRoundTripper. -// Custom APM round tripper wrappers can be configured via WithModRoundtripper. -func WithAPMHTTPInstrumentation() TransportOption { - return withAPMHTTPRountTripper -} - -// WithLogger sets the internal logger that will be used to log dial or TCP level errors. -// Logging at the connection level will only happen if the logger has been set. -func WithLogger(logger *logp.Logger) TransportOption { - return extraOptionFunc(func(s *extraSettings) { - s.logger = logger - }) -} diff --git a/libbeat/common/transport/httpcommon/httpcommon_common.go b/libbeat/common/transport/httpcommon/httpcommon_common.go new file mode 100644 index 000000000000..fed0c8c48330 --- /dev/null +++ b/libbeat/common/transport/httpcommon/httpcommon_common.go @@ -0,0 +1,354 @@ +// 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. + +package httpcommon + +import ( + "net/http" + "time" + + "go.elastic.co/apm/module/apmhttp" + "golang.org/x/net/http2" + + "github.com/elastic/beats/v7/libbeat/common" + "github.com/elastic/beats/v7/libbeat/common/transport" + "github.com/elastic/beats/v7/libbeat/common/transport/tlscommon" + "github.com/elastic/beats/v7/libbeat/logp" +) + +// HTTPTransportSettings provides common HTTP settings for HTTP clients. +type HTTPTransportSettings struct { + // TLS provides ssl/tls setup settings + TLS *tlscommon.Config `config:"ssl" yaml:"ssl,omitempty" json:"ssl,omitempty"` + + // Timeout configures the `(http.Transport).Timeout`. + Timeout time.Duration `config:"timeout" yaml:"timeout,omitempty" json:"timeout,omitempty"` + + Proxy HTTPClientProxySettings `config:",inline" yaml:",inline"` + + // TODO: Add more settings: + // - DisableKeepAlive + // - MaxIdleConns + // - IdleConnTimeout + // - ResponseHeaderTimeout + // - ConnectionTimeout (currently 'Timeout' is used for both) +} + +// WithKeepaliveSettings options can be used to modify the Keepalive +type WithKeepaliveSettings struct { + Disable bool + MaxIdleConns int + MaxIdleConnsPerHost int + IdleConnTimeout time.Duration +} + +var _ httpTransportOption = WithKeepaliveSettings{} + +const defaultHTTPTimeout = 90 * time.Second + +type ( + // TransportOption are applied to the http.RoundTripper to be build + // from HTTPTransportSettings. + TransportOption interface{ sealTransportOption() } + + extraSettings struct { + logger *logp.Logger + http2 bool + } + + dialerOption interface { + TransportOption + baseDialer() transport.Dialer + } + dialerModOption interface { + TransportOption + applyDialer(*HTTPTransportSettings, transport.Dialer) transport.Dialer + } + httpTransportOption interface { + TransportOption + applyTransport(*HTTPTransportSettings, *http.Transport) + } + roundTripperOption interface { + TransportOption + applyRoundTripper(*HTTPTransportSettings, http.RoundTripper) http.RoundTripper + } + extraOption interface { + TransportOption + applyExtra(*extraSettings) + } +) + +type baseDialerFunc func() transport.Dialer + +var _ dialerOption = baseDialerFunc(nil) + +func (baseDialerFunc) sealTransportOption() {} +func (fn baseDialerFunc) baseDialer() transport.Dialer { + return fn() +} + +type dialerOptFunc func(transport.Dialer) transport.Dialer + +var _ dialerModOption = dialerOptFunc(nil) + +func (dialerOptFunc) sealTransportOption() {} +func (fn dialerOptFunc) applyDialer(_ *HTTPTransportSettings, d transport.Dialer) transport.Dialer { + return fn(d) + +} + +type transportOptFunc func(*HTTPTransportSettings, *http.Transport) + +var _ httpTransportOption = transportOptFunc(nil) + +func (transportOptFunc) sealTransportOption() {} +func (fn transportOptFunc) applyTransport(s *HTTPTransportSettings, t *http.Transport) { + fn(s, t) +} + +type rtOptFunc func(http.RoundTripper) http.RoundTripper + +var _ roundTripperOption = rtOptFunc(nil) + +func (rtOptFunc) sealTransportOption() {} +func (fn rtOptFunc) applyRoundTripper(_ *HTTPTransportSettings, rt http.RoundTripper) http.RoundTripper { + return fn(rt) +} + +type extraOptionFunc func(*extraSettings) + +func (extraOptionFunc) sealTransportOption() {} +func (fn extraOptionFunc) applyExtra(s *extraSettings) { fn(s) } + +// DefaultHTTPTransportSettings returns the default HTTP transport setting. +func DefaultHTTPTransportSettings() HTTPTransportSettings { + return HTTPTransportSettings{ + Proxy: DefaultHTTPClientProxySettings(), + Timeout: defaultHTTPTimeout, + } +} + +// Unpack reads a config object into the settings. +func (settings *HTTPTransportSettings) Unpack(cfg *common.Config) error { + tmp := struct { + TLS *tlscommon.Config `config:"ssl"` + Timeout time.Duration `config:"timeout"` + }{Timeout: settings.Timeout} + + if err := cfg.Unpack(&tmp); err != nil { + return err + } + + var proxy HTTPClientProxySettings + if err := cfg.Unpack(&proxy); err != nil { + return err + } + + _, err := tlscommon.LoadTLSConfig(tmp.TLS) + if err != nil { + return err + } + + *settings = HTTPTransportSettings{ + TLS: tmp.TLS, + Timeout: tmp.Timeout, + Proxy: proxy, + } + return nil +} + +// RoundTripper creates a http.RoundTripper for use with http.Client. +// +// The dialers will registers with stats if given. Stats is used to collect metrics for io errors, +// bytes in, and bytes out. +func (settings *HTTPTransportSettings) RoundTripper(opts ...TransportOption) (http.RoundTripper, error) { + var dialer transport.Dialer + + var extra extraSettings + for _, opt := range opts { + if opt, ok := opt.(extraOption); ok { + opt.applyExtra(&extra) + } + } + + for _, opt := range opts { + if dialOpt, ok := opt.(dialerOption); ok { + dialer = dialOpt.baseDialer() + } + } + + if dialer == nil { + dialer = transport.NetDialer(settings.Timeout) + } + + tls, err := tlscommon.LoadTLSConfig(settings.TLS) + if err != nil { + return nil, err + } + + tlsDialer := transport.TLSDialer(dialer, tls, settings.Timeout) + for _, opt := range opts { + if dialOpt, ok := opt.(dialerModOption); ok { + dialer = dialOpt.applyDialer(settings, dialer) + tlsDialer = dialOpt.applyDialer(settings, tlsDialer) + } + } + + if logger := extra.logger; logger != nil { + dialer = transport.LoggingDialer(dialer, logger) + tlsDialer = transport.LoggingDialer(tlsDialer, logger) + } + + var rt http.RoundTripper + if extra.http2 { + rt, err = settings.http2RoundTripper(tls, dialer, tlsDialer, opts...) + } else { + rt, err = settings.httpRoundTripper(tls, dialer, tlsDialer, opts...) + } + + for _, opt := range opts { + if rtOpt, ok := opt.(roundTripperOption); ok { + rt = rtOpt.applyRoundTripper(settings, rt) + } + } + return rt, nil +} + +func (settings *HTTPTransportSettings) http2RoundTripper( + tls *tlscommon.TLSConfig, + dialer, tlsDialer transport.Dialer, + opts ...TransportOption, +) (*http2.Transport, error) { + t1, err := settings.httpRoundTripper(tls, dialer, tlsDialer, opts...) + if err != nil { + return nil, err + } + + t2, err := http2.ConfigureTransports(t1) + if err != nil { + return nil, err + } + + t2.AllowHTTP = true + return t2, nil +} + +// Client creates a new http.Client with configured Transport. The transport is +// instrumented using apmhttp.WrapRoundTripper. +func (settings HTTPTransportSettings) Client(opts ...TransportOption) (*http.Client, error) { + rt, err := settings.RoundTripper(opts...) + if err != nil { + return nil, err + } + + return &http.Client{Transport: rt, Timeout: settings.Timeout}, nil +} + +func (opts WithKeepaliveSettings) sealTransportOption() {} +func (opts WithKeepaliveSettings) applyTransport(_ *HTTPTransportSettings, t *http.Transport) { + t.DisableKeepAlives = opts.Disable + if opts.IdleConnTimeout != 0 { + t.IdleConnTimeout = opts.IdleConnTimeout + } + if opts.MaxIdleConns != 0 { + t.MaxIdleConns = opts.MaxIdleConns + } + if opts.MaxIdleConnsPerHost != 0 { + t.MaxIdleConnsPerHost = opts.MaxIdleConnsPerHost + } +} + +// WithBaseDialer configures the dialer used for TCP and TLS connections. +func WithBaseDialer(d transport.Dialer) TransportOption { + return baseDialerFunc(func() transport.Dialer { + return d + }) +} + +// WithIOStats instruments the RoundTripper dialers with the given statser, such +// that bytes in, bytes out, and errors can be monitored. +func WithIOStats(stats transport.IOStatser) TransportOption { + return dialerOptFunc(func(d transport.Dialer) transport.Dialer { + if stats == nil { + return d + } + return transport.StatsDialer(d, stats) + }) +} + +// WithTransportFunc register a custom function that is used to apply +// custom changes to the net.Transport, when the Client is build. +func WithTransportFunc(fn func(*http.Transport)) TransportOption { + return transportOptFunc(func(_ *HTTPTransportSettings, t *http.Transport) { + fn(t) + }) +} + +// WithHTTP2Only will ensure that a HTTP 2 only roundtripper is created. +func WithHTTP2Only(b bool) TransportOption { + return extraOptionFunc(func(settings *extraSettings) { + settings.http2 = b + }) +} + +// WithForceAttemptHTTP2 sets the `http.Tansport.ForceAttemptHTTP2` field. +func WithForceAttemptHTTP2(b bool) TransportOption { + return transportOptFunc(func(settings *HTTPTransportSettings, t *http.Transport) { + t.ForceAttemptHTTP2 = b + }) +} + +// WithNOProxy disables the configured proxy. Proxy environment variables +// like HTTP_PROXY and HTTPS_PROXY will have no affect. +func WithNOProxy() TransportOption { + return transportOptFunc(func(s *HTTPTransportSettings, t *http.Transport) { + t.Proxy = nil + }) +} + +// WithoutProxyEnvironmentVariables disables support for the HTTP_PROXY, HTTPS_PROXY and +// NO_PROXY envionrment variables. Explicitely configured proxy URLs will still applied. +func WithoutProxyEnvironmentVariables() TransportOption { + return transportOptFunc(func(settings *HTTPTransportSettings, t *http.Transport) { + if settings.Proxy.Disable || settings.Proxy.URL == nil { + t.Proxy = nil + } + }) +} + +// WithModRoundtripper allows customization of the roundtipper. +func WithModRoundtripper(w func(http.RoundTripper) http.RoundTripper) TransportOption { + return rtOptFunc(w) +} + +var withAPMHTTPRountTripper = WithModRoundtripper(func(rt http.RoundTripper) http.RoundTripper { + return apmhttp.WrapRoundTripper(rt) +}) + +// WithAPMHTTPInstrumentation insruments the HTTP client via apmhttp.WrapRoundTripper. +// Custom APM round tripper wrappers can be configured via WithModRoundtripper. +func WithAPMHTTPInstrumentation() TransportOption { + return withAPMHTTPRountTripper +} + +// WithLogger sets the internal logger that will be used to log dial or TCP level errors. +// Logging at the connection level will only happen if the logger has been set. +func WithLogger(logger *logp.Logger) TransportOption { + return extraOptionFunc(func(s *extraSettings) { + s.logger = logger + }) +} diff --git a/libbeat/common/transport/httpcommon/httpcommon_legacy.go b/libbeat/common/transport/httpcommon/httpcommon_legacy.go new file mode 100644 index 000000000000..c3e1e2e31a7b --- /dev/null +++ b/libbeat/common/transport/httpcommon/httpcommon_legacy.go @@ -0,0 +1,54 @@ +// 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. + +// +build !go1.15 + +package httpcommon + +import ( + "net/http" + + "github.com/elastic/beats/v7/libbeat/common/transport" + "github.com/elastic/beats/v7/libbeat/common/transport/tlscommon" +) + +func (settings *HTTPTransportSettings) httpRoundTripper( + tls *tlscommon.TLSConfig, + dialer, tlsDialer transport.Dialer, + opts ...TransportOption, +) (*http.Transport, error) { + t := http.DefaultTransport.(*http.Transport).Clone() + t.DialContext = nil + t.Dial = dialer.Dial + t.DialTLS = tlsDialer.Dial + t.TLSClientConfig = tls.ToConfig() + t.ForceAttemptHTTP2 = false + t.Proxy = settings.Proxy.ProxyFunc() + t.ProxyConnectHeader = settings.Proxy.Headers + + // reset some internal timeouts to not change old Beats defaults + t.TLSHandshakeTimeout = 0 + t.ExpectContinueTimeout = 0 + + for _, opt := range opts { + if transportOpt, ok := opt.(httpTransportOption); ok { + transportOpt.applyTransport(settings, t) + } + } + + return t, nil +} diff --git a/libbeat/common/transport/tls.go b/libbeat/common/transport/tls.go index 5f8ade67012c..b32268497567 100644 --- a/libbeat/common/transport/tls.go +++ b/libbeat/common/transport/tls.go @@ -15,11 +15,12 @@ // specific language governing permissions and limitations // under the License. +// +build go1.15 + package transport import ( "crypto/tls" - "errors" "fmt" "net" "sync" @@ -29,10 +30,6 @@ import ( "github.com/elastic/beats/v7/libbeat/testing" ) -func TLSDialer(forward Dialer, config *tlscommon.TLSConfig, timeout time.Duration) Dialer { - return TestTLSDialer(testing.NullDriver, forward, config, timeout) -} - func TestTLSDialer( d testing.Driver, forward Dialer, @@ -129,86 +126,3 @@ func TestTLSDialerH2( return tlsDialWith(d, forward, network, address, timeout, tlsConfig, config) }), nil } - -func tlsDialWith( - d testing.Driver, - dialer Dialer, - network, address string, - timeout time.Duration, - tlsConfig *tls.Config, - config *tlscommon.TLSConfig, -) (net.Conn, error) { - socket, err := dialer.Dial(network, address) - if err != nil { - return nil, err - } - - conn := tls.Client(socket, tlsConfig) - - withTimeout := timeout > 0 - if withTimeout { - if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil { - d.Fatal("timeout", err) - _ = conn.Close() - return nil, err - } - } - - if tlsConfig.InsecureSkipVerify { - d.Warn("security", "server's certificate chain verification is disabled") - } else { - d.Info("security", "server's certificate chain verification is enabled") - } - - err = conn.Handshake() - d.Fatal("handshake", err) - if err != nil { - _ = conn.Close() - return nil, err - } - - // remove timeout if handshake was subject to timeout: - if withTimeout { - conn.SetDeadline(time.Time{}) - } - - if err := postVerifyTLSConnection(d, conn, config); err != nil { - _ = conn.Close() - return nil, err - } - - return conn, nil -} - -func postVerifyTLSConnection(d testing.Driver, conn *tls.Conn, config *tlscommon.TLSConfig) error { - st := conn.ConnectionState() - - if !st.HandshakeComplete { - err := errors.New("incomplete handshake") - d.Fatal("incomplete handshake", err) - return err - } - - d.Info("TLS version", fmt.Sprintf("%v", tlscommon.TLSVersion(st.Version))) - - // no more checks if no extra configs available - if config == nil { - return nil - } - - versions := config.Versions - if versions == nil { - versions = tlscommon.TLSDefaultVersions - } - versionOK := false - for _, version := range versions { - versionOK = versionOK || st.Version == uint16(version) - } - if !versionOK { - err := fmt.Errorf("tls version %v not configured", tlscommon.TLSVersion(st.Version)) - d.Fatal("TLS version", err) - return err - } - - return nil -} diff --git a/libbeat/common/transport/tls_common.go b/libbeat/common/transport/tls_common.go new file mode 100644 index 000000000000..232b42f9c094 --- /dev/null +++ b/libbeat/common/transport/tls_common.go @@ -0,0 +1,116 @@ +// 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. + +package transport + +import ( + "crypto/tls" + "errors" + "fmt" + "net" + "time" + + "github.com/elastic/beats/v7/libbeat/common/transport/tlscommon" + "github.com/elastic/beats/v7/libbeat/testing" +) + +func TLSDialer(forward Dialer, config *tlscommon.TLSConfig, timeout time.Duration) Dialer { + return TestTLSDialer(testing.NullDriver, forward, config, timeout) +} + +func tlsDialWith( + d testing.Driver, + dialer Dialer, + network, address string, + timeout time.Duration, + tlsConfig *tls.Config, + config *tlscommon.TLSConfig, +) (net.Conn, error) { + socket, err := dialer.Dial(network, address) + if err != nil { + return nil, err + } + + conn := tls.Client(socket, tlsConfig) + + withTimeout := timeout > 0 + if withTimeout { + if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil { + d.Fatal("timeout", err) + _ = conn.Close() + return nil, err + } + } + + if tlsConfig.InsecureSkipVerify { + d.Warn("security", "server's certificate chain verification is disabled") + } else { + d.Info("security", "server's certificate chain verification is enabled") + } + + err = conn.Handshake() + d.Fatal("handshake", err) + if err != nil { + _ = conn.Close() + return nil, err + } + + // remove timeout if handshake was subject to timeout: + if withTimeout { + conn.SetDeadline(time.Time{}) + } + + if err := postVerifyTLSConnection(d, conn, config); err != nil { + _ = conn.Close() + return nil, err + } + + return conn, nil +} + +func postVerifyTLSConnection(d testing.Driver, conn *tls.Conn, config *tlscommon.TLSConfig) error { + st := conn.ConnectionState() + + if !st.HandshakeComplete { + err := errors.New("incomplete handshake") + d.Fatal("incomplete handshake", err) + return err + } + + d.Info("TLS version", fmt.Sprintf("%v", tlscommon.TLSVersion(st.Version))) + + // no more checks if no extra configs available + if config == nil { + return nil + } + + versions := config.Versions + if versions == nil { + versions = tlscommon.TLSDefaultVersions + } + versionOK := false + for _, version := range versions { + versionOK = versionOK || st.Version == uint16(version) + } + if !versionOK { + err := fmt.Errorf("tls version %v not configured", tlscommon.TLSVersion(st.Version)) + d.Fatal("TLS version", err) + return err + } + + return nil +} diff --git a/libbeat/common/transport/tls_legacy.go b/libbeat/common/transport/tls_legacy.go new file mode 100644 index 000000000000..533a3cf8e56f --- /dev/null +++ b/libbeat/common/transport/tls_legacy.go @@ -0,0 +1,71 @@ +// 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. + +// +build !go1.15 + +package transport + +import ( + "crypto/tls" + "fmt" + "net" + "sync" + "time" + + "github.com/elastic/beats/v7/libbeat/common/transport/tlscommon" + "github.com/elastic/beats/v7/libbeat/testing" +) + +func TestTLSDialer( + d testing.Driver, + forward Dialer, + config *tlscommon.TLSConfig, + timeout time.Duration, +) Dialer { + var lastTLSConfig *tls.Config + var lastNetwork string + var lastAddress string + var m sync.Mutex + + return DialerFunc(func(network, address string) (net.Conn, error) { + switch network { + case "tcp", "tcp4", "tcp6": + default: + return nil, fmt.Errorf("unsupported network type %v", network) + } + + host, _, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + var tlsConfig *tls.Config + m.Lock() + if network == lastNetwork && address == lastAddress { + tlsConfig = lastTLSConfig + } + if tlsConfig == nil { + tlsConfig = config.BuildModuleConfig(host) + lastNetwork = network + lastAddress = address + lastTLSConfig = tlsConfig + } + m.Unlock() + + return tlsDialWith(d, forward, network, address, timeout, tlsConfig, config) + }) +} diff --git a/libbeat/common/transport/tlscommon/config.go b/libbeat/common/transport/tlscommon/config.go index 41d1ad6532ce..04fd1088e4a7 100644 --- a/libbeat/common/transport/tlscommon/config.go +++ b/libbeat/common/transport/tlscommon/config.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +// +build go1.15 + package tlscommon import ( @@ -102,8 +104,3 @@ func (c *Config) Validate() error { return c.Certificate.Validate() } - -// IsEnabled returns true if the `enable` field is set to true in the yaml. -func (c *Config) IsEnabled() bool { - return c != nil && (c.Enabled == nil || *c.Enabled) -} diff --git a/libbeat/common/transport/tlscommon/config_common.go b/libbeat/common/transport/tlscommon/config_common.go new file mode 100644 index 000000000000..b61c26fb4956 --- /dev/null +++ b/libbeat/common/transport/tlscommon/config_common.go @@ -0,0 +1,23 @@ +// 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. + +package tlscommon + +// IsEnabled returns true if the `enable` field is set to true in the yaml. +func (c *Config) IsEnabled() bool { + return c != nil && (c.Enabled == nil || *c.Enabled) +} diff --git a/libbeat/common/transport/tlscommon/config_legacy.go b/libbeat/common/transport/tlscommon/config_legacy.go new file mode 100644 index 000000000000..333dbc340dc8 --- /dev/null +++ b/libbeat/common/transport/tlscommon/config_legacy.go @@ -0,0 +1,102 @@ +// 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. + +// +build !go1.15 + +package tlscommon + +import ( + "crypto/tls" + + "github.com/joeshaw/multierror" +) + +// Config defines the user configurable options in the yaml file. +type Config struct { + Enabled *bool `config:"enabled" yaml:"enabled,omitempty"` + VerificationMode TLSVerificationMode `config:"verification_mode" yaml:"verification_mode"` // one of 'none', 'full' + Versions []TLSVersion `config:"supported_protocols" yaml:"supported_protocols,omitempty"` + CipherSuites []tlsCipherSuite `config:"cipher_suites" yaml:"cipher_suites,omitempty"` + CAs []string `config:"certificate_authorities" yaml:"certificate_authorities,omitempty"` + Certificate CertificateConfig `config:",inline" yaml:",inline"` + CurveTypes []tlsCurveType `config:"curve_types" yaml:"curve_types,omitempty"` + Renegotiation tlsRenegotiationSupport `config:"renegotiation" yaml:"renegotiation"` + CASha256 []string `config:"ca_sha256" yaml:"ca_sha256,omitempty"` +} + +// LoadTLSConfig will load a certificate from config with all TLS based keys +// defined. If Certificate and CertificateKey are configured, client authentication +// will be configured. If no CAs are configured, the host CA will be used by go +// built-in TLS support. +func LoadTLSConfig(config *Config) (*TLSConfig, error) { + if !config.IsEnabled() { + return nil, nil + } + + fail := multierror.Errors{} + logFail := func(es ...error) { + for _, e := range es { + if e != nil { + fail = append(fail, e) + } + } + } + + var cipherSuites []uint16 + for _, suite := range config.CipherSuites { + cipherSuites = append(cipherSuites, uint16(suite)) + } + + var curves []tls.CurveID + for _, id := range config.CurveTypes { + curves = append(curves, tls.CurveID(id)) + } + + cert, err := LoadCertificate(&config.Certificate) + logFail(err) + + cas, errs := LoadCertificateAuthorities(config.CAs) + logFail(errs...) + + // fail, if any error occurred when loading certificate files + if err = fail.Err(); err != nil { + return nil, err + } + + var certs []tls.Certificate + if cert != nil { + certs = []tls.Certificate{*cert} + } + + // return config if no error occurred + return &TLSConfig{ + Versions: config.Versions, + Verification: config.VerificationMode, + Certificates: certs, + RootCAs: cas, + CipherSuites: cipherSuites, + CurvePreferences: curves, + Renegotiation: tls.RenegotiationSupport(config.Renegotiation), + CASha256: config.CASha256, + }, nil +} + +// Validate values the TLSConfig struct making sure certificate sure we have both a certificate and +// a key. +func (c *Config) Validate() error { + return c.Certificate.Validate() +} diff --git a/libbeat/common/transport/tlscommon/server_config.go b/libbeat/common/transport/tlscommon/server_config.go index cf7ab9a390a9..a37de7ef7d76 100644 --- a/libbeat/common/transport/tlscommon/server_config.go +++ b/libbeat/common/transport/tlscommon/server_config.go @@ -15,14 +15,14 @@ // specific language governing permissions and limitations // under the License. +// +build go1.15 + package tlscommon import ( "crypto/tls" "github.com/joeshaw/multierror" - - "github.com/elastic/beats/v7/libbeat/common" ) // ServerConfig defines the user configurable tls options for any TCP based service. @@ -92,41 +92,3 @@ func LoadTLSServerConfig(config *ServerConfig) (*TLSConfig, error) { CASha256: config.CASha256, }, nil } - -// Unpack unpacks the TLS Server configuration. -func (c *ServerConfig) Unpack(cfg common.Config) error { - const clientAuthKey = "client_authentication" - const ca = "certificate_authorities" - - // When we have explicitely defined the `certificate_authorities` in the configuration we default - // to `required` for the `client_authentication`, when CA is not defined we should set to `none`. - if cfg.HasField(ca) && !cfg.HasField(clientAuthKey) { - cfg.SetString(clientAuthKey, -1, "required") - } - type serverCfg ServerConfig - var sCfg serverCfg - if err := cfg.Unpack(&sCfg); err != nil { - return err - } - *c = ServerConfig(sCfg) - return nil -} - -// Validate values the TLSConfig struct making sure certificate sure we have both a certificate and -// a key. -func (c *ServerConfig) Validate() error { - if c.IsEnabled() { - // c.Certificate.Validate() ensures that both a certificate and key - // are specified, or neither are specified. For server-side TLS we - // require both to be specified. - if c.Certificate.Certificate == "" { - return ErrCertificateUnspecified - } - } - return c.Certificate.Validate() -} - -// IsEnabled returns true if the `enable` field is set to true in the yaml. -func (c *ServerConfig) IsEnabled() bool { - return c != nil && (c.Enabled == nil || *c.Enabled) -} diff --git a/libbeat/common/transport/tlscommon/server_config_common.go b/libbeat/common/transport/tlscommon/server_config_common.go new file mode 100644 index 000000000000..cce8d3236806 --- /dev/null +++ b/libbeat/common/transport/tlscommon/server_config_common.go @@ -0,0 +1,60 @@ +// 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. + +package tlscommon + +import ( + "github.com/elastic/beats/v7/libbeat/common" +) + +// Unpack unpacks the TLS Server configuration. +func (c *ServerConfig) Unpack(cfg common.Config) error { + const clientAuthKey = "client_authentication" + const ca = "certificate_authorities" + + // When we have explicitely defined the `certificate_authorities` in the configuration we default + // to `required` for the `client_authentication`, when CA is not defined we should set to `none`. + if cfg.HasField(ca) && !cfg.HasField(clientAuthKey) { + cfg.SetString(clientAuthKey, -1, "required") + } + type serverCfg ServerConfig + var sCfg serverCfg + if err := cfg.Unpack(&sCfg); err != nil { + return err + } + *c = ServerConfig(sCfg) + return nil +} + +// Validate values the TLSConfig struct making sure certificate sure we have both a certificate and +// a key. +func (c *ServerConfig) Validate() error { + if c.IsEnabled() { + // c.Certificate.Validate() ensures that both a certificate and key + // are specified, or neither are specified. For server-side TLS we + // require both to be specified. + if c.Certificate.Certificate == "" { + return ErrCertificateUnspecified + } + } + return c.Certificate.Validate() +} + +// IsEnabled returns true if the `enable` field is set to true in the yaml. +func (c *ServerConfig) IsEnabled() bool { + return c != nil && (c.Enabled == nil || *c.Enabled) +} diff --git a/libbeat/common/transport/tlscommon/server_config_legacy.go b/libbeat/common/transport/tlscommon/server_config_legacy.go new file mode 100644 index 000000000000..89a45adc94ec --- /dev/null +++ b/libbeat/common/transport/tlscommon/server_config_legacy.go @@ -0,0 +1,92 @@ +// 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. + +// +build !go1.15 + +package tlscommon + +import ( + "crypto/tls" + + "github.com/joeshaw/multierror" +) + +// ServerConfig defines the user configurable tls options for any TCP based service. +type ServerConfig struct { + Enabled *bool `config:"enabled"` + VerificationMode TLSVerificationMode `config:"verification_mode"` // one of 'none', 'full' + Versions []TLSVersion `config:"supported_protocols"` + CipherSuites []tlsCipherSuite `config:"cipher_suites"` + CAs []string `config:"certificate_authorities"` + Certificate CertificateConfig `config:",inline"` + CurveTypes []tlsCurveType `config:"curve_types"` + ClientAuth tlsClientAuth `config:"client_authentication"` //`none`, `optional` or `required` +} + +// LoadTLSServerConfig tranforms a ServerConfig into a `tls.Config` to be used directly with golang +// network types. +func LoadTLSServerConfig(config *ServerConfig) (*TLSConfig, error) { + if !config.IsEnabled() { + return nil, nil + } + + fail := multierror.Errors{} + logFail := func(es ...error) { + for _, e := range es { + if e != nil { + fail = append(fail, e) + } + } + } + + var cipherSuites []uint16 + for _, suite := range config.CipherSuites { + cipherSuites = append(cipherSuites, uint16(suite)) + } + + var curves []tls.CurveID + for _, id := range config.CurveTypes { + curves = append(curves, tls.CurveID(id)) + } + + cert, err := LoadCertificate(&config.Certificate) + logFail(err) + + cas, errs := LoadCertificateAuthorities(config.CAs) + logFail(errs...) + + // fail, if any error occurred when loading certificate files + if err = fail.Err(); err != nil { + return nil, err + } + + var certs []tls.Certificate + if cert != nil { + certs = []tls.Certificate{*cert} + } + + // return config if no error occurred + return &TLSConfig{ + Versions: config.Versions, + Verification: config.VerificationMode, + Certificates: certs, + ClientCAs: cas, + CipherSuites: cipherSuites, + CurvePreferences: curves, + ClientAuth: tls.ClientAuthType(config.ClientAuth), + }, nil +} diff --git a/libbeat/common/transport/tlscommon/tls.go b/libbeat/common/transport/tlscommon/tls.go index 9850546f221f..ec3fd60815fc 100644 --- a/libbeat/common/transport/tlscommon/tls.go +++ b/libbeat/common/transport/tlscommon/tls.go @@ -15,202 +15,21 @@ // specific language governing permissions and limitations // under the License. +// +build go1.15 + package tlscommon import ( - "bytes" - "crypto/tls" - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - "io" "io/ioutil" "os" "strings" - - "github.com/elastic/beats/v7/libbeat/logp" ) -const logSelector = "tls" - -// LoadCertificate will load a certificate from disk and return a tls.Certificate or error -func LoadCertificate(config *CertificateConfig) (*tls.Certificate, error) { - if err := config.Validate(); err != nil { - return nil, err - } - - certificate := config.Certificate - key := config.Key - if certificate == "" { - return nil, nil - } - - log := logp.NewLogger(logSelector) - - certPEM, err := ReadPEMFile(log, certificate, config.Passphrase) - if err != nil { - log.Errorf("Failed reading certificate file %v: %+v", certificate, err) - return nil, fmt.Errorf("%v %v", err, certificate) - } - - keyPEM, err := ReadPEMFile(log, key, config.Passphrase) - if err != nil { - log.Errorf("Failed reading key file %v: %+v", key, err) - return nil, fmt.Errorf("%v %v", err, key) - } - - cert, err := tls.X509KeyPair(certPEM, keyPEM) - if err != nil { - log.Errorf("Failed loading client certificate %+v", err) - return nil, err - } - - log.Debugf("Loading certificate: %v and key %v", certificate, key) - return &cert, nil -} - -// ReadPEMFile reads a PEM formatted string either from disk or passed as a plain text starting with a "-" -// and decrypt it with the provided password and return the raw content. -func ReadPEMFile(log *logp.Logger, s, passphrase string) ([]byte, error) { - pass := []byte(passphrase) - var blocks []*pem.Block - - r, err := NewPEMReader(s) - if err != nil { - return nil, err - } - defer r.Close() - - content, err := ioutil.ReadAll(r) - if err != nil { - return nil, err - } - - for len(content) > 0 { - var block *pem.Block - - block, content = pem.Decode(content) - if block == nil { - if len(blocks) == 0 { - return nil, errors.New("no pem file") - } - break - } - - if x509.IsEncryptedPEMBlock(block) { - var buffer []byte - var err error - if len(pass) == 0 { - err = errors.New("No passphrase available") - } else { - // Note, decrypting pem might succeed even with wrong password, but - // only noise will be stored in buffer in this case. - buffer, err = x509.DecryptPEMBlock(block, pass) - } - - if err != nil { - log.Errorf("Dropping encrypted pem '%v' block read from %v. %+v", - block.Type, r, err) - continue - } - - // DEK-Info contains encryption info. Remove header to mark block as - // unencrypted. - delete(block.Headers, "DEK-Info") - block.Bytes = buffer - } - blocks = append(blocks, block) - } - - if len(blocks) == 0 { - return nil, errors.New("no PEM blocks") - } - - // re-encode available, decrypted blocks - buffer := bytes.NewBuffer(nil) - for _, block := range blocks { - err := pem.Encode(buffer, block) - if err != nil { - return nil, err - } - } - return buffer.Bytes(), nil -} - -// LoadCertificateAuthorities read the slice of CAcert and return a Certpool. -func LoadCertificateAuthorities(CAs []string) (*x509.CertPool, []error) { - errors := []error{} - - if len(CAs) == 0 { - return nil, nil - } - - log := logp.NewLogger(logSelector) - roots := x509.NewCertPool() - for _, s := range CAs { - r, err := NewPEMReader(s) - if err != nil { - log.Errorf("Failed reading CA certificate: %+v", err) - errors = append(errors, fmt.Errorf("%v reading %v", err, r)) - continue - } - defer r.Close() - - pemData, err := ioutil.ReadAll(r) - if err != nil { - log.Errorf("Failed reading CA certificate: %+v", err) - errors = append(errors, fmt.Errorf("%v reading %v", err, r)) - continue - } - - if ok := roots.AppendCertsFromPEM(pemData); !ok { - log.Error("Failed to add CA to the cert pool, CA is not a valid PEM document") - errors = append(errors, fmt.Errorf("%v adding %v to the list of known CAs", ErrNotACertificate, r)) - continue - } - log.Debugf("Successfully loaded CA certificate: %v", r) - } - - return roots, errors -} - -func extractMinMaxVersion(versions []TLSVersion) (uint16, uint16) { - if len(versions) == 0 { - versions = TLSDefaultVersions - } - - minVersion := uint16(0xffff) - maxVersion := uint16(0) - for _, version := range versions { - v := uint16(version) - if v < minVersion { - minVersion = v - } - if v > maxVersion { - maxVersion = v - } - } - - return minVersion, maxVersion -} - -// ResolveTLSVersion takes the integer representation and return the name. -func ResolveTLSVersion(v uint16) string { - return TLSVersion(v).String() -} - // ResolveCipherSuite takes the integer representation and return the cipher name. func ResolveCipherSuite(cipher uint16) string { return CipherSuite(cipher).String() } -// PEMReader allows to read a certificate in PEM format either through the disk or from a string. -type PEMReader struct { - reader io.ReadCloser - debugStr string -} - // NewPEMReader returns a new PEMReader. func NewPEMReader(certificate string) (*PEMReader, error) { if IsPEMString(certificate) { @@ -223,24 +42,3 @@ func NewPEMReader(certificate string) (*PEMReader, error) { } return &PEMReader{reader: r, debugStr: certificate}, nil } - -// Close closes the target io.ReadCloser. -func (p *PEMReader) Close() error { - return p.reader.Close() -} - -// Read read bytes from the io.ReadCloser. -func (p *PEMReader) Read(b []byte) (n int, err error) { - return p.reader.Read(b) -} - -func (p *PEMReader) String() string { - return p.debugStr -} - -// IsPEMString returns true if the provided string match a PEM formatted certificate. try to pem decode to validate. -func IsPEMString(s string) bool { - // Trim the certificates to make sure we tolerate any yaml weirdness, we assume that the string starts - // with "-" and let further validation verifies the PEM format. - return strings.HasPrefix(strings.TrimSpace(s), "-") -} diff --git a/libbeat/common/transport/tlscommon/tls_common.go b/libbeat/common/transport/tlscommon/tls_common.go new file mode 100644 index 000000000000..e2cb40aceb76 --- /dev/null +++ b/libbeat/common/transport/tlscommon/tls_common.go @@ -0,0 +1,227 @@ +// 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. + +package tlscommon + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io" + "io/ioutil" + "strings" + + "github.com/elastic/beats/v7/libbeat/logp" +) + +const logSelector = "tls" + +// LoadCertificate will load a certificate from disk and return a tls.Certificate or error +func LoadCertificate(config *CertificateConfig) (*tls.Certificate, error) { + if err := config.Validate(); err != nil { + return nil, err + } + + certificate := config.Certificate + key := config.Key + if certificate == "" { + return nil, nil + } + + log := logp.NewLogger(logSelector) + + certPEM, err := ReadPEMFile(log, certificate, config.Passphrase) + if err != nil { + log.Errorf("Failed reading certificate file %v: %+v", certificate, err) + return nil, fmt.Errorf("%v %v", err, certificate) + } + + keyPEM, err := ReadPEMFile(log, key, config.Passphrase) + if err != nil { + log.Errorf("Failed reading key file %v: %+v", key, err) + return nil, fmt.Errorf("%v %v", err, key) + } + + cert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + log.Errorf("Failed loading client certificate %+v", err) + return nil, err + } + + log.Debugf("Loading certificate: %v and key %v", certificate, key) + return &cert, nil +} + +// ReadPEMFile reads a PEM formatted string either from disk or passed as a plain text starting with a "-" +// and decrypt it with the provided password and return the raw content. +func ReadPEMFile(log *logp.Logger, s, passphrase string) ([]byte, error) { + pass := []byte(passphrase) + var blocks []*pem.Block + + r, err := NewPEMReader(s) + if err != nil { + return nil, err + } + defer r.Close() + + content, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + + for len(content) > 0 { + var block *pem.Block + + block, content = pem.Decode(content) + if block == nil { + if len(blocks) == 0 { + return nil, errors.New("no pem file") + } + break + } + + if x509.IsEncryptedPEMBlock(block) { + var buffer []byte + var err error + if len(pass) == 0 { + err = errors.New("No passphrase available") + } else { + // Note, decrypting pem might succeed even with wrong password, but + // only noise will be stored in buffer in this case. + buffer, err = x509.DecryptPEMBlock(block, pass) + } + + if err != nil { + log.Errorf("Dropping encrypted pem '%v' block read from %v. %+v", + block.Type, r, err) + continue + } + + // DEK-Info contains encryption info. Remove header to mark block as + // unencrypted. + delete(block.Headers, "DEK-Info") + block.Bytes = buffer + } + blocks = append(blocks, block) + } + + if len(blocks) == 0 { + return nil, errors.New("no PEM blocks") + } + + // re-encode available, decrypted blocks + buffer := bytes.NewBuffer(nil) + for _, block := range blocks { + err := pem.Encode(buffer, block) + if err != nil { + return nil, err + } + } + return buffer.Bytes(), nil +} + +// LoadCertificateAuthorities read the slice of CAcert and return a Certpool. +func LoadCertificateAuthorities(CAs []string) (*x509.CertPool, []error) { + errors := []error{} + + if len(CAs) == 0 { + return nil, nil + } + + log := logp.NewLogger(logSelector) + roots := x509.NewCertPool() + for _, s := range CAs { + r, err := NewPEMReader(s) + if err != nil { + log.Errorf("Failed reading CA certificate: %+v", err) + errors = append(errors, fmt.Errorf("%v reading %v", err, r)) + continue + } + defer r.Close() + + pemData, err := ioutil.ReadAll(r) + if err != nil { + log.Errorf("Failed reading CA certificate: %+v", err) + errors = append(errors, fmt.Errorf("%v reading %v", err, r)) + continue + } + + if ok := roots.AppendCertsFromPEM(pemData); !ok { + log.Error("Failed to add CA to the cert pool, CA is not a valid PEM document") + errors = append(errors, fmt.Errorf("%v adding %v to the list of known CAs", ErrNotACertificate, r)) + continue + } + log.Debugf("Successfully loaded CA certificate: %v", r) + } + + return roots, errors +} + +func extractMinMaxVersion(versions []TLSVersion) (uint16, uint16) { + if len(versions) == 0 { + versions = TLSDefaultVersions + } + + minVersion := uint16(0xffff) + maxVersion := uint16(0) + for _, version := range versions { + v := uint16(version) + if v < minVersion { + minVersion = v + } + if v > maxVersion { + maxVersion = v + } + } + + return minVersion, maxVersion +} + +// ResolveTLSVersion takes the integer representation and return the name. +func ResolveTLSVersion(v uint16) string { + return TLSVersion(v).String() +} + +// PEMReader allows to read a certificate in PEM format either through the disk or from a string. +type PEMReader struct { + reader io.ReadCloser + debugStr string +} + +// Close closes the target io.ReadCloser. +func (p *PEMReader) Close() error { + return p.reader.Close() +} + +// Read read bytes from the io.ReadCloser. +func (p *PEMReader) Read(b []byte) (n int, err error) { + return p.reader.Read(b) +} + +func (p *PEMReader) String() string { + return p.debugStr +} + +// IsPEMString returns true if the provided string match a PEM formatted certificate. try to pem decode to validate. +func IsPEMString(s string) bool { + // Trim the certificates to make sure we tolerate any yaml weirdness, we assume that the string starts + // with "-" and let further validation verifies the PEM format. + return strings.HasPrefix(strings.TrimSpace(s), "-") +} diff --git a/libbeat/common/transport/tlscommon/tls_config.go b/libbeat/common/transport/tlscommon/tls_config.go index 77c60f951f84..46ffe502194f 100644 --- a/libbeat/common/transport/tlscommon/tls_config.go +++ b/libbeat/common/transport/tlscommon/tls_config.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +// +build go1.15 + package tlscommon import ( diff --git a/libbeat/common/transport/tlscommon/tls_config_legacy.go b/libbeat/common/transport/tlscommon/tls_config_legacy.go new file mode 100644 index 000000000000..d82a72aa6425 --- /dev/null +++ b/libbeat/common/transport/tlscommon/tls_config_legacy.go @@ -0,0 +1,183 @@ +// 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. + +// +build !go1.15 + +package tlscommon + +import ( + "crypto/tls" + "crypto/x509" + "time" + + "github.com/elastic/beats/v7/libbeat/logp" +) + +// TLSConfig is the interface used to configure a tcp client or server from a `Config` +type TLSConfig struct { + + // List of allowed SSL/TLS protocol versions. Connections might be dropped + // after handshake succeeded, if TLS version in use is not listed. + Versions []TLSVersion + + // Configure SSL/TLS verification mode used during handshake. By default + // VerifyFull will be used. + Verification TLSVerificationMode + + // List of certificate chains to present to the other side of the + // connection. + Certificates []tls.Certificate + + // Set of root certificate authorities use to verify server certificates. + // If RootCAs is nil, TLS might use the system its root CA set (not supported + // on MS Windows). + RootCAs *x509.CertPool + + // Set of root certificate authorities use to verify client certificates. + // If ClientCAs is nil, TLS might use the system its root CA set (not supported + // on MS Windows). + ClientCAs *x509.CertPool + + // List of supported cipher suites. If nil, a default list provided by the + // implementation will be used. + CipherSuites []uint16 + + // Types of elliptic curves that will be used in an ECDHE handshake. If empty, + // the implementation will choose a default. + CurvePreferences []tls.CurveID + + // Renegotiation controls what types of renegotiation are supported. + // The default, never, is correct for the vast majority of applications. + Renegotiation tls.RenegotiationSupport + + // ClientAuth controls how we want to verify certificate from a client, `none`, `optional` and + // `required`, default to required. Do not affect TCP client. + ClientAuth tls.ClientAuthType + + // CASha256 is the CA certificate pin, this is used to validate the CA that will be used to trust + // the server certificate. + CASha256 []string + + // time returns the current time as the number of seconds since the epoch. + // If time is nil, TLS uses time.Now. + time func() time.Time +} + +// ToConfig generates a tls.Config object. Note, you must use BuildModuleConfig to generate a config with +// ServerName set, use that method for servers with SNI. +func (c *TLSConfig) ToConfig() *tls.Config { + if c == nil { + return &tls.Config{} + } + + minVersion, maxVersion := extractMinMaxVersion(c.Versions) + + // When we are using the CAsha256 pin to validate the CA used to validate the chain, + // or when we are using 'certificate' TLS verification mode, we add a custom callback + verifyPeerCertFn := makeVerifyPeerCertificate(c) + + insecure := c.Verification != VerifyFull + if c.Verification == VerifyNone { + logp.NewLogger("tls").Warn("SSL/TLS verifications disabled.") + } + + return &tls.Config{ + MinVersion: minVersion, + MaxVersion: maxVersion, + Certificates: c.Certificates, + RootCAs: c.RootCAs, + ClientCAs: c.ClientCAs, + InsecureSkipVerify: insecure, + CipherSuites: c.CipherSuites, + CurvePreferences: c.CurvePreferences, + Renegotiation: c.Renegotiation, + ClientAuth: c.ClientAuth, + VerifyPeerCertificate: verifyPeerCertFn, + Time: c.time, + } +} + +func (c *TLSConfig) BuildModuleClientConfig(host string) *tls.Config { + if c == nil { + // use default TLS settings, if config is empty. + return &tls.Config{ + ServerName: host, + InsecureSkipVerify: true, + } + } + + config := c.ToConfig() + config.ServerName = host + return config +} + +// BuildServerConfig takes the TLSConfig and transform it into a `tls.Config` for server side objects. +func (c *TLSConfig) BuildServerConfig(host string) *tls.Config { + if c == nil { + // use default TLS settings, if config is empty. + return &tls.Config{ + ServerName: host, + } + } + + config := c.ToConfig() + config.ServerName = host + return config +} + +// BuildModuleConfig takes the TLSConfig and transform it into a `tls.Config`. +func (c *TLSConfig) BuildModuleConfig(host string) *tls.Config { + if c == nil { + // use default TLS settings, if config is empty. + return &tls.Config{ServerName: host} + } + + config := c.ToConfig() + config.ServerName = host + return config +} + +// makeVerifyPeerCertificate creates the verification combination of checking certificate pins and skipping host name validation depending on the config +func makeVerifyPeerCertificate(cfg *TLSConfig) verifyPeerCertFunc { + pin := len(cfg.CASha256) > 0 + skipHostName := cfg.Verification == VerifyCertificate + + if pin && !skipHostName { + return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return verifyCAPin(cfg.CASha256, verifiedChains) + } + } + + if pin && skipHostName { + return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + _, _, err := verifyCertificateExceptServerName(rawCerts, cfg) + if err != nil { + return err + } + return verifyCAPin(cfg.CASha256, verifiedChains) + } + } + + if !pin && skipHostName { + return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + _, _, err := verifyCertificateExceptServerName(rawCerts, cfg) + return err + } + } + + return nil +} diff --git a/libbeat/common/transport/tlscommon/tls_legacy.go b/libbeat/common/transport/tlscommon/tls_legacy.go new file mode 100644 index 000000000000..ec97ab8d8ba1 --- /dev/null +++ b/libbeat/common/transport/tlscommon/tls_legacy.go @@ -0,0 +1,46 @@ +// 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. + +// +build !go1.15 + +package tlscommon + +import ( + "io/ioutil" + "os" + "strings" +) + +// ResolveCipherSuite takes the integer representation and return the cipher name. +func ResolveCipherSuite(cipher uint16) string { + return tlsCipherSuite(cipher).String() +} + +// NewPEMReader returns a new PEMReader. +func NewPEMReader(certificate string) (*PEMReader, error) { + if IsPEMString(certificate) { + // Take a substring of the certificate so we do not leak the whole certificate or private key in the log. + debugStr := certificate[0:256] + "..." + return &PEMReader{reader: ioutil.NopCloser(strings.NewReader(certificate)), debugStr: debugStr}, nil + } + + r, err := os.Open(certificate) + if err != nil { + return nil, err + } + return &PEMReader{reader: r, debugStr: certificate}, nil +} diff --git a/libbeat/common/transport/tlscommon/types.go b/libbeat/common/transport/tlscommon/types.go index ed24138394d7..d7eabf8dadc4 100644 --- a/libbeat/common/transport/tlscommon/types.go +++ b/libbeat/common/transport/tlscommon/types.go @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +// +build go1.15 + package tlscommon import ( diff --git a/libbeat/common/transport/tlscommon/types_legacy.go b/libbeat/common/transport/tlscommon/types_legacy.go new file mode 100644 index 000000000000..d6aa27fcdca2 --- /dev/null +++ b/libbeat/common/transport/tlscommon/types_legacy.go @@ -0,0 +1,267 @@ +// 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. + +// +build !go1.15 + +package tlscommon + +import ( + "crypto/tls" + "errors" + "fmt" +) + +var ( + // ErrNotACertificate indicates a PEM file to be loaded not being a valid + // PEM file or certificate. + ErrNotACertificate = errors.New("file is not a certificate") + + // ErrCertificateNoKey indicate a configuration error with missing key file + ErrKeyUnspecified = errors.New("key file not configured") + + // ErrKeyNoCertificate indicate a configuration error with missing certificate file + ErrCertificateUnspecified = errors.New("certificate file not configured") +) + +var tlsCipherSuites = map[string]tlsCipherSuite{ + // ECDHE-ECDSA + "ECDHE-ECDSA-AES-128-CBC-SHA": tlsCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA), + "ECDHE-ECDSA-AES-128-CBC-SHA256": tlsCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256), + "ECDHE-ECDSA-AES-128-GCM-SHA256": tlsCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256), + "ECDHE-ECDSA-AES-256-CBC-SHA": tlsCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA), + "ECDHE-ECDSA-AES-256-GCM-SHA384": tlsCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384), + "ECDHE-ECDSA-CHACHA20-POLY1305": tlsCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305), + "ECDHE-ECDSA-RC4-128-SHA": tlsCipherSuite(tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA), + + // ECDHE-RSA + "ECDHE-RSA-3DES-CBC3-SHA": tlsCipherSuite(tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA), + "ECDHE-RSA-AES-128-CBC-SHA": tlsCipherSuite(tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA), + "ECDHE-RSA-AES-128-CBC-SHA256": tlsCipherSuite(tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256), + "ECDHE-RSA-AES-128-GCM-SHA256": tlsCipherSuite(tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256), + "ECDHE-RSA-AES-256-CBC-SHA": tlsCipherSuite(tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA), + "ECDHE-RSA-AES-256-GCM-SHA384": tlsCipherSuite(tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384), + "ECDHE-RSA-CHACHA20-POLY1205": tlsCipherSuite(tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305), + "ECDHE-RSA-RC4-128-SHA": tlsCipherSuite(tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA), + + // RSA-X + "RSA-RC4-128-SHA": tlsCipherSuite(tls.TLS_RSA_WITH_RC4_128_SHA), + "RSA-3DES-CBC3-SHA": tlsCipherSuite(tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA), + + // RSA-AES + "RSA-AES-128-CBC-SHA": tlsCipherSuite(tls.TLS_RSA_WITH_AES_128_CBC_SHA), + "RSA-AES-128-CBC-SHA256": tlsCipherSuite(tls.TLS_RSA_WITH_AES_128_CBC_SHA256), + "RSA-AES-128-GCM-SHA256": tlsCipherSuite(tls.TLS_RSA_WITH_AES_128_GCM_SHA256), + "RSA-AES-256-CBC-SHA": tlsCipherSuite(tls.TLS_RSA_WITH_AES_256_CBC_SHA), + "RSA-AES-256-GCM-SHA384": tlsCipherSuite(tls.TLS_RSA_WITH_AES_256_GCM_SHA384), + + "TLS-AES-128-GCM-SHA256": tlsCipherSuite(tls.TLS_AES_128_GCM_SHA256), + "TLS-AES-256-GCM-SHA384": tlsCipherSuite(tls.TLS_AES_256_GCM_SHA384), + "TLS-CHACHA20-POLY1305-SHA256": tlsCipherSuite(tls.TLS_CHACHA20_POLY1305_SHA256), +} + +var tlsCipherSuitesInverse = make(map[tlsCipherSuite]string, len(tlsCipherSuites)) +var tlsRenegotiationSupportTypesInverse = make(map[tlsRenegotiationSupport]string, len(tlsRenegotiationSupportTypes)) +var tlsVerificationModesInverse = make(map[TLSVerificationMode]string, len(tlsVerificationModes)) + +// Init creates a inverse representation of the values mapping. +func init() { + for cipherName, i := range tlsCipherSuites { + tlsCipherSuitesInverse[i] = cipherName + } + + for name, t := range tlsRenegotiationSupportTypes { + tlsRenegotiationSupportTypesInverse[t] = name + } + + for name, t := range tlsVerificationModes { + tlsVerificationModesInverse[t] = name + } +} + +var tlsCurveTypes = map[string]tlsCurveType{ + "P-256": tlsCurveType(tls.CurveP256), + "P-384": tlsCurveType(tls.CurveP384), + "P-521": tlsCurveType(tls.CurveP521), + "X25519": tlsCurveType(tls.X25519), +} + +var tlsRenegotiationSupportTypes = map[string]tlsRenegotiationSupport{ + "never": tlsRenegotiationSupport(tls.RenegotiateNever), + "once": tlsRenegotiationSupport(tls.RenegotiateOnceAsClient), + "freely": tlsRenegotiationSupport(tls.RenegotiateFreelyAsClient), +} + +type tlsClientAuth int + +const ( + tlsClientAuthNone tlsClientAuth = tlsClientAuth(tls.NoClientCert) + tlsClientAuthOptional = tlsClientAuth(tls.VerifyClientCertIfGiven) + tlsClientAuthRequired = tlsClientAuth(tls.RequireAndVerifyClientCert) +) + +var tlsClientAuthTypes = map[string]tlsClientAuth{ + "none": tlsClientAuthNone, + "optional": tlsClientAuthOptional, + "required": tlsClientAuthRequired, +} + +// TLSVerificationMode represents the type of verification to do on the remote host: +// `none`, `certificate`, and `full` and we default to `full`. +// Internally this option is transformed into the `insecure` field in the `tls.Config` struct. +type TLSVerificationMode uint8 + +// Constants of the supported verification mode. +const ( + VerifyFull TLSVerificationMode = iota + VerifyNone + VerifyCertificate +) + +var tlsVerificationModes = map[string]TLSVerificationMode{ + "": VerifyFull, + "full": VerifyFull, + "none": VerifyNone, + "certificate": VerifyCertificate, +} + +func (m TLSVerificationMode) String() string { + if s, ok := tlsVerificationModesInverse[m]; ok { + return s + } + return "unknown" +} + +// MarshalText marshal the verification mode into a human readable value. +func (m TLSVerificationMode) MarshalText() ([]byte, error) { + if s, ok := tlsVerificationModesInverse[m]; ok { + return []byte(s), nil + } + return nil, fmt.Errorf("could not marshal '%+v' to text", m) +} + +// Unpack unpacks the string into constants. +func (m *TLSVerificationMode) Unpack(in interface{}) error { + if in == nil { + *m = VerifyFull + return nil + } + + s, ok := in.(string) + if !ok { + return fmt.Errorf("verification mode must be an identifier") + } + + mode, found := tlsVerificationModes[s] + if !found { + return fmt.Errorf("unknown verification mode '%v'", s) + } + + *m = mode + return nil +} + +func (m *tlsClientAuth) Unpack(in interface{}) error { + if in == nil { + *m = tlsClientAuthRequired + return nil + } + + s, ok := in.(string) + if !ok { + return fmt.Errorf("client authentication must be an identifier") + } + + mode, found := tlsClientAuthTypes[s] + if !found { + return fmt.Errorf("unknown client authentication mode'%v'", s) + } + + *m = mode + return nil +} + +type tlsCipherSuite uint16 + +func (cs *tlsCipherSuite) Unpack(s string) error { + suite, found := tlsCipherSuites[s] + if !found { + return fmt.Errorf("invalid tls cipher suite '%v'", s) + } + + *cs = suite + return nil +} + +func (cs tlsCipherSuite) String() string { + if s, found := tlsCipherSuitesInverse[cs]; found { + return s + } + return "unknown" +} + +type tlsCurveType tls.CurveID + +func (ct *tlsCurveType) Unpack(s string) error { + t, found := tlsCurveTypes[s] + if !found { + return fmt.Errorf("invalid tls curve type '%v'", s) + } + + *ct = t + return nil +} + +type tlsRenegotiationSupport tls.RenegotiationSupport + +func (r *tlsRenegotiationSupport) Unpack(s string) error { + t, found := tlsRenegotiationSupportTypes[s] + if !found { + return fmt.Errorf("invalid tls renegotiation type '%v'", s) + } + + *r = t + return nil +} + +func (r tlsRenegotiationSupport) MarshalText() ([]byte, error) { + if t, found := tlsRenegotiationSupportTypesInverse[r]; found { + return []byte(t), nil + } + + return nil, fmt.Errorf("could not marshal '%+v' to text", r) +} + +// CertificateConfig define a common set of fields for a certificate. +type CertificateConfig struct { + Certificate string `config:"certificate" yaml:"certificate,omitempty"` + Key string `config:"key" yaml:"key,omitempty"` + Passphrase string `config:"key_passphrase" yaml:"key_passphrase,omitempty"` +} + +// Validate validates the CertificateConfig +func (c *CertificateConfig) Validate() error { + hasCertificate := c.Certificate != "" + hasKey := c.Key != "" + + switch { + case hasCertificate && !hasKey: + return ErrKeyUnspecified + case !hasCertificate && hasKey: + return ErrCertificateUnspecified + } + return nil +} diff --git a/libbeat/common/transport/tlscommon/verify_legacy.go b/libbeat/common/transport/tlscommon/verify_legacy.go new file mode 100644 index 000000000000..f0f066465640 --- /dev/null +++ b/libbeat/common/transport/tlscommon/verify_legacy.go @@ -0,0 +1,102 @@ +// 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. + +// Copyright (c) 2009 The Go Authors. All rights reserved. + +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: + +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// This file contains code adapted from golang's crypto/tls/handshake_client.go + +// +build !go1.15 + +package tlscommon + +import ( + "crypto/x509" + "time" + + "github.com/pkg/errors" +) + +// verifyCertificateExceptServerName is a TLS Certificate verification utility method that verifies that the provided +// certificate chain is valid and is signed by one of the root CAs in the provided tls.Config. It is intended to be +// as similar as possible to the default verify, but does not verify that the provided certificate matches the +// ServerName in the tls.Config. +func verifyCertificateExceptServerName( + rawCerts [][]byte, + c *TLSConfig, +) ([]*x509.Certificate, [][]*x509.Certificate, error) { + // this is where we're a bit suboptimal, as we have to re-parse the certificates that have been presented + // during the handshake. + // the verification code here is taken from verifyServerCertificate in crypto/tls/handshake_client.go:824 + certs := make([]*x509.Certificate, len(rawCerts)) + for i, asn1Data := range rawCerts { + cert, err := x509.ParseCertificate(asn1Data) + if err != nil { + return nil, nil, errors.Wrap(err, "tls: failed to parse certificate from server") + } + certs[i] = cert + } + + var t time.Time + if c.time != nil { + t = c.time() + } else { + t = time.Now() + } + + // DNSName omitted in VerifyOptions in order to skip ServerName verification + opts := x509.VerifyOptions{ + Roots: c.RootCAs, + CurrentTime: t, + Intermediates: x509.NewCertPool(), + } + + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + + headCert := certs[0] + + // defer to the default verification performed + chains, err := headCert.Verify(opts) + return certs, chains, err +}