Skip to content

Commit 54532fc

Browse files
Merge pull request #67 from kaleido-io/ws-mtls
feat: Add tls configuration for wsclient upgrade
2 parents 18ea0dc + b1dbfd2 commit 54532fc

6 files changed

Lines changed: 233 additions & 8 deletions

File tree

pkg/fftls/fftls.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ const (
3333
)
3434

3535
func ConstructTLSConfig(ctx context.Context, conf config.Section, tlsType string) (*tls.Config, error) {
36+
if !conf.GetBool(HTTPConfTLSEnabled) {
37+
return nil, nil
38+
}
39+
3640
tlsConfig := &tls.Config{
3741
MinVersion: tls.VersionTLS12,
3842
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
@@ -42,10 +46,6 @@ func ConstructTLSConfig(ctx context.Context, conf config.Section, tlsType string
4246
},
4347
}
4448

45-
if !conf.GetBool(HTTPConfTLSEnabled) {
46-
return tlsConfig, nil
47-
}
48-
4949
var err error
5050
// Support custom CA file
5151
var rootCAs *x509.CertPool

pkg/wsclient/wsclient.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package wsclient
1818

1919
import (
2020
"context"
21+
"crypto/tls"
2122
"encoding/base64"
2223
"fmt"
2324
"io"
@@ -45,6 +46,7 @@ type WSConfig struct {
4546
AuthPassword string `json:"authPassword,omitempty"`
4647
HTTPHeaders fftypes.JSONObject `json:"headers,omitempty"`
4748
HeartbeatInterval time.Duration `json:"heartbeatInterval,omitempty"`
49+
TLSClientConfig *tls.Config `json:"tlsClientConfig,omitempty"`
4850
}
4951

5052
type WSClient interface {
@@ -96,6 +98,7 @@ func New(ctx context.Context, config *WSConfig, beforeConnect WSPreConnectHandle
9698
wsdialer: &websocket.Dialer{
9799
ReadBufferSize: config.ReadBufferSize,
98100
WriteBufferSize: config.WriteBufferSize,
101+
TLSClientConfig: config.TLSClientConfig,
99102
},
100103
retry: retry.Retry{
101104
InitialDelay: config.InitialDelay,

pkg/wsclient/wsclient_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ package wsclient
1818

1919
import (
2020
"context"
21+
"crypto/tls"
22+
"crypto/x509"
2123
"fmt"
2224
"net/http"
2325
"net/http/httptest"
26+
"os"
2427
"testing"
2528
"time"
2629

@@ -32,6 +35,85 @@ func generateConfig() *WSConfig {
3235
return &WSConfig{}
3336
}
3437

38+
func TestWSClientE2ETLS(t *testing.T) {
39+
40+
publicKeyFile, privateKeyFile := GenerateTLSCertficates(t)
41+
defer os.Remove(privateKeyFile.Name())
42+
defer os.Remove(publicKeyFile.Name())
43+
toServer, fromServer, url, close, err := NewTestTLSWSServer(func(req *http.Request) {
44+
assert.Equal(t, "/test/updated", req.URL.Path)
45+
}, publicKeyFile, privateKeyFile)
46+
defer close()
47+
assert.NoError(t, err)
48+
49+
first := true
50+
beforeConnect := func(ctx context.Context) error {
51+
if first {
52+
first = false
53+
return fmt.Errorf("first run fails")
54+
}
55+
return nil
56+
}
57+
afterConnect := func(ctx context.Context, w WSClient) error {
58+
return w.Send(ctx, []byte(`after connect message`))
59+
}
60+
61+
// Init clean config
62+
wsConfig := generateConfig()
63+
64+
wsConfig.HTTPURL = url
65+
wsConfig.WSKeyPath = "/test"
66+
wsConfig.HeartbeatInterval = 50 * time.Millisecond
67+
wsConfig.InitialConnectAttempts = 2
68+
rootCAs := x509.NewCertPool()
69+
caPEM, _ := os.ReadFile(publicKeyFile.Name())
70+
ok := rootCAs.AppendCertsFromPEM(caPEM)
71+
assert.True(t, ok)
72+
cert, err := tls.LoadX509KeyPair(publicKeyFile.Name(), privateKeyFile.Name())
73+
assert.NoError(t, err)
74+
75+
wsConfig.TLSClientConfig = &tls.Config{
76+
MinVersion: tls.VersionTLS12,
77+
Certificates: []tls.Certificate{cert},
78+
RootCAs: rootCAs,
79+
}
80+
81+
wsc, err := New(context.Background(), wsConfig, beforeConnect, afterConnect)
82+
assert.NoError(t, err)
83+
84+
// Change the settings and connect
85+
wsc.SetURL(wsc.URL() + "/updated")
86+
err = wsc.Connect()
87+
assert.NoError(t, err)
88+
89+
// Receive the message automatically sent in afterConnect
90+
message1 := <-toServer
91+
assert.Equal(t, `after connect message`, message1)
92+
93+
// Tell the unit test server to send us a reply, and confirm it
94+
fromServer <- `some data from server`
95+
reply := <-wsc.Receive()
96+
assert.Equal(t, `some data from server`, string(reply))
97+
98+
// Send some data back
99+
err = wsc.Send(context.Background(), []byte(`some data to server`))
100+
assert.NoError(t, err)
101+
102+
// Check the sevrer got it
103+
message2 := <-toServer
104+
assert.Equal(t, `some data to server`, message2)
105+
106+
// Check heartbeating works
107+
beforePing := time.Now()
108+
for wsc.(*wsClient).lastPingCompleted.Before(beforePing) {
109+
time.Sleep(10 * time.Millisecond)
110+
}
111+
112+
// Close the client
113+
wsc.Close()
114+
115+
}
116+
35117
func TestWSClientE2E(t *testing.T) {
36118

37119
toServer, fromServer, url, close := NewTestWSServer(func(req *http.Request) {

pkg/wsclient/wsconfig.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
package wsclient
1818

1919
import (
20+
"context"
21+
2022
"github.com/hyperledger/firefly-common/pkg/config"
2123
"github.com/hyperledger/firefly-common/pkg/ffresty"
24+
"github.com/hyperledger/firefly-common/pkg/fftls"
2225
)
2326

2427
const (
@@ -53,8 +56,8 @@ func InitConfig(conf config.Section) {
5356
conf.AddKnownKey(WSConfigHeartbeatInterval, defaultHeartbeatInterval)
5457
}
5558

56-
func GenerateConfig(conf config.Section) *WSConfig {
57-
return &WSConfig{
59+
func GenerateConfig(ctx context.Context, conf config.Section) (*WSConfig, error) {
60+
wsConfig := &WSConfig{
5861
HTTPURL: conf.GetString(ffresty.HTTPConfigURL),
5962
WSKeyPath: conf.GetString(WSConfigKeyPath),
6063
ReadBufferSize: int(conf.GetByteSize(WSConfigKeyReadBufferSize)),
@@ -67,4 +70,13 @@ func GenerateConfig(conf config.Section) *WSConfig {
6770
AuthPassword: conf.GetString(ffresty.HTTPConfigAuthPassword),
6871
HeartbeatInterval: conf.GetDuration(WSConfigHeartbeatInterval),
6972
}
73+
tlsSection := conf.SubSection("tls")
74+
tlsClientConfig, err := fftls.ConstructTLSConfig(ctx, tlsSection, fftls.ClientType)
75+
if err != nil {
76+
return nil, err
77+
}
78+
79+
wsConfig.TLSClientConfig = tlsClientConfig
80+
81+
return wsConfig, nil
7082
}

pkg/wsclient/wsconfig_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package wsclient
22

33
import (
4+
"context"
45
"testing"
56
"time"
67

78
"github.com/hyperledger/firefly-common/pkg/config"
89
"github.com/hyperledger/firefly-common/pkg/ffresty"
10+
"github.com/hyperledger/firefly-common/pkg/fftls"
911
"github.com/stretchr/testify/assert"
1012
)
1113

@@ -32,7 +34,9 @@ func TestWSConfigGeneration(t *testing.T) {
3234
utConf.Set(WSConfigKeyInitialConnectAttempts, 1)
3335
utConf.Set(WSConfigKeyPath, "/websocket")
3436

35-
wsConfig := GenerateConfig(utConf)
37+
ctx := context.Background()
38+
wsConfig, err := GenerateConfig(ctx, utConf)
39+
assert.NoError(t, err)
3640

3741
assert.Equal(t, "http://test:12345", wsConfig.HTTPURL)
3842
assert.Equal(t, "user", wsConfig.AuthUsername)
@@ -45,3 +49,15 @@ func TestWSConfigGeneration(t *testing.T) {
4549
assert.Equal(t, 1024, wsConfig.ReadBufferSize)
4650
assert.Equal(t, 1024, wsConfig.WriteBufferSize)
4751
}
52+
53+
func TestWSConfigTLSGenerationFail(t *testing.T) {
54+
resetConf()
55+
56+
tlsSection := utConf.SubSection("tls")
57+
tlsSection.Set(fftls.HTTPConfTLSEnabled, true)
58+
tlsSection.Set(fftls.HTTPConfTLSCAFile, "bad-ca")
59+
60+
ctx := context.Background()
61+
_, err := GenerateConfig(ctx, utConf)
62+
assert.Regexp(t, "FF00153", err)
63+
}

pkg/wsclient/wstestserver.go

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright © 2022 Kaleido, Inc.
1+
// Copyright © 2023 Kaleido, Inc.
22
//
33
// SPDX-License-Identifier: Apache-2.0
44
//
@@ -17,13 +17,125 @@
1717
package wsclient
1818

1919
import (
20+
"crypto/rand"
21+
"crypto/rsa"
22+
"crypto/tls"
23+
"crypto/x509"
24+
"crypto/x509/pkix"
25+
"encoding/pem"
2026
"fmt"
27+
"math/big"
28+
"net"
2129
"net/http"
2230
"net/http/httptest"
31+
"os"
32+
"testing"
33+
"time"
2334

2435
"github.com/gorilla/websocket"
36+
"github.com/stretchr/testify/assert"
2537
)
2638

39+
// GenerateTLSCertificates creates a key pair for server and client auth
40+
func GenerateTLSCertficates(t *testing.T) (publicKeyFile *os.File, privateKeyFile *os.File) {
41+
// Create an X509 certificate pair
42+
privatekey, _ := rsa.GenerateKey(rand.Reader, 2048)
43+
publickey := &privatekey.PublicKey
44+
var privateKeyBytes = x509.MarshalPKCS1PrivateKey(privatekey)
45+
privateKeyFile, _ = os.CreateTemp("", "key.pem")
46+
privateKeyBlock := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: privateKeyBytes}
47+
err := pem.Encode(privateKeyFile, privateKeyBlock)
48+
assert.NoError(t, err)
49+
serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
50+
x509Template := &x509.Certificate{
51+
SerialNumber: serialNumber,
52+
Subject: pkix.Name{
53+
Organization: []string{"Unit Tests"},
54+
},
55+
NotBefore: time.Now(),
56+
NotAfter: time.Now().Add(1000 * time.Second),
57+
KeyUsage: x509.KeyUsageDigitalSignature,
58+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
59+
BasicConstraintsValid: true,
60+
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)},
61+
}
62+
derBytes, err := x509.CreateCertificate(rand.Reader, x509Template, x509Template, publickey, privatekey)
63+
assert.NoError(t, err)
64+
publicKeyFile, _ = os.CreateTemp("", "cert.pem")
65+
err = pem.Encode(publicKeyFile, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
66+
assert.NoError(t, err)
67+
68+
return publicKeyFile, privateKeyFile
69+
}
70+
71+
// NewTestTLSWSServer creates a little test server for packages (including wsclient itself) to use in unit tests
72+
// and secured with mTLS by passing in a key pair
73+
func NewTestTLSWSServer(testReq func(req *http.Request), publicKeyFile *os.File, privateKeyFile *os.File) (toServer, fromServer chan string, url string, done func(), err error) {
74+
upgrader := &websocket.Upgrader{WriteBufferSize: 1024, ReadBufferSize: 1024}
75+
toServer = make(chan string, 1)
76+
fromServer = make(chan string, 1)
77+
sendDone := make(chan struct{})
78+
receiveDone := make(chan struct{})
79+
connected := false
80+
81+
handlerFunc := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
82+
if testReq != nil {
83+
testReq(req)
84+
}
85+
if connected {
86+
// test server only handles one open connection, as it only has one set of channels
87+
res.WriteHeader(409)
88+
return
89+
}
90+
ws, _ := upgrader.Upgrade(res, req, http.Header{})
91+
go func() {
92+
defer close(receiveDone)
93+
for {
94+
_, data, err := ws.ReadMessage()
95+
if err != nil {
96+
return
97+
}
98+
toServer <- string(data)
99+
}
100+
}()
101+
go func() {
102+
defer close(sendDone)
103+
defer ws.Close()
104+
for data := range fromServer {
105+
_ = ws.WriteMessage(websocket.TextMessage, []byte(data))
106+
}
107+
}()
108+
connected = true
109+
})
110+
111+
svr := httptest.NewUnstartedServer(handlerFunc)
112+
113+
cert, err := tls.LoadX509KeyPair(publicKeyFile.Name(), privateKeyFile.Name())
114+
if err != nil {
115+
return toServer, fromServer, "", nil, err
116+
}
117+
rootCAs := x509.NewCertPool()
118+
caPEM, _ := os.ReadFile(publicKeyFile.Name())
119+
rootCAs.AppendCertsFromPEM(caPEM)
120+
svr.TLS = &tls.Config{
121+
MinVersion: tls.VersionTLS12,
122+
ClientAuth: tls.RequireAndVerifyClientCert,
123+
Certificates: []tls.Certificate{cert},
124+
ClientCAs: rootCAs,
125+
}
126+
svr.StartTLS()
127+
addr := svr.Listener.Addr()
128+
129+
return toServer, fromServer, fmt.Sprintf("wss://%s", addr), func() {
130+
close(fromServer)
131+
svr.Close()
132+
if connected {
133+
<-sendDone
134+
<-receiveDone
135+
}
136+
}, nil
137+
}
138+
27139
// NewTestWSServer creates a little test server for packages (including wsclient itself) to use in unit tests
28140
func NewTestWSServer(testReq func(req *http.Request)) (toServer, fromServer chan string, url string, done func()) {
29141
upgrader := &websocket.Upgrader{WriteBufferSize: 1024, ReadBufferSize: 1024}

0 commit comments

Comments
 (0)