Skip to content

Commit

Permalink
Merge pull request #65 from kaleido-io/add_mtls_ffresty
Browse files Browse the repository at this point in the history
feat: Add mTLS configuration for ffresty
  • Loading branch information
peterbroadhurst authored Apr 25, 2023
2 parents f74fa67 + 089551f commit d13465d
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 93 deletions.
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ require (
github.com/x-cray/logrus-prefixed-formatter v0.5.2
gitlab.com/hfuss/mux-prometheus v0.0.4
golang.org/x/crypto v0.1.0
golang.org/x/text v0.7.0
golang.org/x/text v0.8.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)

Expand Down Expand Up @@ -70,9 +70,9 @@ require (
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
go.uber.org/atomic v1.10.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1358,8 +1358,8 @@ golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
Expand Down Expand Up @@ -1514,15 +1514,15 @@ golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand All @@ -1532,8 +1532,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down
4 changes: 3 additions & 1 deletion pkg/ffapi/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/getkin/kin-openapi/openapi3"
"github.com/gorilla/mux"
"github.com/hyperledger/firefly-common/pkg/config"
"github.com/hyperledger/firefly-common/pkg/fftls"
"github.com/hyperledger/firefly-common/pkg/httpserver"
"github.com/hyperledger/firefly-common/pkg/i18n"
"github.com/hyperledger/firefly-common/pkg/metric"
Expand Down Expand Up @@ -171,7 +172,8 @@ func buildPublicURL(conf config.Section, a net.Addr) string {
publicURL := conf.GetString(httpserver.HTTPConfPublicURL)
if publicURL == "" {
proto := "https"
if !conf.GetBool(httpserver.HTTPConfTLSEnabled) {
tlsConfig := conf.SubSection("tls")
if !tlsConfig.GetBool(fftls.HTTPConfTLSEnabled) {
proto = "http"
}
publicURL = fmt.Sprintf("%s://%s", proto, a.String())
Expand Down
12 changes: 9 additions & 3 deletions pkg/ffresty/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2022 Kaleido, Inc.
// Copyright © 2023 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand All @@ -16,7 +16,10 @@

package ffresty

import "github.com/hyperledger/firefly-common/pkg/config"
import (
"github.com/hyperledger/firefly-common/pkg/config"
"github.com/hyperledger/firefly-common/pkg/fftls"
)

const (
defaultRetryEnabled = false
Expand Down Expand Up @@ -70,7 +73,7 @@ const (
HTTPCustomClient = "customClient"
)

func InitConfig(conf config.KeySet) {
func InitConfig(conf config.Section) {
conf.AddKnownKey(HTTPConfigURL)
conf.AddKnownKey(HTTPConfigProxyURL)
conf.AddKnownKey(HTTPConfigHeaders)
Expand All @@ -88,4 +91,7 @@ func InitConfig(conf config.KeySet) {
conf.AddKnownKey(HTTPExpectContinueTimeout, defaultHTTPExpectContinueTimeout)
conf.AddKnownKey(HTTPPassthroughHeadersEnabled, defaultHTTPPassthroughHeadersEnabled)
conf.AddKnownKey(HTTPCustomClient)

tlsConfig := conf.SubSection("tls")
fftls.InitTLSConfig(tlsConfig)
}
17 changes: 12 additions & 5 deletions pkg/ffresty/ffresty.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/go-resty/resty/v2"
"github.com/hyperledger/firefly-common/pkg/config"
"github.com/hyperledger/firefly-common/pkg/ffapi"
"github.com/hyperledger/firefly-common/pkg/fftls"
"github.com/hyperledger/firefly-common/pkg/fftypes"
"github.com/hyperledger/firefly-common/pkg/i18n"
"github.com/hyperledger/firefly-common/pkg/log"
Expand Down Expand Up @@ -67,10 +68,7 @@ func OnAfterResponse(c *resty.Client, resp *resty.Response) {
//
// You can use the normal Resty builder pattern, to set per-instance configuration
// as required.
func New(ctx context.Context, staticConfig config.Section) *resty.Client {

var client *resty.Client

func New(ctx context.Context, staticConfig config.Section) (client *resty.Client, err error) {
passthroughHeadersEnabled := staticConfig.GetBool(HTTPPassthroughHeadersEnabled)

iHTTPClient := staticConfig.Get(HTTPCustomClient)
Expand All @@ -80,6 +78,7 @@ func New(ctx context.Context, staticConfig config.Section) *resty.Client {
}
}
if client == nil {

httpTransport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Expand All @@ -92,6 +91,14 @@ func New(ctx context.Context, staticConfig config.Section) *resty.Client {
TLSHandshakeTimeout: staticConfig.GetDuration(HTTPTLSHandshakeTimeout),
ExpectContinueTimeout: staticConfig.GetDuration(HTTPExpectContinueTimeout),
}

tlsConfig, err := fftls.ConstructTLSConfig(ctx, staticConfig.SubSection("tls"), "client")
if err != nil {
return nil, err
}

httpTransport.TLSClientConfig = tlsConfig

httpClient := &http.Client{
Transport: httpTransport,
}
Expand Down Expand Up @@ -184,7 +191,7 @@ func New(ctx context.Context, staticConfig config.Section) *resty.Client {
})
}

return client
return client, nil
}

func WrapRestErr(ctx context.Context, res *resty.Response, err error, key i18n.ErrorMessageKey) error {
Expand Down
162 changes: 156 additions & 6 deletions pkg/ffresty/ffresty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,33 @@ package ffresty

import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"log"
"math/big"
"net"
"net/http"
"os"
"strings"
"testing"
"time"

"github.com/hyperledger/firefly-common/pkg/config"
"github.com/hyperledger/firefly-common/pkg/ffapi"
"github.com/hyperledger/firefly-common/pkg/fftls"
"github.com/hyperledger/firefly-common/pkg/i18n"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
)

const configDir = "../../test/data/config"

var utConf = config.RootSection("http_unit_tests")

func resetConf() {
Expand All @@ -51,7 +66,8 @@ func TestRequestOK(t *testing.T) {
utConf.Set(HTTPConfigRetryEnabled, true)
utConf.Set(HTTPCustomClient, customClient)

c := New(context.Background(), utConf)
c, err := New(context.Background(), utConf)
assert.Nil(t, err)
httpmock.ActivateNonDefault(customClient)
defer httpmock.DeactivateAndReset()

Expand Down Expand Up @@ -79,7 +95,8 @@ func TestRequestRetry(t *testing.T) {
utConf.Set(HTTPConfigRetryEnabled, true)
utConf.Set(HTTPConfigRetryInitDelay, 1)

c := New(ctx, utConf)
c, err := New(ctx, utConf)
assert.Nil(t, err)
httpmock.ActivateNonDefault(c.GetClient())
defer httpmock.DeactivateAndReset()

Expand All @@ -105,7 +122,8 @@ func TestConfWithProxy(t *testing.T) {
utConf.Set(HTTPConfigProxyURL, "http://myproxy.example.com:12345")
utConf.Set(HTTPConfigRetryEnabled, false)

c := New(ctx, utConf)
c, err := New(ctx, utConf)
assert.Nil(t, err)
assert.True(t, c.IsProxySet())
}

Expand All @@ -117,7 +135,8 @@ func TestLongResponse(t *testing.T) {
utConf.Set(HTTPConfigURL, "http://localhost:12345")
utConf.Set(HTTPConfigRetryEnabled, false)

c := New(ctx, utConf)
c, err := New(ctx, utConf)
assert.Nil(t, err)
httpmock.ActivateNonDefault(c.GetClient())
defer httpmock.DeactivateAndReset()

Expand All @@ -141,7 +160,8 @@ func TestErrResponse(t *testing.T) {
utConf.Set(HTTPConfigURL, "http://localhost:12345")
utConf.Set(HTTPConfigRetryEnabled, false)

c := New(ctx, utConf)
c, err := New(ctx, utConf)
assert.Nil(t, err)
httpmock.ActivateNonDefault(c.GetClient())
defer httpmock.DeactivateAndReset()

Expand Down Expand Up @@ -180,7 +200,8 @@ func TestPassthroughHeaders(t *testing.T) {
utConf.Set(HTTPCustomClient, customClient)
utConf.Set(HTTPPassthroughHeadersEnabled, true)

c := New(context.Background(), utConf)
c, err := New(context.Background(), utConf)
assert.Nil(t, err)
httpmock.ActivateNonDefault(customClient)
defer httpmock.DeactivateAndReset()

Expand All @@ -200,3 +221,132 @@ func TestPassthroughHeaders(t *testing.T) {

assert.Equal(t, 1, httpmock.GetTotalCallCount())
}

func TestMissingCAFile(t *testing.T) {
resetConf()
utConf.Set(HTTPConfigURL, "https://localhost:12345")
tlsSection := utConf.SubSection("tls")
tlsSection.Set(fftls.HTTPConfTLSEnabled, true)
tlsSection.Set(fftls.HTTPConfTLSCAFile, "non-existent.pem")

_, err := New(context.Background(), utConf)
assert.Regexp(t, "FF00153", err)
}

func TestBadCAFile(t *testing.T) {
resetConf()
utConf.Set(HTTPConfigURL, "https://localhost:12345")
tlsSection := utConf.SubSection("tls")
tlsSection.Set(fftls.HTTPConfTLSEnabled, true)
tlsSection.Set(fftls.HTTPConfTLSCAFile, configDir+"/firefly.common.yaml")

_, err := New(context.Background(), utConf)
assert.Regexp(t, "FF00152", err)
}

func TestBadKeyPair(t *testing.T) {
resetConf()
utConf.Set(HTTPConfigURL, "https://localhost:12345")
tlsSection := utConf.SubSection("tls")
tlsSection.Set(fftls.HTTPConfTLSEnabled, true)
tlsSection.Set(fftls.HTTPConfTLSCertFile, configDir+"/firefly.common.yaml")
tlsSection.Set(fftls.HTTPConfTLSKeyFile, configDir+"/firefly.common.yaml")

_, err := New(context.Background(), utConf)
assert.Regexp(t, "FF00206", err)
}

func TestMTLSClientWithServer(t *testing.T) {
// Create an X509 certificate pair
privatekey, _ := rsa.GenerateKey(rand.Reader, 2048)
publickey := &privatekey.PublicKey
var privateKeyBytes []byte = x509.MarshalPKCS1PrivateKey(privatekey)
privateKeyFile, _ := os.CreateTemp("", "key.pem")
defer os.Remove(privateKeyFile.Name())
privateKeyBlock := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: privateKeyBytes}
pem.Encode(privateKeyFile, privateKeyBlock)
serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
x509Template := &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Unit Tests"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(100 * time.Second),
KeyUsage: x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)},
}
derBytes, err := x509.CreateCertificate(rand.Reader, x509Template, x509Template, publickey, privatekey)
assert.NoError(t, err)
publicKeyFile, _ := os.CreateTemp("", "cert.pem")
defer os.Remove(publicKeyFile.Name())
pem.Encode(publicKeyFile, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})

http.HandleFunc("/hello", func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(200)
json.NewEncoder(res).Encode(map[string]interface{}{"hello": "world"})
})

// Create a CA certificate pool and add cert.pem to it
caCert, err := os.ReadFile(publicKeyFile.Name())
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

// Create the TLS Config with the CA pool and enable Client certificate validation
tlsConfig := &tls.Config{
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}
tlsConfig.BuildNameToCertificate()

// Create a Server instance to listen on port 8443 with the TLS config
server := &http.Server{
Addr: "127.0.0.1:8443",
TLSConfig: tlsConfig,
}

ctx, cancelCtx := context.WithCancel(context.Background())
go func() {
select {
case <-ctx.Done():
shutdownContext, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := server.Shutdown(shutdownContext); err != nil {
return
}
}
}()

go server.ListenAndServeTLS(publicKeyFile.Name(), privateKeyFile.Name())

// Use ffresty to test the mTLS client as well
var restyConfig = config.RootSection("resty")
InitConfig(restyConfig)
clientTLSSection := restyConfig.SubSection("tls")
restyConfig.Set(HTTPConfigURL, "https://127.0.0.1")
clientTLSSection.Set(fftls.HTTPConfTLSEnabled, true)
clientTLSSection.Set(fftls.HTTPConfTLSKeyFile, privateKeyFile.Name())
clientTLSSection.Set(fftls.HTTPConfTLSCertFile, publicKeyFile.Name())
clientTLSSection.Set(fftls.HTTPConfTLSCAFile, publicKeyFile.Name())

c, err := New(context.Background(), restyConfig)
assert.Nil(t, err)

//httpsAddr := fmt.Sprintf("https://localhost:8443/hello", server.Addr)
res, err := c.R().Get("https://127.0.0.1:8443/hello")
assert.NoError(t, err)

assert.NoError(t, err)
if res != nil {
assert.Equal(t, 200, res.StatusCode())
var resBody map[string]interface{}
err = json.Unmarshal(res.Body(), &resBody)
assert.NoError(t, err)
assert.Equal(t, "world", resBody["hello"])
}
cancelCtx()
}
Loading

0 comments on commit d13465d

Please sign in to comment.