// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package bsr_test import ( "context" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "regexp" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/internal/bsr" "github.com/hashicorp/boundary/internal/bsr/internal/fstest" "github.com/hashicorp/boundary/internal/bsr/kms" "github.com/hashicorp/boundary/internal/storage" wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) func assertContainer(ctx context.Context, t *testing.T, path, state string, typ string, fs *fstest.MemContainer, keys *kms.Keys) { t.Helper() td := filepath.Join("testdata", t.Name(), state, path) // meta wantMeta, err := os.ReadFile(filepath.Join(td, string(typ)+"-recording.meta")) require.NoError(t, err, "unable to find test data file") meta, ok := fs.Files[string(typ)+"-recording.meta"] require.True(t, ok, "container is missing meta file") wantChecksumsRegex, err := regexp.Compile(string(wantMeta)) require.NoError(t, err) assert.True(t, wantChecksumsRegex.MatchString(meta.Buf.String())) // summary wantSummary, err := os.ReadFile(filepath.Join(td, string(typ)+"-recording-summary.json")) require.NoError(t, err, "unable to find test data file") summary, ok := fs.Files[string(typ)+"-recording-summary.json"] require.True(t, ok, "container is missing summary file") assert.Equal(t, string(wantSummary), summary.Buf.String()) // SHA256SUM checksums wantChecksums, err := os.ReadFile(filepath.Join(td, "SHA256SUM")) require.NoError(t, err, "unable to find test data file") checksums, ok := fs.Files["SHA256SUM"] require.True(t, ok, "container is missing checksums file") checksumSlice := strings.Split(string(wantChecksums), "\n") var wChecksumsRegex []*regexp.Regexp for _, c := range checksumSlice { r, err := regexp.Compile(c) require.NoError(t, err) wChecksumsRegex = append(wChecksumsRegex, r) } for _, cr := range wChecksumsRegex { assert.True(t, cr.MatchString(checksums.Buf.String()), "got:\n%s\nmust match:\n%s", checksums.Buf.String(), cr.String()) } // SHA256SUM.sig signature file sig, ok := fs.Files["SHA256SUM.sig"] require.True(t, ok, "container is missing sig file") switch state { case "closed": want, err := keys.SignWithPrivKey(ctx, checksums.Buf.Bytes()) require.NoError(t, err) got := &wrapping.SigInfo{} err = proto.Unmarshal(sig.Buf.Bytes(), got) require.NoError(t, err) assert.Empty(t, cmp.Diff( want, got, cmpopts.IgnoreUnexported(wrapping.SigInfo{}, wrapping.KeyInfo{}), ), ) default: assert.Equal(t, "", sig.Buf.String()) } // journal wantJournal, err := os.ReadFile(filepath.Join(td, ".journal")) require.NoError(t, err, "unable to find test data file") journal, ok := fs.Files[".journal"] require.True(t, ok, "container is missing journal file") journalSlice := strings.Split(string(wantJournal), "\n") var wJournalRegex []*regexp.Regexp for _, c := range journalSlice { r, err := regexp.Compile(c) require.NoError(t, err) wJournalRegex = append(wJournalRegex, r) } for _, cr := range wJournalRegex { assert.True(t, cr.MatchString(journal.Buf.String()), "got:\n%s\nmust match:\n%s", journal.Buf.String(), cr.String()) } if typ == "session" { // BSR keys, if this is a session container bsrPub, ok := fs.Files["bsrKey.pub"] require.True(t, ok, "container is missing bsrPub file") assert.NotEmpty(t, bsrPub.Buf.String()) wrappedBsrKey, ok := fs.Files["wrappedBsrKey"] require.True(t, ok, "container is missing wrappedBsrKey file") assert.NotEmpty(t, wrappedBsrKey.Buf.String()) wrappedPrivKey, ok := fs.Files["wrappedPrivKey"] require.True(t, ok, "container is missing wrappedPrivKey file") assert.NotEmpty(t, wrappedPrivKey.Buf.String()) pubKeyBsrSignature, ok := fs.Files["pubKeyBsrSignature.sign"] require.True(t, ok, "container is missing pubKeyBsrSignature.sign file") assert.NotEmpty(t, pubKeyBsrSignature.Buf.String()) pubKeySelfSignature, ok := fs.Files["pubKeySelfSignature.sign"] require.True(t, ok, "container is missing pubKeySelfSignature.sign file") assert.NotEmpty(t, pubKeySelfSignature.Buf.String()) sessionMeta, ok := fs.Files["session-meta.json"] require.True(t, ok, "container is missing session-meta.json file") assert.NotEmpty(t, sessionMeta.Buf.String()) sm := &bsr.SessionMeta{} err = json.Unmarshal(sessionMeta.Buf.Bytes(), sm) require.NoError(t, err) assert.Equal(t, sm, bsr.TestSessionMeta(strings.ReplaceAll(fs.Name, ".bsr", ""))) } } type connection struct { mem *fstest.MemContainer conn *bsr.Connection id string channels []*channel files []*file } type channel struct { mem *fstest.MemContainer channel *bsr.Channel id string files []*file } type file struct { mem *fstest.MemFile file io.Writer } type createConn struct { id string channels []createChannel files []createFile } type createChannel struct { id string files []createFile } type createFile struct { typ string dir bsr.Direction } func TestBsr(t *testing.T) { ctx := context.Background() cases := []struct { name string id string opts []bsr.Option c *fstest.MemFS keys *kms.Keys conns []createConn }{ { "session_not_multiplexed", "session_123456789", []bsr.Option{}, fstest.NewMemFS(), func() *kms.Keys { keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session_123456789") require.NoError(t, err) return keys }(), []createConn{ { "conn_1", nil, []createFile{ {"messages", bsr.Inbound}, {"messages", bsr.Outbound}, {"requests", bsr.Inbound}, {"requests", bsr.Outbound}, }, }, }, }, { "session_multiplexed", "session_123456789", []bsr.Option{bsr.WithSupportsMultiplex(true)}, fstest.NewMemFS(), func() *kms.Keys { keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session_123456789") require.NoError(t, err) return keys }(), []createConn{ { "conn_1", []createChannel{ { "chan_1", []createFile{ {"messages", bsr.Inbound}, {"requests", bsr.Inbound}, {"messages", bsr.Outbound}, {"requests", bsr.Outbound}, }, }, }, []createFile{ {"requests", bsr.Inbound}, {"requests", bsr.Outbound}, }, }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { srm := &bsr.SessionRecordingMeta{ Id: tc.id, Protocol: bsr.Protocol("TEST"), } sessionMeta := bsr.TestSessionMeta(tc.id) s, err := bsr.NewSession(ctx, srm, sessionMeta, tc.c, tc.keys, tc.opts...) require.NoError(t, err) require.NotNil(t, s) sContainer, ok := tc.c.Containers[tc.id+".bsr"] require.True(t, ok) assertContainer(ctx, t, "", "opened", "session", sContainer, tc.keys) createdConnections := make([]*connection, 0) // create all the things for _, conn := range tc.conns { c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: conn.id}) require.NoError(t, err) require.NotNil(t, c) cContainer, ok := sContainer.Sub[conn.id+".connection"] require.True(t, ok) assertContainer(ctx, t, conn.id, "opened", "connection", cContainer, tc.keys) ff := make([]*file, 0, len(conn.files)) for _, f := range conn.files { var w io.Writer var err error switch f.typ { case "messages": w, err = c.NewMessagesWriter(ctx, f.dir) case "requests": w, err = c.NewRequestsWriter(ctx, f.dir) } require.NoError(t, err) fname := fmt.Sprintf("%s-%s.data", f.typ, f.dir.String()) memf, ok := cContainer.Files[fname] require.True(t, ok, "file %s not in container %s", fname, cContainer.Name) require.NoError(t, err) ff = append(ff, &file{ mem: memf, file: w, }) } createdChannels := make([]*channel, 0, len(conn.channels)) for _, chann := range conn.channels { ch, err := c.NewChannel(ctx, &bsr.ChannelRecordingMeta{Id: chann.id, Type: "chan"}) require.NoError(t, err) require.NotNil(t, ch) chContainer, ok := cContainer.Sub[chann.id+".channel"] require.True(t, ok) assertContainer(ctx, t, filepath.Join(conn.id, chann.id), "opened", "channel", chContainer, tc.keys) ff := make([]*file, 0, len(chann.files)) for _, f := range chann.files { var w io.Writer var err error switch f.typ { case "messages": w, err = ch.NewMessagesWriter(ctx, f.dir) case "requests": w, err = ch.NewRequestsWriter(ctx, f.dir) } require.NoError(t, err) fname := fmt.Sprintf("%s-%s.data", f.typ, f.dir.String()) memf, ok := chContainer.Files[fname] require.True(t, ok, "file %s not in container %s", fname, chContainer.Name) require.NoError(t, err) ff = append(ff, &file{ mem: memf, file: w, }) } createdChannels = append(createdChannels, &channel{ mem: chContainer, channel: ch, id: chann.id, files: ff, }) } createdConnections = append(createdConnections, &connection{ mem: cContainer, conn: c, id: conn.id, channels: createdChannels, files: ff, }) } // now close all the things that where created. for _, conn := range createdConnections { for _, channel := range conn.channels { for _, f := range channel.files { v, ok := f.file.(io.Closer) require.True(t, ok, "file is not a io.Closer") err = v.Close() require.NoError(t, err) } err = channel.channel.Close(ctx) require.NoError(t, err) assertContainer(ctx, t, filepath.Join(conn.id, channel.id), "closed", "channel", channel.mem, tc.keys) } for _, f := range conn.files { v, ok := f.file.(io.Closer) require.True(t, ok, "file is not a io.Closer") err = v.Close() require.NoError(t, err) } err = conn.conn.Close(ctx) require.NoError(t, err) assertContainer(ctx, t, conn.id, "closed", "connection", conn.mem, tc.keys) } err = s.Close(ctx) require.NoError(t, err) assertContainer(ctx, t, "", "closed", "session", sContainer, tc.keys) }) } } func TestNewSessionErrors(t *testing.T) { ctx := context.Background() keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session") require.NoError(t, err) cases := []struct { name string meta *bsr.SessionRecordingMeta sessionMeta *bsr.SessionMeta f storage.FS keys *kms.Keys wantError error }{ { "nil-meta", nil, bsr.TestSessionMeta("session"), &fstest.MemFS{}, keys, errors.New("bsr.NewSession: missing meta: invalid parameter"), }, { "nil-session-meta", bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), nil, &fstest.MemFS{}, keys, errors.New("bsr.NewSession: missing session meta: invalid parameter"), }, { "empty-session-id", bsr.TestSessionRecordingMeta("", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), &fstest.MemFS{}, keys, errors.New("bsr.NewSession: missing session id: invalid parameter"), }, { "nil-fs", bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), nil, keys, errors.New("bsr.NewSession: missing storage fs: invalid parameter"), }, { "nil-keys", bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), &fstest.MemFS{}, nil, errors.New("bsr.NewSession: missing kms keys: invalid parameter"), }, { "missing-bsr-signature", bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), &fstest.MemFS{}, &kms.Keys{ PubKey: keys.PubKey, WrappedBsrKey: keys.WrappedBsrKey, WrappedPrivKey: keys.WrappedPrivKey, PubKeySelfSignature: keys.PubKeySelfSignature, }, errors.New("bsr.persistBsrSessionKeys: missing kms pub key BSR signature: invalid parameter\nbsr.NewSession: could not persist BSR keys"), }, { "missing-pub-signature", bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), &fstest.MemFS{}, &kms.Keys{ PubKey: keys.PubKey, WrappedBsrKey: keys.WrappedBsrKey, WrappedPrivKey: keys.WrappedPrivKey, PubKeyBsrSignature: keys.PubKeyBsrSignature, }, errors.New("bsr.persistBsrSessionKeys: missing kms pub key self signature: invalid parameter\nbsr.NewSession: could not persist BSR keys"), }, { "missing-pub-key", bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), &fstest.MemFS{}, &kms.Keys{ WrappedBsrKey: keys.WrappedBsrKey, WrappedPrivKey: keys.WrappedPrivKey, PubKeySelfSignature: keys.PubKeySelfSignature, PubKeyBsrSignature: keys.PubKeyBsrSignature, }, errors.New("bsr.persistBsrSessionKeys: missing kms pub key: invalid parameter\nbsr.NewSession: could not persist BSR keys"), }, { "missing-wrapped-bsr-key", bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), &fstest.MemFS{}, &kms.Keys{ PubKey: keys.PubKey, WrappedPrivKey: keys.WrappedPrivKey, PubKeySelfSignature: keys.PubKeySelfSignature, PubKeyBsrSignature: keys.PubKeyBsrSignature, }, errors.New("bsr.persistBsrSessionKeys: missing kms wrapped BSR key: invalid parameter\nbsr.NewSession: could not persist BSR keys"), }, { "missing-wrapped-priv-key", bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), &fstest.MemFS{}, &kms.Keys{ PubKey: keys.PubKey, WrappedBsrKey: keys.WrappedBsrKey, PubKeySelfSignature: keys.PubKeySelfSignature, PubKeyBsrSignature: keys.PubKeyBsrSignature, }, errors.New("bsr.persistBsrSessionKeys: missing kms wrapped priv key: invalid parameter\nbsr.NewSession: could not persist BSR keys"), }, { "fs-new-error", bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), fstest.NewMemFS(fstest.WithNewFunc(func(_ context.Context, _ string) (storage.Container, error) { return nil, errors.New("fs new error") })), keys, errors.New("fs new error"), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := bsr.NewSession(ctx, tc.meta, tc.sessionMeta, tc.f, tc.keys) require.Error(t, err) assert.EqualError(t, err, tc.wantError.Error()) }) } } func TestNewConnectionErrors(t *testing.T) { ctx := context.Background() keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session") require.NoError(t, err) cases := []struct { name string session *bsr.Session meta *bsr.ConnectionRecordingMeta wantError error }{ { "nil-meta", func() *bsr.Session { s, err := bsr.NewSession(ctx, bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), &fstest.MemFS{}, keys) require.NoError(t, err) return s }(), nil, errors.New("bsr.(Session).NewConnection: missing connection meta: invalid parameter"), }, { "empty-connection-id", func() *bsr.Session { s, err := bsr.NewSession(ctx, bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), &fstest.MemFS{}, keys) require.NoError(t, err) return s }(), &bsr.ConnectionRecordingMeta{Id: ""}, errors.New("bsr.(Session).NewConnection: missing connection id: invalid parameter"), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := tc.session.NewConnection(ctx, tc.meta) require.Error(t, err) assert.EqualError(t, err, tc.wantError.Error()) }) } } func TestNewChannelErrors(t *testing.T) { ctx := context.Background() keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session") require.NoError(t, err) srm := bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")) sessionMeta := bsr.TestSessionMeta("session") cases := []struct { name string connection *bsr.Connection meta *bsr.ChannelRecordingMeta wantError error }{ { "nil-meta", func() *bsr.Connection { s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true)) require.NoError(t, err) c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"}) require.NoError(t, err) return c }(), nil, errors.New("bsr.(Connection).NewChannel: missing channel meta: invalid parameter"), }, { "empty-connection-id", func() *bsr.Connection { s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true)) require.NoError(t, err) c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"}) require.NoError(t, err) return c }(), &bsr.ChannelRecordingMeta{Id: ""}, errors.New("bsr.(Connection).NewChannel: missing channel id: invalid parameter"), }, { "not-multiplexed", func() *bsr.Connection { s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(false)) require.NoError(t, err) c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"}) require.NoError(t, err) return c }(), &bsr.ChannelRecordingMeta{Id: ""}, errors.New("bsr.(Connection).NewChannel: connection cannot make channels: not supported by protocol"), }, { "not-multiplexed-default", func() *bsr.Connection { s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys) require.NoError(t, err) c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"}) require.NoError(t, err) return c }(), &bsr.ChannelRecordingMeta{Id: ""}, errors.New("bsr.(Connection).NewChannel: connection cannot make channels: not supported by protocol"), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := tc.connection.NewChannel(ctx, tc.meta) require.Error(t, err) assert.EqualError(t, err, tc.wantError.Error()) }) } } func TestChannelNewMessagesWriterErrors(t *testing.T) { ctx := context.Background() keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session") require.NoError(t, err) srm := bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")) sessionMeta := bsr.TestSessionMeta("session") cases := []struct { name string channel *bsr.Channel dir bsr.Direction wantError error }{ { "invalid-dir", func() *bsr.Channel { s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true)) require.NoError(t, err) c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"}) require.NoError(t, err) ch, err := c.NewChannel(ctx, &bsr.ChannelRecordingMeta{Id: "channel"}) require.NoError(t, err) return ch }(), bsr.Direction(uint8(255)), errors.New("bsr.(Channel).NewMessagesWriter: invalid direction: invalid parameter"), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := tc.channel.NewMessagesWriter(ctx, tc.dir) require.Error(t, err) assert.EqualError(t, err, tc.wantError.Error()) }) } } func TestChannelNewRequestsWriterErrors(t *testing.T) { ctx := context.Background() keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session") require.NoError(t, err) srm := bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")) sessionMeta := bsr.TestSessionMeta("session") cases := []struct { name string channel *bsr.Channel dir bsr.Direction wantError error }{ { "invalid-dir", func() *bsr.Channel { s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true)) require.NoError(t, err) c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"}) require.NoError(t, err) ch, err := c.NewChannel(ctx, &bsr.ChannelRecordingMeta{Id: "channel"}) require.NoError(t, err) return ch }(), bsr.Direction(uint8(255)), errors.New("bsr.(Channel).NewRequestsWriter: invalid direction: invalid parameter"), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { _, err := tc.channel.NewRequestsWriter(ctx, tc.dir) require.Error(t, err) assert.EqualError(t, err, tc.wantError.Error()) }) } }