diff --git a/internal/auth/oidc/auth_method.go b/internal/auth/oidc/auth_method.go index 1f2417424f..de519a68f0 100644 --- a/internal/auth/oidc/auth_method.go +++ b/internal/auth/oidc/auth_method.go @@ -2,10 +2,6 @@ package oidc import ( "context" - "crypto/ed25519" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" "fmt" "net/url" "sort" @@ -13,7 +9,7 @@ import ( "github.com/hashicorp/boundary/internal/auth/oidc/store" "github.com/hashicorp/boundary/internal/errors" - "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/libs/crypto" "github.com/hashicorp/boundary/internal/oplog" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/structwrapping" @@ -236,17 +232,14 @@ func (a *AuthMethod) hmacClientSecret(ctx context.Context, cipher wrapping.Wrapp if cipher == nil { return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") } - reader, err := kms.NewDerivedReader(cipher, 32, []byte(a.PublicId), nil) + // this operation currently uses the legacy WithEd25519 option for hmac'ing. + // we should likely deprecate this and introduce a new "crypto version" of + // this attribute. + hm, err := crypto.HmacSha256(ctx, []byte(a.ClientSecret), cipher, []byte(a.PublicId), nil, crypto.WithBase64Encoding(), crypto.WithEd25519()) if err != nil { - return errors.Wrap(ctx, err, op) - } - key, _, err := ed25519.GenerateKey(reader) - if err != nil { - return errors.New(ctx, errors.Encrypt, op, "unable to generate derived key") + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Code(errors.Encryption))) } - mac := hmac.New(sha256.New, key) - _, _ = mac.Write([]byte(a.ClientSecret)) - a.ClientSecretHmac = base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) + a.ClientSecretHmac = hm return nil } diff --git a/internal/auth/oidc/service.go b/internal/auth/oidc/service.go index 568668a624..e3a074258f 100644 --- a/internal/auth/oidc/service.go +++ b/internal/auth/oidc/service.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/libs/crypto" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/wrappers/aead" @@ -189,7 +190,7 @@ func requestWrappingWrapper(ctx context.Context, k *kms.Kms, scopeId, authMethod } // okay, I guess we need to derive a new key for this combo of oidcWrapper and authMethod - reader, err := kms.NewDerivedReader(oidcWrapper, 32, []byte(authMethodId), []byte(scopeId)) + reader, err := crypto.NewDerivedReader(oidcWrapper, 32, []byte(authMethodId), []byte(scopeId)) if err != nil { return nil, errors.Wrap(ctx, err, op) } diff --git a/internal/credential/vault/client_certificate.go b/internal/credential/vault/client_certificate.go index b368c7d723..345f97c808 100644 --- a/internal/credential/vault/client_certificate.go +++ b/internal/credential/vault/client_certificate.go @@ -2,15 +2,12 @@ package vault import ( "context" - "crypto/ed25519" - "crypto/hmac" - "crypto/sha256" "database/sql" "github.com/hashicorp/boundary/internal/credential/vault/store" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" - "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/libs/crypto" "github.com/hashicorp/boundary/internal/oplog" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/structwrapping" @@ -103,17 +100,14 @@ func (c *ClientCertificate) hmacCertificateKey(ctx context.Context, cipher wrapp if cipher == nil { return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") } - reader, err := kms.NewDerivedReader(cipher, 32, []byte(c.StoreId), nil) + // this operation currently uses the legacy WithEd25519 option for hmac'ing. + // we should likely deprecate this and introduce a new "crypto version" of + // this attribute. + hm, err := crypto.HmacSha256(ctx, c.CertificateKey, cipher, []byte(c.StoreId), nil, crypto.WithEd25519()) if err != nil { return errors.Wrap(ctx, err, op) } - key, _, err := ed25519.GenerateKey(reader) - if err != nil { - return errors.New(ctx, errors.Encrypt, op, "unable to generate derived key") - } - mac := hmac.New(sha256.New, key) - _, _ = mac.Write(c.CertificateKey) - c.CertificateKeyHmac = mac.Sum(nil) + c.CertificateKeyHmac = []byte(hm) return nil } diff --git a/internal/credential/vault/vault_token.go b/internal/credential/vault/vault_token.go index 5e7321f65e..c653ec751c 100644 --- a/internal/credential/vault/vault_token.go +++ b/internal/credential/vault/vault_token.go @@ -2,18 +2,16 @@ package vault import ( "context" - "crypto/hmac" - "crypto/sha256" "database/sql" "time" "github.com/hashicorp/boundary/internal/credential/vault/store" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/libs/crypto" "github.com/hashicorp/boundary/internal/oplog" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/structwrapping" - "golang.org/x/crypto/blake2b" "google.golang.org/protobuf/proto" ) @@ -74,16 +72,15 @@ func newToken(storeId string, token TokenSecret, accessor []byte, expiration tim accessorCopy := make([]byte, len(accessor)) copy(accessorCopy, accessor) - key := blake2b.Sum256(accessorCopy) - mac := hmac.New(sha256.New, key[:]) - _, _ = mac.Write(tokenCopy) - hmac := mac.Sum(nil) - + hmac, err := crypto.HmacSha256WithPrk(context.Background(), tokenCopy, accessorCopy) + if err != nil { + return nil, errors.WrapDeprecated(err, op, errors.WithCode(errors.Encrypt)) + } t := &Token{ expiration: expiration.Round(time.Second), Token: &store.Token{ StoreId: storeId, - TokenHmac: hmac, + TokenHmac: []byte(hmac), Token: tokenCopy, Status: string(CurrentToken), }, diff --git a/internal/kms/kms.go b/internal/kms/kms.go index 11b2ddcdd8..9e0f64e36c 100644 --- a/internal/kms/kms.go +++ b/internal/kms/kms.go @@ -2,9 +2,7 @@ package kms import ( "context" - "crypto/sha256" "fmt" - "io" "sync" "github.com/hashicorp/boundary/internal/db" @@ -13,7 +11,6 @@ import ( 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" ) // ExternalWrappers holds wrappers defined outside of Boundary, e.g. in its @@ -371,41 +368,3 @@ func (k *Kms) loadDek(ctx context.Context, scopeId string, purpose KeyPurpose, r return multi, nil } - -// 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 = "kms.NewDerivedReader" - if wrapper == nil { - return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing wrapper") - } - if lenLimit < 20 { - return nil, errors.NewDeprecated(errors.InvalidParameter, op, "lenLimit must be >= 20") - } - 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, errors.NewDeprecated(errors.InvalidParameter, op, "unexpected wrapper type from multiwrapper base") - } - case *aead.Wrapper: - if w.GetKeyBytes() == nil { - return nil, errors.NewDeprecated(errors.InvalidParameter, op, "aead wrapper missing bytes") - } - aeadWrapper = w - default: - return nil, errors.NewDeprecated(errors.InvalidParameter, op, "unknown wrapper type") - } - reader := hkdf.New(sha256.New, aeadWrapper.GetKeyBytes(), salt, info) - return &io.LimitedReader{ - R: reader, - N: lenLimit, - }, nil -} diff --git a/internal/kms/kms_test.go b/internal/kms/kms_test.go index dbd8e964e5..0ce3bb44b4 100644 --- a/internal/kms/kms_test.go +++ b/internal/kms/kms_test.go @@ -3,19 +3,12 @@ package kms import ( "context" "crypto/rand" - "crypto/sha256" - "io" "testing" "github.com/hashicorp/boundary/internal/db" - "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/types/scope" - wrapping "github.com/hashicorp/go-kms-wrapping" - "github.com/hashicorp/go-kms-wrapping/wrappers/aead" "github.com/hashicorp/go-uuid" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/crypto/hkdf" ) func TestKms_KeyId(t *testing.T) { @@ -73,101 +66,3 @@ func TestKms_KeyId(t *testing.T) { _, err = kmsCache.GetWrapper(ctx, scope.Global.String(), KeyPurposeDatabase, WithKeyId("foo")) require.Error(err) } - -func TestNewDerivedReader(t *testing.T) { - wrapper := db.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 errors.Code - 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: errors.InvalidParameter, - wantErrContains: "missing wrapper", - }, - { - name: "too-short", - args: args{ - wrapper: wrapper, - lenLimit: 10, - info: []byte("info"), - salt: []byte("salt"), - }, - wantErr: true, - wantErrCode: errors.InvalidParameter, - 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: errors.InvalidParameter, - 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.Truef(errors.Match(errors.T(errors.InvalidParameter), err), "unexpected error: %s", err) - if tt.wantErrContains != "" { - assert.Contains(err.Error(), tt.wantErrContains) - } - return - } - require.NoError(err) - assert.Equal(tt.want, got) - }) - } -} diff --git a/internal/libs/crypto/derived_reader.go b/internal/libs/crypto/derived_reader.go new file mode 100644 index 0000000000..387d65d4de --- /dev/null +++ b/internal/libs/crypto/derived_reader.go @@ -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 +} diff --git a/internal/libs/crypto/derived_reader_test.go b/internal/libs/crypto/derived_reader_test.go new file mode 100644 index 0000000000..bcd6d86a9f --- /dev/null +++ b/internal/libs/crypto/derived_reader_test.go @@ -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) + }) + } +} diff --git a/internal/libs/crypto/error.go b/internal/libs/crypto/error.go new file mode 100644 index 0000000000..6e97908cc2 --- /dev/null +++ b/internal/libs/crypto/error.go @@ -0,0 +1,9 @@ +package crypto + +import ( + "errors" +) + +var ( + ErrInvalidParameter = errors.New("Invalid parameter") +) diff --git a/internal/libs/crypto/hmac_sha256.go b/internal/libs/crypto/hmac_sha256.go new file mode 100644 index 0000000000..05491e6ef9 --- /dev/null +++ b/internal/libs/crypto/hmac_sha256.go @@ -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 +} diff --git a/internal/libs/crypto/hmac_sha256_test.go b/internal/libs/crypto/hmac_sha256_test.go new file mode 100644 index 0000000000..41782c830d --- /dev/null +++ b/internal/libs/crypto/hmac_sha256_test.go @@ -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 +} diff --git a/internal/libs/crypto/options.go b/internal/libs/crypto/options.go new file mode 100644 index 0000000000..e3a88e3122 --- /dev/null +++ b/internal/libs/crypto/options.go @@ -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 + } +} diff --git a/internal/libs/crypto/options_test.go b/internal/libs/crypto/options_test.go new file mode 100644 index 0000000000..f44b87eda6 --- /dev/null +++ b/internal/libs/crypto/options_test.go @@ -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) + }) +} diff --git a/internal/libs/crypto/testing.go b/internal/libs/crypto/testing.go new file mode 100644 index 0000000000..d470cebc8e --- /dev/null +++ b/internal/libs/crypto/testing.go @@ -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 +} diff --git a/internal/session/util.go b/internal/session/util.go index bbdbe63162..f94f98dd91 100644 --- a/internal/session/util.go +++ b/internal/session/util.go @@ -4,7 +4,7 @@ import ( "crypto/ed25519" "github.com/hashicorp/boundary/internal/errors" - "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/libs/crypto" wrapping "github.com/hashicorp/go-kms-wrapping" ) @@ -19,8 +19,11 @@ func DeriveED25519Key(wrapper wrapping.Wrapper, userId, jobId string) (ed25519.P if jobId != "" { jId = []byte(jobId) } + if wrapper == nil { + return nil, nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing wrapper") + } - reader, err := kms.NewDerivedReader(wrapper, 32, uId, jId) + reader, err := crypto.NewDerivedReader(wrapper, 32, uId, jId) if err != nil { return nil, nil, errors.WrapDeprecated(err, op) } diff --git a/internal/session/util_test.go b/internal/session/util_test.go index 78203ec144..09e8eee1c0 100644 --- a/internal/session/util_test.go +++ b/internal/session/util_test.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" - "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/libs/crypto" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -39,7 +39,7 @@ func TestDeriveED25519Key(t *testing.T) { jobId: "jobId", }, want: func() keys { - reader, err := kms.NewDerivedReader(wrapper, 32, []byte("userId"), []byte("jobId")) + reader, err := crypto.NewDerivedReader(wrapper, 32, []byte("userId"), []byte("jobId")) require.NoError(t, err) pub, priv, err := ed25519.GenerateKey(reader) require.NoError(t, err)