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/bsr/container.go

774 lines
22 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package bsr
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"github.com/hashicorp/boundary/internal/bsr/internal/checksum"
"github.com/hashicorp/boundary/internal/bsr/internal/is"
"github.com/hashicorp/boundary/internal/bsr/internal/journal"
"github.com/hashicorp/boundary/internal/bsr/internal/sign"
"github.com/hashicorp/boundary/internal/bsr/kms"
"github.com/hashicorp/boundary/internal/storage"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/extras/crypto"
"google.golang.org/protobuf/proto"
)
const (
metaFileNameTemplate = "%s-recording.meta"
summaryFileNameTemplate = "%s-recording-summary.json"
checksumFileName = "SHA256SUM"
sigFileName = "SHA256SUM.sig"
journalFileName = ".journal"
// sigFileBufferSize represents the number of free bytes required in the CHECKSUM.sig file to close safely
sigFileBufferSize = 89
// checksumFileBufferSize represents the number of free bytes required in the CHECKSUM file to safely
// write a single line: "{hash_value} {file_name}\n"
checksumFileBufferSize = 320
// sessionRecordingSummaryBufferSize represents the number of free bytes required in the session-recording-summary.json
// to close safely. This value is determined by the BaseSessionSummary definition.
sessionRecordingSummaryBufferSize = 183
// connectionRecordingSummaryBufferSize represents the number of free bytes required in the connection-recording-summary.json
// to close safely. This value is determined by the BaseConnectionSummary definition.
connectionRecordingSummaryBufferSize = 252
// connectionRecordingSummaryBufferSize represents the number of free bytes required in the channel-recording-summary.json
// to close safely. This value is determined by the ChannelSummary definition.
channelRecordingSummaryBufferSize = 518
// sessionRecordingMetaBufferSize represents the number of free bytes required in the session-recording.meta to safely
// write a single line: "connection: {connection_recorder_id}"
sessionRecordingMetaBufferSize = 36
// connectionRecordingMetaBufferSize represents the number of free bytes required in the connection-recording.meta to safely
// write a single line: "channel: {channel_recorder_id}"
connnectionRecordingMetaBufferSize = 31
// channelRecordingMetaBufferSize represents the number of free bytes required in the channel-recording.meta
// to close safely. This value is determined by the ChannelRecordingMeta definition.
channelRecordingMetaBufferSize = 114
// sessionJournalBufferSize represents the number of free bytes required in the session container .journal file to
// close safely. This value is determined by the number of files that exist in a session container.
sessionJournalBufferSize = 1060
// connectionJournalBufferSize represents the number of free bytes required in the connection container .journal file to
// close safely. This value is determined by the number of files that exist in a connection container.
connectionJournalBufferSize = 708
// connectionJournalBufferSize represents the number of free bytes required in the channel container .journal file to
// close safely. This value is determined by the number of files that exist in a channel container.
channelJournalBufferSize = 928
)
// ContainerType defines the type of container.
type ContainerType string
// ChecksumBufferSize returns the buffer size needed
// for the rolling buffer of a checksum file
func (c ContainerType) ChecksumBufferSize() (uint64, error) {
switch c {
case SessionContainer:
fallthrough
case ConnectionContainer:
fallthrough
case ChannelContainer:
return checksumFileBufferSize, nil
}
return 0, fmt.Errorf("unknown container type: %s", c)
}
// ChecksumSignatureBufferSize returns the buffer size
// needed for the rolling buffer of a checksum file
func (c ContainerType) ChecksumSignatureBufferSize() (uint64, error) {
switch c {
case SessionContainer:
fallthrough
case ConnectionContainer:
fallthrough
case ChannelContainer:
return sigFileBufferSize, nil
}
return 0, fmt.Errorf("unknown container type: %s", c)
}
// RecordingSummaryBufferSize returns the buffer size needed
// for the rolling buffer of a recording summary file based
// on the container type
func (c ContainerType) RecordingSummaryBufferSize() (uint64, error) {
switch c {
case SessionContainer:
return sessionRecordingSummaryBufferSize, nil
case ConnectionContainer:
return connectionRecordingSummaryBufferSize, nil
case ChannelContainer:
return channelRecordingSummaryBufferSize, nil
}
return 0, fmt.Errorf("unknown container type: %s", c)
}
// RecordingMetaBufferSize returns the buffer size needed
// for the rolling buffer of a recording meta file based
// on the container type
func (c ContainerType) RecordingMetaBufferSize() (uint64, error) {
switch c {
case SessionContainer:
return sessionRecordingMetaBufferSize, nil
case ConnectionContainer:
return connnectionRecordingMetaBufferSize, nil
case ChannelContainer:
return channelRecordingMetaBufferSize, nil
}
return 0, fmt.Errorf("unknown container type: %s", c)
}
// JournalBufferSize returns the buffer size needed
// for the rolling buffer for a journal file based
// on the container type
func (c ContainerType) JournalBufferSize() (uint64, error) {
switch c {
case SessionContainer:
return sessionJournalBufferSize, nil
case ConnectionContainer:
return connectionJournalBufferSize, nil
case ChannelContainer:
return channelJournalBufferSize, nil
}
return 0, fmt.Errorf("unknown container type: %s", c)
}
// FileChecksumValidation is a validation report on a file's checksum value
type FileChecksumValidation struct {
Filename string
Passed bool
Error error
}
// ContainerChecksumValidation is a map where the key is a file name
// and the value contains a validation report on whether or
// not the file matches its expected checksum
type ContainerChecksumValidation map[string]*FileChecksumValidation
// GetFailedItems returns a filtered map of FileChecksumValidation that have failed
func (cv ContainerChecksumValidation) GetFailedItems() ContainerChecksumValidation {
failedValidations := ContainerChecksumValidation{}
for fileName, validation := range cv {
if validation.Passed {
continue
}
failedValidations[fileName] = validation
}
return failedValidations
}
// Valid container types.
const (
SessionContainer ContainerType = "session"
ConnectionContainer ContainerType = "connection"
ChannelContainer ContainerType = "channel"
)
// container contains a group of files in a BSR.
// Each container has corresponding .meta, .summary, SHA256SUM, and SHA256SUM.sig files.
type container struct {
container storage.Container
// Fields primarily used for writing
journal *journal.Journal
meta *checksum.File
sum *checksum.File
sumName string
sumEncoder *json.Encoder
checksums *sign.File
sigs storage.File
// Field used for reading
shaSums checksum.Sha256Sums
metaFile storage.File
// Field used for reading and writing
keys *kms.Keys
metaName string
}
// newContainer creates a container for the given type backed by the provide storage.Container.
func newContainer(ctx context.Context, t ContainerType, c storage.Container, keys *kms.Keys) (*container, error) {
journalBufferSize, err := t.JournalBufferSize()
if err != nil {
return nil, err
}
j, err := c.OpenFile(ctx, journalFileName,
storage.WithCreateFile(),
storage.WithFileAccessMode(storage.ReadWrite),
storage.WithCloseSyncMode(storage.NoSync),
storage.WithBuffer(journalBufferSize),
)
if err != nil {
return nil, err
}
jj, err := journal.New(ctx, j)
if err != nil {
return nil, err
}
cc := &container{
container: c,
journal: jj,
keys: keys,
}
sigFileBufferSize, err := t.ChecksumSignatureBufferSize()
if err != nil {
return nil, err
}
cc.sigs, err = cc.create(ctx, sigFileName,
storage.WithCreateFile(),
storage.WithFileAccessMode(storage.ReadWrite),
storage.WithBuffer(sigFileBufferSize),
)
if err != nil {
return nil, err
}
checksumFileBufferSize, err := t.ChecksumBufferSize()
if err != nil {
return nil, err
}
cs, err := cc.create(ctx, checksumFileName,
storage.WithCreateFile(),
storage.WithFileAccessMode(storage.ReadWrite),
storage.WithBuffer(checksumFileBufferSize),
)
if err != nil {
return nil, err
}
cc.checksums, err = sign.NewFile(ctx, cs, cc.sigs, keys)
if err != nil {
return nil, err
}
metaBufferSize, err := t.RecordingMetaBufferSize()
if err != nil {
return nil, err
}
cc.metaName = fmt.Sprintf(metaFileNameTemplate, t)
meta, err := cc.create(ctx, cc.metaName,
storage.WithCreateFile(),
storage.WithFileAccessMode(storage.ReadWrite),
storage.WithBuffer(metaBufferSize),
)
if err != nil {
return nil, err
}
cc.meta, err = checksum.NewFile(ctx, meta, cc.checksums)
if err != nil {
return nil, err
}
summaryBufferSize, err := t.RecordingSummaryBufferSize()
if err != nil {
return nil, err
}
cc.sumName = fmt.Sprintf(summaryFileNameTemplate, t)
sum, err := cc.create(ctx, cc.sumName,
storage.WithCreateFile(),
storage.WithFileAccessMode(storage.ReadWrite),
storage.WithBuffer(summaryBufferSize),
)
if err != nil {
return nil, err
}
cc.sum, err = checksum.NewFile(ctx, sum, cc.checksums)
if err != nil {
return nil, err
}
cc.sumEncoder = json.NewEncoder(cc.sum)
cc.sumEncoder.SetIndent("", " ")
return cc, nil
}
type populateKeyFunc func(c *container) (*kms.Keys, error)
// openContainer will set keys and load and verify the checksums for this container
func openContainer(ctx context.Context, t ContainerType, c storage.Container, keyGetFunc populateKeyFunc) (*container, error) {
const op = "bsr.openContainer"
switch {
case t == "":
return nil, fmt.Errorf("%s: missing container type: %w", op, ErrInvalidParameter)
case is.Nil(c):
return nil, fmt.Errorf("%s: missing container: %w", op, ErrInvalidParameter)
case is.Nil(keyGetFunc):
return nil, fmt.Errorf("%s: missing key function: %w", op, ErrInvalidParameter)
}
cc := &container{
container: c,
}
keys, err := keyGetFunc(cc)
if err != nil {
return nil, err
}
cc.keys = keys
err = cc.loadChecksums(ctx)
if err != nil {
return nil, err
}
// Load the meta file
cc.metaName = fmt.Sprintf(metaFileNameTemplate, t)
mFile, err := cc.container.OpenFile(ctx, cc.metaName)
if err != nil {
return nil, err
}
cc.metaFile = mFile
return cc, nil
}
func (c *container) loadChecksums(ctx context.Context) (err error) {
const op = "bsr.(container).loadChecksums"
// Open and extract checksum signature
checksumsSigFile, err := c.container.OpenFile(ctx, sigFileName)
if err != nil {
return err
}
defer func() {
if closeErr := checksumsSigFile.Close(); closeErr != nil {
err = errors.Join(err, fmt.Errorf("%s: %w", op, closeErr))
}
}()
checksumSigBytes := new(bytes.Buffer)
_, err = checksumSigBytes.ReadFrom(checksumsSigFile)
if err != nil {
return err
}
sig := new(wrapping.SigInfo)
err = proto.Unmarshal(checksumSigBytes.Bytes(), sig)
if err != nil {
return err
}
// Open and extract checksum file bytes
checksumsFile, err := c.container.OpenFile(ctx, checksumFileName)
if err != nil {
return err
}
defer func() {
if closeErr := checksumsFile.Close(); closeErr != nil {
err = errors.Join(err, fmt.Errorf("%s: %w", op, closeErr))
}
}()
var checksumsBuffer bytes.Buffer
cTee := io.TeeReader(checksumsFile, &checksumsBuffer)
checksumBytes := new(bytes.Buffer)
_, err = checksumBytes.ReadFrom(cTee)
if err != nil {
return err
}
verified, err := c.keys.VerifySignatureWithPubKey(ctx, sig, checksumBytes.Bytes())
if err != nil {
return err
}
if !verified {
return fmt.Errorf("%s: failed to verify checksums signature: %w", op, ErrSignatureVerification)
}
// Load checksums
sums, err := checksum.LoadSha256Sums(&checksumsBuffer)
if err != nil {
return err
}
c.shaSums = sums
return nil
}
func (c *container) loadKey(ctx context.Context, keyFileName string) (k *wrapping.KeyInfo, err error) {
keyFile, err := c.container.OpenFile(ctx, keyFileName)
if err != nil {
return nil, err
}
defer func() {
if closeErr := keyFile.Close(); closeErr != nil {
err = errors.Join(err, closeErr)
}
}()
keyBytes := new(bytes.Buffer)
_, err = keyBytes.ReadFrom(keyFile)
if err != nil {
return nil, err
}
key := new(wrapping.KeyInfo)
err = proto.Unmarshal(keyBytes.Bytes(), key)
if err != nil {
return nil, err
}
return key, nil
}
func (c *container) loadSignature(ctx context.Context, sigFileName string) (s *wrapping.SigInfo, err error) {
sigFile, err := c.container.OpenFile(ctx, sigFileName)
if err != nil {
return nil, err
}
defer func() {
if closeErr := sigFile.Close(); closeErr != nil {
err = errors.Join(err, closeErr)
}
}()
sigBytes := new(bytes.Buffer)
_, err = sigBytes.ReadFrom(sigFile)
if err != nil {
return nil, err
}
signature := new(wrapping.SigInfo)
err = proto.Unmarshal(sigBytes.Bytes(), signature)
if err != nil {
return nil, err
}
return signature, nil
}
// loadKeys will load the BSR keys from storage, unmarshal and unwrap them.
// After unwrapping, it will verify the key signature files before setting the keys
// on the container
func (c *container) loadKeys(ctx context.Context, keyUnwrapFn kms.KeyUnwrapCallbackFunc) (*kms.Keys, error) {
const op = "bsr.(container).loadKeys"
switch {
case keyUnwrapFn == nil:
return nil, fmt.Errorf("%s: missing key unwrap function: %w", op, ErrInvalidParameter)
}
bsrPubKey, err := c.loadKey(ctx, bsrPubKeyFileName)
if err != nil {
return nil, err
}
wrappedBsrKey, err := c.loadKey(ctx, wrappedBsrKeyFileName)
if err != nil {
return nil, err
}
wrappedPrivKey, err := c.loadKey(ctx, wrappedPrivKeyFileName)
if err != nil {
return nil, err
}
pubKeyBsrSignature, err := c.loadSignature(ctx, pubKeyBsrSignatureFileName)
if err != nil {
return nil, err
}
pubKeySelfSignature, err := c.loadSignature(ctx, pubKeySelfSignatureFileName)
if err != nil {
return nil, err
}
unwrappedKeys, err := keyUnwrapFn(kms.WrappedKeys{
WrappedBsrKey: wrappedBsrKey,
WrappedPrivKey: wrappedPrivKey,
})
if err != nil {
return nil, err
}
keys := &kms.Keys{
PubKey: bsrPubKey,
BsrKey: unwrappedKeys.BsrKey,
PrivKey: unwrappedKeys.PrivKey,
PubKeyBsrSignature: pubKeyBsrSignature,
PubKeySelfSignature: pubKeySelfSignature,
}
verified, err := keys.VerifyPubKeySelfSignature(ctx)
if err != nil {
return nil, err
}
if !verified {
return nil, fmt.Errorf("%s: failed to verify public self signed key: %w", op, ErrSignatureVerification)
}
verified, err = keys.VerifyPubKeyBsrSignature(ctx)
if err != nil {
return nil, err
}
if !verified {
return nil, fmt.Errorf("%s: failed to verify pub key signature: %w", op, ErrSignatureVerification)
}
return keys, nil
}
func (c *container) verifyMetadata(ctx context.Context, sha256Reader *crypto.Sha256SumReader) error {
const op = "bsr.(container).verifyMetadata"
metaSum, err := sha256Reader.Sum(ctx, crypto.WithHexEncoding(true))
if err != nil {
return err
}
expectedMetaSum, err := c.shaSums.Sum(c.metaName)
if err != nil {
return err
}
if !bytes.Equal(expectedMetaSum, metaSum) {
return fmt.Errorf("%s: meta checksum did not match expected value", op)
}
return nil
}
// create creates a new file in the container for writing.
func (c *container) create(ctx context.Context, s string, options ...storage.Option) (storage.File, error) {
const op = "bsr.(container).create"
err := c.journal.Record("CREATING", s)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
var f storage.File
switch len(options) {
case 0:
f, err = c.container.Create(ctx, s)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
default:
f, err = c.container.OpenFile(ctx, s, options...)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
}
jf, err := journal.NewFile(ctx, f, c.journal)
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}
defer c.journal.Record("CREATED", s)
return jf, nil
}
func (c *container) decodeJsonFile(ctx context.Context, s string, v any) error {
const op = "bsr.(container).decodeJsonFile"
return c.decodeFile(ctx, s, func(_ context.Context, r io.Reader) error {
dec := json.NewDecoder(r)
if err := dec.Decode(v); err != nil {
return fmt.Errorf("%s: %w", op, err)
}
return nil
})
}
func (c *container) decodeFile(ctx context.Context, s string, fn func(context.Context, io.Reader) error) error {
const op = "bsr.(container).decodeFile"
expectedSum, err := c.shaSums.Sum(s)
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
f, err := c.container.OpenFile(ctx, s)
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
sha256Reader, err := crypto.NewSha256SumReader(ctx, f)
if err != nil {
f.Close()
return fmt.Errorf("%s: %w", op, err)
}
defer sha256Reader.Close()
if err := fn(ctx, sha256Reader); err != nil {
return fmt.Errorf("%s: %w", op, err)
}
gotSum, err := sha256Reader.Sum(ctx, crypto.WithHexEncoding(true))
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
if !bytes.Equal(expectedSum, gotSum) {
return fmt.Errorf("%s: checksum did not match expected value", op)
}
return nil
}
// syncBsrKey will take the marshalled bytes of a key, write its contents to a local file,
// and then close it. The key file is created using the synchronous storage option, so
// close will block until the file is synced to remote storage
func (c *container) syncBsrKey(ctx context.Context, s string, data []byte) error {
const op = "bsr.(container).syncBsrKey"
switch {
case len(s) == 0:
return fmt.Errorf("%s: missing file name %w", op, ErrInvalidParameter)
case data == nil:
return fmt.Errorf("%s: missing data payload %w", op, ErrInvalidParameter)
}
jf, err := c.create(ctx, s, storage.WithCreateFile(),
storage.WithFileAccessMode(storage.WriteOnly),
storage.WithCloseSyncMode(storage.Synchronous))
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
cf, err := checksum.NewFile(ctx, jf, c.checksums)
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
_, err = cf.Write(data)
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
err = cf.Close()
if err != nil {
return fmt.Errorf("%s: %w", op, err)
}
return nil
}
// writeMetaString writes a string to the containers meta file.
func (c *container) writeMetaString(_ context.Context, s string) (int, error) {
return c.meta.WriteString(s)
}
// writeMetaLine writes a new line terminated line to the container's meta file.
func (c *container) writeMetaLine(_ context.Context, s string) (int, error) {
return c.meta.WriteString(s + "\n")
}
// WriteMeta writes a new line terminated key : value pair to the container's meta file
func (c *container) WriteMeta(_ context.Context, k, v string) (int, error) {
return c.meta.WriteString(fmt.Sprintf("%s: %s\n", k, v))
}
// EncodeSummary writes a new line terminated key : value pair to the container's summary file
func (c *container) EncodeSummary(_ context.Context, s any) error {
return c.sumEncoder.Encode(s)
}
// WriteBinaryChecksum writes a checksum for a binary file to the checksum file.
func (c *container) WriteBinaryChecksum(_ context.Context, sum []byte, fname string) (int, error) {
return c.checksums.WriteString(fmt.Sprintf("%x *%s\n", sum, fname))
}
// close closes a container, closing the underlying files in a container.
func (c *container) close(_ context.Context) error {
const op = "bsr.(container).close"
var closeError error
if !is.Nil(c.meta) {
if err := c.meta.Close(); err != nil {
closeError = errors.Join(closeError, fmt.Errorf("%s: %w", op, err))
}
}
if !is.Nil(c.sum) {
if err := c.sum.Close(); err != nil {
closeError = errors.Join(closeError, fmt.Errorf("%s: %w", op, err))
}
}
if !is.Nil(c.checksums) {
if err := c.checksums.Close(); err != nil {
closeError = errors.Join(closeError, fmt.Errorf("%s: %w", op, err))
}
}
if !is.Nil(c.sigs) {
if err := c.sigs.Close(); err != nil {
closeError = errors.Join(closeError, fmt.Errorf("%s: %w", op, err))
}
}
if !is.Nil(c.journal) {
if err := c.journal.Close(); err != nil {
closeError = errors.Join(closeError, fmt.Errorf("%s: %w", op, err))
}
}
if !is.Nil(c.container) {
if err := c.container.Close(); err != nil {
closeError = errors.Join(closeError, fmt.Errorf("%s: %w", op, err))
}
}
return closeError
}
// ValidateChecksums iterates over all files in the SHA256SUM
// file and verifies that each file checksum is as expected.
//
// This function expects that the container's kms keys
// are loaded into memory and the signature files are
// verified.
func (c *container) ValidateChecksums(ctx context.Context) (ContainerChecksumValidation, error) {
const op = "bsr.(container).Validate"
if len(c.shaSums) == 0 {
return nil, fmt.Errorf("%s: missing checksums", op)
}
if c.keys == nil {
return nil, fmt.Errorf("%s: missing keys", op)
}
checksumValidation := make(ContainerChecksumValidation, len(c.shaSums))
for fileName, expectedChecksum := range c.shaSums {
report := &FileChecksumValidation{
Filename: fileName,
}
checksumValidation[fileName] = report
actualChecksum, err := c.computeFileChecksum(ctx, fileName, crypto.WithHexEncoding(true))
if err != nil {
report.Error = err
continue
}
if !bytes.Equal(expectedChecksum, actualChecksum) {
report.Error = fmt.Errorf("checksum mismatch")
continue
}
report.Passed = true
}
return checksumValidation, nil
}
// computeFileChecksum will open a file, read its contents, and computes SHA256 message digest
func (c *container) computeFileChecksum(ctx context.Context, fileName string, opt ...wrapping.Option) (checksum []byte, err error) {
const op = "bsr.(container).computeFileChecksum"
var f storage.File
f, err = c.container.OpenFile(ctx, fileName)
if err != nil {
err = fmt.Errorf("%s: %w", op, err)
return checksum, err
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
err = errors.Join(err, fmt.Errorf("%s: %w", op, closeErr))
}
}()
checksum, err = crypto.Sha256Sum(ctx, f, opt...)
if err != nil {
err = fmt.Errorf("%s: %w", op, err)
}
return checksum, err
}