Files
threshold/internal/bsr/container.go
T
hashicorp-copywrite[bot]andJohan Brandhorst-Satzkorn 29da0bcb92 [COMPLIANCE] License changes (#3567)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* Update copyright file headers to BUS-1.1

* Rerun make gen

This will pick up the last of the license changes

* Revert "Rerun make gen"

This reverts commit dbe0968bffd92745d8663c6940debae098395a6b.

* Remove copywrite from generation script

The current version of copywrite is not sophisticated enough
to only apply the copywrite changes to certain subdirectories,
so to avoid accidentally relicensing our api and sdk modules,
we will stop running the copywrite script until such time
that it can do the right thing.

* License protobuf API as MPL

The protobuf API files are used to generate the SDK,
since we want the SDK to be MPL, the protobuf files
must also be MPL. This extends to all protobuf files
whose sources end up in the SDK directory, including:

- internal/proto/controller/api
- internal/proto/controller/custom_options
- internal/proto/plugin

It also includes the generated service directory in
internal/gen/controller/api/services, since they are
generated from the protobuf sources used for the SDK.

* Change website license file name

This makes the naming scheme consistent with the rest
of the repository.

* Add note on licensing to README

* Set copywrite license to BUSL

* Revert "Add note on licensing to README"

This reverts commit 4d865a91a7a4f11605f0dc4609355d7e3c3cfffe.

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
2023-08-11 10:31:53 -07:00

630 lines
16 KiB
Go

// 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"
)
// ContainerType defines the type of container.
type ContainerType string
// 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) {
j, err := c.OpenFile(ctx, journalFileName,
storage.WithCreateFile(),
storage.WithFileAccessMode(storage.WriteOnly),
storage.WithCloseSyncMode(storage.NoSync))
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,
}
cc.sigs, err = cc.create(ctx, sigFileName)
if err != nil {
return nil, err
}
cs, err := cc.create(ctx, checksumFileName)
if err != nil {
return nil, err
}
cc.checksums, err = sign.NewFile(ctx, cs, cc.sigs, keys)
if err != nil {
return nil, err
}
cc.metaName = fmt.Sprintf(metaFileNameTemplate, t)
meta, err := cc.create(ctx, cc.metaName)
if err != nil {
return nil, err
}
cc.meta, err = checksum.NewFile(ctx, meta, cc.checksums)
if err != nil {
return nil, err
}
cc.sumName = fmt.Sprintf(summaryFileNameTemplate, t)
sum, err := cc.create(ctx, cc.sumName)
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
}
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
}