Add support for SSH private key passphrases (#2331)

This adds support for encrypted SSH keys via passphrase.

Co-authored-by: Louis Ruch <louisruch@gmail.com>
Co-authored-by: Michael Gaffney <mgaffney@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2022-08-08 15:15:03 -04:00
1 parent 07bca9dd66
commit 02dd28f587
45 files changed
+1921 -382

No files matched your search

@@ -38,9 +38,10 @@ const (
// Credential mapping override attributes
const (
usernameAttribute string = "username_attribute"
passwordAttribute string = "password_attribute"
privateKeyAttribute string = "private_key_attribute"
usernameAttribute string = "username_attribute"
passwordAttribute string = "password_attribute"
privateKeyAttribute string = "private_key_attribute"
pkPassphraseAttribute string = "private_key_passphrase_attribute"
)
var (
@@ -540,6 +541,9 @@ func toProto(in credential.Library, opt ...handlers.Option) (*pb.CredentialLibra
if mapping.PrivateKeyAttribute != "" {
m[privateKeyAttribute] = mapping.PrivateKeyAttribute
}
if mapping.PrivateKeyPassphraseAttribute != "" {
m[pkPassphraseAttribute] = mapping.PrivateKeyPassphraseAttribute
}
}
if len(m) > 0 {
mp, err := structpb.NewStruct(m)
@@ -612,6 +616,9 @@ func toStorageVaultLibrary(storeId string, in *pb.CredentialLibrary) (out *vault
if pk := overrides[privateKeyAttribute]; pk != nil {
mapOpts = append(mapOpts, vault.WithOverridePrivateKeyAttribute(pk.(string)))
}
if pass := overrides[pkPassphraseAttribute]; pass != nil {
mapOpts = append(mapOpts, vault.WithOverridePrivateKeyPassphraseAttribute(pass.(string)))
}
if len(mapOpts) > 0 {
opts = append(opts, vault.WithMappingOverride(vault.NewSshPrivateKeyOverride(mapOpts...)))
}
@@ -723,6 +730,7 @@ func validateMapping(badFields map[string]string, credentialType credential.Type
case credential.SshPrivateKeyType:
validFields[usernameAttribute] = true
validFields[privateKeyAttribute] = true
validFields[pkPassphraseAttribute] = true
default:
badFields[globals.CredentialTypeField] = fmt.Sprintf("Unknown credential type %q", credentialType)
return
@@ -782,10 +790,11 @@ func getMappingUpdates(credentialType credential.Type, current vault.MappingOver
}
case credential.SshPrivateKeyType:
var currentUser, currentPk interface{}
var currentUser, currentpPass, currentPk interface{}
if overrides, ok := current.(*vault.SshPrivateKeyOverride); ok {
currentUser = overrides.UsernameAttribute
currentPk = overrides.PrivateKeyAttribute
currentpPass = overrides.PrivateKeyPassphraseAttribute
}
switch {
@@ -801,6 +810,13 @@ func getMappingUpdates(credentialType credential.Type, current vault.MappingOver
default:
ret[privateKeyAttribute] = currentPk
}
switch {
case masks[pkPassphraseAttribute]:
ret[pkPassphraseAttribute] = new[pkPassphraseAttribute]
default:
ret[pkPassphraseAttribute] = currentpPass
}
}
return ret, true
@@ -542,6 +542,92 @@ func TestCreate(t *testing.T) {
},
},
},
{
name: "Create a valid vault CredentialLibrary ssh_private_key type",
req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{
CredentialStoreId: store.GetPublicId(),
Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{
VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{
Path: wrapperspb.String("something"),
},
},
CredentialType: string(credential.SshPrivateKeyType),
}},
idPrefix: vault.CredentialLibraryPrefix + "_",
res: &pbs.CreateCredentialLibraryResponse{
Uri: fmt.Sprintf("credential-libraries/%s_", vault.CredentialLibraryPrefix),
Item: &pb.CredentialLibrary{
Id: store.GetPublicId(),
CredentialStoreId: store.GetPublicId(),
CreatedTime: store.GetCreateTime().GetTimestamp(),
UpdatedTime: store.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()},
Version: 1,
Type: vault.Subtype.String(),
Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{
VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{
Path: wrapperspb.String("something"),
HttpMethod: wrapperspb.String("GET"),
},
},
CredentialType: string(credential.SshPrivateKeyType),
AuthorizedActions: testAuthorizedActions,
},
},
},
{
name: "Create a valid vault CredentialLibrary ssh_private_key type with mapping",
req: &pbs.CreateCredentialLibraryRequest{Item: &pb.CredentialLibrary{
CredentialStoreId: store.GetPublicId(),
Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{
VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{
Path: wrapperspb.String("something"),
},
},
CredentialMappingOverrides: func() *structpb.Struct {
v := map[string]interface{}{
usernameAttribute: "user-test",
privateKeyAttribute: "pk-test",
pkPassphraseAttribute: "pass-test",
}
ret, err := structpb.NewStruct(v)
require.NoError(t, err)
return ret
}(),
CredentialType: string(credential.SshPrivateKeyType),
}},
idPrefix: vault.CredentialLibraryPrefix + "_",
res: &pbs.CreateCredentialLibraryResponse{
Uri: fmt.Sprintf("credential-libraries/%s_", vault.CredentialLibraryPrefix),
Item: &pb.CredentialLibrary{
Id: store.GetPublicId(),
CredentialStoreId: store.GetPublicId(),
CreatedTime: store.GetCreateTime().GetTimestamp(),
UpdatedTime: store.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()},
Version: 1,
Type: vault.Subtype.String(),
Attrs: &pb.CredentialLibrary_VaultCredentialLibraryAttributes{
VaultCredentialLibraryAttributes: &pb.VaultCredentialLibraryAttributes{
Path: wrapperspb.String("something"),
HttpMethod: wrapperspb.String("GET"),
},
},
CredentialType: string(credential.SshPrivateKeyType),
CredentialMappingOverrides: func() *structpb.Struct {
v := map[string]interface{}{
usernameAttribute: "user-test",
privateKeyAttribute: "pk-test",
pkPassphraseAttribute: "pass-test",
}
ret, err := structpb.NewStruct(v)
require.NoError(t, err)
return ret
}(),
AuthorizedActions: testAuthorizedActions,
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -624,6 +710,7 @@ func TestGet(t *testing.T) {
vault.NewSshPrivateKeyOverride(
vault.WithOverrideUsernameAttribute("user"),
vault.WithOverridePrivateKeyAttribute("pk"),
vault.WithOverridePrivateKeyPassphraseAttribute("pass"),
)))
require.NoError(t, err)
sshPkLib, err := repo.CreateCredentialLibrary(context.Background(), prj.GetPublicId(), lib1)
@@ -711,8 +798,9 @@ func TestGet(t *testing.T) {
CredentialType: "ssh_private_key",
CredentialMappingOverrides: func() *structpb.Struct {
v := map[string]interface{}{
usernameAttribute: "user",
privateKeyAttribute: "pk",
usernameAttribute: "user",
privateKeyAttribute: "pk",
pkPassphraseAttribute: "pass",
}
ret, err := structpb.NewStruct(v)
require.NoError(t, err)
@@ -864,6 +952,7 @@ func TestUpdate(t *testing.T) {
usernameAttrField := fmt.Sprintf("%v.%v", credentialMappingPathField, usernameAttribute)
passwordAttrField := fmt.Sprintf("%v.%v", credentialMappingPathField, passwordAttribute)
privateKeyAttrField := fmt.Sprintf("%v.%v", credentialMappingPathField, privateKeyAttribute)
passphraseAttrField := fmt.Sprintf("%v.%v", credentialMappingPathField, pkPassphraseAttribute)
successCases := []struct {
name string
@@ -1167,6 +1256,7 @@ func TestUpdate(t *testing.T) {
vault.NewSshPrivateKeyOverride(
vault.WithOverrideUsernameAttribute("orig-user"),
vault.WithOverridePrivateKeyAttribute("orig-pk"),
vault.WithOverridePrivateKeyPassphraseAttribute("orig-pass"),
)),
},
req: &pbs.UpdateCredentialLibraryRequest{
@@ -1196,6 +1286,7 @@ func TestUpdate(t *testing.T) {
vault.NewSshPrivateKeyOverride(
vault.WithOverrideUsernameAttribute("orig-user"),
vault.WithOverridePrivateKeyAttribute("orig-pk"),
vault.WithOverridePrivateKeyPassphraseAttribute("orig-pass"),
)),
},
req: &pbs.UpdateCredentialLibraryRequest{
@@ -1218,22 +1309,54 @@ func TestUpdate(t *testing.T) {
},
},
{
name: "ssh-private-key-attributes-change-username-and-private-key-attributes",
name: "ssh-private-key-attributes-change-passphrase-attribute",
opts: []vault.Option{
vault.WithCredentialType("ssh_private_key"),
vault.WithMappingOverride(
vault.NewSshPrivateKeyOverride(
vault.WithOverrideUsernameAttribute("orig-user"),
vault.WithOverridePrivateKeyAttribute("orig-pk"),
vault.WithOverridePrivateKeyPassphraseAttribute("orig-pass"),
)),
},
req: &pbs.UpdateCredentialLibraryRequest{
UpdateMask: fieldmask(privateKeyAttrField, usernameAttrField),
UpdateMask: fieldmask(passphraseAttrField),
Item: &pb.CredentialLibrary{
CredentialMappingOverrides: func() *structpb.Struct {
v := map[string]interface{}{
usernameAttribute: "changed-user",
privateKeyAttribute: "changed-pk",
pkPassphraseAttribute: "changed-pass",
}
ret, err := structpb.NewStruct(v)
require.NoError(t, err)
return ret
}(),
},
},
res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary {
out := proto.Clone(in).(*pb.CredentialLibrary)
out.CredentialMappingOverrides.Fields[pkPassphraseAttribute] = structpb.NewStringValue("changed-pass")
return out
},
},
{
name: "ssh-private-key-attributes-change-all-attributes",
opts: []vault.Option{
vault.WithCredentialType("ssh_private_key"),
vault.WithMappingOverride(
vault.NewSshPrivateKeyOverride(
vault.WithOverrideUsernameAttribute("orig-user"),
vault.WithOverridePrivateKeyAttribute("orig-pk"),
vault.WithOverridePrivateKeyPassphraseAttribute("orig-pass"),
)),
},
req: &pbs.UpdateCredentialLibraryRequest{
UpdateMask: fieldmask(privateKeyAttrField, usernameAttrField, passphraseAttrField),
Item: &pb.CredentialLibrary{
CredentialMappingOverrides: func() *structpb.Struct {
v := map[string]interface{}{
usernameAttribute: "changed-user",
privateKeyAttribute: "changed-pk",
pkPassphraseAttribute: "changed-pass",
}
ret, err := structpb.NewStruct(v)
require.NoError(t, err)
@@ -1245,21 +1368,23 @@ func TestUpdate(t *testing.T) {
out := proto.Clone(in).(*pb.CredentialLibrary)
out.CredentialMappingOverrides.Fields[usernameAttribute] = structpb.NewStringValue("changed-user")
out.CredentialMappingOverrides.Fields[privateKeyAttribute] = structpb.NewStringValue("changed-pk")
out.CredentialMappingOverrides.Fields[pkPassphraseAttribute] = structpb.NewStringValue("changed-pass")
return out
},
},
{
name: "no-mapping-override-change-username-and-private-key-attributes",
name: "no-mapping-override-change-all-attributes",
opts: []vault.Option{
vault.WithCredentialType("ssh_private_key"),
},
req: &pbs.UpdateCredentialLibraryRequest{
UpdateMask: fieldmask(privateKeyAttrField, usernameAttrField),
UpdateMask: fieldmask(privateKeyAttrField, usernameAttrField, passphraseAttrField),
Item: &pb.CredentialLibrary{
CredentialMappingOverrides: func() *structpb.Struct {
v := map[string]interface{}{
usernameAttribute: "new-user",
privateKeyAttribute: "new-pk",
usernameAttribute: "new-user",
privateKeyAttribute: "new-pk",
pkPassphraseAttribute: "new-pass",
}
ret, err := structpb.NewStruct(v)
require.NoError(t, err)
@@ -1270,8 +1395,9 @@ func TestUpdate(t *testing.T) {
res: func(in *pb.CredentialLibrary) *pb.CredentialLibrary {
out := proto.Clone(in).(*pb.CredentialLibrary)
v := map[string]interface{}{
usernameAttribute: "new-user",
privateKeyAttribute: "new-pk",
usernameAttribute: "new-user",
privateKeyAttribute: "new-pk",
pkPassphraseAttribute: "new-pass",
}
var err error
out.CredentialMappingOverrides, err = structpb.NewStruct(v)
@@ -1287,6 +1413,7 @@ func TestUpdate(t *testing.T) {
vault.NewSshPrivateKeyOverride(
vault.WithOverrideUsernameAttribute("orig-user"),
vault.WithOverridePrivateKeyAttribute("orig-pk"),
vault.WithOverridePrivateKeyPassphraseAttribute("orig-pass"),
)),
},
req: &pbs.UpdateCredentialLibraryRequest{
@@ -1326,15 +1453,17 @@ func TestUpdate(t *testing.T) {
vault.NewSshPrivateKeyOverride(
vault.WithOverrideUsernameAttribute("orig-user"),
vault.WithOverridePrivateKeyAttribute("orig-pk"),
vault.WithOverridePrivateKeyPassphraseAttribute("orig-pass"),
)),
},
req: &pbs.UpdateCredentialLibraryRequest{
UpdateMask: fieldmask(privateKeyAttrField, usernameAttrField),
UpdateMask: fieldmask(privateKeyAttrField, usernameAttrField, passphraseAttrField),
Item: &pb.CredentialLibrary{
CredentialMappingOverrides: func() *structpb.Struct {
v := map[string]interface{}{
usernameAttribute: nil,
privateKeyAttribute: nil,
usernameAttribute: nil,
privateKeyAttribute: nil,
pkPassphraseAttribute: nil,
}
ret, err := structpb.NewStruct(v)
require.NoError(t, err)
@@ -2,6 +2,7 @@ package credentials
import (
"context"
"crypto/x509"
"encoding/base64"
"fmt"
@@ -27,10 +28,11 @@ import (
)
const (
usernameField = "attributes.username"
passwordField = "attributes.password"
privateKeyField = "attributes.private_key"
domain = "credential"
usernameField = "attributes.username"
passwordField = "attributes.password"
privateKeyField = "attributes.private_key"
privateKeyPassphraseField = "attributes.private_key_passphrase"
domain = "credential"
)
var (
@@ -409,6 +411,16 @@ func (s Service) updateInRepo(
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert to ssh private key storage credential"))
}
if cred.PassphraseUnneeded {
// This happens when we have a private key given and no passphrase
// given and everything parses correctly. In that case we want to
// ensure that if a passphrase was in the database for the previous
// key that we get rid of it. We'll have nilled out several values
// above. Note that adding the passphrase field will, once we get to
// the repo, result in the mask for the other two related fields as
// well.
dbMasks = append(dbMasks, static.PrivateKeyPassphraseField)
}
cred.PublicId = id
repo, err := s.repoFn()
if err != nil {
@@ -553,8 +565,9 @@ func toProto(in credential.Static, opt ...handlers.Option) (*pb.Credential, erro
if outputFields.Has(globals.AttributesField) {
out.Attrs = &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
Username: wrapperspb.String(cred.GetUsername()),
PrivateKeyHmac: base64.RawURLEncoding.EncodeToString(cred.GetPrivateKeyHmac()),
Username: wrapperspb.String(cred.GetUsername()),
PrivateKeyHmac: base64.RawURLEncoding.EncodeToString(cred.GetPrivateKeyHmac()),
PrivateKeyPassphraseHmac: base64.RawURLEncoding.EncodeToString(cred.GetPrivateKeyPassphraseHmac()),
},
}
}
@@ -596,6 +609,9 @@ func toSshPrivateKeyStorageCredential(ctx context.Context, storeId string, in *p
}
attrs := in.GetSshPrivateKeyAttributes()
if attrs.GetPrivateKeyPassphrase() != nil {
opts = append(opts, static.WithPrivateKeyPassphrase([]byte(attrs.GetPrivateKeyPassphrase().GetValue())))
}
cs, err := static.NewSshPrivateKeyCredential(
ctx,
storeId,
@@ -645,16 +661,27 @@ func validateCreateRequest(req *pbs.CreateCredentialRequest) error {
badFields[usernameField] = "Field required for creating an SSH private key credential."
}
privateKey := req.Item.GetSshPrivateKeyAttributes().GetPrivateKey().GetValue()
passphrase := req.Item.GetSshPrivateKeyAttributes().GetPrivateKeyPassphrase().GetValue()
if privateKey == "" {
badFields[privateKeyField] = "Field required for creating an SSH private key credential."
} else {
_, err := ssh.ParsePrivateKey([]byte(privateKey))
switch {
case err == nil:
case err.Error() == (&ssh.PassphraseMissingError{}).Error():
// This is okay, if it's brokered and the client can use it, no worries
switch passphrase {
case "":
if _, err := ssh.ParsePrivateKey([]byte(privateKey)); err != nil {
badFields[privateKeyField] = fmt.Sprintf("Unable to parse given private key value: %v.", err)
}
default:
badFields[privateKeyField] = "Unable to parse given private key value."
if _, err := ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(passphrase)); err != nil {
if errors.Is(err, x509.IncorrectPasswordError) {
badFields[privateKeyPassphraseField] = "Incorrect private key passphrase."
} else {
if _, err := ssh.ParsePrivateKey([]byte(privateKey)); err == nil {
badFields[privateKeyPassphraseField] = "Passphrase supplied for unencrypted key."
} else {
badFields[privateKeyField] = fmt.Sprintf("Unable to parse given private key value: %v.", err)
}
}
}
}
}
@@ -669,9 +696,8 @@ func validateCreateRequest(req *pbs.CreateCredentialRequest) error {
func validateUpdateRequest(req *pbs.UpdateCredentialRequest) error {
return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string {
badFields := map[string]string{}
switch req.GetItem().GetType() {
case "":
case credential.UsernamePasswordSubtype.String():
switch subtypes.SubtypeFromId(domain, req.GetId()) {
case credential.UsernamePasswordSubtype:
attrs := req.GetItem().GetUsernamePasswordAttributes()
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), usernameField) && attrs.GetUsername().GetValue() == "" {
badFields[usernameField] = "This is a required field and cannot be set to empty."
@@ -680,27 +706,40 @@ func validateUpdateRequest(req *pbs.UpdateCredentialRequest) error {
badFields[passwordField] = "This is a required field and cannot be set to empty."
}
case credential.SshPrivateKeySubtype.String():
case credential.SshPrivateKeySubtype:
attrs := req.GetItem().GetSshPrivateKeyAttributes()
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), usernameField) && attrs.GetUsername().GetValue() == "" {
badFields[usernameField] = "This is a required field and cannot be set to empty."
}
privateKey := attrs.GetPrivateKey().GetValue()
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), privateKeyField) && privateKey == "" {
badFields[privateKeyField] = "This is a required field and cannot be set to empty."
} else {
_, err := ssh.ParsePrivateKey([]byte(privateKey))
switch {
case err == nil:
case err.Error() == (&ssh.PassphraseMissingError{}).Error():
// This is okay, if it's brokered and the client can use it, no worries
default:
badFields[privateKeyField] = "Unable to parse given private key value."
passphrase := attrs.GetPrivateKeyPassphrase().GetValue()
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), privateKeyField) {
if privateKey == "" {
badFields[privateKeyField] = "This is a required field and cannot be set to empty."
} else {
switch passphrase {
case "":
if _, err := ssh.ParsePrivateKey([]byte(privateKey)); err != nil {
badFields[privateKeyField] = fmt.Sprintf("Unable to parse given private key value: %v.", err)
}
default:
if _, err := ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(passphrase)); err != nil {
if errors.Is(err, x509.IncorrectPasswordError) {
badFields[privateKeyPassphraseField] = "Incorrect private key passphrase."
} else {
if _, err := ssh.ParsePrivateKey([]byte(privateKey)); err == nil {
badFields[privateKeyPassphraseField] = "Passphrase supplied for unencrypted key."
} else {
badFields[privateKeyField] = fmt.Sprintf("Unable to parse given private key value: %v.", err)
}
}
}
}
}
}
default:
badFields[globals.TypeField] = "Cannot modify resource type."
badFields[globals.IdField] = "Unknown credential type."
}
return badFields
@@ -25,6 +25,7 @@ import (
scopepb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh/testdata"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
@@ -205,6 +206,15 @@ func TestGet(t *testing.T) {
spkHm, err := crypto.HmacSha256(context.Background(), []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(t, err)
spkCredWithPass := static.TestSshPrivateKeyCredential(t, conn, wrapper,
"user", string(testdata.PEMEncryptedKeys[0].PEMBytes),
store.GetPublicId(), prj.GetPublicId(),
static.WithPrivateKeyPassphrase([]byte(testdata.PEMEncryptedKeys[0].EncryptionKey)))
spkWithPassHm, err := crypto.HmacSha256(context.Background(), []byte(testdata.PEMEncryptedKeys[0].PEMBytes), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(t, err)
passHm, err := crypto.HmacSha256(context.Background(), []byte(testdata.PEMEncryptedKeys[0].EncryptionKey), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(t, err)
cases := []struct {
name string
id string
@@ -277,6 +287,29 @@ func TestGet(t *testing.T) {
},
},
},
{
name: "success-spk-with-pass",
id: spkCredWithPass.GetPublicId(),
res: &pbs.GetCredentialResponse{
Item: &pb.Credential{
Id: spkCredWithPass.GetPublicId(),
CredentialStoreId: spkCredWithPass.GetStoreId(),
Scope: &scopepb.ScopeInfo{Id: store.GetScopeId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()},
Type: credential.SshPrivateKeySubtype.String(),
AuthorizedActions: testAuthorizedActions,
CreatedTime: spkCredWithPass.CreateTime.GetTimestamp(),
UpdatedTime: spkCredWithPass.UpdateTime.GetTimestamp(),
Version: 1,
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
Username: wrapperspb.String("user"),
PrivateKeyHmac: base64.RawURLEncoding.EncodeToString([]byte(spkWithPassHm)),
PrivateKeyPassphraseHmac: base64.RawURLEncoding.EncodeToString([]byte(passHm)),
},
},
},
},
},
{
name: "not found error",
id: fmt.Sprintf("%s_1234567890", credential.UsernamePasswordCredentialPrefix),
@@ -576,6 +609,34 @@ func TestCreate(t *testing.T) {
},
},
},
{
name: "valid-spk-with-passphrase",
req: &pbs.CreateCredentialRequest{Item: &pb.Credential{
CredentialStoreId: store.GetPublicId(),
Type: credential.SshPrivateKeySubtype.String(),
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
Username: wrapperspb.String("username"),
PrivateKey: wrapperspb.String(string(testdata.PEMEncryptedKeys[0].PEMBytes)),
PrivateKeyPassphrase: wrapperspb.String(testdata.PEMEncryptedKeys[0].EncryptionKey),
},
},
}},
idPrefix: credential.SshPrivateKeyCredentialPrefix + "_",
res: &pbs.CreateCredentialResponse{
Uri: fmt.Sprintf("credentials/%s_", credential.SshPrivateKeyCredentialPrefix),
Item: &pb.Credential{
Id: store.GetPublicId(),
CredentialStoreId: store.GetPublicId(),
CreatedTime: store.GetCreateTime().GetTimestamp(),
UpdatedTime: store.GetUpdateTime().GetTimestamp(),
Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()},
Version: 1,
Type: credential.SshPrivateKeySubtype.String(),
AuthorizedActions: testAuthorizedActions,
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -623,7 +684,8 @@ func TestCreate(t *testing.T) {
assert.Empty(got.GetItem().GetUsernamePasswordAttributes().GetPassword())
case credential.SshPrivateKeySubtype.String():
hm, err := crypto.HmacSha256(context.Background(), []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil)
pk := tc.req.GetItem().GetSshPrivateKeyAttributes().GetPrivateKey().GetValue()
hm, err := crypto.HmacSha256(context.Background(), []byte(pk), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(err)
// Validate attributes equal
@@ -632,6 +694,14 @@ func TestCreate(t *testing.T) {
assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetSshPrivateKeyAttributes().GetPrivateKeyHmac())
assert.Empty(got.GetItem().GetSshPrivateKeyAttributes().GetPrivateKey())
if pass := tc.req.GetItem().GetSshPrivateKeyAttributes().GetPrivateKeyPassphrase().GetValue(); pass != "" {
hm, err := crypto.HmacSha256(context.Background(), []byte(pass), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(err)
assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetSshPrivateKeyAttributes().GetPrivateKeyPassphraseHmac())
assert.Empty(got.GetItem().GetSshPrivateKeyAttributes().GetPrivateKeyPassphrase())
}
default:
require.Fail("unknown type")
}
@@ -708,10 +778,11 @@ func TestUpdate(t *testing.T) {
databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase)
require.NoError(t, err)
successCases := []struct {
name string
req *pbs.UpdateCredentialRequest
res func(cred *pb.Credential) *pb.Credential
successFailCases := []struct {
name string
req *pbs.UpdateCredentialRequest
res func(cred *pb.Credential) *pb.Credential
expErrorContains string
}{
{
name: "name",
@@ -897,9 +968,87 @@ func TestUpdate(t *testing.T) {
return out
},
},
{
name: "update-spk-with-passphrase",
req: &pbs.UpdateCredentialRequest{
UpdateMask: fieldmask("attributes.private_key", "attributes.private_key_passphrase"),
Item: &pb.Credential{
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
PrivateKey: wrapperspb.String(string(testdata.PEMEncryptedKeys[0].PEMBytes)),
PrivateKeyPassphrase: wrapperspb.String(testdata.PEMEncryptedKeys[0].EncryptionKey),
},
},
},
},
res: func(in *pb.Credential) *pb.Credential {
out := proto.Clone(in).(*pb.Credential)
hm, err := crypto.HmacSha256(context.Background(), testdata.PEMEncryptedKeys[0].PEMBytes, databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(t, err)
out.GetSshPrivateKeyAttributes().PrivateKeyHmac = base64.RawURLEncoding.EncodeToString([]byte(hm))
hm, err = crypto.HmacSha256(context.Background(), []byte(testdata.PEMEncryptedKeys[0].EncryptionKey), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(t, err)
out.GetSshPrivateKeyAttributes().PrivateKeyPassphraseHmac = base64.RawURLEncoding.EncodeToString([]byte(hm))
return out
},
},
{
name: "update-spk-with-bad-passphrase",
req: &pbs.UpdateCredentialRequest{
UpdateMask: fieldmask("attributes.private_key", "attributes.private_key_passphrase"),
Item: &pb.Credential{
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
PrivateKey: wrapperspb.String(string(testdata.PEMEncryptedKeys[0].PEMBytes)),
PrivateKeyPassphrase: wrapperspb.String(strings.ToLower(testdata.PEMEncryptedKeys[0].EncryptionKey)),
},
},
},
},
expErrorContains: "Incorrect private key passphrase",
},
{
name: "update-non-passphrase-spk-with-passphrase",
req: &pbs.UpdateCredentialRequest{
UpdateMask: fieldmask("attributes.private_key"),
Item: &pb.Credential{
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
PrivateKey: wrapperspb.String(static.TestSshPrivateKeyPem),
PrivateKeyPassphrase: wrapperspb.String(testdata.PEMEncryptedKeys[0].EncryptionKey),
},
},
},
},
expErrorContains: "Passphrase supplied for unencrypted key",
},
{
name: "update-non-passphrase-spk-with-no-passphrase",
req: &pbs.UpdateCredentialRequest{
UpdateMask: fieldmask("attributes.private_key"),
Item: &pb.Credential{
Attrs: &pb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &pb.SshPrivateKeyAttributes{
PrivateKey: wrapperspb.String(static.TestSshPrivateKeyPem),
},
},
},
},
res: func(in *pb.Credential) *pb.Credential {
out := proto.Clone(in).(*pb.Credential)
hm, err := crypto.HmacSha256(context.Background(), []byte(static.TestSshPrivateKeyPem), databaseWrapper, []byte(store.GetPublicId()), nil)
require.NoError(t, err)
out.GetSshPrivateKeyAttributes().PrivateKeyHmac = base64.RawURLEncoding.EncodeToString([]byte(hm))
out.GetSshPrivateKeyAttributes().PrivateKeyPassphraseHmac = ""
return out
},
},
}
for _, tc := range successCases {
for _, tc := range successFailCases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
var cred credential.Static
@@ -920,12 +1069,18 @@ func TestUpdate(t *testing.T) {
}
resToChange, err := s.GetCredential(ctx, &pbs.GetCredentialRequest{Id: cred.GetPublicId()})
require.NoError(err)
want := &pbs.UpdateCredentialResponse{Item: tc.res(resToChange.GetItem())}
got, gErr := s.UpdateCredential(ctx, tc.req)
if tc.expErrorContains != "" {
require.NotNil(gErr)
assert.Contains(gErr.Error(), tc.expErrorContains)
return
}
require.NoError(gErr)
require.NotNil(got)
want := &pbs.UpdateCredentialResponse{Item: tc.res(resToChange.GetItem())}
gotUpdateTime := got.GetItem().GetUpdatedTime()
created := cred.GetCreateTime().GetTimestamp()
assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created)
@@ -38,8 +38,9 @@ func dynamicToWorkerCredential(ctx context.Context, cred credential.Dynamic) (se
workerCred = &serverpb.Credential{
Credential: &serverpb.Credential_SshPrivateKey{
SshPrivateKey: &serverpb.SshPrivateKey{
Username: c.Username(),
PrivateKey: string(c.PrivateKey()),
Username: c.Username(),
PrivateKey: string(c.PrivateKey()),
PrivateKeyPassphrase: string(c.PrivateKeyPassphrase()),
},
},
}
@@ -103,8 +104,9 @@ func dynamicToSessionCredential(ctx context.Context, cred credential.Dynamic) (*
case credential.SshPrivateKey:
credData, err = handlers.ProtoToStruct(
&pb.SshPrivateKeyCredential{
Username: c.Username(),
PrivateKey: string(c.PrivateKey()),
Username: c.Username(),
PrivateKey: string(c.PrivateKey()),
PrivateKeyPassphrase: string(c.PrivateKeyPassphrase()),
},
)
if err != nil {
@@ -153,8 +155,9 @@ func staticToWorkerCredential(ctx context.Context, cred credential.Static) (sess
workerCred = &serverpb.Credential{
Credential: &serverpb.Credential_SshPrivateKey{
SshPrivateKey: &serverpb.SshPrivateKey{
Username: c.GetUsername(),
PrivateKey: string(c.GetPrivateKey()),
Username: c.GetUsername(),
PrivateKey: string(c.GetPrivateKey()),
PrivateKeyPassphrase: string(c.GetPrivateKeyPassphrase()),
},
},
}
@@ -199,14 +202,18 @@ func staticToSessionCredential(ctx context.Context, cred credential.Static) (*pb
credType = string(credential.SshPrivateKeyType)
credData, err = handlers.ProtoToStruct(
&pb.SshPrivateKeyCredential{
Username: c.GetUsername(),
PrivateKey: string(c.GetPrivateKey()),
Username: c.GetUsername(),
PrivateKey: string(c.GetPrivateKey()),
PrivateKeyPassphrase: string(c.GetPrivateKeyPassphrase()),
},
)
secret = map[string]interface{}{
"username": c.GetUsername(),
"private_key": string(c.GetPrivateKey()),
}
if len(c.GetPrivateKeyPassphrase()) > 0 {
secret["private_key_passphrase"] = string(c.GetPrivateKeyPassphrase())
}
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for ssh private key credential"))
}
@@ -2772,7 +2772,23 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, sshPkCredResp)
// Create secret in vault with default username and password fields
sshPkWithPassCredResp, err := credService.CreateCredential(ctx, &pbs.CreateCredentialRequest{Item: &credpb.Credential{
CredentialStoreId: staticStore.GetPublicId(),
Type: credential.SshPrivateKeySubtype.String(),
Name: wrapperspb.String("Cred SSH With Pass Name"),
Description: wrapperspb.String("Cred SSH Description"),
Attrs: &credpb.Credential_SshPrivateKeyAttributes{
SshPrivateKeyAttributes: &credpb.SshPrivateKeyAttributes{
Username: wrapperspb.String("static-username"),
PrivateKey: wrapperspb.String(string(testdata.PEMEncryptedKeys[0].PEMBytes)),
PrivateKeyPassphrase: wrapperspb.String(testdata.PEMEncryptedKeys[0].EncryptionKey),
},
},
}})
require.NoError(t, err)
require.NotNil(t, sshPkWithPassCredResp)
// Create secret in vault with default username and private key fields
defaultSshPrivateKey := v.CreateKVSecret(t, "default-sshpk", []byte(`{"data": {"username": "my-user", "private_key": "my-pk"}}`))
require.NotNil(t, defaultSshPrivateKey)
@@ -2790,7 +2806,7 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) {
}})
require.NoError(t, err)
// Create secret in vault with non default username and password fields
// Create secret in vault with non default username and private key fields
nonDefaultSshPrivateKey := v.CreateKVSecret(t, "non-default-sshpk", []byte(`{"data": {"non-default-user": "my-user", "non-default-pk": "my-special-pk"}}`))
require.NotNil(t, nonDefaultSshPrivateKey)
@@ -2812,6 +2828,49 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) {
}})
require.NoError(t, err)
// Create secret in vault with default username, private key and passphrase fields
defaultSshPrivateKeyWithPass := v.CreateKVSecret(t, "default-sshpk-with-pass", []byte(`{"data": {"username": "my-user", "private_key": "my-pk", "private_key_passphrase": "my-pass"}}`))
require.NotNil(t, defaultSshPrivateKeyWithPass)
clsRespSshPrivateKeyWithPass, err := credLibService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{
CredentialStoreId: vaultStore.GetPublicId(),
Name: wrapperspb.String("SSH Private Key With Passphrase Library"),
Description: wrapperspb.String("SSH Private Key Library Description"),
Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{
VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{
Path: wrapperspb.String(path.Join("secret", "data", "default-sshpk-with-pass")),
HttpMethod: wrapperspb.String("GET"),
},
},
CredentialType: "ssh_private_key",
}})
require.NoError(t, err)
require.NotNil(t, clsRespSshPrivateKeyWithPass)
// Create secret in vault with non default username, private key and private key passphrase fields
nonDefaultSshPrivateKeyWithPass := v.CreateKVSecret(t, "non-default-sshpk-with-pass", []byte(`{"data": {"non-default-user": "my-user", "non-default-pk": "my-special-pk", "embedded": {"secret-pass": "my-special-pass"}}}}`))
require.NotNil(t, nonDefaultSshPrivateKeyWithPass)
clsRespSshPrivateKeyWithPassWithMapping, err := credLibService.CreateCredentialLibrary(ctx, &pbs.CreateCredentialLibraryRequest{Item: &credlibpb.CredentialLibrary{
CredentialStoreId: vaultStore.GetPublicId(),
Name: wrapperspb.String("SSH Private Key With Passphrase Mapping Library"),
Description: wrapperspb.String("SSH Private Key Mapping Library Description"),
Attrs: &credlibpb.CredentialLibrary_VaultCredentialLibraryAttributes{
VaultCredentialLibraryAttributes: &credlibpb.VaultCredentialLibraryAttributes{
Path: wrapperspb.String(path.Join("secret", "data", "non-default-sshpk-with-pass")),
HttpMethod: wrapperspb.String("GET"),
},
},
CredentialType: "ssh_private_key",
CredentialMappingOverrides: &structpb.Struct{Fields: map[string]*structpb.Value{
"username_attribute": structpb.NewStringValue("/data/non-default-user"),
"private_key_attribute": structpb.NewStringValue("/data/non-default-pk"),
"private_key_passphrase_attribute": structpb.NewStringValue("/data/embedded/secret-pass"),
}},
}})
require.NoError(t, err)
require.NotNil(t, clsRespSshPrivateKeyWithPassWithMapping)
cases := []struct {
name string
hostSourceId string
@@ -2992,6 +3051,87 @@ func TestAuthorizeSessionTypedCredentials(t *testing.T) {
}(),
},
},
{
name: "static-ssh-private-key-with-pass",
hostSourceId: shs.GetPublicId(),
credSourceId: sshPkWithPassCredResp.GetItem().GetId(),
wantedHostId: h.GetPublicId(),
wantedEndpoint: h.GetAddress(),
wantedCred: &pb.SessionCredential{
CredentialSource: &pb.CredentialSource{
Id: sshPkWithPassCredResp.GetItem().GetId(),
Name: sshPkWithPassCredResp.GetItem().GetName().GetValue(),
Description: sshPkWithPassCredResp.GetItem().GetDescription().GetValue(),
CredentialStoreId: staticStore.GetPublicId(),
Type: credstatic.Subtype.String(),
CredentialType: string(credential.SshPrivateKeyType),
},
Credential: func() *structpb.Struct {
data := map[string]interface{}{
"private_key_passphrase": testdata.PEMEncryptedKeys[0].EncryptionKey,
"private_key": string(testdata.PEMEncryptedKeys[0].PEMBytes),
"username": "static-username",
}
st, err := structpb.NewStruct(data)
require.NoError(t, err)
return st
}(),
},
},
{
name: "vault-ssh-private-key-with-pass",
hostSourceId: shs.GetPublicId(),
credSourceId: clsRespSshPrivateKeyWithPass.GetItem().GetId(),
wantedHostId: h.GetPublicId(),
wantedEndpoint: h.GetAddress(),
wantedCred: &pb.SessionCredential{
CredentialSource: &pb.CredentialSource{
Id: clsRespSshPrivateKeyWithPass.GetItem().GetId(),
Name: clsRespSshPrivateKeyWithPass.GetItem().GetName().GetValue(),
Description: clsRespSshPrivateKeyWithPass.GetItem().GetDescription().GetValue(),
CredentialStoreId: vaultStore.GetPublicId(),
Type: vault.Subtype.String(),
CredentialType: string(credential.SshPrivateKeyType),
},
Credential: func() *structpb.Struct {
data := map[string]interface{}{
"username": "my-user",
"private_key": "my-pk",
"private_key_passphrase": "my-pass",
}
st, err := structpb.NewStruct(data)
require.NoError(t, err)
return st
}(),
},
},
{
name: "vault-ssh-private-key-with-pass-with-mapping",
hostSourceId: shs.GetPublicId(),
credSourceId: clsRespSshPrivateKeyWithPassWithMapping.GetItem().GetId(),
wantedHostId: h.GetPublicId(),
wantedEndpoint: h.GetAddress(),
wantedCred: &pb.SessionCredential{
CredentialSource: &pb.CredentialSource{
Id: clsRespSshPrivateKeyWithPassWithMapping.GetItem().GetId(),
Name: clsRespSshPrivateKeyWithPassWithMapping.GetItem().GetName().GetValue(),
Description: clsRespSshPrivateKeyWithPassWithMapping.GetItem().GetDescription().GetValue(),
CredentialStoreId: vaultStore.GetPublicId(),
Type: vault.Subtype.String(),
CredentialType: string(credential.SshPrivateKeyType),
},
Credential: func() *structpb.Struct {
data := map[string]interface{}{
"username": "my-user",
"private_key": "my-special-pk",
"private_key_passphrase": "my-special-pass",
}
st, err := structpb.NewStruct(data)
require.NoError(t, err)
return st
}(),
},
},
}
for _, tc := range cases {