Skip to content

Commit

Permalink
Test server TLS config factory
Browse files Browse the repository at this point in the history
  • Loading branch information
everesio committed Nov 1, 2022
1 parent 7e0baaf commit 00a4cdc
Show file tree
Hide file tree
Showing 23 changed files with 4,285 additions and 28 deletions.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ lint: ## Lint
golint $$(go list ./...) 2>&1

test: ## Test
GO111MODULE=on go test -mod=vendor $(BUILD_FLAGS) -v ./...
GO111MODULE=on go test -count=1 -mod=vendor $(BUILD_FLAGS) -v ./...

test.race: ## Test with race detection
GO111MODULE=on go test -race -count=1 -mod=vendor $(BUILD_FLAGS) -v ./...

build: vet ## Build executable
CGO_ENABLED=1 GO111MODULE=on go build -mod=vendor -o $(BINARY) $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" .
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.13.0
github.com/prometheus/common v0.37.0
github.com/stretchr/testify v1.7.1
github.com/stretchr/testify v1.8.1
github.com/sykesm/zap-logfmt v0.0.4
go.uber.org/atomic v1.9.0
go.uber.org/automaxprocs v1.5.1
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
Expand All @@ -261,6 +263,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/sykesm/zap-logfmt v0.0.4 h1:U2WzRvmIWG1wDLCFY3sz8UeEmsdHQjHFNlIdmroVFaI=
github.com/sykesm/zap-logfmt v0.0.4/go.mod h1:AuBd9xQjAe3URrWT1BBDk2v2onAZHkZkWRMiYZXiZWA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down
32 changes: 22 additions & 10 deletions pkg/tls/cert/filesource/filesource.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ const (
)

type fileSource struct {
certFile string
keyFile string
clientAuthFile string
clientCRLFile string
refresh time.Duration
logger log.Logger

certFile string
keyFile string
clientAuthFile string
clientCRLFile string
refresh time.Duration
logger log.Logger
notifyFunc func()
lastServerCerts atomic.Pointer[tlscert.ServerCerts]
}

Expand All @@ -48,6 +48,14 @@ func New(opts ...Option) (tlscert.ServerSource, error) {
return s, nil
}

func MustNew(opts ...Option) tlscert.ServerSource {
serverSource, err := New(opts...)
if err != nil {
panic(`filesource: New(): ` + err.Error())
}
return serverSource
}

func (s *fileSource) getServerCerts() (*tlscert.ServerCerts, error) {
pemBlocks, err := s.Load()
if err != nil {
Expand Down Expand Up @@ -91,10 +99,14 @@ func (s *fileSource) ServerCerts() chan tlscert.ServerCerts {
if initialServerCert != nil {
ch <- *initialServerCert
}
go func() {
tlscert.Watch(s.logger, ch, s.refresh, initialServerCert, s.refreshServerCerts)
if s.refresh <= 0 {
close(ch)
}()
} else {
go func() {
tlscert.Watch(s.logger, ch, s.refresh, initialServerCert, s.refreshServerCerts, s.notifyFunc)
close(ch)
}()
}
return ch
}

Expand Down
211 changes: 211 additions & 0 deletions pkg/tls/cert/filesource/filesource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package filesource

import (
"crypto/tls"
"crypto/x509"
"github.com/grepplabs/mqtt-proxy/pkg/log"
servertls "github.com/grepplabs/mqtt-proxy/pkg/tls"
"github.com/stretchr/testify/require"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
)

func TestServerConfig(t *testing.T) {
logger := log.GetInstance()
bundle := newCertsBundle()
defer bundle.Close()

tests := []struct {
name string
transportFunc func() http.RoundTripper
configFunc func() *tls.Config
requestError bool
}{
{
name: "Client unknown authority",
transportFunc: func() http.RoundTripper {
return newRoundTripper()
},
configFunc: func() *tls.Config {
return servertls.MustNewServerConfig(log.GetInstance(), MustNew(
WithLogger(logger),
WithX509KeyPair(bundle.ServerCert.Name(), bundle.ServerKey.Name()),
))
},
requestError: true,
},
{
name: "Client insecure",
transportFunc: func() http.RoundTripper {
return newRoundTripper(withClientTLSSkipVerify(true))
},
configFunc: func() *tls.Config {
return servertls.MustNewServerConfig(log.GetInstance(), MustNew(
WithX509KeyPair(bundle.ServerCert.Name(), bundle.ServerKey.Name()),
))
},
},
{
name: "Client trusted CA",
transportFunc: func() http.RoundTripper {
return newRoundTripper(withRootCAs(bundle.CAX509Cert))
},
configFunc: func() *tls.Config {
return servertls.MustNewServerConfig(log.GetInstance(), MustNew(
WithX509KeyPair(bundle.ServerCert.Name(), bundle.ServerKey.Name()),
))
},
},
{
name: "Client without required certificate",
transportFunc: func() http.RoundTripper {
return newRoundTripper(withRootCAs(bundle.CAX509Cert))
},
configFunc: func() *tls.Config {
return servertls.MustNewServerConfig(log.GetInstance(), MustNew(
WithX509KeyPair(bundle.ServerCert.Name(), bundle.ServerKey.Name()),
WithClientAuthFile(bundle.CACert.Name()),
))
},
requestError: true,
},
{
name: "Client verification success",
transportFunc: func() http.RoundTripper {
return newRoundTripper(withRootCAs(bundle.CAX509Cert), withClientCertificate(bundle.ClientTLSCert))
},
configFunc: func() *tls.Config {
return servertls.MustNewServerConfig(log.GetInstance(), MustNew(
WithX509KeyPair(bundle.ServerCert.Name(), bundle.ServerKey.Name()),
WithClientAuthFile(bundle.CACert.Name()),
))
},
},
{
name: "Client verification success - empty CRL",
transportFunc: func() http.RoundTripper {
return newRoundTripper(withRootCAs(bundle.CAX509Cert), withClientCertificate(bundle.ClientTLSCert))
},
configFunc: func() *tls.Config {
return servertls.MustNewServerConfig(log.GetInstance(), MustNew(
WithX509KeyPair(bundle.ServerCert.Name(), bundle.ServerKey.Name()),
WithClientAuthFile(bundle.CACert.Name()),
WithClientCRLFile(bundle.CAEmptyCRL.Name()),
))
},
},
{
name: "Client certificate revoked",
transportFunc: func() http.RoundTripper {
return newRoundTripper(withRootCAs(bundle.CAX509Cert), withClientCertificate(bundle.ClientTLSCert))
},
configFunc: func() *tls.Config {
return servertls.MustNewServerConfig(log.GetInstance(), MustNew(
WithX509KeyPair(bundle.ServerCert.Name(), bundle.ServerKey.Name()),
WithClientAuthFile(bundle.CACert.Name()),
WithClientCRLFile(bundle.ClientCRL.Name()),
))
},
requestError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// given
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
ts.TLS = tc.configFunc()
ts.StartTLS()

httpClient := &http.Client{
Transport: tc.transportFunc(),
}
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
require.NoError(t, err)

// when
res, err := httpClient.Do(req)

// then
if tc.requestError {
t.Log(err)
require.NotNil(t, err)
return
}
require.NoError(t, err)

_, err = io.ReadAll(res.Body)
require.NoError(t, err)

_ = res.Body.Close()
require.NoError(t, err)
require.Equal(t, res.StatusCode, http.StatusOK)

})
}
}

func TestCertRotation(t *testing.T) {
bundle1 := newCertsBundle()
defer bundle1.Close()

bundle2 := newCertsBundle()
defer bundle2.Close()

ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()

rotatedCh := make(chan struct{}, 1)
notifyFunc := func() {
rotatedCh <- struct{}{}
}
source := MustNew(
WithX509KeyPair(bundle1.ServerCert.Name(), bundle1.ServerKey.Name()),
WithClientAuthFile(bundle1.CACert.Name()),
WithClientCRLFile(bundle1.CAEmptyCRL.Name()),
WithRefresh(1*time.Second),
WithNotifyFunc(notifyFunc),
).(*fileSource)

ts.TLS = servertls.MustNewServerConfig(log.GetInstance(), source)
ts.StartTLS()

req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
require.NoError(t, err)

// when
_, err = bundle1.newHttpClient().Do(req)
require.NoError(t, err)

// copy new certificates to be used by server
require.NoError(t, os.Rename(bundle2.ServerCert.Name(), bundle1.ServerCert.Name()))
require.NoError(t, os.Rename(bundle2.ServerKey.Name(), bundle1.ServerKey.Name()))
require.NoError(t, os.Rename(bundle2.CACert.Name(), bundle1.CACert.Name()))
require.NoError(t, os.Rename(bundle2.CAEmptyCRL.Name(), bundle1.CAEmptyCRL.Name()))

select {
case <-rotatedCh:
t.Log("certificates were changed")
time.Sleep(100 * time.Millisecond)
case <-time.After(3 * time.Second):
t.Fatal("expected certificate change notification")
}
// old client - bad certificate
_, err = bundle1.newHttpClient().Do(req)
require.NotNil(t, err)
var unknownAuthorityError x509.UnknownAuthorityError
require.ErrorAs(t, err.(*url.Error).Err, &unknownAuthorityError)

// new client - success
_, err = bundle2.newHttpClient().Do(req)
require.NoError(t, err)
}
Loading

0 comments on commit 00a4cdc

Please sign in to comment.