From aa10d1dbfe17cc5397890cb0ef916b9cc3df8ad6 Mon Sep 17 00:00:00 2001 From: Jim Date: Sat, 18 Mar 2023 15:52:50 -0400 Subject: [PATCH] feat(KMS): add BsrKeys type and req. receiver functions --- internal/bsr/internal/is/nil.go | 18 + internal/bsr/kms/errors.go | 32 + internal/bsr/kms/keys.go | 467 +++++++++++ internal/bsr/kms/keys_test.go | 1369 +++++++++++++++++++++++++++++++ internal/bsr/kms/option_test.go | 24 + internal/bsr/kms/options.go | 55 ++ internal/bsr/kms/testing.go | 135 +++ internal/errors/code.go | 2 + internal/errors/info.go | 8 + 9 files changed, 2110 insertions(+) create mode 100644 internal/bsr/internal/is/nil.go create mode 100644 internal/bsr/kms/errors.go create mode 100644 internal/bsr/kms/keys.go create mode 100644 internal/bsr/kms/keys_test.go create mode 100644 internal/bsr/kms/option_test.go create mode 100644 internal/bsr/kms/options.go create mode 100644 internal/bsr/kms/testing.go diff --git a/internal/bsr/internal/is/nil.go b/internal/bsr/internal/is/nil.go new file mode 100644 index 000000000..86ceeffee --- /dev/null +++ b/internal/bsr/internal/is/nil.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package is + +import "reflect" + +// Nil checks if the interface is nil +func Nil(i any) bool { + if i == nil { + return true + } + switch reflect.TypeOf(i).Kind() { + case reflect.Ptr, reflect.Map, reflect.Chan, reflect.Slice: + return reflect.ValueOf(i).IsNil() + } + return false +} diff --git a/internal/bsr/kms/errors.go b/internal/bsr/kms/errors.go new file mode 100644 index 000000000..55ac2c5df --- /dev/null +++ b/internal/bsr/kms/errors.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package kms + +import ( + "errors" +) + +// ErrUnknown represents an unknown error +var ErrUnknown = errors.New("unknown error") + +// ErrInvalidParameter represents an invalid parameter error +var ErrInvalidParameter = errors.New("invalid parameter") + +// ErrGenKey represents a key gen error +var ErrGenKey = errors.New("error occurred during key generation") + +// ErrEncrypt represents an encryption error +var ErrEncrypt = errors.New("error occurred during encrypt") + +// ErrDecrypt represents a decryption error +var ErrDecrypt = errors.New("error occurred during decrypt") + +// ErrEncode represents an encoding error +var ErrEncode = errors.New("error occurred during encode") + +// ErrDecode represents a decoding error +var ErrDecode = errors.New("error occurred during decode") + +// ErrSign represents a signing error +var ErrSign = errors.New("error occurred during signing") diff --git a/internal/bsr/kms/keys.go b/internal/bsr/kms/keys.go new file mode 100644 index 000000000..8d19f058c --- /dev/null +++ b/internal/bsr/kms/keys.go @@ -0,0 +1,467 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package kms + +import ( + "bytes" + "context" + "crypto/ed25519" + "crypto/rand" + "fmt" + "sync" + + "github.com/hashicorp/boundary/internal/bsr/internal/is" + "github.com/hashicorp/boundary/internal/util" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + wrappingEd "github.com/hashicorp/go-kms-wrapping/v2/ed25519" + + "github.com/hashicorp/go-kms-wrapping/v2/aead" + "github.com/hashicorp/go-kms-wrapping/v2/extras/crypto" + "google.golang.org/protobuf/proto" +) + +// Keys are the keys required/associated with a BSR for crypto operations. +// +// Operations on this type are concurrently safe. +type Keys struct { + // WrappedBsrKey is the "bsr key" which is a wrapped AES-GCM key + WrappedBsrKey *wrapping.KeyInfo + + // WrappedPrivKey is the BSR's wrapped ed25519.PrivateKey + WrappedPrivKey *wrapping.KeyInfo + + // BsrKey is the plaintext "bsr key" which is an AES-GCM key + BsrKey *wrapping.KeyInfo + + // PrivKey is the BSR's plaintext ed25519.PrivateKey + PrivKey *wrapping.KeyInfo + + // PubKey is the BSR's plaintext ed25519.PublicKey + PubKey *wrapping.KeyInfo + + // PubKeySelfSignature is a self-signature of the BSR's plaintext + // ed25519.PublicKey created with its ed25519.PrivateKey + PubKeySelfSignature *wrapping.SigInfo + + // PubKeyBsrSignature is a signature of the BSR's plaintext ed25519.Public + // key created with the BsrKey (AES-GCM) + PubKeyBsrSignature *wrapping.SigInfo + + l sync.RWMutex +} + +// CreateKeys creates new bsr keys, wrapping and signing keys as required +// using the provided bsrWrapper. Supported options: WithRandomReader +func CreateKeys(ctx context.Context, bsrWrapper wrapping.Wrapper, sessionId string, opt ...Option) (*Keys, error) { + const op = "kms.CreateBsrKeys" + switch { + case sessionId == "": + return nil, fmt.Errorf("%s: missing session id: %w", op, ErrInvalidParameter) + case is.Nil(bsrWrapper): + return nil, fmt.Errorf("%s: missing external bsr wrapper: %w", op, ErrInvalidParameter) + } + + opts := getOpts(opt...) + + if opts.withRandomReader == nil { + opts.withRandomReader = rand.Reader + } + + retKeys := &Keys{} + + bsrKey := make([]byte, 32) + n, err := opts.withRandomReader.Read(bsrKey) + switch { + case err != nil: + return nil, fmt.Errorf("%s: error reading random bytes for bsr: %w", op, ErrGenKey) + case n != 32: + return nil, fmt.Errorf("%s: wanted 32 bytes and got %d: %w", op, n, ErrGenKey) + } + bsrBlob, err := bsrWrapper.Encrypt(ctx, bsrKey) + if err != nil { + return nil, fmt.Errorf("%s: unable to encrypt bsr key: %w", op, ErrEncrypt) + } + marshaledBsrBlob, err := proto.Marshal(bsrBlob) + if err != nil { + return nil, fmt.Errorf("%s: unable to marshal bsr key %w: %w", op, err, ErrEncode) + } + retKeys.WrappedBsrKey = &wrapping.KeyInfo{ + KeyId: sessionId, + WrappedKey: marshaledBsrBlob, + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign, wrapping.KeyPurpose_Encrypt, wrapping.KeyPurpose_Decrypt}, + } + retKeys.BsrKey = &wrapping.KeyInfo{ + KeyId: sessionId, + Key: bsrKey, + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign, wrapping.KeyPurpose_Encrypt, wrapping.KeyPurpose_Decrypt}, + } + + pub, priv, err := ed25519.GenerateKey(opts.withRandomReader) + if err != nil { + return nil, fmt.Errorf("%s: unable to generate bsr ed25519 key-pair %w: %w", op, err, ErrGenKey) + } + privBlob, err := bsrWrapper.Encrypt(ctx, priv) + if err != nil { + return nil, fmt.Errorf("%s: unable to encrypt bsr ed25519 priv key %w: %w", op, err, ErrEncrypt) + } + marshaledPrivBlob, err := proto.Marshal(privBlob) + if err != nil { + return nil, fmt.Errorf("%s: unable to marshal bsr ed25519 priv key %w: %w", op, err, ErrEncode) + } + retKeys.WrappedPrivKey = &wrapping.KeyInfo{ + KeyId: sessionId, + WrappedKey: marshaledPrivBlob, + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + } + retKeys.PrivKey = &wrapping.KeyInfo{ + KeyId: sessionId, + Key: priv, + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + } + retKeys.PubKey = &wrapping.KeyInfo{ + KeyId: sessionId, + Key: pub, + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Verify}, + } + privKeySigner, err := wrappingEd.NewSigner(ctx, wrappingEd.WithPrivKey(priv), wrapping.WithKeyId(sessionId)) + if err != nil { + return nil, fmt.Errorf("%s: %w", op, err) + } + retKeys.PubKeySelfSignature, err = privKeySigner.Sign(ctx, pub) + if err != nil { + return nil, fmt.Errorf("%s: %w: %w", op, err, ErrSign) + } + + bsrKeyWrapper := aead.NewWrapper() + if _, err := bsrKeyWrapper.SetConfig(ctx, wrapping.WithKeyId(sessionId)); err != nil { + return nil, fmt.Errorf("%s: setting config on aead wrapper %w: %w", op, err, ErrUnknown) + } + if err := bsrKeyWrapper.SetAesGcmKeyBytes(bsrKey); err != nil { + return nil, fmt.Errorf("%s: error setting key bytes on aead wrapper %w: %w", op, err, ErrUnknown) + } + marshaledSig, err := crypto.HmacSha256(ctx, pub, bsrKeyWrapper, crypto.WithMarshaledSigInfo()) + if err != nil { + return nil, fmt.Errorf("%s: error signing pub key with bsr key %w: %w", op, err, ErrSign) + } + retKeys.PubKeyBsrSignature = &wrapping.SigInfo{} + if err := proto.Unmarshal([]byte(marshaledSig), retKeys.PubKeyBsrSignature); err != nil { + return nil, fmt.Errorf("%s: unable to unmarshal sig info %w: %w", op, err, ErrEncode) + } + + return retKeys, nil +} + +// UnwrapBsrKey will unwrap the bsr key (k.WrappedBsrKey which is an AES-GCM) +// using the provide bsr kms. The k.BsrKey will be set to the unwrapped key +// key and returned in the form of an aead.Wrapper +// +// This is a concurrently safe operation. +func (k *Keys) UnwrapBsrKey(ctx context.Context, bsrWrapper wrapping.Wrapper, opt ...Option) (*aead.Wrapper, error) { + const op = "kms.(BsrKeys).UnwrapBsrKey" + if k == nil { + return nil, fmt.Errorf("%s: nil bsr keys: %w", op, ErrInvalidParameter) + } + opts := getOpts(opt...) + + // support just using an existing lock... + if !opts.withAlreadyLocked { + k.l.Lock() + defer k.l.Unlock() + } + + switch { + case util.IsNil(bsrWrapper): + return nil, fmt.Errorf("%s: missing bsr wrapper: %w", op, ErrInvalidParameter) + case k.WrappedBsrKey == nil: + return nil, fmt.Errorf("%s: missing wrapped bsr key: %w", op, ErrInvalidParameter) + case k.WrappedBsrKey.KeyId == "": + return nil, fmt.Errorf("%s: missing wrapped bsr key id: %w", op, ErrInvalidParameter) + case k.WrappedBsrKey.WrappedKey == nil: + return nil, fmt.Errorf("%s: missing wrapped bsr key bytes: %w", op, ErrInvalidParameter) + case k.WrappedBsrKey.KeyType != wrapping.KeyType_Aes256: + return nil, keyTypeError(ctx, op, wrapping.KeyType_Aes256, k.WrappedBsrKey.KeyType) + case k.WrappedBsrKey.KeyEncoding != wrapping.KeyEncoding_Bytes: + return nil, keyEncodingError(ctx, op, wrapping.KeyEncoding_Bytes, k.WrappedBsrKey.KeyEncoding) + } + + var blob wrapping.BlobInfo + + if err := proto.Unmarshal(k.WrappedBsrKey.WrappedKey, &blob); err != nil { + return nil, fmt.Errorf("%s: error unmarshaling wrapped bsr key %w: %w", op, err, ErrDecode) + } + + keyBytes, err := bsrWrapper.Decrypt(ctx, &blob) + if err != nil { + return nil, fmt.Errorf("%s: error decrypting wrapped bsr key %w: %w", op, err, ErrDecrypt) + } + + bsrKeyWrapper := aead.NewWrapper() + if _, err := bsrKeyWrapper.SetConfig(ctx, wrapping.WithKeyId(k.WrappedBsrKey.KeyId)); err != nil { + return nil, fmt.Errorf("%s: error setting config on aead wrapper %w: %w", op, err, ErrUnknown) + } + if err := bsrKeyWrapper.SetAesGcmKeyBytes(keyBytes); err != nil { + return nil, fmt.Errorf("%s: error setting key bytes on aead wrapper %w: %w", op, err, ErrUnknown) + } + + k.BsrKey = &wrapping.KeyInfo{ + Key: keyBytes, + KeyId: k.WrappedBsrKey.KeyId, + KeyType: k.WrappedBsrKey.KeyType, + KeyPurposes: k.WrappedBsrKey.KeyPurposes, + KeyEncoding: wrapping.KeyEncoding_Bytes, + } + + return bsrKeyWrapper, nil +} + +// UnwrapPrivKey will unwrap the priv key (k.WrappedPrivKey which is an +// ed25519.PrivateKey) using the provide bsr kms. The k.PrivKey will be set to +// the unwrapped key and returned in the form of an ed25519.PrivateKey +// +// This is a concurrently safe operation. +func (k *Keys) UnwrapPrivKey(ctx context.Context, bsrWrapper wrapping.Wrapper) (ed25519.PrivateKey, error) { + const op = "kms.(BsrKeys).UnwrapPrivKey" + if k == nil { + return nil, fmt.Errorf("%s: nil bsr keys: %w", op, ErrInvalidParameter) + } + k.l.Lock() + defer k.l.Unlock() + + switch { + case util.IsNil(bsrWrapper): + return nil, fmt.Errorf("%s: missing bsr wrapper: %w", op, ErrInvalidParameter) + case k.WrappedPrivKey == nil: + return nil, fmt.Errorf("%s: missing wrapped priv key: %w", op, ErrInvalidParameter) + case k.WrappedPrivKey.KeyId == "": + return nil, fmt.Errorf("%s: missing wrapped priv key id: %w", op, ErrInvalidParameter) + case k.WrappedPrivKey.WrappedKey == nil: + return nil, fmt.Errorf("%s: missing wrapped priv key bytes: %w", op, ErrInvalidParameter) + case k.WrappedPrivKey.KeyType != wrapping.KeyType_Ed25519: + return nil, keyTypeError(ctx, op, wrapping.KeyType_Ed25519, k.WrappedPrivKey.KeyType) + case k.WrappedPrivKey.KeyEncoding != wrapping.KeyEncoding_Bytes: + return nil, keyEncodingError(ctx, op, wrapping.KeyEncoding_Bytes, k.WrappedPrivKey.WrappedKeyEncoding) + } + var privKeyBlob wrapping.BlobInfo + if err := proto.Unmarshal(k.WrappedPrivKey.WrappedKey, &privKeyBlob); err != nil { + return nil, fmt.Errorf("%s: error unmarshaling wrapped priv key %w: %w", op, err, ErrDecode) + } + privKeyBytes, err := bsrWrapper.Decrypt(ctx, &privKeyBlob) + if err != nil { + return nil, fmt.Errorf("%s: error decrypting wrapped priv key %w: %w", op, err, ErrDecrypt) + } + if len(privKeyBytes) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("%s: priv key must be %d bytes; got %d: %w", op, ed25519.PrivateKeySize, len(privKeyBytes), ErrInvalidParameter) + } + + k.PrivKey = &wrapping.KeyInfo{ + Key: privKeyBytes, + KeyId: k.WrappedPrivKey.KeyId, + KeyType: k.WrappedPrivKey.KeyType, + KeyPurposes: k.WrappedPrivKey.KeyPurposes, + KeyEncoding: wrapping.KeyEncoding_Bytes, + } + + return ed25519.PrivateKey(privKeyBytes), nil +} + +// VerifyPubKeyBsrSignature will verify the pub key signature created with the +// bsr key. It will first try to use k.BsrKey, if that's not available it will +// attempt to unwrap k.WrappedBsrKey and use it for verification. Supported +// options: WithBsrWrapper which is required if using k.WrappedBsrKey to verify +// the signature. +// +// This is a concurrently safe operation. +func (k *Keys) VerifyPubKeyBsrSignature(ctx context.Context, opt ...Option) (bool, error) { + const op = "kms.(BsrKeys).VerifyPubKeyBsrSignature" + if k == nil { + return false, fmt.Errorf("%s: nil bsr keys: %w", op, ErrInvalidParameter) + } + // needs a write lock since it may need to UnwrapBsrKey(...) + k.l.Lock() + defer k.l.Unlock() + + switch { + case k.BsrKey == nil && k.WrappedBsrKey == nil: + return false, fmt.Errorf("%s: missing bsr key and wrapped bsr key: %w", op, ErrInvalidParameter) + case k.BsrKey != nil && k.BsrKey.Key == nil: + return false, fmt.Errorf("%s: missing bsr key bytes: %w", op, ErrInvalidParameter) + case k.WrappedBsrKey != nil && k.WrappedBsrKey.WrappedKey == nil: + return false, fmt.Errorf("%s: missing wrapped bsr key bytes: %w", op, ErrInvalidParameter) + case k.PubKey == nil: + return false, fmt.Errorf("%s: missing pub key: %w", op, ErrInvalidParameter) + case k.PubKeyBsrSignature == nil: + return false, fmt.Errorf("%s: missing pub key signature: %w", op, ErrInvalidParameter) + } + + opts := getOpts(opt...) + var bsrKeyWrapper *aead.Wrapper + switch { + case k.BsrKey != nil: + switch { + case k.BsrKey.KeyType != wrapping.KeyType_Aes256: + return false, keyTypeError(ctx, op, wrapping.KeyType_Aes256, k.BsrKey.KeyType) + case k.BsrKey.KeyEncoding != wrapping.KeyEncoding_Bytes: + return false, keyEncodingError(ctx, op, wrapping.KeyEncoding_Bytes, k.BsrKey.KeyEncoding) + } + bsrKeyWrapper = aead.NewWrapper() + if _, err := bsrKeyWrapper.SetConfig(ctx, wrapping.WithKeyId(k.BsrKey.KeyId)); err != nil { + return false, fmt.Errorf("%s: error setting config on bsr key wrapper %w: %w", op, err, ErrUnknown) + } + if err := bsrKeyWrapper.SetAesGcmKeyBytes(k.BsrKey.Key); err != nil { + return false, fmt.Errorf("%s: error setting key bytes on bsr key wrapper %w: %w", op, err, ErrUnknown) + } + case k.WrappedBsrKey != nil: + switch { + case util.IsNil(opts.withBsrWrapper): + return false, fmt.Errorf("%s: missing bsr wrapper: %w", op, ErrInvalidParameter) + case k.WrappedBsrKey.KeyType != wrapping.KeyType_Aes256: + return false, keyTypeError(ctx, op, wrapping.KeyType_Aes256, k.WrappedBsrKey.KeyType) + case k.WrappedBsrKey.KeyEncoding != wrapping.KeyEncoding_Bytes: + return false, keyEncodingError(ctx, op, wrapping.KeyEncoding_Bytes, k.WrappedBsrKey.KeyEncoding) + } + var err error + if bsrKeyWrapper, err = k.UnwrapBsrKey(ctx, opts.withBsrWrapper, withAlreadyLocked(true)); err != nil { + return false, fmt.Errorf("%s: error unwrapping bsr key: %w", op, err) + } + default: + return false, fmt.Errorf("%s: missing bsr key and wrapped bsr key: %w", op, ErrInvalidParameter) + } + marshaledSigInfo, err := crypto.HmacSha256(ctx, ed25519.PublicKey(k.PubKey.Key), bsrKeyWrapper, crypto.WithMarshaledSigInfo()) + if err != nil { + return false, fmt.Errorf("%s: error signing pub key %w: %w", op, err, ErrSign) + } + var sigInfo wrapping.SigInfo + if err = proto.Unmarshal([]byte(marshaledSigInfo), &sigInfo); err != nil { + return false, fmt.Errorf("%s: error unmarshaling sig info %w: %w", op, err, ErrDecode) + } + bsrKeyId, err := bsrKeyWrapper.KeyId(ctx) + if err != nil { + return false, fmt.Errorf("%s: error getting bsr key id %w: %w", op, err, ErrUnknown) + } + if k.PubKeyBsrSignature.KeyInfo.KeyId != bsrKeyId { + return false, fmt.Errorf("%s: pub signature key id %q doesn't match verifying key id %q: %w", op, k.PubKeyBsrSignature.KeyInfo.KeyId, bsrKeyId, ErrInvalidParameter) + } + if bytes.Compare(k.PubKeyBsrSignature.Signature, sigInfo.Signature) == 0 { + return true, nil + } + return false, nil +} + +// VerifyPubKeySelfSignature will verify the self-signed pub key signature using +// k.PubKey. Note: this will tell you the signature is correct, but not if the +// public key is the right key; you want to use VerifyPubKeyBsrSignature(...) +// for that. +// +// This is a concurrently safe operation. +func (k *Keys) VerifyPubKeySelfSignature(ctx context.Context) (bool, error) { + const op = "kms.(BsrKeys).VerifyPubKeySelfSignature" + if k == nil { + return false, fmt.Errorf("%s: bsr keys: %w", op, ErrInvalidParameter) + } + k.l.RLock() + defer k.l.RUnlock() + + switch { + case k.PubKey == nil: + return false, fmt.Errorf("%s: missing pub key: %w", op, ErrInvalidParameter) + case k.PubKey.Key == nil: + return false, fmt.Errorf("%s: missing pub key bytes: %w", op, ErrInvalidParameter) + case k.PubKey.KeyType != wrapping.KeyType_Ed25519: + return false, keyTypeError(ctx, op, wrapping.KeyType_Ed25519, k.PubKey.KeyType) + case k.PubKey.KeyEncoding != wrapping.KeyEncoding_Bytes: + return false, keyEncodingError(ctx, op, wrapping.KeyEncoding_Bytes, k.PubKey.KeyEncoding) + case len(k.PubKey.Key) != ed25519.PublicKeySize: + return false, fmt.Errorf("%s: expected public key with %d bytes and got %d: %w", op, ed25519.PublicKeySize, len(k.PubKey.Key), ErrInvalidParameter) + case k.PubKeySelfSignature == nil: + return false, fmt.Errorf("%s: missing pub key self signature: %w", op, ErrInvalidParameter) + case k.PubKeySelfSignature.KeyInfo.KeyId != k.PubKey.KeyId: + return false, fmt.Errorf("%s: pub self signature key id %q doesn't match verifying pub key id %q: %w", op, k.PubKeySelfSignature.KeyInfo.KeyId, k.PubKey.KeyId, ErrInvalidParameter) + } + + verifier, err := wrappingEd.NewVerifier(ctx, wrappingEd.WithPubKey(k.PubKey.Key)) + if err != nil { + return false, fmt.Errorf("%s: %w", op, err) + } + + ok, err := verifier.Verify(ctx, k.PubKey.Key, k.PubKeySelfSignature) + if err != nil { + return false, fmt.Errorf("%s: %w", op, err) + } + return ok, nil +} + +// SignWithPrivKey will sign the msg with the BsrKeys.PrivKey (aka the BSR's +// ed25519.PrivateKey). Typical usage is to sign the BSR's checksums file. +// +// This is a concurrently safe operation. +func (k *Keys) SignWithPrivKey(ctx context.Context, msg []byte) (*wrapping.SigInfo, error) { + const op = "kms.(BsrKeys).SignWithPrivKey" + if k == nil { + return nil, fmt.Errorf("%s: nil bsr keys: %w", op, ErrInvalidParameter) + } + k.l.RLock() + defer k.l.RUnlock() + + switch { + case msg == nil: + return nil, fmt.Errorf("%s: missing msg: %w", op, ErrInvalidParameter) + case k.PrivKey == nil: + return nil, fmt.Errorf("%s: missing priv key: %w", op, ErrInvalidParameter) + case k.PrivKey.KeyId == "": + return nil, fmt.Errorf("%s: missing priv key id: %w", op, ErrInvalidParameter) + case k.PrivKey.Key == nil: + return nil, fmt.Errorf("%s: missing priv key bytes: %w", op, ErrInvalidParameter) + case k.PrivKey.KeyType != wrapping.KeyType_Ed25519: + return nil, keyTypeError(ctx, op, wrapping.KeyType_Ed25519, k.PrivKey.KeyType) + case k.PrivKey.KeyEncoding != wrapping.KeyEncoding_Bytes: + return nil, keyEncodingError(ctx, op, wrapping.KeyEncoding_Bytes, k.PrivKey.KeyEncoding) + case len(k.PrivKey.Key) != ed25519.PrivateKeySize: + return nil, fmt.Errorf("%s: expected priv key with %d bytes and got %d: %w", op, ed25519.PrivateKeySize, len(k.PrivKey.Key), ErrInvalidParameter) + } + + s, err := wrappingEd.NewSigner(ctx, + wrappingEd.WithPrivKey(k.PrivKey.Key), + wrapping.WithKeyId(k.PrivKey.KeyId), + wrapping.WithKeyEncoding(k.PrivKey.KeyEncoding), + wrapping.WithKeyPurposes(k.PrivKey.KeyPurposes...), + ) + if err != nil { + return nil, fmt.Errorf("%s: unable to create priv key signer: %w", op, err) + } + si, err := s.Sign(ctx, msg) + if err != nil { + return nil, fmt.Errorf("%s: %w: %w", op, err, ErrSign) + } + return si, nil +} + +func keyTypeError(ctx context.Context, fromOp string, want wrapping.KeyType, got wrapping.KeyType) error { + return fmt.Errorf( + "%s: unexpected key type %q; expected %q: %w", + fromOp, + wrapping.KeyType_name[int32(got)], + wrapping.KeyType_name[int32(want)], + ErrInvalidParameter, + ) +} + +func keyEncodingError(ctx context.Context, fromOp string, want wrapping.KeyEncoding, got wrapping.KeyEncoding) error { + return fmt.Errorf( + "%s: unexpected key encoding %q; expected %q: %w", + fromOp, + wrapping.KeyEncoding_name[int32(got)], + wrapping.KeyEncoding_name[int32(want)], + ErrInvalidParameter, + ) +} diff --git a/internal/bsr/kms/keys_test.go b/internal/bsr/kms/keys_test.go new file mode 100644 index 000000000..6fd923e4a --- /dev/null +++ b/internal/bsr/kms/keys_test.go @@ -0,0 +1,1369 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package kms_test + +import ( + "context" + stdcrypto "crypto" + "crypto/rand" + "fmt" + "testing" + + stdLibEd25519 "crypto/ed25519" + + "github.com/hashicorp/boundary/internal/bsr/kms" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/go-kms-wrapping/v2/aead" + "github.com/hashicorp/go-kms-wrapping/v2/ed25519" + crypto "github.com/hashicorp/go-kms-wrapping/v2/extras/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestCreateBsrKeys(t *testing.T) { + const ( + ed25519_PrivKey_Length = 64 + ed25519_PubKey_Length = 32 + bsrKey_Length = 32 + + testMsg = "test-msg" + ) + t.Parallel() + testCtx := context.Background() + testBsrKmsWrapper := kms.TestWrapper(t) + tests := []struct { + name string + ctx context.Context + bsrWrapper wrapping.Wrapper + opt []kms.Option + sessionId string + wantErr bool + wantErrMatch error + wantErrContains string + }{ + { + name: "success", + ctx: testCtx, + bsrWrapper: testBsrKmsWrapper, + sessionId: "session-id", + }, + { + name: "success-WithRandomReader", + ctx: testCtx, + bsrWrapper: testBsrKmsWrapper, + sessionId: "session-id", + opt: []kms.Option{kms.WithRandomReader(rand.Reader)}, + }, + { + name: "missing-session-id", + ctx: testCtx, + bsrWrapper: testBsrKmsWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing session id", + }, + { + name: "missing-bsr-wrapper", + ctx: testCtx, + sessionId: "session-id", + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing external bsr wrapper", + }, + { + name: "rand-err-first-read", + ctx: testCtx, + bsrWrapper: testBsrKmsWrapper, + sessionId: "session-id", + opt: []kms.Option{ + kms.WithRandomReader( + &kms.MockReader{ + WithError: fmt.Errorf("rand err"), + WithBytesRead: 32, + WithMockReadOn: 1, + Reader: rand.Reader, + }, + ), + }, + wantErr: true, + wantErrMatch: kms.ErrGenKey, + wantErrContains: "error reading random bytes for bsr", + }, + { + name: "rand-bytes-read-first-read", + ctx: testCtx, + bsrWrapper: testBsrKmsWrapper, + sessionId: "session-id", + opt: []kms.Option{ + kms.WithRandomReader( + &kms.MockReader{ + WithBytesRead: 1, + WithMockReadOn: 1, + Reader: rand.Reader, + }, + ), + }, + wantErr: true, + wantErrMatch: kms.ErrGenKey, + wantErrContains: "wanted 32 bytes and got 1", + }, + { + name: "encrypt-bsr-err", + ctx: testCtx, + bsrWrapper: &kms.MockWrapper{ + Wrapper: testBsrKmsWrapper, + WithEncryptErrorOn: 1, + EncryptErr: fmt.Errorf("bsr-encrypt-err"), + }, + sessionId: "session-id", + wantErr: true, + wantErrMatch: kms.ErrEncrypt, + wantErrContains: "unable to encrypt bsr key", + }, + { + name: "ed25519.Generate-err", + ctx: testCtx, + bsrWrapper: testBsrKmsWrapper, + sessionId: "session-id", + opt: []kms.Option{ + kms.WithRandomReader( + &kms.MockReader{ + WithError: fmt.Errorf("rand err"), + WithBytesRead: 1, + WithMockReadOn: 2, + Reader: rand.Reader, + }, + ), + }, + wantErr: true, + wantErrMatch: kms.ErrGenKey, + wantErrContains: "unable to generate bsr ed25519 key-pair", + }, + { + name: "encrypt-priv-key-err", + ctx: testCtx, + bsrWrapper: &kms.MockWrapper{ + Wrapper: testBsrKmsWrapper, + WithEncryptErrorOn: 2, + EncryptErr: fmt.Errorf("encrypt-priv-key-err"), + }, + sessionId: "session-id", + wantErr: true, + wantErrMatch: kms.ErrEncrypt, + wantErrContains: "unable to encrypt bsr ed25519 priv key", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := kms.CreateKeys(tc.ctx, tc.bsrWrapper, tc.sessionId, tc.opt...) + if tc.wantErr { + require.Error(err) + assert.Empty(got) + if tc.wantErrMatch != nil { + assert.ErrorIsf(err, tc.wantErrMatch, "expected %q and got err: %+v", tc.wantErrMatch, err) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.NotNil(got) + + // check the WrappedBsrKey + assert.NotEmpty(got.WrappedBsrKey) + assert.NotEmpty(got.WrappedBsrKey.WrappedKey) + assert.Empty(got.WrappedBsrKey.Key) + assert.Equal(tc.sessionId, got.WrappedBsrKey.KeyId) + assert.Equal(wrapping.KeyType_Aes256, got.WrappedBsrKey.KeyType) + assert.Equal(wrapping.KeyEncoding_Bytes, got.WrappedBsrKey.KeyEncoding) + assert.Equal([]wrapping.KeyPurpose{wrapping.KeyPurpose_Sign, wrapping.KeyPurpose_Encrypt, wrapping.KeyPurpose_Decrypt}, got.WrappedBsrKey.KeyPurposes) + var bsrBlob wrapping.BlobInfo + require.NoError(proto.Unmarshal(got.WrappedBsrKey.WrappedKey, &bsrBlob)) + bsrKeyBytes, err := tc.bsrWrapper.Decrypt(testCtx, &bsrBlob) + require.NoError(err) + assert.NotEmpty(bsrKeyBytes) + assert.Len(bsrKeyBytes, bsrKey_Length) + + // check the WrappedPrivKey + assert.NotEmpty(got.WrappedPrivKey) + assert.NotEmpty(got.WrappedPrivKey.WrappedKey) + assert.Empty(got.WrappedPrivKey.Key) + assert.Equal(tc.sessionId, got.WrappedPrivKey.KeyId) + assert.Equal(wrapping.KeyType_Ed25519, got.WrappedPrivKey.KeyType) + assert.Equal(wrapping.KeyEncoding_Bytes, got.WrappedPrivKey.KeyEncoding) + assert.Equal([]wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, got.WrappedPrivKey.KeyPurposes) + var privKeyBlob wrapping.BlobInfo + require.NoError(proto.Unmarshal(got.WrappedPrivKey.WrappedKey, &privKeyBlob)) + privKeyBytes, err := tc.bsrWrapper.Decrypt(testCtx, &privKeyBlob) + require.NoError(err) + assert.NotEmpty(privKeyBytes) + assert.Len(privKeyBytes, ed25519_PrivKey_Length) + + // check the bsr key (unwrapped) + assert.NotEmpty(got.BsrKey) + assert.NotEmpty(got.BsrKey.Key) + assert.Empty(got.BsrKey.WrappedKey) + assert.Equal(tc.sessionId, got.BsrKey.KeyId) + assert.Equal(wrapping.KeyType_Aes256, got.BsrKey.KeyType) + assert.Equal(wrapping.KeyEncoding_Bytes, got.BsrKey.KeyEncoding) + assert.Equal([]wrapping.KeyPurpose{wrapping.KeyPurpose_Sign, wrapping.KeyPurpose_Encrypt, wrapping.KeyPurpose_Decrypt}, got.BsrKey.KeyPurposes) + assert.Len(got.BsrKey.Key, bsrKey_Length) + + // check the priv key (unwrapped) + assert.NotEmpty(got.PrivKey) + assert.NotEmpty(got.PrivKey.Key) + assert.Empty(got.PrivKey.WrappedKey) + assert.Equal(tc.sessionId, got.PrivKey.KeyId) + assert.Equal(wrapping.KeyType_Ed25519, got.PrivKey.KeyType) + assert.Equal(wrapping.KeyEncoding_Bytes, got.PrivKey.KeyEncoding) + assert.Equal([]wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, got.PrivKey.KeyPurposes) + assert.Len(got.PrivKey.Key, ed25519_PrivKey_Length) + + // check the pub key (unwrapped) + assert.NotEmpty(got.PubKey) + assert.NotEmpty(got.PubKey.Key) + assert.Empty(got.PubKey.WrappedKey) + assert.Equal(tc.sessionId, got.PubKey.KeyId) + assert.Equal(wrapping.KeyType_Ed25519, got.PubKey.KeyType) + assert.Equal(wrapping.KeyEncoding_Bytes, got.PubKey.KeyEncoding) + assert.Equal([]wrapping.KeyPurpose{wrapping.KeyPurpose_Verify}, got.PubKey.KeyPurposes) + assert.Len(got.PubKey.Key, ed25519_PubKey_Length) + + // check the pub key self-signature + // first we'll check it "by hand" + assert.NotEmpty(got.PubKeySelfSignature) + s, err := ed25519.NewSigner(testCtx, ed25519.WithPrivKey(stdLibEd25519.PrivateKey(got.PrivKey.Key))) + require.NoError(err) + testSig, err := s.Sign(testCtx, stdLibEd25519.PublicKey(got.PubKey.Key)) + require.NoError(err) + v, err := ed25519.NewVerifier(testCtx, ed25519.WithPubKey(stdLibEd25519.PublicKey(got.PubKey.Key)), wrapping.WithKeyId("session-id")) + require.NoError(err) + verified, err := v.Verify(testCtx, stdLibEd25519.PublicKey(got.PubKey.Key), testSig) + require.NoError(err) + assert.True(verified) + // check the keys match + privKey, err := got.UnwrapPrivKey(testCtx, tc.bsrWrapper) + require.NoError(err) + assert.Equal(got.PrivKey.Key, []byte(privKey)) + // now lets' check via the provided oracle + verified, err = got.VerifyPubKeySelfSignature(testCtx) + require.NoError(err) + assert.True(verified) + + // check the pub key bsr signature + // first we'll check it "by hand" + assert.NotEmpty(got.PubKeyBsrSignature) + bsrKeyWrapper := aead.NewWrapper() + _, err = bsrKeyWrapper.SetConfig(testCtx, wrapping.WithKeyId(got.WrappedBsrKey.KeyId)) + require.NoError(err) + err = bsrKeyWrapper.SetAesGcmKeyBytes(bsrKeyBytes) + require.NoError(err) + marshaledTestSigInfo, err := crypto.HmacSha256(testCtx, stdLibEd25519.PublicKey(got.PubKey.Key), bsrKeyWrapper, crypto.WithMarshaledSigInfo()) + require.NoError(err) + var testSigInfo wrapping.SigInfo + err = proto.Unmarshal([]byte(marshaledTestSigInfo), &testSigInfo) + require.NoError(err) + assert.Equal(&testSigInfo, got.PubKeyBsrSignature) + // do the wrappers match + unwrappedBsrKey, err := got.UnwrapBsrKey(testCtx, tc.bsrWrapper) + require.NoError(err) + assert.Equal(bsrKeyWrapper, unwrappedBsrKey) + // now let's check via the provided oracle using the plaintext + // got.PrivKey + verified, err = got.VerifyPubKeyBsrSignature(testCtx) + require.NoError(err) + assert.True(verified) + }) + } +} + +func TestBsrKeys_UnwrapBsrKey(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testBsrWrapper := kms.TestWrapper(t) + + testBytes := make([]byte, 32) + _, err := rand.Read(testBytes) + require.NoError(t, err) + testAeadWrapper := aead.NewWrapper() + _, err = testAeadWrapper.SetConfig(testCtx, wrapping.WithKeyId("session-id")) + require.NoError(t, err) + err = testAeadWrapper.SetAesGcmKeyBytes(testBytes) + require.NoError(t, err) + + bsrBlob, err := testBsrWrapper.Encrypt(testCtx, testBytes) + require.NoError(t, err) + marshaledBsrBlob, err := proto.Marshal(bsrBlob) + require.NoError(t, err) + + tests := []struct { + name string + bsrKeys *kms.Keys + bsrWrapper wrapping.Wrapper + want *aead.Wrapper + wantErr bool + wantErrMatch error + wantErrContains string + }{ + { + name: "success", + bsrWrapper: testBsrWrapper, + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: marshaledBsrBlob, + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign, wrapping.KeyPurpose_Encrypt, wrapping.KeyPurpose_Decrypt}, + }, + }, + want: testAeadWrapper, + }, + { + name: "nil-bsr-keys", + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "nil bsr keys", + }, + { + name: "missing-bsr-wrapper", + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: marshaledBsrBlob, + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign, wrapping.KeyPurpose_Encrypt, wrapping.KeyPurpose_Decrypt}, + }, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing bsr wrapper", + }, + { + name: "missing-wrapped-bsr-key", + bsrKeys: &kms.Keys{}, + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing wrapped bsr key", + }, + { + name: "missing-wrapped-bsr-key-id", + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + WrappedKey: marshaledBsrBlob, + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing wrapped bsr key id", + }, + { + name: "missing-wrapped-key-bytes", + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing wrapped bsr key bytes", + }, + { + name: "unexpected-key-type", + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: marshaledBsrBlob, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key type \"Unknown_KeyType\"; expected \"Aes256\"", + }, + { + name: "missing-key-encoding", + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: marshaledBsrBlob, + KeyType: wrapping.KeyType_Aes256, + }, + }, + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key encoding \"Unknown_KeyEncoding\"; expected \"Bytes\"", + }, + { + name: "blob-info-err", + bsrWrapper: testBsrWrapper, + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: []byte("not-blob-info"), + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + wantErr: true, + wantErrMatch: kms.ErrDecode, + wantErrContains: "error unmarshaling wrapped bsr key", + }, + { + name: "decrypt-err", + bsrWrapper: kms.TestWrapper(t), + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: marshaledBsrBlob, + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + wantErr: true, + wantErrMatch: kms.ErrDecrypt, + wantErrContains: "error decrypting wrapped bsr key", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.bsrKeys.UnwrapBsrKey(testCtx, tc.bsrWrapper) + if tc.wantErr { + require.Error(err) + assert.Empty(got) + if tc.wantErrMatch != nil { + assert.ErrorIsf(err, tc.wantErrMatch, "expected %q and got err: %+v", tc.wantErrMatch, err) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.NotNil(got) + assert.Equal(tc.want, got) + assert.Equal(&wrapping.KeyInfo{ + KeyId: "session-id", + Key: testBytes, + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign, wrapping.KeyPurpose_Encrypt, wrapping.KeyPurpose_Decrypt}, + }, tc.bsrKeys.BsrKey) + }) + } +} + +func TestBsrKeys_UnwrapPrivKey(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testBsrWrapper := kms.TestWrapper(t) + + _, priv, err := stdLibEd25519.GenerateKey(rand.Reader) + require.NoError(t, err) + bsrBlob, err := testBsrWrapper.Encrypt(testCtx, priv) + require.NoError(t, err) + marshaledBsrBlob, err := proto.Marshal(bsrBlob) + require.NoError(t, err) + + badBlob, err := testBsrWrapper.Encrypt(testCtx, []byte("bad-key-len")) + require.NoError(t, err) + marshaledBadBlob, err := proto.Marshal(badBlob) + require.NoError(t, err) + + tests := []struct { + name string + bsrKeys *kms.Keys + bsrWrapper wrapping.Wrapper + want stdLibEd25519.PrivateKey + wantErr bool + wantErrMatch error + wantErrContains string + }{ + { + name: "success", + bsrWrapper: testBsrWrapper, + bsrKeys: &kms.Keys{ + WrappedPrivKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: marshaledBsrBlob, + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + }, + }, + want: priv, + }, + { + name: "nil-bsr-keys", + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "nil bsr keys", + }, + { + name: "missing-bsr-wrapper", + bsrKeys: &kms.Keys{ + WrappedPrivKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: marshaledBsrBlob, + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing bsr wrapper", + }, + { + name: "missing-wrapped-priv-key", + bsrKeys: &kms.Keys{}, + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing wrapped priv key", + }, + { + name: "missing-wrapped-bsr-key-id", + bsrKeys: &kms.Keys{ + WrappedPrivKey: &wrapping.KeyInfo{ + WrappedKey: marshaledBsrBlob, + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing wrapped priv key id", + }, + { + name: "missing-wrapped-key-bytes", + bsrKeys: &kms.Keys{ + WrappedPrivKey: &wrapping.KeyInfo{ + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing wrapped priv key bytes", + }, + { + name: "missing-key-type", + bsrKeys: &kms.Keys{ + WrappedPrivKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: marshaledBsrBlob, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key type \"Unknown_KeyType\"; expected \"Ed25519\"", + }, + { + name: "missing-key-encoding", + bsrKeys: &kms.Keys{ + WrappedPrivKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: marshaledBsrBlob, + KeyType: wrapping.KeyType_Ed25519, + }, + }, + bsrWrapper: testBsrWrapper, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key encoding \"Unknown_KeyEncoding\"; expected \"Bytes\"", + }, + { + name: "blob-info-err", + bsrWrapper: testBsrWrapper, + bsrKeys: &kms.Keys{ + WrappedPrivKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: []byte("not-blob-info"), + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + wantErr: true, + wantErrMatch: kms.ErrDecode, + wantErrContains: "error unmarshaling wrapped priv key", + }, + { + name: "decrypt-err", + bsrWrapper: kms.TestWrapper(t), + bsrKeys: &kms.Keys{ + WrappedPrivKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: marshaledBsrBlob, + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + wantErr: true, + wantErrMatch: kms.ErrDecrypt, + wantErrContains: "error decrypting wrapped priv key", + }, + { + name: "invalid-key-len-err", + bsrWrapper: testBsrWrapper, + bsrKeys: &kms.Keys{ + WrappedPrivKey: &wrapping.KeyInfo{ + KeyId: "session-id", + WrappedKey: marshaledBadBlob, + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "priv key must be 64 bytes; got 11", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.bsrKeys.UnwrapPrivKey(testCtx, tc.bsrWrapper) + if tc.wantErr { + require.Error(err) + assert.Empty(got) + if tc.wantErrMatch != nil { + assert.ErrorIsf(err, tc.wantErrMatch, "expected %q and got err: %+v", tc.wantErrMatch, err) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.NotNil(got) + assert.Equal(tc.want, got) + assert.Equal(&wrapping.KeyInfo{ + KeyId: "session-id", + Key: priv, + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + }, tc.bsrKeys.PrivKey) + }) + } +} + +func TestBsrKeys_VerifyPubBsrSignature(t *testing.T) { + t.Parallel() + testCtx := context.Background() + testBsrWrapper := kms.TestWrapper(t) + + testBytes := make([]byte, 32) + _, err := rand.Read(testBytes) + require.NoError(t, err) + testAeadWrapper := aead.NewWrapper() + _, err = testAeadWrapper.SetConfig(testCtx, wrapping.WithKeyId("session-id")) + require.NoError(t, err) + err = testAeadWrapper.SetAesGcmKeyBytes(testBytes) + require.NoError(t, err) + + bsrBlob, err := testBsrWrapper.Encrypt(testCtx, testBytes) + require.NoError(t, err) + marshaledBsrBlob, err := proto.Marshal(bsrBlob) + require.NoError(t, err) + + pub, _, err := stdLibEd25519.GenerateKey(rand.Reader) + require.NoError(t, err) + sigStr, err := crypto.HmacSha256(testCtx, pub, testAeadWrapper, crypto.WithMarshaledSigInfo()) + require.NoError(t, err) + var testValidPubKeySignature wrapping.SigInfo + require.NoError(t, proto.Unmarshal([]byte(sigStr), &testValidPubKeySignature)) + + invalidPub, _, err := stdLibEd25519.GenerateKey(rand.Reader) + require.NoError(t, err) + sigStr, err = crypto.HmacSha256(testCtx, invalidPub, testAeadWrapper, crypto.WithMarshaledSigInfo()) + require.NoError(t, err) + var testInValidPubKeySignature wrapping.SigInfo + require.NoError(t, proto.Unmarshal([]byte(sigStr), &testInValidPubKeySignature)) + + tests := []struct { + name string + bsrKeys *kms.Keys + opt []kms.Option + want bool + wantErr bool + wantErrMatch error + wantErrContains string + }{ + { + name: "not-equal", + bsrKeys: &kms.Keys{ + BsrKey: &wrapping.KeyInfo{ + Key: testBytes, + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testInValidPubKeySignature, + }, + want: false, + }, + { + name: "success", + bsrKeys: &kms.Keys{ + BsrKey: &wrapping.KeyInfo{ + Key: testBytes, + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + want: true, + }, + { + name: "success-with-wrapper", + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + WrappedKey: marshaledBsrBlob, + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + opt: []kms.Option{kms.WithBsrWrapper(testBsrWrapper)}, + want: true, + }, + { + name: "nil-bsr-keys", + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "nil bsr keys", + }, + { + name: "missing-bsr-key-and-wrapped-bsr-key", + bsrKeys: &kms.Keys{ + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing bsr key and wrapped bsr key", + }, + { + name: "missing-bsr-key-bytes", + bsrKeys: &kms.Keys{ + BsrKey: &wrapping.KeyInfo{ + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing bsr key bytes", + }, + { + name: "missing-wrapped-bsr-key-bytes", + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + opt: []kms.Option{kms.WithBsrWrapper(testBsrWrapper)}, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing wrapped bsr key bytes", + }, + { + name: "missing-pub-key", + bsrKeys: &kms.Keys{ + BsrKey: &wrapping.KeyInfo{ + Key: testBytes, + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing pub key", + }, + { + name: "missing-signature", + bsrKeys: &kms.Keys{ + BsrKey: &wrapping.KeyInfo{ + Key: testBytes, + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing pub key signature", + }, + { + name: "bsr-key-invalid-key-type", + bsrKeys: &kms.Keys{ + BsrKey: &wrapping.KeyInfo{ + Key: testBytes, + KeyId: "session-id", + KeyType: wrapping.KeyType_EdsaP521, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key type \"EdsaP521\"; expected \"Aes256\"", + }, + { + name: "bsr-key-invalid-key-encoding", + bsrKeys: &kms.Keys{ + BsrKey: &wrapping.KeyInfo{ + Key: testBytes, + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Pkix, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key encoding \"Pkix\"; expected \"Bytes\"", + }, + { + name: "wrapped-bsr-key-missing-wrapper", + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + WrappedKey: marshaledBsrBlob, + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing bsr wrapper", + }, + { + name: "wrapped-bsr-key-invalid-key-type", + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + WrappedKey: marshaledBsrBlob, + KeyId: "session-id", + KeyType: wrapping.KeyType_Rsa4096, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + opt: []kms.Option{kms.WithBsrWrapper(testBsrWrapper)}, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key type \"Rsa4096\"; expected \"Aes256\"", + }, + { + name: "wrapped-bsr-key-invalid-key-encoding", + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + WrappedKey: marshaledBsrBlob, + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Pkix, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + opt: []kms.Option{kms.WithBsrWrapper(testBsrWrapper)}, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key encoding \"Pkix\"; expected \"Bytes\"", + }, + { + name: "wrapped-bsr-key-invalid-wrapper", + bsrKeys: &kms.Keys{ + WrappedBsrKey: &wrapping.KeyInfo{ + WrappedKey: marshaledBsrBlob, + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + opt: []kms.Option{kms.WithBsrWrapper(kms.TestWrapper(t))}, + wantErr: true, + wantErrMatch: kms.ErrDecrypt, + wantErrContains: "error unwrapping bsr key", + }, + { + name: "mismatched-key-id", + bsrKeys: &kms.Keys{ + BsrKey: &wrapping.KeyInfo{ + Key: testBytes, + KeyId: "mismatched-key-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeyBsrSignature: &testValidPubKeySignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "pub signature key id \"session-id\" doesn't match verifying key id \"mismatched-key-id\"", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.bsrKeys.VerifyPubKeyBsrSignature(testCtx, tc.opt...) + if tc.wantErr { + require.Error(err) + assert.Empty(got) + if tc.wantErrMatch != nil { + assert.ErrorIsf(err, tc.wantErrMatch, "expected %q and got err: %+v", tc.wantErrMatch, err) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} + +func TestBsrKeys_VerifyPubKeySelfSignature(t *testing.T) { + t.Parallel() + testCtx := context.Background() + pub, priv, err := stdLibEd25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + privKeySigner, err := ed25519.NewSigner(testCtx, ed25519.WithPrivKey(priv), wrapping.WithKeyId("session-id")) + require.NoError(t, err) + testPubKeySelfSignature, err := privKeySigner.Sign(testCtx, pub) + require.NoError(t, err) + testInvalidPubKeySelfSignature, err := privKeySigner.Sign(testCtx, []byte("not-the-pub-key")) + require.NoError(t, err) + + tests := []struct { + name string + bsrKeys *kms.Keys + opt []kms.Option + want bool + wantErr bool + wantErrMatch error + wantErrContains string + }{ + { + name: "not-equal", + bsrKeys: &kms.Keys{ + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeySelfSignature: testInvalidPubKeySelfSignature, + }, + want: false, + }, + { + name: "success", + bsrKeys: &kms.Keys{ + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeySelfSignature: testPubKeySelfSignature, + }, + want: true, + }, + { + name: "missing-pub-key", + bsrKeys: &kms.Keys{ + PubKeySelfSignature: testPubKeySelfSignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing pub key", + }, + + { + name: "missing-pub-key-bytes", + bsrKeys: &kms.Keys{ + PubKey: &wrapping.KeyInfo{ + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeySelfSignature: testPubKeySelfSignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing pub key bytes", + }, + { + name: "invalid-key-type", + bsrKeys: &kms.Keys{ + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Aes256, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeySelfSignature: testPubKeySelfSignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key type \"Aes256\"; expected \"Ed25519\"", + }, + { + name: "invalid-key-encoding", + bsrKeys: &kms.Keys{ + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Pkcs8, + }, + PubKeySelfSignature: testPubKeySelfSignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key encoding \"Pkcs8\"; expected \"Bytes\"", + }, + { + name: "missing-signature", + bsrKeys: &kms.Keys{ + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing pub key self signature", + }, + { + name: "mismatched-key-id", + bsrKeys: &kms.Keys{ + PubKey: &wrapping.KeyInfo{ + Key: pub, + KeyId: "mismatched-key-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeySelfSignature: testPubKeySelfSignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "pub self signature key id \"session-id\" doesn't match verifying pub key id \"mismatched-key-id\"", + }, + { + name: "bad-pub-key", + bsrKeys: &kms.Keys{ + PubKey: &wrapping.KeyInfo{ + Key: []byte("bad-pub-key"), + KeyId: "session-id", + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + }, + PubKeySelfSignature: testPubKeySelfSignature, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "expected public key with 32 bytes and got 11", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.bsrKeys.VerifyPubKeySelfSignature(testCtx) + if tc.wantErr { + require.Error(err) + assert.Empty(got) + if tc.wantErrMatch != nil { + assert.ErrorIsf(err, tc.wantErrMatch, "expected %q and got err: %+v", tc.wantErrMatch, err) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} + +func TestBsrKeys_SignWithPrivKey(t *testing.T) { + t.Parallel() + testCtx := context.Background() + + _, priv, err := stdLibEd25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + testMsg := []byte("test-msg") + testSig, err := priv.Sign(nil, testMsg, stdcrypto.Hash(0)) + require.NoError(t, err) + + tests := []struct { + name string + bsrKeys *kms.Keys + msg []byte + want *wrapping.SigInfo + wantErr bool + wantErrMatch error + wantErrContains string + }{ + { + name: "success", + bsrKeys: &kms.Keys{ + PrivKey: &wrapping.KeyInfo{ + Key: priv, + KeyType: wrapping.KeyType_Ed25519, + KeyId: "session-id", + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + }, + }, + msg: testMsg, + want: &wrapping.SigInfo{ + KeyInfo: &wrapping.KeyInfo{ + KeyType: wrapping.KeyType_Ed25519, + KeyId: "session-id", + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + }, + Signature: testSig, + }, + }, + { + name: "missing-msg", + bsrKeys: &kms.Keys{ + PrivKey: &wrapping.KeyInfo{ + Key: priv, + KeyType: wrapping.KeyType_Ed25519, + KeyId: "session-id", + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + }, + }, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing msg", + }, + { + name: "missing-priv-key", + bsrKeys: &kms.Keys{}, + msg: testMsg, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing priv key", + }, + { + name: "missing-priv-key-id", + bsrKeys: &kms.Keys{ + PrivKey: &wrapping.KeyInfo{ + Key: priv, + KeyType: wrapping.KeyType_Ed25519, + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + }, + }, + msg: testMsg, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing priv key id", + }, + { + name: "missing-key-bytes", + bsrKeys: &kms.Keys{ + PrivKey: &wrapping.KeyInfo{ + KeyType: wrapping.KeyType_Ed25519, + KeyId: "session-id", + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + }, + }, + msg: testMsg, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "missing priv key bytes", + }, + { + name: "invalid-key-type", + bsrKeys: &kms.Keys{ + PrivKey: &wrapping.KeyInfo{ + Key: priv, + KeyId: "session-id", + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + }, + }, + msg: testMsg, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key type \"Unknown_KeyType\"; expected \"Ed25519\"", + }, + { + name: "invalid-key-encoding", + bsrKeys: &kms.Keys{ + PrivKey: &wrapping.KeyInfo{ + Key: priv, + KeyType: wrapping.KeyType_Ed25519, + KeyId: "session-id", + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + }, + }, + msg: testMsg, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "unexpected key encoding \"Unknown_KeyEncoding\"; expected \"Bytes\"", + }, + { + name: "invalid-key-len", + bsrKeys: &kms.Keys{ + PrivKey: &wrapping.KeyInfo{ + Key: []byte("invalid-len"), + KeyType: wrapping.KeyType_Ed25519, + KeyId: "session-id", + KeyEncoding: wrapping.KeyEncoding_Bytes, + KeyPurposes: []wrapping.KeyPurpose{wrapping.KeyPurpose_Sign}, + }, + }, + msg: testMsg, + wantErr: true, + wantErrMatch: kms.ErrInvalidParameter, + wantErrContains: "expected priv key with 64 bytes and got 11", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := tc.bsrKeys.SignWithPrivKey(testCtx, tc.msg) + if tc.wantErr { + require.Error(err) + assert.Empty(got) + if tc.wantErrMatch != nil { + assert.ErrorIsf(err, tc.wantErrMatch, "expected %q and got err: %+v", tc.wantErrMatch, err) + } + if tc.wantErrContains != "" { + assert.Contains(err.Error(), tc.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tc.want, got) + }) + } +} diff --git a/internal/bsr/kms/option_test.go b/internal/bsr/kms/option_test.go new file mode 100644 index 000000000..e9f845d0c --- /dev/null +++ b/internal/bsr/kms/option_test.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package kms + +import ( + "crypto/rand" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test_GetOpts provides unit tests for GetOpts and all the options +func Test_GetOpts(t *testing.T) { + t.Parallel() + + t.Run("WithRandomReader", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithRandomReader(rand.Reader)) + testOpts := getDefaultOptions() + testOpts.withRandomReader = rand.Reader + assert.Equal(opts, testOpts) + }) +} diff --git a/internal/bsr/kms/options.go b/internal/bsr/kms/options.go new file mode 100644 index 000000000..c81f78ade --- /dev/null +++ b/internal/bsr/kms/options.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package kms + +import ( + "io" + + wrapping "github.com/hashicorp/go-kms-wrapping/v2" +) + +// getOpts - iterate the inbound Options and return a struct +func getOpts(opt ...Option) options { + opts := getDefaultOptions() + for _, o := range opt { + o(&opts) + } + return opts +} + +// Option - how Options are passed as arguments +type Option func(*options) + +// options = how options are represented +type options struct { + withBsrWrapper wrapping.Wrapper + withRandomReader io.Reader + withAlreadyLocked bool +} + +func getDefaultOptions() options { + return options{} +} + +// WithBsrWrapper sets the external Bsr wrapper for a KMS +func WithBsrWrapper(w wrapping.Wrapper) Option { + return func(o *options) { + o.withBsrWrapper = w + } +} + +// WithRandomReader(...) option allows an optional random reader to be +// provided. By default the reader from crypto/rand will be used. +func WithRandomReader(randomReader io.Reader) Option { + return func(o *options) { + o.withRandomReader = randomReader + } +} + +// not exported... just an internal option +func withAlreadyLocked(isLockedAlready bool) Option { + return func(o *options) { + o.withAlreadyLocked = isLockedAlready + } +} diff --git a/internal/bsr/kms/testing.go b/internal/bsr/kms/testing.go new file mode 100644 index 000000000..13f3ab299 --- /dev/null +++ b/internal/bsr/kms/testing.go @@ -0,0 +1,135 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package kms + +import ( + "context" + "crypto/rand" + "encoding/base64" + "io" + "testing" + + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/go-kms-wrapping/v2/aead" +) + +// TestWrapper initializes an AEAD wrapping.Wrapper for testing +func TestWrapper(t testing.TB) wrapping.Wrapper { + t.Helper() + rootKey := make([]byte, 32) + n, err := rand.Read(rootKey) + if err != nil { + t.Fatal(err) + } + if n != 32 { + t.Fatal(n) + } + root := aead.NewWrapper() + _, err = root.SetConfig(context.Background(), wrapping.WithKeyId(base64.StdEncoding.EncodeToString(rootKey))) + if err != nil { + t.Fatal(err) + } + if err := root.SetAesGcmKeyBytes(rootKey); err != nil { + t.Fatal(err) + } + return root +} + +// MockReader provides a mock reader for testing +type MockReader struct { + readCount int + + // WithMockReadOn determines which read attempt the mock read results should + // be returned on. + WithMockReadOn int + + // WithError specifies a mock read result of the specified error + WithError error + + // WithBytesRead specifies a mock read result of the specified bytes read + WithBytesRead int + + // Reader is the underlying reader + Reader io.Reader +} + +// Read implements the mock read operation. Mock values supported: WithBytesRead, WithMockReadOn +func (m *MockReader) Read(p []byte) (n int, err error) { + m.readCount++ + if m.readCount == m.WithMockReadOn { + _, _ = m.Reader.Read(p) + return m.WithBytesRead, m.WithError + } + return m.Reader.Read(p) +} + +type MockWrapper struct { + // Wrapper is the underlying wrapping.Wrapper which is used to provide the + // mock's default behavior + Wrapper wrapping.Wrapper + + encryptCount int + + // WithEncryptErrorOn determines which encrypt attempt the mock encrypt + // error should be returned on. + WithEncryptErrorOn int + // EncryptErr is a mock value to return for the Encrypt(...) operation + EncryptErr error + + // DecryptErr is a mock value to return for the Decrypt(...) operation + DecryptErr error + + // KeyIdErr is a mock value to return for the KeyId(...) operation + KeyIdErr error + + // KeyIdReturned is a mock value to return for the KeyId(...) operation + KeyIdReturned string + + keyBytesCount int + // WithKeyBytesErrorOn determines which key bytes attempt the mock key bytes + // error should be returned on. + WithKeyBytesErrorOn int +} + +// Type of the wrapper. No mock values supported +func (w *MockWrapper) Type(ctx context.Context) (wrapping.WrapperType, error) { + return w.Wrapper.Type(ctx) +} + +// KeyId is the id of the key currently used for encryption operations. Mock +// values supported: KeyIdErr, KeyIdReturned +func (w *MockWrapper) KeyId(ctx context.Context) (string, error) { // nolint + switch { + case w.KeyIdErr != nil: + return "", w.KeyIdErr + case w.KeyIdReturned != "": + return w.KeyIdReturned, nil + default: + return w.Wrapper.KeyId(ctx) + } +} + +// SetConfig applies the given options to a wrapper and returns +// configuration information. No mock values supported. +func (w *MockWrapper) SetConfig(ctx context.Context, options ...wrapping.Option) (*wrapping.WrapperConfig, error) { + return w.Wrapper.SetConfig(ctx, options...) +} + +// Decrypt decrypts the given byte slice and stores the resulting information in +// the returned byte slice. Mock values supported: DecryptErr +func (w *MockWrapper) Decrypt(ctx context.Context, ciphertext *wrapping.BlobInfo, options ...wrapping.Option) ([]byte, error) { + if w.DecryptErr != nil { + return nil, w.DecryptErr + } + return w.Wrapper.Decrypt(ctx, ciphertext, options...) +} + +// Encrypt encrypts the given byte slice. Mock values supported: EncryptErr and WithEncryptErrorOn +func (w *MockWrapper) Encrypt(ctx context.Context, plaintext []byte, options ...wrapping.Option) (*wrapping.BlobInfo, error) { + w.encryptCount++ + if w.encryptCount == w.WithEncryptErrorOn { + return nil, w.EncryptErr + } + return w.Wrapper.Encrypt(ctx, plaintext, options...) +} diff --git a/internal/errors/code.go b/internal/errors/code.go index 4d94f35c6..8a557384e 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -77,6 +77,8 @@ const ( Decode Code = 303 // Decode represents an error occurred during the underlying decoding/unmarshaling process GenKey Code = 304 // GenKey represents an error occurred during the underlying key generation process GenCert Code = 305 // GenCert represents an error occurred during the underlying certificate generation process + Sign Code = 306 // Sign represents an error occurred during the underlying signing process + Verify Code = 307 // Verify represents an error occurred during the underlying verification process // General system errors are reserved Codes 400-599 and align with http // client and server error codes diff --git a/internal/errors/info.go b/internal/errors/info.go index cc80832a6..602e31b31 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -143,6 +143,14 @@ var errorCodeInfo = map[Code]Info{ Message: "error occurred during certification generation", Kind: Encryption, }, + Sign: { + Message: "error occurred during signing", + Kind: Encryption, + }, + Verify: { + Message: "error occurred during verification", + Kind: Encryption, + }, Internal: { Message: "internal error", Kind: Other,