// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package target import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "testing" "time" talias "github.com/hashicorp/boundary/internal/alias/target" astore "github.com/hashicorp/boundary/internal/alias/target/store" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/hashicorp/go-kms-wrapping/v2/aead" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGenerateTargetCert(t *testing.T) { t.Parallel() ctx := context.Background() privKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) require.NoError(t, err) aliasValue := "test-alias-value" alias := &talias.Alias{ Alias: &astore.Alias{ Value: aliasValue, }, } tests := []struct { name string privKey *ecdsa.PrivateKey exp time.Time opt []Option wantErr bool wantErrContains string }{ { name: "valid-target-proxy-cert", privKey: privKey, exp: time.Now().Add(1 * time.Hour), }, { name: "valid-with-alias", privKey: privKey, exp: time.Now().Add(1 * time.Hour), opt: []Option{ WithAlias(alias), }, }, { name: "invalid-expiration", privKey: privKey, exp: time.Now().Add(-1 * time.Hour), wantErr: true, wantErrContains: "expiration time must be in the future", }, { name: "missing-key", exp: time.Now().Add(1 * time.Hour), wantErr: true, wantErrContains: "missing private key", }, { name: "missing-expiry", privKey: privKey, wantErr: true, wantErrContains: "missing expiry", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) got, err := generateTargetCert(ctx, tt.privKey, tt.exp, tt.opt...) if tt.wantErr { assert.Error(err) return } require.NoError(err) require.NotNil(got) pCert, err := x509.ParseCertificate(got) require.NoError(err) require.NotNil(pCert) require.Equal("localhost", pCert.Subject.CommonName) // Cert timestamps do not have ms resolution tt.exp = tt.exp.Truncate(time.Second) require.True(tt.exp.Equal(pCert.NotAfter)) require.Contains(pCert.DNSNames, "localhost") if tt.opt != nil { opts := GetOpts(tt.opt...) require.Contains(pCert.DNSNames, opts.WithAlias.Value) } }) } } func TestTargetProxyCertificate(t *testing.T) { t.Parallel() ctx := context.Background() tId := "test-target-id" tests := []struct { name string opt []Option wantErr bool wantErrContains string }{ { name: "valid-target-proxy-cert", }, { name: "valid-with-options", opt: []Option{ WithTargetId(tId), }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) gotCert, err := NewTargetProxyCertificate(ctx, tt.opt...) if tt.wantErr { assert.Error(err) return } require.NoError(err) require.NotNil(gotCert) if tt.opt != nil { assert.Equal(tId, gotCert.TargetId) } }) } } func Test_encrypt_decrypt_TargetCert(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") rootWrapper := db.TestWrapper(t) kmsCache := kms.TestKms(t, conn, rootWrapper) org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) projDatabaseWrapper, err := kmsCache.GetWrapper(ctx, proj.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) proxyCert, err := NewTargetProxyCertificate(ctx) require.NoError(t, err) tests := []struct { name string certKey *TargetProxyCertificate encryptWrapper wrapping.Wrapper wantEncryptErrMatch *errors.Template decryptWrapper wrapping.Wrapper wantDecryptErrMatch *errors.Template }{ { name: "success", certKey: proxyCert, encryptWrapper: databaseWrapper, decryptWrapper: databaseWrapper, }, { name: "encrypt-missing-wrapper", certKey: proxyCert, wantEncryptErrMatch: errors.T(errors.InvalidParameter), }, { name: "encrypt-bad-wrapper", certKey: proxyCert, encryptWrapper: &aead.Wrapper{}, wantEncryptErrMatch: errors.T(errors.Encrypt), }, { name: "encrypt-missing-wrapper", certKey: proxyCert, encryptWrapper: databaseWrapper, wantDecryptErrMatch: errors.T(errors.InvalidParameter), }, { name: "decrypt-bad-wrapper", certKey: proxyCert, encryptWrapper: databaseWrapper, decryptWrapper: &aead.Wrapper{}, wantDecryptErrMatch: errors.T(errors.Decrypt), }, { name: "decrypt-wrong-wrapper", certKey: proxyCert, encryptWrapper: databaseWrapper, decryptWrapper: projDatabaseWrapper, wantDecryptErrMatch: errors.T(errors.Decrypt), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) encryptedCert := tt.certKey.Clone() err = encryptedCert.Encrypt(ctx, tt.encryptWrapper) if tt.wantEncryptErrMatch != nil { require.Error(err) assert.Truef(errors.Match(tt.wantEncryptErrMatch, err), "expected %q and got err: %+v", tt.wantEncryptErrMatch.Code, err) return } require.NoError(err) assert.NotEmpty(encryptedCert.PrivateKeyEncrypted) decryptedCert := encryptedCert.Clone() decryptedCert.PrivateKey = []byte("") err = decryptedCert.Decrypt(ctx, tt.decryptWrapper) if tt.wantDecryptErrMatch != nil { require.Error(err) assert.Truef(errors.Match(tt.wantDecryptErrMatch, err), "expected %q and got err: %+v", tt.wantDecryptErrMatch.Code, err) return } require.NoError(err) assert.Equal(tt.certKey.PrivateKey, decryptedCert.PrivateKey) }) } } func TestTargetAliasProxyCertificate(t *testing.T) { t.Parallel() ctx := context.Background() tId := "test-target-id" tAId := "test-alias-id" aliasValue := "test-alias-value" alias := &talias.Alias{ Alias: &astore.Alias{ PublicId: tAId, Value: aliasValue, }, } tests := []struct { name string targetId string alias *talias.Alias wantErr bool wantErrContains string }{ { name: "valid-target-cert", targetId: tId, alias: alias, }, { name: "missing-target-id", alias: alias, wantErr: true, wantErrContains: "missing target id", }, { name: "missing-alias", targetId: tId, wantErr: true, wantErrContains: "missing alias", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) gotCert, err := NewTargetAliasProxyCertificate(ctx, tt.targetId, tt.alias) if tt.wantErr { assert.Error(err) return } require.NoError(err) require.NotNil(gotCert) assert.Equal(tId, gotCert.TargetId) assert.Equal(tAId, gotCert.AliasId) assert.NotNil(gotCert.Certificate) }) } } func Test_encrypt_decrypt_TargetAliasCert(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") rootWrapper := db.TestWrapper(t) kmsCache := kms.TestKms(t, conn, rootWrapper) org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) projDatabaseWrapper, err := kmsCache.GetWrapper(ctx, proj.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) tId := "test-target-id" tAId := "test-alias-id" aliasValue := "test-alias-value" alias := &talias.Alias{ Alias: &astore.Alias{ PublicId: tAId, Value: aliasValue, }, } proxyCert, err := NewTargetAliasProxyCertificate(ctx, tId, alias) require.NoError(t, err) tests := []struct { name string certKey *TargetAliasProxyCertificate encryptWrapper wrapping.Wrapper wantEncryptErrMatch *errors.Template decryptWrapper wrapping.Wrapper wantDecryptErrMatch *errors.Template }{ { name: "success", certKey: proxyCert, encryptWrapper: databaseWrapper, decryptWrapper: databaseWrapper, }, { name: "encrypt-missing-wrapper", certKey: proxyCert, wantEncryptErrMatch: errors.T(errors.InvalidParameter), }, { name: "encrypt-bad-wrapper", certKey: proxyCert, encryptWrapper: &aead.Wrapper{}, wantEncryptErrMatch: errors.T(errors.Encrypt), }, { name: "encrypt-missing-wrapper", certKey: proxyCert, encryptWrapper: databaseWrapper, wantDecryptErrMatch: errors.T(errors.InvalidParameter), }, { name: "decrypt-bad-wrapper", certKey: proxyCert, encryptWrapper: databaseWrapper, decryptWrapper: &aead.Wrapper{}, wantDecryptErrMatch: errors.T(errors.Decrypt), }, { name: "decrypt-wrong-wrapper", certKey: proxyCert, encryptWrapper: databaseWrapper, decryptWrapper: projDatabaseWrapper, wantDecryptErrMatch: errors.T(errors.Decrypt), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) encryptedCert := tt.certKey.Clone() err = encryptedCert.Encrypt(ctx, tt.encryptWrapper) if tt.wantEncryptErrMatch != nil { require.Error(err) assert.Truef(errors.Match(tt.wantEncryptErrMatch, err), "expected %q and got err: %+v", tt.wantEncryptErrMatch.Code, err) return } require.NoError(err) assert.NotEmpty(encryptedCert.PrivateKeyEncrypted) decryptedCert := encryptedCert.Clone() decryptedCert.PrivateKey = []byte("") err = decryptedCert.Decrypt(ctx, tt.decryptWrapper) if tt.wantDecryptErrMatch != nil { require.Error(err) assert.Truef(errors.Match(tt.wantDecryptErrMatch, err), "expected %q and got err: %+v", tt.wantDecryptErrMatch.Code, err) return } require.NoError(err) assert.Equal(tt.certKey.PrivateKey, decryptedCert.PrivateKey) }) } } func TestTargetProxyCertToAndFromServerCert(t *testing.T) { t.Parallel() ctx := context.Background() cert, err := NewTargetProxyCertificate(ctx) require.NoError(t, err) require.NotNil(t, cert) serverCert, err := cert.ToServerCertificate(ctx) require.NoError(t, err) require.NotEmpty(t, serverCert.CertificatePem) require.NotEmpty(t, serverCert.PrivateKeyPem) newCert := allocTargetProxyCertificate() err = newCert.fromServerCertificate(ctx, serverCert) require.NoError(t, err) require.Equal(t, cert.Certificate, newCert.Certificate) } func TestTargetAliasProxyCertToAndFromServerCert(t *testing.T) { t.Parallel() ctx := context.Background() tId := "test-target-id" tAId := "test-alias-id" aliasValue := "test-alias-value" alias := &talias.Alias{ Alias: &astore.Alias{ PublicId: tAId, Value: aliasValue, }, } cert, err := NewTargetAliasProxyCertificate(ctx, tId, alias) require.NoError(t, err) require.NotNil(t, cert) serverCert, err := cert.ToServerCertificate(ctx) require.NoError(t, err) require.NotEmpty(t, serverCert.CertificatePem) require.NotEmpty(t, serverCert.PrivateKeyPem) newCert := allocTargetAliasProxyCertificate() err = newCert.fromServerCertificate(ctx, serverCert) require.NoError(t, err) require.Equal(t, cert.Certificate, newCert.Certificate) }