You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/tests/cluster/sequential/x509_verification_test.go

301 lines
9.1 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package sequential
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"net"
"net/http"
"sync"
"testing"
"time"
"github.com/coder/websocket"
apiproxy "github.com/hashicorp/boundary/api/proxy"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/daemon/controller"
"github.com/hashicorp/boundary/internal/daemon/worker"
"github.com/hashicorp/boundary/internal/event"
"github.com/hashicorp/boundary/internal/server"
"github.com/hashicorp/boundary/internal/tests/helper"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require"
)
func TestCustomX509Verification_Client(t *testing.T) {
req := require.New(t)
ctx := context.Background()
ec := event.TestEventerConfig(t, "TestWorkerReplay", event.TestWithObservationSink(t), event.TestWithSysSink(t))
testLock := &sync.Mutex{}
// This prevents us from running tests in parallel.
server.TestUseCommunityFilterWorkersFn(t)
logger := hclog.New(&hclog.LoggerOptions{
Mutex: testLock,
Name: "test",
})
req.NoError(event.InitSysEventer(logger, testLock, "use-TestCustomX509Verification", event.WithEventerConfig(&ec.EventerConfig)))
t.Cleanup(func() {
event.TestResetSystEventer(t)
})
t.Cleanup(func() {
all, err := io.ReadAll(ec.AllEvents)
require.NoError(t, err)
t.Log(string(all))
})
conf, err := config.DevController()
conf.Eventing = &ec.EventerConfig
req.NoError(err)
c1 := controller.NewTestController(t, &controller.TestControllerOpts{
Config: conf,
InitialResourcesSuffix: "1234567890",
Logger: logger.Named("c1"),
})
conf, err = config.DevWorker()
conf.Eventing = &ec.EventerConfig
req.NoError(err)
w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{
WorkerAuthKms: c1.Config().WorkerAuthKms,
InitialUpstreams: c1.ClusterAddrs(),
Logger: logger.Named("w1"),
WorkerRPCInterval: time.Second,
})
helper.ExpectWorkers(t, c1, w1)
// Connect target
client := c1.Client()
client.SetToken(c1.Token().Token)
tcl := targets.NewClient(client)
tgt, err := tcl.Read(ctx, "ttcp_1234567890")
req.NoError(err)
req.NotNil(tgt)
// Create test server, update default port on target
ts := helper.NewTestTcpServer(t)
require.NotNil(t, ts)
t.Cleanup(ts.Close)
tgt, err = tcl.Update(ctx, tgt.Item.Id, tgt.Item.Version, targets.WithTcpTargetDefaultPort(ts.Port()), targets.WithSessionConnectionLimit(-1))
req.NoError(err)
req.NotNil(tgt)
tests := []struct {
name string
certPool *x509.CertPool
dnsName *bytes.Buffer
wantErrContains string
}{
{
name: "base",
},
{
name: "modified cert pool",
certPool: x509.NewCertPool(),
wantErrContains: "signed by unknown authority",
},
{
name: "modified dns name",
dnsName: bytes.NewBuffer([]byte("foobar")),
wantErrContains: "not foobar",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
sar, err := tcl.AuthorizeSession(ctx, "ttcp_1234567890")
require.NoError(err)
require.NotNil(sar)
sessAuth, err := sar.GetSessionAuthorization()
require.NoError(err)
sessAuthzData, err := sessAuth.GetSessionAuthorizationData()
require.NoError(err)
require.LessOrEqual(1, len(sessAuthzData.WorkerInfo))
tlsConf := apiproxy.TestClientTlsConfig(t, sessAuth.AuthorizationToken, apiproxy.WithWorkerHost(sessAuth.SessionId))
certPool := tc.certPool
if certPool == nil {
parsedCert, err := x509.ParseCertificate(sessAuthzData.Certificate)
require.NoError(err)
certPool = x509.NewCertPool()
certPool.AddCert(parsedCert)
}
dnsName := sessAuth.SessionId
if tc.dnsName != nil {
dnsName = tc.dnsName.String()
}
verifyOpts := x509.VerifyOptions{
DNSName: dnsName,
Roots: certPool,
KeyUsages: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
}
tlsConf.VerifyConnection = func(cs tls.ConnectionState) error {
// Go will not run this without at least one peer certificate, but
// doesn't hurt to check
if len(cs.PeerCertificates) == 0 {
return errors.New("no peer certificates provided")
}
_, err := cs.PeerCertificates[0].Verify(verifyOpts)
return err
}
transport := cleanhttp.DefaultTransport()
transport.DisableKeepAlives = false
// This isn't/shouldn't used anyways really because the connection is
// hijacked, just setting for completeness
transport.IdleConnTimeout = 0
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
dialer := &tls.Dialer{Config: tlsConf}
return dialer.DialContext(ctx, network, addr)
}
conn, _, err := websocket.Dial(
ctx,
fmt.Sprintf("ws://%s/v1/proxy", sessAuthzData.WorkerInfo[0].Address),
&websocket.DialOptions{
HTTPClient: &http.Client{
Transport: transport,
},
Subprotocols: []string{globals.TcpProxyV1},
},
)
if conn != nil {
defer conn.Close(websocket.StatusNormalClosure, "done")
}
switch tc.wantErrContains != "" {
case true:
require.Error(err)
require.Contains(err.Error(), tc.wantErrContains)
default:
require.NoError(err)
}
})
}
}
func TestCustomX509Verification_Server(t *testing.T) {
t.Skip("These tests are currently not working and will need further investigation")
ec := event.TestEventerConfig(t, "TestCustomX509Verification_Server", event.TestWithObservationSink(t), event.TestWithSysSink(t))
testLock := &sync.Mutex{}
logger := hclog.New(&hclog.LoggerOptions{
Mutex: testLock,
Name: "test",
Level: hclog.Trace,
})
require.NoError(t, event.InitSysEventer(logger, testLock, "use-TestCustomX509Verification_Server", event.WithEventerConfig(&ec.EventerConfig)))
t.Cleanup(func() { event.TestResetSystEventer(t) })
t.Cleanup(func() {
all, err := io.ReadAll(ec.AllEvents)
require.NoError(t, err)
t.Log(string(all))
})
t.Run("bad cert pool", testCustomX509Verification_Server(ec, x509.NewCertPool(), "", "bad certificate"))
t.Run("bad dns name", testCustomX509Verification_Server(ec, nil, "foobar", "bad certificate"))
}
func testCustomX509Verification_Server(ec event.TestConfig, certPool *x509.CertPool, dnsName, wantErrContains string) func(t *testing.T) {
return func(t *testing.T) {
req := require.New(t)
ctx := context.Background()
// This prevents us from running tests in parallel.
server.TestUseCommunityFilterWorkersFn(t)
conf, err := config.DevController()
conf.Eventing = &ec.EventerConfig
req.NoError(err)
c1 := controller.NewTestController(t, &controller.TestControllerOpts{
Config: conf,
InitialResourcesSuffix: "1234567890",
})
conf, err = config.DevWorker()
conf.Eventing = &ec.EventerConfig
req.NoError(err)
w1 := worker.NewTestWorker(t, &worker.TestWorkerOpts{
WorkerAuthKms: c1.Config().WorkerAuthKms,
InitialUpstreams: c1.ClusterAddrs(),
WorkerRPCInterval: time.Second,
})
w1.Worker().TestOverrideX509VerifyCertPool = certPool
w1.Worker().TestOverrideX509VerifyDnsName = dnsName
// Give time for it to connect
helper.ExpectWorkers(t, c1, w1)
// Connect target
client := c1.Client()
client.SetToken(c1.Token().Token)
tcl := targets.NewClient(client)
tgt, err := tcl.Read(ctx, "ttcp_1234567890")
req.NoError(err)
req.NotNil(tgt)
// Create test server, update default port on target
ts := helper.NewTestTcpServer(t)
require.NotNil(t, ts)
t.Cleanup(ts.Close)
tgt, err = tcl.Update(ctx, tgt.Item.Id, tgt.Item.Version, targets.WithTcpTargetDefaultPort(ts.Port()), targets.WithSessionConnectionLimit(-1))
req.NoError(err)
req.NotNil(tgt)
sar, err := tcl.AuthorizeSession(ctx, "ttcp_1234567890")
req.NoError(err)
req.NotNil(sar)
sessAuth, err := sar.GetSessionAuthorization()
req.NoError(err)
sessAuthzData, err := sessAuth.GetSessionAuthorizationData()
req.NoError(err)
req.LessOrEqual(1, len(sessAuthzData.WorkerInfo))
tlsConf := apiproxy.TestClientTlsConfig(t, sessAuth.AuthorizationToken, apiproxy.WithWorkerHost(sessAuth.SessionId))
transport := cleanhttp.DefaultTransport()
transport.DisableKeepAlives = false
// This isn't/shouldn't used anyways really because the connection is
// hijacked, just setting for completeness
transport.IdleConnTimeout = 0
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
dialer := &tls.Dialer{Config: tlsConf}
return dialer.DialContext(ctx, network, addr)
}
conn, _, err := websocket.Dial(
ctx,
fmt.Sprintf("wss://%s/v1/proxy", sessAuthzData.WorkerInfo[0].Address),
&websocket.DialOptions{
HTTPClient: &http.Client{
Transport: transport,
},
Subprotocols: []string{globals.TcpProxyV1},
},
)
if conn != nil {
defer conn.Close(websocket.StatusNormalClosure, "done")
}
switch wantErrContains != "" {
case true:
req.Error(err)
req.Contains(err.Error(), wantErrContains)
default:
req.NoError(err)
}
}
}