mirror of https://github.com/hashicorp/boundary
refactor: Move functions from kms pkg to new libs/crypto pkg (#1650)
parent
a86ee59bcd
commit
dd2c3807cd
@ -0,0 +1,50 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/go-kms-wrapping/wrappers/aead"
|
||||
"github.com/hashicorp/go-kms-wrapping/wrappers/multiwrapper"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// DerivedReader returns a reader from which keys can be read, using the
|
||||
// given wrapper, reader length limit, salt and context info. Salt and info can
|
||||
// be nil.
|
||||
//
|
||||
// Example:
|
||||
// reader, _ := NewDerivedReader(wrapper, userId, jobId)
|
||||
// key := ed25519.GenerateKey(reader)
|
||||
func NewDerivedReader(wrapper wrapping.Wrapper, lenLimit int64, salt, info []byte) (*io.LimitedReader, error) {
|
||||
const op = "crypto.NewDerivedReader"
|
||||
if wrapper == nil {
|
||||
return nil, fmt.Errorf("%s: missing wrapper: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
if lenLimit < 20 {
|
||||
return nil, fmt.Errorf("%s: lenLimit must be >= 20: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
var aeadWrapper *aead.Wrapper
|
||||
switch w := wrapper.(type) {
|
||||
case *multiwrapper.MultiWrapper:
|
||||
raw := w.WrapperForKeyID("__base__")
|
||||
var ok bool
|
||||
if aeadWrapper, ok = raw.(*aead.Wrapper); !ok {
|
||||
return nil, fmt.Errorf("%s: unexpected wrapper type from multiwrapper base: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
case *aead.Wrapper:
|
||||
if w.GetKeyBytes() == nil {
|
||||
return nil, fmt.Errorf("%s: aead wrapper missing bytes: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
aeadWrapper = w
|
||||
default:
|
||||
return nil, fmt.Errorf("%s: unknown wrapper type: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
reader := hkdf.New(sha256.New, aeadWrapper.GetKeyBytes(), salt, info)
|
||||
return &io.LimitedReader{
|
||||
R: reader,
|
||||
N: lenLimit,
|
||||
}, nil
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/go-kms-wrapping/wrappers/aead"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
func TestNewDerivedReader(t *testing.T) {
|
||||
wrapper := TestWrapper(t)
|
||||
|
||||
type args struct {
|
||||
wrapper wrapping.Wrapper
|
||||
lenLimit int64
|
||||
salt []byte
|
||||
info []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *io.LimitedReader
|
||||
wantErr bool
|
||||
wantErrCode error
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid-with-salt",
|
||||
args: args{
|
||||
wrapper: wrapper,
|
||||
lenLimit: 32,
|
||||
info: nil,
|
||||
salt: []byte("salt"),
|
||||
},
|
||||
want: &io.LimitedReader{
|
||||
R: hkdf.New(sha256.New, wrapper.(*aead.Wrapper).GetKeyBytes(), []byte("salt"), nil),
|
||||
N: 32,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid-with-salt-info",
|
||||
args: args{
|
||||
wrapper: wrapper,
|
||||
lenLimit: 32,
|
||||
info: []byte("info"),
|
||||
salt: []byte("salt"),
|
||||
},
|
||||
want: &io.LimitedReader{
|
||||
R: hkdf.New(sha256.New, wrapper.(*aead.Wrapper).GetKeyBytes(), []byte("salt"), []byte("info")),
|
||||
N: 32,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil-wrapper",
|
||||
args: args{
|
||||
wrapper: nil,
|
||||
lenLimit: 10,
|
||||
info: []byte("info"),
|
||||
salt: []byte("salt"),
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: ErrInvalidParameter,
|
||||
wantErrContains: "missing wrapper",
|
||||
},
|
||||
{
|
||||
name: "too-short",
|
||||
args: args{
|
||||
wrapper: wrapper,
|
||||
lenLimit: 10,
|
||||
info: []byte("info"),
|
||||
salt: []byte("salt"),
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: ErrInvalidParameter,
|
||||
wantErrContains: "lenLimit must be >= 20",
|
||||
},
|
||||
{
|
||||
name: "wrapper-with-no-bytes",
|
||||
args: args{
|
||||
wrapper: &aead.Wrapper{},
|
||||
lenLimit: 32,
|
||||
info: nil,
|
||||
salt: []byte("salt"),
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: ErrInvalidParameter,
|
||||
wantErrContains: "missing bytes",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewDerivedReader(tt.args.wrapper, tt.args.lenLimit, tt.args.salt, tt.args.info)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
assert.ErrorIsf(err, tt.wantErrCode, "unexpected error: %s", err)
|
||||
if tt.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tt.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidParameter = errors.New("Invalid parameter")
|
||||
)
|
||||
@ -0,0 +1,94 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
// HmacSha256WithPrk will HmacSha256 using the provided prk. See HmacSha256 for
|
||||
// options supported.
|
||||
func HmacSha256WithPrk(ctx context.Context, data, prk []byte, opt ...Option) (string, error) {
|
||||
opt = append(opt, WithPrk(prk))
|
||||
return HmacSha256(ctx, data, nil, nil, nil, opt...)
|
||||
}
|
||||
|
||||
// HmacSha256 the provided data. Supports WithPrefix, WithEd25519 and WithPrk
|
||||
// options. WithEd25519 is a "legacy" way to complete this operation and should
|
||||
// not be used in new operations unless backward compatibility is needed. The
|
||||
// WithPrefix option will prepend the prefix to the hmac-sha256 value.
|
||||
func HmacSha256(ctx context.Context, data []byte, cipher wrapping.Wrapper, salt, info []byte, opt ...Option) (string, error) {
|
||||
const op = "crypto.HmacSha256"
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: unable to get options: %w", op, err)
|
||||
}
|
||||
if data == nil {
|
||||
return "", fmt.Errorf("%s: missing data: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
if cipher == nil && opts.withPrk == nil {
|
||||
return "", fmt.Errorf("%s: you must specify either a wrapper or prk: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
if cipher != nil && opts.withPrk != nil {
|
||||
return "", fmt.Errorf("%s: you cannot specify both a wrapper or prk: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
if opts.withEd25519 && opts.withPrk != nil {
|
||||
return "", fmt.Errorf("%s: you cannot specify both ed25519 and a prk: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
var key [32]byte
|
||||
switch {
|
||||
case opts.withPrk != nil:
|
||||
key = blake2b.Sum256(opts.withPrk)
|
||||
|
||||
case opts.withEd25519:
|
||||
reader, err := NewDerivedReader(cipher, 32, salt, info)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
edKey, _, err := ed25519.GenerateKey(reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: unable to generate derived key: %w", op, ErrInvalidParameter)
|
||||
}
|
||||
n := copy(key[:], edKey)
|
||||
if n != 32 {
|
||||
return "", fmt.Errorf("%s: expected to copy 32 bytes and got: %d", op, n)
|
||||
}
|
||||
|
||||
default:
|
||||
reader, err := NewDerivedReader(cipher, 32, salt, info)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
readerKey := make([]byte, 32)
|
||||
n, err := io.ReadFull(reader, readerKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", op, err)
|
||||
}
|
||||
if n != 32 {
|
||||
return "", fmt.Errorf("%s: expected to read 32 bytes and got: %d", op, n)
|
||||
}
|
||||
key = blake2b.Sum256(readerKey)
|
||||
}
|
||||
mac := hmac.New(sha256.New, key[:])
|
||||
_, _ = mac.Write(data)
|
||||
hmac := mac.Sum(nil)
|
||||
|
||||
var hmacString string
|
||||
switch opts.withBase64Encoding {
|
||||
case true:
|
||||
hmacString = base64.RawURLEncoding.EncodeToString(hmac)
|
||||
case false:
|
||||
hmacString = string(hmac)
|
||||
}
|
||||
if opts.withPrefix != "" {
|
||||
return opts.withPrefix + hmacString, nil
|
||||
}
|
||||
return hmacString, nil
|
||||
}
|
||||
@ -0,0 +1,168 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
func Test_HmacSha256(t *testing.T) {
|
||||
testCtx := context.Background()
|
||||
testWrapper := TestWrapper(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
wrapper wrapping.Wrapper
|
||||
salt []byte
|
||||
info []byte
|
||||
opts []Option
|
||||
wantHmac string
|
||||
wantErr bool
|
||||
wantErrIs error
|
||||
}{
|
||||
{
|
||||
name: "missing data",
|
||||
wrapper: testWrapper,
|
||||
wantErr: true,
|
||||
wantErrIs: ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "missing wrapper",
|
||||
data: []byte("test"),
|
||||
wantErr: true,
|
||||
wantErrIs: ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "prk-and-ed25519",
|
||||
data: []byte("test"),
|
||||
wrapper: testWrapper,
|
||||
opts: []Option{WithPrk([]byte("prk")), WithEd25519()},
|
||||
wantErr: true,
|
||||
wantErrIs: ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "prk-and-wrapper",
|
||||
data: []byte("test"),
|
||||
wrapper: testWrapper,
|
||||
opts: []Option{WithPrk([]byte("prk")), WithEd25519()},
|
||||
wantErr: true,
|
||||
wantErrIs: ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "blake2b-with-prefix",
|
||||
data: []byte("test"),
|
||||
wrapper: testWrapper,
|
||||
opts: []Option{WithPrefix("prefix:")},
|
||||
wantHmac: testWithBlake2b(t, []byte("test"), testWrapper, nil, nil, WithPrefix("prefix:")),
|
||||
},
|
||||
{
|
||||
name: "blake2b-with-prefix-with-bas64",
|
||||
data: []byte("test"),
|
||||
wrapper: testWrapper,
|
||||
opts: []Option{WithPrefix("prefix:"), WithBase64Encoding()},
|
||||
wantHmac: testWithBlake2b(t, []byte("test"), testWrapper, nil, nil, WithPrefix("prefix:"), WithBase64Encoding()),
|
||||
},
|
||||
{
|
||||
name: "with-prk",
|
||||
data: []byte("test"),
|
||||
opts: []Option{WithPrk([]byte("prk-0123456789012345678901234567890"))},
|
||||
wantHmac: testWithBlake2b(t, []byte("test"), testWrapper, nil, nil, WithPrk([]byte("prk-0123456789012345678901234567890"))),
|
||||
},
|
||||
{
|
||||
name: "withEd25519",
|
||||
data: []byte("test"),
|
||||
wrapper: testWrapper,
|
||||
opts: []Option{WithEd25519()},
|
||||
wantHmac: testWithEd25519(t, []byte("test"), testWrapper, nil, nil),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
hm, err := HmacSha256(testCtx, tt.data, tt.wrapper, tt.salt, tt.info, tt.opts...)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
if tt.wantErrIs != nil {
|
||||
assert.ErrorIs(err, tt.wantErrIs)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tt.wantHmac, hm)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("HmacSha256WithPrk", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
hm, err := HmacSha256WithPrk(testCtx, []byte("test"), []byte("prk-0123456789012345678901234567890"))
|
||||
require.NoError(err)
|
||||
want := testWithBlake2b(t, []byte("test"), testWrapper, nil, nil, WithPrk([]byte("prk-0123456789012345678901234567890")))
|
||||
assert.Equal(want, hm)
|
||||
})
|
||||
}
|
||||
|
||||
func testWithEd25519(t *testing.T, data []byte, w wrapping.Wrapper, salt, info []byte, opt ...Option) string {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
reader, err := NewDerivedReader(w, 32, salt, info)
|
||||
require.NoError(err)
|
||||
edKey, _, err := ed25519.GenerateKey(reader)
|
||||
require.NoError(err)
|
||||
var key [32]byte
|
||||
n := copy(key[:], edKey)
|
||||
require.Equal(n, 32)
|
||||
return testHmac(t, key[:], data, opt...)
|
||||
}
|
||||
|
||||
func testWithBlake2b(t *testing.T, data []byte, w wrapping.Wrapper, salt, info []byte, opt ...Option) string {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
require.NotNil(data)
|
||||
require.NotNil(w)
|
||||
opts, err := getOpts(opt...)
|
||||
require.NoError(err)
|
||||
var key [32]byte
|
||||
switch {
|
||||
case opts.withPrk != nil:
|
||||
key = blake2b.Sum256(opts.withPrk)
|
||||
default:
|
||||
reader, err := NewDerivedReader(w, 32, salt, info)
|
||||
require.NoError(err)
|
||||
readerKey := make([]byte, 32)
|
||||
n, err := io.ReadFull(reader, readerKey)
|
||||
require.NoError(err)
|
||||
require.Equal(n, 32)
|
||||
key = blake2b.Sum256(readerKey)
|
||||
}
|
||||
return testHmac(t, key[:], data, opt...)
|
||||
}
|
||||
|
||||
func testHmac(t *testing.T, key, data []byte, opt ...Option) string {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
mac := hmac.New(sha256.New, key)
|
||||
_, _ = mac.Write(data)
|
||||
hmac := mac.Sum(nil)
|
||||
var hmacString string
|
||||
opts, err := getOpts(opt...)
|
||||
require.NoError(err)
|
||||
switch opts.withBase64Encoding {
|
||||
case true:
|
||||
hmacString = base64.RawURLEncoding.EncodeToString(hmac)
|
||||
case false:
|
||||
hmacString = string(hmac)
|
||||
}
|
||||
if opts.withPrefix != "" {
|
||||
return opts.withPrefix + hmacString
|
||||
}
|
||||
return hmacString
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package crypto
|
||||
|
||||
// getOpts - iterate the inbound Options and return a struct.
|
||||
func getOpts(opt ...Option) (*options, error) {
|
||||
opts := getDefaultOptions()
|
||||
for _, o := range opt {
|
||||
if o != nil {
|
||||
if err := o(opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// Option - how Options are passed as arguments.
|
||||
type Option func(*options) error
|
||||
|
||||
// options = how options are represented
|
||||
type options struct {
|
||||
withPrefix string
|
||||
withPrk []byte
|
||||
withEd25519 bool
|
||||
withBase64Encoding bool
|
||||
}
|
||||
|
||||
func getDefaultOptions() *options {
|
||||
return &options{}
|
||||
}
|
||||
|
||||
// WithPrefix allows an optional prefix to be specified for the data returned
|
||||
func WithPrefix(prefix string) Option {
|
||||
return func(o *options) error {
|
||||
o.withPrefix = prefix
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPrk allows an optional PRK (pseudorandom key) to be specified for an
|
||||
// operation. If you're using this option with HmacSha256, you might consider
|
||||
// using HmacSha256WithPrk instead.
|
||||
func WithPrk(prk []byte) Option {
|
||||
return func(o *options) error {
|
||||
o.withPrk = prk
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEd25519 allows an optional request to use ed25519 during the operation
|
||||
func WithEd25519() Option {
|
||||
return func(o *options) error {
|
||||
o.withEd25519 = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithBase64Encoding allows an optional request to base64 encode the data returned
|
||||
func WithBase64Encoding() Option {
|
||||
return func(o *options) error {
|
||||
o.withBase64Encoding = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Test_GetOpts provides unit tests for GetOpts and all the options
|
||||
func Test_GetOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("WithPrefix", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
opts, err := getOpts(WithPrefix("test"))
|
||||
require.NoError(err)
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withPrefix = "test"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithPrk", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
opts, err := getOpts(WithPrk([]byte("test")))
|
||||
require.NoError(err)
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withPrk = []byte("test")
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithEd25519", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
opts, err := getOpts(WithEd25519())
|
||||
require.NoError(err)
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withEd25519 = true
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithBase64Encoding", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
opts, err := getOpts(WithBase64Encoding())
|
||||
require.NoError(err)
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withBase64Encoding = true
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/go-kms-wrapping/wrappers/aead"
|
||||
)
|
||||
|
||||
// TestWrapper initializes an AEAD wrapping.Wrapper for testing
|
||||
func TestWrapper(t *testing.T) wrapping.Wrapper {
|
||||
rootKey := make([]byte, 32)
|
||||
n, err := rand.Read(rootKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 32 {
|
||||
t.Fatal(n)
|
||||
}
|
||||
root := aead.NewWrapper(nil)
|
||||
_, err = root.SetConfig(map[string]string{
|
||||
"key_id": base64.StdEncoding.EncodeToString(rootKey),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := root.SetAESGCMKeyBytes(rootKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return root
|
||||
}
|
||||
Loading…
Reference in new issue