From 1541dff3ba8a1bf3dd8cfbbbbc55b41d14e6f609 Mon Sep 17 00:00:00 2001 From: Genady Gurevich Date: Wed, 21 Jan 2026 17:16:19 +0200 Subject: [PATCH 1/3] saving Signed-off-by: Genady Gurevich From a39c3de9fcae5bb9ed26ad978bd65e0f4c60ab52 Mon Sep 17 00:00:00 2001 From: Genady Gurevich Date: Wed, 21 Jan 2026 17:16:19 +0200 Subject: [PATCH 2/3] saving Signed-off-by: Genady Gurevich From 8223f6bb13b4c1dae5d4e09e9ca0f66546558487 Mon Sep 17 00:00:00 2001 From: Genady Gurevich Date: Wed, 21 Jan 2026 17:16:19 +0200 Subject: [PATCH 3/3] Operations endpoints: TLS support (issue #45) Signed-off-by: Genady Gurevich --- common/fabhttp/tls_test.go | 141 ++++++++++++++++++++++++++++++++++++ common/operations/system.go | 16 ++-- config/config.go | 24 +++--- testutil/network_utils.go | 21 +++++- 4 files changed, 183 insertions(+), 19 deletions(-) create mode 100644 common/fabhttp/tls_test.go diff --git a/common/fabhttp/tls_test.go b/common/fabhttp/tls_test.go new file mode 100644 index 000000000..ff22cd7ef --- /dev/null +++ b/common/fabhttp/tls_test.go @@ -0,0 +1,141 @@ +/* +Copyright IBM Corp All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package fabhttp_test + +import ( + "crypto/tls" + "crypto/x509" + "os" + "path/filepath" + + "github.com/hyperledger/fabric/common/fabhttp" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("TLS", func() { + var httpTLS fabhttp.TLS + var tempDir string + + BeforeEach(func() { + var err error + tempDir, err = os.MkdirTemp("", "opstls") + Expect(err).NotTo(HaveOccurred()) + + generateCertificates(tempDir) + + httpTLS = fabhttp.TLS{ + Enabled: true, + CertFile: filepath.Join(tempDir, "server-cert.pem"), + KeyFile: filepath.Join(tempDir, "server-key.pem"), + ClientCertRequired: true, + ClientCACertFiles: []string{ + filepath.Join(tempDir, "client-ca.pem"), + }, + } + }) + + AfterEach(func() { + os.RemoveAll(tempDir) + }) + + It("creates a valid TLS configuration", func() { + cert, err := tls.LoadX509KeyPair( + filepath.Join(tempDir, "server-cert.pem"), + filepath.Join(tempDir, "server-key.pem"), + ) + Expect(err).NotTo(HaveOccurred()) + + pemBytes, err := os.ReadFile(filepath.Join(tempDir, "client-ca.pem")) + Expect(err).NotTo(HaveOccurred()) + + clientCAPool := x509.NewCertPool() + clientCAPool.AppendCertsFromPEM(pemBytes) + + tlsConfig, err := httpTLS.Config() + Expect(err).NotTo(HaveOccurred()) + + // https://go-review.googlesource.com/c/go/+/229917 + Expect(tlsConfig.ClientCAs.Equal(clientCAPool)).To(BeTrue()) + tlsConfig.ClientCAs = nil + + Expect(tlsConfig).To(Equal(&tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{cert}, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + }, + ClientAuth: tls.RequireAndVerifyClientCert, + })) + }) + + Context("when TLS is not enabled", func() { + BeforeEach(func() { + httpTLS.Enabled = false + }) + + It("returns a nil config", func() { + tlsConfig, err := httpTLS.Config() + Expect(err).NotTo(HaveOccurred()) + Expect(tlsConfig).To(BeNil()) + }) + }) + + Context("when a client certificate is not required", func() { + BeforeEach(func() { + httpTLS.ClientCertRequired = false + }) + + It("requests a client cert with verification", func() { + tlsConfig, err := httpTLS.Config() + Expect(err).NotTo(HaveOccurred()) + Expect(tlsConfig.ClientAuth).To(Equal(tls.VerifyClientCertIfGiven)) + }) + }) + + Context("when the server certificate cannot be constructed", func() { + BeforeEach(func() { + httpTLS.CertFile = "non-existent-file" + }) + + It("returns an error", func() { + _, err := httpTLS.Config() + Expect(err).To(MatchError("open non-existent-file: no such file or directory")) + }) + }) + + Context("the client CA slice is empty", func() { + BeforeEach(func() { + httpTLS.ClientCACertFiles = nil + }) + + It("builds a TLS configuration without an empty CA pool", func() { + emptyPool := x509.NewCertPool() + tlsConfig, err := httpTLS.Config() + Expect(err).NotTo(HaveOccurred()) + Expect(tlsConfig.ClientCAs.Equal(emptyPool)).To(BeTrue()) + }) + }) + + Context("when a client CA cert cannot be read", func() { + BeforeEach(func() { + httpTLS.ClientCACertFiles = []string{ + "non-existent-file", + } + }) + + It("returns an error", func() { + _, err := httpTLS.Config() + Expect(err).To(MatchError("open non-existent-file: no such file or directory")) + }) + }) +}) diff --git a/common/operations/system.go b/common/operations/system.go index cb471fde3..c1f9c030f 100644 --- a/common/operations/system.go +++ b/common/operations/system.go @@ -71,8 +71,8 @@ type Options struct { Version string } -func operationServiceURL(operationSubPath string, address string, logger *flogging.FabricLogger) string { - uRL, err := url.JoinPath("http://", address, operationSubPath) +func operationServiceURL(scheme string, operationSubPath string, address string, logger *flogging.FabricLogger) string { + uRL, err := url.JoinPath(scheme, address, operationSubPath) if err != nil { logger.Panicf("failed to construct metrics URL: %s", err) } @@ -80,11 +80,17 @@ func operationServiceURL(operationSubPath string, address string, logger *floggi } func PrometheusMetricsServiceURL(system *System, logger *flogging.FabricLogger) string { - return operationServiceURL("metrics", system.Addr(), logger) + if system.options.TLS.Enabled { + return operationServiceURL("https://", "metrics", system.Addr(), logger) + } + return operationServiceURL("http://", "metrics", system.Addr(), logger) } func HealthCheckServiceURL(system *System, logger *flogging.FabricLogger) string { - return operationServiceURL("healthz", system.Addr(), logger) + if system.options.TLS.Enabled { + return operationServiceURL("https://", "healthz", system.Addr(), logger) + } + return operationServiceURL("http://", "healthz", system.Addr(), logger) } // NewOperationsSystem creates a new operations system with the provided configuration. @@ -94,7 +100,7 @@ func NewOperationsSystem(ops Operations, metricsConfig Metrics) *System { Logger: flogging.MustGetLogger("orderer.operations"), ListenAddress: ops.ListenAddress, TLS: fabhttp.TLS{ - Enabled: false, // TLS is not currently supported for the operations server --- IGNORE --- + Enabled: ops.TLS.Enabled, CertFile: ops.TLS.Certificate, KeyFile: ops.TLS.PrivateKey, ClientCertRequired: ops.TLS.ClientAuthRequired, diff --git a/config/config.go b/config/config.go index ab8f608ea..c3deacb72 100644 --- a/config/config.go +++ b/config/config.go @@ -274,8 +274,8 @@ func (config *Configuration) ExtractRouterConfig(configBlock *common.Block) *nod if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey == "" { config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.PrivateKey } - if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs == nil { - config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.RootCAs + if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs == nil { + config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.RootCAs } } @@ -314,7 +314,7 @@ func (config *Configuration) ExtractRouterConfig(configBlock *common.Block) *nod Certificate: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.Certificate, PrivateKey: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey, ClientAuthRequired: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientAuthRequired, - RootCAs: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs, + ClientRootCAs: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs, }, }, Metrics: &operations.Metrics{ @@ -348,8 +348,8 @@ func (config *Configuration) ExtractBatcherConfig(configBlock *common.Block) *no if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey == "" { config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.PrivateKey } - if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs == nil { - config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.RootCAs + if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs == nil { + config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.RootCAs } } @@ -399,7 +399,7 @@ func (config *Configuration) ExtractBatcherConfig(configBlock *common.Block) *no Certificate: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.Certificate, PrivateKey: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey, ClientAuthRequired: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientAuthRequired, - RootCAs: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs, + ClientRootCAs: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs, }, }, Metrics: &operations.Metrics{ @@ -463,8 +463,8 @@ func (config *Configuration) ExtractConsenterConfig(configBlock *common.Block) * if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey == "" { config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.PrivateKey } - if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs == nil { - config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.RootCAs + if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs == nil { + config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.RootCAs } } @@ -501,7 +501,7 @@ func (config *Configuration) ExtractConsenterConfig(configBlock *common.Block) * Certificate: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.Certificate, PrivateKey: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey, ClientAuthRequired: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientAuthRequired, - RootCAs: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs, + ClientRootCAs: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs, }, }, Metrics: &operations.Metrics{ @@ -543,8 +543,8 @@ func (config *Configuration) ExtractAssemblerConfig(configBlock *common.Block) * if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey == "" { config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.PrivateKey } - if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs == nil { - config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.RootCAs + if config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs == nil { + config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs = config.LocalConfig.NodeLocalConfig.GeneralConfig.TLSConfig.RootCAs } } @@ -583,7 +583,7 @@ func (config *Configuration) ExtractAssemblerConfig(configBlock *common.Block) * Certificate: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.Certificate, PrivateKey: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.PrivateKey, ClientAuthRequired: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientAuthRequired, - RootCAs: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.RootCAs, + ClientRootCAs: config.LocalConfig.NodeLocalConfig.OperationsConfig.TLSConfig.ClientRootCAs, }, }, Metrics: &operations.Metrics{ diff --git a/testutil/network_utils.go b/testutil/network_utils.go index 0ebb112e1..2bf3236fa 100644 --- a/testutil/network_utils.go +++ b/testutil/network_utils.go @@ -8,6 +8,7 @@ package testutil import ( "context" + "crypto/tls" "io" "net" "net/http" @@ -53,8 +54,24 @@ func GetAvailablePort(t testing.TB) (port string, ll net.Listener) { // FetchPrometheusMetricValue fetches the value of a Prometheus metric from the specified URL using the provided regular expression. // It returns the metric value as an integer. If the metric is not found or cannot be converted to an integer, it returns -1. -func FetchPrometheusMetricValue(t *testing.T, re *regexp.Regexp, url string) int { - resp, err := http.Get(url) +func FetchPrometheusMetricValue(t *testing.T, re *regexp.Regexp, url string, tlsOpts ...func(config *tls.Config)) int { + require.NotEmpty(t, url, "URL cannot be empty") + require.NotNil(t, re, "Regular expression cannot be nil") + + tlsClientConfig := &tls.Config{} + + for _, opt := range tlsOpts { + opt(tlsClientConfig) + } + + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = tlsClientConfig + + client := &http.Client{ + Transport: transport, + Timeout: 10 * time.Second, + } + resp, err := client.Get(url) require.NoError(t, err) defer resp.Body.Close()