diff --git a/integration/benchmark/node/bench_test.go b/integration/benchmark/node/bench_test.go index 7e1cf1337..c5b69737f 100644 --- a/integration/benchmark/node/bench_test.go +++ b/integration/benchmark/node/bench_test.go @@ -183,10 +183,13 @@ func setupClient(tb testing.TB, confPath string) (*benchmark.ViewClient, error) return nil, err } + tlsEnabled := config.TLSConfig.Enabled + tlsRootCACert := path.Clean(config.TLSConfig.RootCACertPath) + cc := &grpc.ConnectionConfig{ Address: config.Address, - TLSEnabled: true, - TLSRootCertFile: path.Join(config.TLSConfig.PeerCACertPath), + TLSEnabled: tlsEnabled, + TLSRootCertFile: tlsRootCACert, ConnectionTimeout: 10 * time.Second, } diff --git a/integration/fsc/pingpong/pingpong_test.go b/integration/fsc/pingpong/pingpong_test.go index f26437d08..61feefa68 100644 --- a/integration/fsc/pingpong/pingpong_test.go +++ b/integration/fsc/pingpong/pingpong_test.go @@ -9,13 +9,13 @@ package pingpong_test import ( "bytes" "fmt" + "path" "strings" "time" "github.com/hyperledger-labs/fabric-smart-client/integration" "github.com/hyperledger-labs/fabric-smart-client/integration/fsc/pingpong" "github.com/hyperledger-labs/fabric-smart-client/integration/fsc/pingpong/mock" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/client" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/common" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fsc" "github.com/hyperledger-labs/fabric-smart-client/pkg/node" @@ -62,7 +62,8 @@ var _ = Describe("EndToEnd", func() { time.Sleep(3 * time.Second) - webClientConfig, err := client.NewWebClientConfigFromFSC("./testdata/fsc/nodes/initiator.0") + //webClientConfig, err := client.NewWebClientConfigFromFSC("./testdata/fsc/nodes/initiator.0") + webClientConfig, err := client2.ConfigFromFile(path.Join("./testdata/fsc/nodes/initiator.0", "client-config.yaml")) Expect(err).NotTo(HaveOccurred()) initiatorWebClient, err := client2.NewClient(webClientConfig) Expect(err).NotTo(HaveOccurred()) @@ -70,7 +71,8 @@ var _ = Describe("EndToEnd", func() { Expect(err).NotTo(HaveOccurred()) Expect(common.JSONUnmarshalString(res)).To(BeEquivalentTo("OK")) - webClientConfig.TLSCertPath = "" + // test client without TLS + webClientConfig.TLSConfig.Enabled = false initiatorWebClient, err = client2.NewClient(webClientConfig) Expect(err).NotTo(HaveOccurred()) _, err = initiatorWebClient.CallView("init", bytes.NewBuffer([]byte("hi")).Bytes()) @@ -315,7 +317,7 @@ func (s *TestSuite) TestGenerateAndMockPingPong() { } func newWebClient(confDir string) *client2.Client { - c, err := client.NewWebClientConfigFromFSC(confDir) + c, err := client2.ConfigFromFile(path.Join(confDir, "client-config.yaml")) Expect(err).NotTo(HaveOccurred()) initiator, err := client2.NewClient(c) Expect(err).NotTo(HaveOccurred()) diff --git a/integration/fsc/stoprestart/stoprestart_test.go b/integration/fsc/stoprestart/stoprestart_test.go index 22b636b91..f03e6095d 100644 --- a/integration/fsc/stoprestart/stoprestart_test.go +++ b/integration/fsc/stoprestart/stoprestart_test.go @@ -64,7 +64,13 @@ type TestSuite struct { func NewTestSuite(commType fsc.P2PCommunicationType, nodeOpts *integration.ReplicationOptions) *TestSuite { return &TestSuite{integration.NewTestSuite(func() (*integration.Infrastructure, error) { - return integration.Generate(StartPort(), integration.WithRaceDetection, stoprestart.Topology(commType, nodeOpts)...) + ii, err := integration.GenerateAt(StartPort(), "./out/testdata", integration.WithRaceDetection, stoprestart.Topology(commType, nodeOpts)...) + if err != nil { + return nil, err + } + ii.DeleteOnStart = true + ii.DeleteOnStop = false + return ii, nil })} } diff --git a/integration/nwo/client/web.go b/integration/nwo/client/web.go deleted file mode 100644 index a2b07f911..000000000 --- a/integration/nwo/client/web.go +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright IBM Corp. All Rights Reserved. - -SPDX-License-Identifier: Apache-2.0 -*/ - -package client - -import ( - "errors" - - config2 "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/config" - "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/web/client" -) - -// NewWebClientConfigFromFSC returns a web configuration from an FSC node configuration file -func NewWebClientConfigFromFSC(confDir string) (*client.Config, error) { - config := &client.Config{} - configProvider, err := config2.NewProvider(confDir) - if err != nil { - return nil, err - } - config.Host = configProvider.GetString("fsc.web.address") - if configProvider.GetBool("fsc.web.tls.enabled") { - config.TLSCertPath = configProvider.GetPath("fsc.web.tls.cert.file") - config.TLSKeyPath = configProvider.GetPath("fsc.web.tls.key.file") - if len(config.TLSCertPath) == 0 { - return nil, errors.New("web configuration must have sc.web.tls.cert.file with file key defined") - } - config.CACertPath = configProvider.TranslatePath(config.TLSCertPath) - } - return config, nil -} diff --git a/integration/nwo/fsc/fsc.go b/integration/nwo/fsc/fsc.go index b7c34be75..716d627bb 100755 --- a/integration/nwo/fsc/fsc.go +++ b/integration/nwo/fsc/fsc.go @@ -25,7 +25,6 @@ import ( "time" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/api" - "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/client" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/common" runner2 "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/common/runner" "github.com/hyperledger-labs/fabric-smart-client/integration/nwo/fsc/commands" @@ -151,13 +150,18 @@ func (p *Platform) GenerateArtifacts() { p.GenerateRoutingConfig() } + // TLS settings + var tlsConfig view2.TLSClientConfig + tlsConfig.RootCACertPath = path.Join(p.NodeLocalTLSDir(peer.Peer), "ca.crt") + tlsConfig.Enabled = len(tlsConfig.RootCACertPath) > 0 + + // TODO: note that NWO does not yet support mTLS + tlsConfig.ClientAuthRequired = p.ClientAuthRequired() + c := view2.Config{ - Version: 0, - Address: p.PeerAddress(peer, ListenPort), - TLSConfig: view2.TLSConfig{ - PeerCACertPath: path.Join(p.NodeLocalTLSDir(peer.Peer), "ca.crt"), - Timeout: 10 * time.Minute, - }, + Version: 0, + Address: p.PeerAddress(peer, ListenPort), + TLSConfig: tlsConfig, SignerConfig: view2.SignerConfig{ IdentityPath: p.LocalMSPIdentityCert(peer.Peer), KeyPath: p.LocalMSPPrivateKey(peer.Peer), @@ -344,7 +348,9 @@ func (p *Platform) PostRun(bool) { } // Web Client - webClientConfig, err := client.NewWebClientConfigFromFSC(p.NodeDir(node)) + //webClientConfig, err := client.NewWebClientConfigFromFSC(p.NodeDir(node)) + webClientConfig, err := client2.ConfigFromFile(path.Join(p.NodeDir(node), "client-config.yaml")) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) webClient, err := client2.NewClient(webClientConfig) gomega.Expect(err).NotTo(gomega.HaveOccurred()) diff --git a/integration/nwo/fsc/node/core_template.go b/integration/nwo/fsc/node/core_template.go index f496bbcf2..9ddf1306f 100755 --- a/integration/nwo/fsc/node/core_template.go +++ b/integration/nwo/fsc/node/core_template.go @@ -50,9 +50,9 @@ fsc: # Private key used for TLS server key: file: {{ .NodeLocalTLSDir Peer }}/server.key + {{- if .ClientAuthRequired }} # If mutual TLS is enabled, clientRootCAs.files contains a list of additional root certificates # used for verifying certificates of client connections. - {{- if .ClientAuthRequired }} clientRootCAs: files: - {{ .NodeLocalTLSDir Peer }}/ca.crt @@ -122,20 +122,25 @@ fsc: # HTTPS server listener address address: 0.0.0.0:{{ .NodePort Replica "Web" }} tls: + # Require server-side TLS enabled: true + # Require client certificates / mutual TLS for inbound connections. + # Note that clients that are not configured to use a certificate will + # fail to connect to the node. + clientAuthRequired: {{ .ClientAuthRequired }} + # X.509 certificate used for TLS server cert: file: {{ .NodeLocalTLSDir Peer }}/server.crt + # Private key used for TLS server key: file: {{ .NodeLocalTLSDir Peer }}/server.key - # Require client certificates / mutual TLS for inbound connections. - # Note that clients that are not configured to use a certificate will - # fail to connect to the node. - clientAuthRequired: false + {{- if .ClientAuthRequired }} # If mutual TLS is enabled, clientRootCAs.files contains a list of additional root certificates # used for verifying certificates of client connections. clientRootCAs: files: - {{ .NodeLocalTLSDir Peer }}/ca.crt + {{- end }} tracing: # Type of provider to be used: none (default), file, otlp, console provider: {{ Topology.Monitoring.TracingType }} diff --git a/platform/view/services/view/grpc/client/cmd/config.go b/platform/view/services/view/grpc/client/cmd/config.go index d7527c1e5..9f1628d1e 100644 --- a/platform/view/services/view/grpc/client/cmd/config.go +++ b/platform/view/services/view/grpc/client/cmd/config.go @@ -8,7 +8,6 @@ package view import ( "os" - "time" "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" "gopkg.in/yaml.v2" @@ -20,19 +19,20 @@ type SignerConfig struct { KeyPath string } -// TLSConfig defines configuration of a Client -type TLSConfig struct { - CertPath string - KeyPath string - PeerCACertPath string - Timeout time.Duration +// TLSClientConfig defines configuration of a Client +type TLSClientConfig struct { + Enabled bool `yaml:"enabled"` + RootCACertPath string `yaml:"rootCACertPath,omitempty"` + ClientAuthRequired bool `yaml:"clientAuthRequired,omitempty"` + ClientCertPath string `yaml:"clientCertPath,omitempty"` + ClientKeyPath string `yaml:"clientKeyPath,omitempty"` } // Config aggregates configuration of TLS and signing type Config struct { Version int Address string - TLSConfig TLSConfig + TLSConfig TLSClientConfig SignerConfig SignerConfig } @@ -76,3 +76,31 @@ func validateConfig(conf Config) error { } return nil } + +func ValidateTLSConfig(config TLSClientConfig) error { + isEmpty := func(val string) bool { + return len(val) == 0 + } + + if !config.Enabled { + return nil + } + + if isEmpty(config.RootCACertPath) { + return errors.New("rootCACertPath not set") + } + + if !config.ClientAuthRequired { + return nil + } + + if isEmpty(config.ClientKeyPath) { + return errors.New("clientKeyPath not set") + } + + if isEmpty(config.ClientCertPath) { + return errors.New("ClientCertPath not set") + } + + return nil +} diff --git a/platform/view/services/view/grpc/client/cmd/config_test.go b/platform/view/services/view/grpc/client/cmd/config_test.go index 6e236688f..c00b007a2 100644 --- a/platform/view/services/view/grpc/client/cmd/config_test.go +++ b/platform/view/services/view/grpc/client/cmd/config_test.go @@ -24,11 +24,12 @@ func TestConfig(t *testing.T) { fmt.Println(configFilePath) t.Run("save and load a config", func(t *testing.T) { c := Config{ - TLSConfig: TLSConfig{ - CertPath: "foo", - KeyPath: "foo", - PeerCACertPath: "foo", - Timeout: time.Second * 3, + TLSConfig: TLSClientConfig{ + Enabled: true, + ClientAuthRequired: true, + ClientCertPath: "foo", + ClientKeyPath: "foo", + RootCACertPath: "foo", }, SignerConfig: SignerConfig{ KeyPath: "foo", diff --git a/platform/view/services/view/grpc/client/cmd/view.go b/platform/view/services/view/grpc/client/cmd/view.go index 12e614af4..e3aa2c3b7 100644 --- a/platform/view/services/view/grpc/client/cmd/view.go +++ b/platform/view/services/view/grpc/client/cmd/view.go @@ -110,10 +110,12 @@ func parseFlagsToConfig() Config { IdentityPath: userCert, KeyPath: userKey, }, - TLSConfig: TLSConfig{ - KeyPath: tlsKey, - CertPath: tlsCert, - PeerCACertPath: tlsCA, + TLSConfig: TLSClientConfig{ + Enabled: len(tlsCA) > 0, + RootCACertPath: tlsCA, + ClientAuthRequired: len(tlsCert) > 0 && len(tlsKey) > 0, + ClientCertPath: tlsCert, + ClientKeyPath: tlsKey, }, } return conf @@ -145,10 +147,25 @@ func invoke() error { return err } + if err := ValidateTLSConfig(config.TLSConfig); err != nil { + return err + } + + // parse TLS configuration + tlsEnabled := config.TLSConfig.Enabled + tlsRootCACert := path.Clean(config.TLSConfig.RootCACertPath) + + // TODO: + // parse mTLS configuration + //mtlsEnabled := config.TLSConfig.ClientAuthRequired + //tlsClientCert := path.Clean(config.TLSConfig.ClientCertPath) + //tlsClientKey := path.Clean(config.TLSConfig.ClientKeyPath) + cc := &grpc.ConnectionConfig{ - Address: config.Address, - TLSEnabled: true, - TLSRootCertFile: path.Join(config.TLSConfig.PeerCACertPath), + Address: config.Address, + TLSEnabled: tlsEnabled, + TLSRootCertFile: tlsRootCACert, + //TLSClientSideAuth: mtlsEnabled, ConnectionTimeout: 10 * time.Second, } diff --git a/platform/view/services/web/client/client.go b/platform/view/services/web/client/client.go index 0bab7ef78..8d612e9d0 100644 --- a/platform/view/services/web/client/client.go +++ b/platform/view/services/web/client/client.go @@ -25,22 +25,39 @@ import ( "github.com/prometheus/common/expfmt" "github.com/prometheus/common/model" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "gopkg.in/yaml.v2" ) var logger = logging.MustGetLogger() // Config models the configuration for the web client type Config struct { - // Host to connect to - Host string - // CACertRaw is the certificate authority's certificates - CACertRaw []byte - // CACertPath is the Certificate Authority Cert Path - CACertPath string - // TLSCertPath is the TLS client certificate path - TLSCertPath string - // TLSKeyPath is the TLS client key path - TLSKeyPath string + Address string + TLSConfig TLSClientConfig +} + +// TLSClientConfig defines configuration of a Client +type TLSClientConfig struct { + Enabled bool `yaml:"enabled"` + RootCACertPath string `yaml:"rootCACertPath,omitempty"` + ClientAuthRequired bool `yaml:"clientAuthRequired,omitempty"` + ClientCertPath string `yaml:"clientCertPath,omitempty"` + ClientKeyPath string `yaml:"clientKeyPath,omitempty"` +} + +// ConfigFromFile loads the given file and converts it to a Config +func ConfigFromFile(file string) (*Config, error) { + configData, err := os.ReadFile(file) + if err != nil { + return nil, errors.WithStack(err) + } + var config Config + + if err := yaml.Unmarshal(configData, &config); err != nil { + return nil, errors.Errorf("error unmarshalling YAML file %s: %s", file, err) + } + + return &config, nil } func (c *Config) WsURL() string { @@ -55,10 +72,10 @@ func (c *Config) url(protocol string) string { if c.isTlsEnabled() { protocol = protocol + "s" } - return fmt.Sprintf("%s://%s", protocol, c.Host) + return fmt.Sprintf("%s://%s", protocol, c.Address) } func (c *Config) isTlsEnabled() bool { - return c.CACertPath != "" + return c.TLSConfig.Enabled } // Client models a client for an FSC node @@ -72,51 +89,68 @@ type Client struct { // NewClient returns a new web client func NewClient(config *Config) (*Client, error) { - var tlsClientConfig *tls.Config - - tlsEnabled := len(config.CACertPath) != 0 || len(config.CACertRaw) != 0 - - if tlsEnabled { - rootCAs := x509.NewCertPool() - - caCert := config.CACertRaw - if len(config.CACertPath) != 0 { - var err error - caCert, err = os.ReadFile(config.CACertPath) - if err != nil { - return nil, errors.Wrapf(err, "failed to open ca cert") - } - } - rootCAs.AppendCertsFromPEM(caCert) - tlsClientConfig = &tls.Config{ - RootCAs: rootCAs, - } - - if len(config.TLSCertPath) != 0 && len(config.TLSKeyPath) != 0 { - clientCert, err := tls.LoadX509KeyPair( - config.TLSCertPath, - config.TLSKeyPath, - ) - if err != nil { - return nil, errors.Wrapf(err, "failed to load x509 key pair") - } - tlsClientConfig.Certificates = []tls.Certificate{clientCert} - } + tlsConfig, err := createTLSConfig(config.TLSConfig) + if err != nil { + return nil, err + } + + if tlsConfig == nil { + tlsConfig = &tls.Config{InsecureSkipVerify: true} } return &Client{ c: &http.Client{ Transport: otelhttp.NewTransport(&http.Transport{ - TLSClientConfig: tlsClientConfig, + TLSClientConfig: tlsConfig, }), }, url: config.WebURL(), wsUrl: config.WsURL(), - tlsConfig: tlsClientConfig, + tlsConfig: tlsConfig, metricsParser: expfmt.NewTextParser(model.LegacyValidation), }, nil } +// createTLSConfig returns tls.Config based on the passed TLSClientConfig. +// It returns nil and an error if there is no valid TLS configuration provided. +// If TLS is not enabled, we return nil. +func createTLSConfig(config TLSClientConfig) (*tls.Config, error) { + var cfg tls.Config + + if !config.Enabled { + return nil, nil + } + + if len(config.RootCACertPath) < 1 { + return nil, errors.New("RootCACertPath is not set") + } + + caCert, err := os.ReadFile(config.RootCACertPath) + if err != nil { + return nil, errors.Wrapf(err, "failed to open ca cert") + } + + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(caCert) + cfg.RootCAs = rootCAs + + if !config.ClientAuthRequired { + return &cfg, nil + } + + // mTLS + clientCert, err := tls.LoadX509KeyPair( + config.ClientCertPath, + config.ClientKeyPath, + ) + if err != nil { + return nil, errors.Wrapf(err, "failed to load x509 key pair") + } + cfg.Certificates = []tls.Certificate{clientCert} + + return &cfg, nil +} + func (c *Client) Metrics() (map[string]*dto.MetricFamily, error) { body, err := c.req(context.Background(), http.MethodGet, fmt.Sprintf("%s/metrics", c.url), nil) if err != nil { diff --git a/platform/view/services/web/server/server.go b/platform/view/services/web/server/server.go index 01ed4d4aa..18d3449b5 100644 --- a/platform/view/services/web/server/server.go +++ b/platform/view/services/web/server/server.go @@ -40,23 +40,13 @@ func (t TLS) Config() (*tls.Config, error) { if !t.Enabled { return tlsConfig, nil } + + // setup TLS cert, err := tls.LoadX509KeyPair(t.CertFile, t.KeyFile) if err != nil { return nil, err } - if len(t.ClientCACertFiles) == 0 { - return nil, errors.Errorf("client TLS CA certificate pool must not be empty") - } - - caCertPool := x509.NewCertPool() - for _, caPath := range t.ClientCACertFiles { - caPem, err := os.ReadFile(caPath) - if err != nil { - return nil, err - } - caCertPool.AppendCertsFromPEM(caPem) - } tlsConfig = &tls.Config{ Certificates: []tls.Certificate{cert}, CipherSuites: []uint16{ @@ -71,13 +61,29 @@ func (t TLS) Config() (*tls.Config, error) { }, MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, - ClientCAs: caCertPool, + ClientAuth: tls.VerifyClientCertIfGiven, } - if t.ClientAuth { - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - } else { - tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven + + if !t.ClientAuth { + return tlsConfig, nil + } + + // setup mTLS + if len(t.ClientCACertFiles) == 0 { + return nil, errors.Errorf("client TLS CA certificate pool must not be empty") } + + caCertPool := x509.NewCertPool() + for _, caPath := range t.ClientCACertFiles { + caPem, err := os.ReadFile(caPath) + if err != nil { + return nil, err + } + caCertPool.AppendCertsFromPEM(caPem) + } + tlsConfig.ClientCAs = caCertPool + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + return tlsConfig, nil }