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

+24
View File
@@ -134,6 +134,30 @@ func WithSshPrivateKeyCredentialPrivateKey(inPrivateKey string) Option {
}
}
func WithSshPrivateKeyCredentialPrivateKeyPassphrase(inPrivateKeyPassphrase string) Option {
return func(o *options) {
raw, ok := o.postMap["attributes"]
if !ok {
raw = interface{}(map[string]interface{}{})
}
val := raw.(map[string]interface{})
val["private_key_passphrase"] = inPrivateKeyPassphrase
o.postMap["attributes"] = val
}
}
func DefaultSshPrivateKeyCredentialPrivateKeyPassphrase() Option {
return func(o *options) {
raw, ok := o.postMap["attributes"]
if !ok {
raw = interface{}(map[string]interface{}{})
}
val := raw.(map[string]interface{})
val["private_key_passphrase"] = nil
o.postMap["attributes"] = val
}
}
func WithSshPrivateKeyCredentialUsername(inUsername string) Option {
return func(o *options) {
raw, ok := o.postMap["attributes"]
@@ -8,9 +8,11 @@ import (
)
type SshPrivateKeyAttributes struct {
Username string `json:"username,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
PrivateKeyHmac string `json:"private_key_hmac,omitempty"`
Username string `json:"username,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
PrivateKeyHmac string `json:"private_key_hmac,omitempty"`
PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"`
PrivateKeyPassphraseHmac string `json:"private_key_passphrase_hmac,omitempty"`
}
func AttributesMapToSshPrivateKeyAttributes(in map[string]interface{}) (*SshPrivateKeyAttributes, error) {
@@ -19,6 +19,7 @@ type usernamePassword struct {
type sshPrivateKey struct {
Username string `mapstructure:"username"`
PrivateKey string `mapstructure:"private_key"`
Passphrase string `mapstructure:"private_key_passphrase"`
raw *targets.SessionCredential
consumed bool
+13 -7
View File
@@ -71,7 +71,7 @@ func (s *sshFlags) buildArgs(c *Command, port, ip, addr string, creds credential
var password string
if len(retCreds.usernamePassword) > 0 {
// For now just grab the first username password credential brokered
// Mark credential as consumed so it is not printed to user
// Mark credential as consumed so that it is not printed to user
retCreds.usernamePassword[0].consumed = true
username = retCreds.usernamePassword[0].Username
@@ -109,11 +109,17 @@ func (s *sshFlags) buildArgs(c *Command, port, ip, addr string, creds credential
// If we want to consume check if we have a private key available first
case len(creds.sshPrivateKey) > 0:
// For now just grab the first ssh private key credential brokered
// Mark credential as consumed so it is not printed to user
retCreds.sshPrivateKey[0].consumed = true
cred := retCreds.sshPrivateKey[0]
username = retCreds.sshPrivateKey[0].Username
privateKey := retCreds.sshPrivateKey[0].PrivateKey
username = cred.Username
privateKey := cred.PrivateKey
cred.consumed = true
if cred.Passphrase != "" {
// Don't re-print everything, but print the passphrase they'll need
cred.consumed = false
delete(cred.raw.Credential, "username")
delete(cred.raw.Credential, "private_key")
}
pkFile, err := ioutil.TempFile("", "*")
if err != nil {
@@ -126,7 +132,7 @@ func (s *sshFlags) buildArgs(c *Command, port, ip, addr string, creds credential
return nil
})
// SSH requires the private key file to end with a newline.
// When injesting a ssh_private_key from a file:// Boundary calls strings.TrimSpace
// When ingesting an ssh_private_key from a file:// Boundary calls strings.TrimSpace
// which will also trim newlines.
if !strings.HasSuffix(privateKey, "\n") {
privateKey = fmt.Sprintln(privateKey)
@@ -142,7 +148,7 @@ func (s *sshFlags) buildArgs(c *Command, port, ip, addr string, creds credential
// Next check if we have a username password credential
case len(creds.usernamePassword) > 0:
// We cannot use the password of the credential outside of sshpass but we
// We cannot use the password of the credential outside of sshpass, but we
// can use the username.
// N.B. Do not mark credential as consumed, as user will still need enter
// the password when prompted.
@@ -10,9 +10,10 @@ import (
)
const (
usernameFlagName = "username"
passwordFlagName = "password"
privateKeyFlagName = "private-key"
usernameFlagName = "username"
passwordFlagName = "password"
privateKeyFlagName = "private-key"
privateKeyPassphraseFlagName = "private-key-passphrase"
)
func (c *Command) extraHelpFunc(helpMap map[string]func() string) string {
@@ -177,7 +178,8 @@ func printItemTable(item *credentials.Credential, resp *api.Response) string {
}
var keySubstMap = map[string]string{
"username": "Username",
"password_hmac": "Password HMAC",
"private_key_hmac": "Private Key HMAC",
"username": "Username",
"password_hmac": "Password HMAC",
"private_key_hmac": "Private Key HMAC",
"private_key_passphrase_hmac": "Private Key Passphrase HMAC",
}
@@ -3,10 +3,14 @@ package credentialscmd
import (
"errors"
"fmt"
"os"
"strings"
"github.com/hashicorp/boundary/api/credentials"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/go-secure-stdlib/password"
"golang.org/x/crypto/ssh"
)
func init() {
@@ -16,8 +20,9 @@ func init() {
}
type extraSshPrivateKeyCmdVars struct {
flagUsername string
flagPrivateKey string
flagUsername string
flagPrivateKey string
flagPrivateKeyPassphrase string
}
func extraSshPrivateKeyActionsFlagsMapFuncImpl() map[string][]string {
@@ -25,6 +30,7 @@ func extraSshPrivateKeyActionsFlagsMapFuncImpl() map[string][]string {
"create": {
usernameFlagName,
privateKeyFlagName,
privateKeyPassphraseFlagName,
},
}
flags["update"] = flags["create"]
@@ -46,7 +52,13 @@ func extraSshPrivateKeyFlagsFuncImpl(c *SshPrivateKeyCommand, set *base.FlagSets
f.StringVar(&base.StringVar{
Name: privateKeyFlagName,
Target: &c.flagPrivateKey,
Usage: "The SSH private key associated with the credential. This can be the value itself, refer to a file on disk (file://) from which the value will be read, or an env var (env://) from which the value will be read.",
Usage: "The SSH private key associated with the credential. This can refer to a file on disk (file://) from which the value will be read or an env var (env://) from which the value will be read.",
})
case privateKeyPassphraseFlagName:
f.StringVar(&base.StringVar{
Name: privateKeyPassphraseFlagName,
Target: &c.flagPrivateKeyPassphrase,
Usage: "The passphrase associated with the SSH private key. This value is ignored if the private key does not require a passphrase or if no private key is supplied. This can refer to a file on disk (file://) from which the value will be read, or an env var (env://) from which the value will be read. Or, if left empty, if the key requires a passphrase it can be entered manually.",
})
}
}
@@ -58,22 +70,79 @@ func extraSshPrivateKeyFlagHandlingFuncImpl(c *SshPrivateKeyCommand, _ *base.Fla
default:
*opts = append(*opts, credentials.WithSshPrivateKeyCredentialUsername(c.flagUsername))
}
switch c.flagPrivateKey {
case "":
// If private key not set (e.g. just a username update) then don't check
// either private key or passphrase
if c.flagPrivateKey == "" {
return true
}
// First parse private key
privateKey, err := parseutil.MustParsePath(c.flagPrivateKey)
switch {
case err == nil:
case errors.Is(err, parseutil.ErrNotParsed):
c.UI.Error("Private key flag must be used with env:// or file:// syntax")
return false
default:
privateKey, err := parseutil.MustParsePath(c.flagPrivateKey)
c.UI.Error(fmt.Sprintf("Error parsing private key flag: %v", err))
return false
}
// Now validate it
_, err = ssh.ParsePrivateKey([]byte(privateKey))
// If nil, parsed successfully without passphrase, ignore passphrase flag
if err == nil {
if c.flagPrivateKeyPassphrase != "" {
c.UI.Warn("Ignoring private key passphrase as private key is not encrypted.")
}
*opts = append(*opts, credentials.WithSshPrivateKeyCredentialPrivateKey(privateKey))
return true
}
// If an error but it's not a passphrase missing error, send the error back
if err.Error() != (&ssh.PassphraseMissingError{}).Error() {
c.UI.Error(fmt.Sprintf("Error parsing private key: %v", err))
return false
}
// Passphrase required
var privateKeyPassphrase string
switch c.flagPrivateKeyPassphrase {
case "":
fmt.Print("Please enter the passphrase for the private key (it will be hidden): ")
privateKeyPassphrase, err = password.Read(os.Stdin)
fmt.Print("\n")
if err != nil {
c.UI.Error(fmt.Sprintf("An error occurred attempting to read the passphrase. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error()))
return false
}
privateKeyPassphrase = strings.TrimSpace(privateKeyPassphrase)
default:
privateKeyPassphrase, err = parseutil.MustParsePath(c.flagPrivateKeyPassphrase)
switch {
case err == nil:
case errors.Is(err, parseutil.ErrNotParsed):
c.UI.Error("Private key flag must be used with env:// or file:// syntax")
c.UI.Error("Private key passphrase flag must be used with env:// or file:// syntax")
return false
default:
c.UI.Error(fmt.Sprintf("Error parsing private key flag: %v", err))
c.UI.Error(fmt.Sprintf("Error parsing private key passphrase flag: %v", err))
return false
}
*opts = append(*opts, credentials.WithSshPrivateKeyCredentialPrivateKey(privateKey))
}
if _, err = ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(privateKeyPassphrase)); err != nil {
c.UI.Error(fmt.Sprintf("Error parsing private key passphrase: %v", err))
return false
}
*opts = append(*opts,
credentials.WithSshPrivateKeyCredentialPrivateKey(privateKey),
credentials.WithSshPrivateKeyCredentialPrivateKeyPassphrase(privateKeyPassphrase),
)
return true
}
+3 -1
View File
@@ -126,9 +126,11 @@ type UsernamePassword interface {
Password() Password
}
// SshPrivateKey is a credential containing a username and a SSH private key.
// SshPrivateKey is a credential containing a username an SSH private key and
// an optional private key passphrase.
type SshPrivateKey interface {
Credential
Username() string
PrivateKey() PrivateKey
PrivateKeyPassphrase() []byte
}
+6 -5
View File
@@ -2,9 +2,10 @@ package static
// These constants are the field names used in the static related field masks.
const (
nameField = "Name"
descriptionField = "Description"
usernameField = "Username"
passwordField = "Password"
privateKeyField = "PrivateKey"
nameField = "Name"
descriptionField = "Description"
usernameField = "Username"
passwordField = "Password"
privateKeyField = "PrivateKey"
PrivateKeyPassphraseField = "PrivateKeyPassphrase"
)
+12 -4
View File
@@ -14,10 +14,11 @@ type Option func(*options)
// options = how options are represented
type options struct {
withName string
withDescription string
withLimit int
withPublicId string
withName string
withDescription string
withLimit int
withPublicId string
withPrivateKeyPassphrase []byte
}
func getDefaultOptions() options {
@@ -53,3 +54,10 @@ func WithPublicId(name string) Option {
o.withPublicId = name
}
}
// WithPrivateKeyPassphrase provides an optional SSH private key passphrase to use.
func WithPrivateKeyPassphrase(with []byte) Option {
return func(o *options) {
o.withPrivateKeyPassphrase = with
}
}
@@ -26,4 +26,11 @@ func Test_GetOpts(t *testing.T) {
testOpts.withLimit = 5
assert.Equal(t, opts, testOpts)
})
t.Run("WithPrivateKeyPassphrase", func(t *testing.T) {
opts := getOpts(WithPrivateKeyPassphrase([]byte("my-pass")))
testOpts := getDefaultOptions()
assert.NotEqual(t, opts, testOpts)
testOpts.withPrivateKeyPassphrase = []byte("my-pass")
assert.Equal(t, opts, testOpts)
})
}
@@ -106,8 +106,10 @@ func (r *Repository) CreateUsernamePasswordCredential(
// assigned by this method. c must contain a valid StoreId.
//
// The private key is encrypted and a HmacSha256 of the private key is
// calculated. Only the PrivateKeyHmac is returned, the plain-text and encrypted
// private key is not returned.
// calculated. If a passphrase is supplied, it is also encrypted and an HmacSha256
// of passphrase is calculated. Only the PrivateKeyHmac (and
// PrivateKeyPassphraseHmac) is returned, the plain-text and encrypted private
// key and passphrase are not returned.
//
// Both c.Name and c.Description are optional. If c.Name is set, it must be
// unique within c.ScopeId. Both c.CreateTime and c.UpdateTime are ignored.
@@ -183,6 +185,10 @@ func (r *Repository) CreateSshPrivateKeyCredential(
newCred.PrivateKeyEncrypted = nil
newCred.PrivateKey = nil
// Clear passphrase fields, only PrivateKeyPassphraseHmac should be returned if it exists
newCred.PrivateKeyPassphraseEncrypted = nil
newCred.PrivateKeyPassphrase = nil
return newCred, nil
}
@@ -224,6 +230,9 @@ func (r *Repository) LookupCredential(ctx context.Context, publicId string, _ ..
// Clear private key fields, only privateKeyHmac should be returned
spkCred.PrivateKeyEncrypted = nil
spkCred.PrivateKey = nil
// Clear passphrase fields, only PrivateKeyPassphraseHmac should be returned if it exists
spkCred.PrivateKeyPassphraseEncrypted = nil
spkCred.PrivateKeyPassphrase = nil
cred = spkCred
}
@@ -349,9 +358,9 @@ func (r *Repository) UpdateUsernamePasswordCredential(ctx context.Context,
// new SshPrivateKeyCredential containing the updated values and a count of the
// number of records updated. c is not changed.
//
// c must contain a valid PublicId. Only Name, Description, Username and
// PrivateKey can be changed. If c.Name is set to a non-empty string, it must be
// unique within c.ScopeId.
// c must contain a valid PublicId. Only Name, Description, Username,
// PrivateKey and PrivateKeyPassphrase can be changed. If c.Name is set to a non-empty string, it
// must be unique within c.ScopeId.
//
// An attribute of c will be set to NULL in the database if the attribute in c
// is the zero value and it is included in fieldMaskPaths.
@@ -389,16 +398,18 @@ func (r *Repository) UpdateSshPrivateKeyCredential(ctx context.Context,
case strings.EqualFold(descriptionField, f):
case strings.EqualFold(usernameField, f):
case strings.EqualFold(privateKeyField, f):
case strings.EqualFold(PrivateKeyPassphraseField, f):
default:
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f)
}
}
dbMask, nullFields := dbcommon.BuildUpdatePaths(
map[string]interface{}{
nameField: c.Name,
descriptionField: c.Description,
usernameField: c.Username,
privateKeyField: c.PrivateKey,
nameField: c.Name,
descriptionField: c.Description,
usernameField: c.Username,
privateKeyField: c.PrivateKey,
PrivateKeyPassphraseField: c.PrivateKeyPassphrase,
},
fieldMaskPaths,
nil,
@@ -407,19 +418,30 @@ func (r *Repository) UpdateSshPrivateKeyCredential(ctx context.Context,
return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask")
}
var performedEncryption bool
for _, f := range fieldMaskPaths {
if strings.EqualFold(privateKeyField, f) {
// Private key has been updated, re-encrypt and recalculate hmac
databaseWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
}
if err := c.encrypt(ctx, databaseWrapper); err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
if strings.EqualFold(privateKeyField, f) || strings.EqualFold(PrivateKeyPassphraseField, f) {
if !performedEncryption {
// We don't need to encrypt twice so keep track
performedEncryption = true
databaseWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
}
if err := c.encrypt(ctx, databaseWrapper); err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
}
// Set PrivateKeyHmac and PrivateKeyEncrypted masks for update.
dbMask = append(dbMask, "PrivateKeyHmac", "PrivateKeyEncrypted")
if strings.EqualFold(privateKeyField, f) {
// Set PrivateKeyHmac and PrivateKeyEncrypted masks for update.
dbMask = append(dbMask, "PrivateKeyHmac", "PrivateKeyEncrypted")
}
if strings.EqualFold(PrivateKeyPassphraseField, f) {
// Set PassphraseHmac and PassphraseEncrypted masks for update.
dbMask = append(dbMask, "PrivateKeyPassphraseHmac", "PrivateKeyPassphraseEncrypted")
}
}
}
@@ -456,6 +478,10 @@ func (r *Repository) UpdateSshPrivateKeyCredential(ctx context.Context,
returnedCredential.PrivateKeyEncrypted = nil
returnedCredential.PrivateKey = nil
// Clear passphrase fields, only PrivateKeyPassphraseHmac should be returned if it exists
returnedCredential.PrivateKeyPassphraseEncrypted = nil
returnedCredential.PrivateKeyPassphrase = nil
return returnedCredential, rowsUpdated, nil
}
@@ -499,6 +525,10 @@ func (r *Repository) ListCredentials(ctx context.Context, storeId string, opt ..
// Clear private key fields, only PrivateKeyHmac should be returned
c.PrivateKeyEncrypted = nil
c.PrivateKey = nil
// Clear passphrase fields, only PrivateKeyPassphraseHmac should be returned if it exists
c.PrivateKeyPassphraseEncrypted = nil
c.PrivateKeyPassphrase = nil
ret = append(ret, c)
}
@@ -16,6 +16,7 @@ import (
"github.com/hashicorp/boundary/internal/oplog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh/testdata"
)
func TestRepository_CreateUsernamePasswordCredential(t *testing.T) {
@@ -276,6 +277,18 @@ func TestRepository_CreateSshPrivateKeyCredential(t *testing.T) {
},
},
},
{
name: "valid-with-passphrase",
scopeId: prj.PublicId,
cred: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "my-user",
PrivateKey: []byte(TestSshPrivateKeyPem),
StoreId: cs.PublicId,
PrivateKeyPassphrase: []byte("passphrase"),
},
},
},
}
for _, tt := range tests {
tt := tt
@@ -298,8 +311,10 @@ func TestRepository_CreateSshPrivateKeyCredential(t *testing.T) {
assert.Equal(tt.cred.Username, got.Username)
assert.Nil(got.PrivateKey)
assert.Nil(got.PrivateKeyEncrypted)
assert.Nil(got.PrivateKeyPassphrase)
assert.Nil(got.PrivateKeyPassphraseEncrypted)
// Validate password
// Validate private key and passphrase
lookupCred := allocSshPrivateKeyCredential()
lookupCred.PublicId = got.PublicId
require.NoError(rw.LookupById(ctx, lookupCred))
@@ -318,6 +333,15 @@ func TestRepository_CreateSshPrivateKeyCredential(t *testing.T) {
require.NoError(err)
assert.Equal([]byte(hm), got.PrivateKeyHmac)
// Validate passphrase
assert.Equal(tt.cred.PrivateKeyPassphrase, lookupCred.PrivateKeyPassphrase)
if len(tt.cred.PrivateKeyPassphrase) > 0 {
assert.NotEmpty(got.PrivateKeyPassphraseHmac)
hm, err := crypto.HmacSha256(ctx, tt.cred.PrivateKeyPassphrase, databaseWrapper, []byte(tt.cred.StoreId), nil)
require.NoError(err)
assert.Equal([]byte(hm), got.PrivateKeyPassphraseHmac)
}
// Validate oplog
assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)))
})
@@ -372,6 +396,8 @@ func TestRepository_LookupCredential(t *testing.T) {
store := TestCredentialStore(t, conn, wrapper, prj.PublicId)
upCred := TestUsernamePasswordCredential(t, conn, wrapper, "username", "password", store.PublicId, prj.PublicId)
spkCred := TestSshPrivateKeyCredential(t, conn, wrapper, "username", TestSshPrivateKeyPem, store.PublicId, prj.PublicId)
spkCredWithPass := TestSshPrivateKeyCredential(t, conn, wrapper, "username", string(testdata.PEMEncryptedKeys[0].PEMBytes),
store.PublicId, prj.PublicId, WithPrivateKeyPassphrase([]byte(testdata.PEMEncryptedKeys[0].EncryptionKey)))
tests := []struct {
name string
@@ -389,6 +415,11 @@ func TestRepository_LookupCredential(t *testing.T) {
id: spkCred.GetPublicId(),
want: spkCred,
},
{
name: "spk-valid-with-passphrase",
id: spkCredWithPass.GetPublicId(),
want: spkCredWithPass,
},
{
name: "empty-public-id",
id: "",
@@ -433,6 +464,14 @@ func TestRepository_LookupCredential(t *testing.T) {
assert.Empty(v.PrivateKey)
assert.Empty(v.PrivateKeyEncrypted)
assert.NotEmpty(v.PrivateKeyHmac)
want, ok := tt.want.(*SshPrivateKeyCredential)
require.True(ok)
assert.Empty(v.PrivateKeyPassphrase)
assert.Empty(v.PrivateKeyPassphraseEncrypted)
if len(want.PrivateKeyPassphrase) > 0 {
assert.NotEmpty(v.PrivateKeyPassphraseHmac)
}
default:
require.Fail("unknown type")
}
@@ -1175,6 +1214,13 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ=
}
}
changePrivateKeyPassphrase := func(d string) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential {
c.PrivateKeyPassphrase = []byte(d)
return c
}
}
combine := func(fns ...func(cs *SshPrivateKeyCredential) *SshPrivateKeyCredential) func(*SshPrivateKeyCredential) *SshPrivateKeyCredential {
return func(c *SshPrivateKeyCredential) *SshPrivateKeyCredential {
for _, fn := range fns {
@@ -1385,7 +1431,7 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ=
wantCount: 1,
},
{
name: "change-private-keyu",
name: "change-private-key",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
@@ -1402,6 +1448,26 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ=
},
wantCount: 1,
},
{
name: "change-private-key-and-passphrase",
orig: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(TestSshPrivateKeyPem),
PrivateKeyPassphrase: []byte("foobar"),
},
},
chgFn: combine(changePrivateKey(testSecondarySshPrivateKeyPem), changePrivateKeyPassphrase("barfoo")),
masks: []string{"PrivateKeyPassphrase"},
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "user",
PrivateKey: []byte(testSecondarySshPrivateKeyPem),
PrivateKeyPassphrase: []byte("barfoo"),
},
},
wantCount: 1,
},
{
name: "change-username-and-private-key",
orig: &SshPrivateKeyCredential{
@@ -1620,3 +1686,76 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ=
})
}
}
func TestSshPrivateKeyConstraints(t *testing.T) {
t.Parallel()
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
assert, require := assert.New(t), require.New(t)
ctx := context.Background()
kkms := kms.TestKms(t, conn, wrapper)
repo, err := NewRepository(ctx, rw, rw, kkms)
assert.NoError(err)
require.NotNil(repo)
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId())
// Base case: should work fine
cred, err := NewSshPrivateKeyCredential(
ctx,
store.PublicId,
"foobar",
credential.PrivateKey(testdata.PEMEncryptedKeys[0].PEMBytes),
WithPrivateKeyPassphrase([]byte(testdata.PEMEncryptedKeys[0].EncryptionKey)))
require.NoError(err)
cred.PublicId, err = credential.NewSshPrivateKeyCredentialId(ctx)
require.NoError(err)
databaseWrapper, err := kkms.GetWrapper(ctx, prj.PublicId, kms.KeyPurposeDatabase)
require.NoError(cred.encrypt(ctx, databaseWrapper))
tests := []struct {
name string
nilEncrypted bool
nilHmac bool
}{
{
name: "valid",
},
{
name: "nil-encrypted",
nilEncrypted: true,
},
{
name: "nil-hmac",
nilHmac: true,
},
{
name: "valid-both-nil",
nilEncrypted: true,
nilHmac: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
c := cred.clone()
if tt.nilEncrypted {
c.PrivateKeyPassphraseEncrypted = nil
}
if tt.nilHmac {
c.PrivateKeyPassphraseHmac = nil
}
c.PublicId, err = credential.NewSshPrivateKeyCredentialId(ctx)
require.NoError(err)
err := rw.Create(ctx, c)
switch {
case !tt.nilEncrypted && !tt.nilHmac, tt.nilEncrypted && tt.nilHmac:
assert.NoError(err)
default:
assert.Error(err)
}
})
}
}
@@ -35,6 +35,9 @@ func TestRepository_Retrieve(t *testing.T) {
upCred2 := TestUsernamePasswordCredential(t, conn, wrapper, "different user", "better password", staticStore.GetPublicId(), prj.GetPublicId())
spkCred1 := TestSshPrivateKeyCredential(t, conn, wrapper, "final user", string(testdata.PEMBytes["ed25519"]), staticStore.GetPublicId(), prj.GetPublicId())
spkCred2 := TestSshPrivateKeyCredential(t, conn, wrapper, "last user", string(testdata.PEMBytes["rsa-openssh-format"]), staticStore.GetPublicId(), prj.GetPublicId())
spkCredWithPass := TestSshPrivateKeyCredential(t, conn, wrapper, "another last user",
string(testdata.PEMEncryptedKeys[0].PEMBytes), staticStore.GetPublicId(), prj.GetPublicId(),
WithPrivateKeyPassphrase([]byte(testdata.PEMEncryptedKeys[0].EncryptionKey)))
type args struct {
credIds []string
@@ -95,20 +98,20 @@ func TestRepository_Retrieve(t *testing.T) {
name: "valid-multiple-ssh-pk-creds",
args: args{
scopeId: prj.GetPublicId(),
credIds: []string{spkCred1.GetPublicId(), spkCred2.GetPublicId()},
credIds: []string{spkCred1.GetPublicId(), spkCred2.GetPublicId(), spkCredWithPass.GetPublicId()},
},
wantCreds: []credential.Static{
spkCred1, spkCred2,
spkCred1, spkCred2, spkCredWithPass,
},
},
{
name: "valid-mixed-creds",
args: args{
scopeId: prj.GetPublicId(),
credIds: []string{upCred1.GetPublicId(), spkCred1.GetPublicId(), spkCred2.GetPublicId(), upCred2.GetPublicId()},
credIds: []string{upCred1.GetPublicId(), spkCred1.GetPublicId(), spkCredWithPass.GetPublicId(), spkCred2.GetPublicId(), upCred2.GetPublicId()},
},
wantCreds: []credential.Static{
upCred1, spkCred1, spkCred2, upCred2,
upCred1, spkCred1, spkCred2, upCred2, spkCredWithPass,
},
},
}
@@ -130,6 +133,7 @@ func TestRepository_Retrieve(t *testing.T) {
UsernamePasswordCredential{}, store.UsernamePasswordCredential{},
SshPrivateKeyCredential{}, store.SshPrivateKeyCredential{}),
cmpopts.IgnoreTypes(&timestamp.Timestamp{}),
cmpopts.IgnoreFields(SshPrivateKeyCredential{}, "PassphraseUnneeded"),
cmpopts.SortSlices(func(x, y credential.Static) bool {
return x.GetPublicId() < y.GetPublicId()
}),
@@ -9,7 +9,6 @@ import (
"github.com/hashicorp/boundary/internal/libs/crypto"
"github.com/hashicorp/boundary/internal/oplog"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
"golang.org/x/crypto/ssh"
"google.golang.org/protobuf/proto"
)
@@ -20,7 +19,8 @@ var _ credential.Static = (*SshPrivateKeyCredential)(nil)
// It is owned by a credential store.
type SshPrivateKeyCredential struct {
*store.SshPrivateKeyCredential
tableName string `gorm:"-"`
tableName string `gorm:"-"`
PassphraseUnneeded bool `gorm:"-"`
}
// NewSshPrivateKeyCredential creates a new in memory static Credential containing a
@@ -35,8 +35,14 @@ func NewSshPrivateKeyCredential(
) (*SshPrivateKeyCredential, error) {
const op = "static.NewSshPrivateKeyCredential"
opts := getOpts(opt...)
if len(privateKey) != 0 {
_, err := ssh.ParsePrivateKey(privateKey)
var err error
if len(opts.withPrivateKeyPassphrase) == 0 {
_, err = ssh.ParsePrivateKey(privateKey)
} else {
_, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, opts.withPrivateKeyPassphrase)
}
switch {
case err == nil:
case err.Error() == (&ssh.PassphraseMissingError{}).Error():
@@ -46,16 +52,27 @@ func NewSshPrivateKeyCredential(
}
}
opts := getOpts(opt...)
l := &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
StoreId: storeId,
Name: opts.withName,
Description: opts.withDescription,
Username: username,
PrivateKey: privateKey,
StoreId: storeId,
Name: opts.withName,
Description: opts.withDescription,
Username: username,
PrivateKey: privateKey,
PrivateKeyPassphrase: opts.withPrivateKeyPassphrase,
},
}
// If a private key was given and no passphrase was given and everything is
// okay, we can nil out any passphrase we have, so set this as a hint
if len(privateKey) != 0 && len(opts.withPrivateKeyPassphrase) == 0 {
l.PassphraseUnneeded = true
// These shouldn't be set, but safety
l.PrivateKeyPassphrase = nil
l.PrivateKeyPassphraseEncrypted = nil
l.PrivateKeyPassphraseHmac = nil
}
return l, nil
}
@@ -99,28 +116,77 @@ func (c *SshPrivateKeyCredential) oplog(op oplog.OpType) oplog.Metadata {
func (c *SshPrivateKeyCredential) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "static.(SshPrivateKeyCredential).encrypt"
if len(c.PrivateKey) == 0 {
return errors.New(ctx, errors.InvalidParameter, op, "no private key defined")
}
if err := structwrapping.WrapStruct(ctx, cipher, c.SshPrivateKeyCredential, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
}
keyId, err := cipher.KeyId(ctx)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("error reading cipher key id"))
}
c.KeyId = keyId
// Encrypt private key
blobInfo, err := cipher.Encrypt(ctx, c.PrivateKey)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
}
protoBytes, err := proto.Marshal(blobInfo)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encode))
}
c.PrivateKeyEncrypted = protoBytes
if err := c.hmacPrivateKey(ctx, cipher); err != nil {
return errors.Wrap(ctx, err, op)
}
if len(c.PrivateKeyPassphrase) > 0 {
// Encrypt passphrase
blobInfo, err := cipher.Encrypt(ctx, c.PrivateKeyPassphrase)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
}
protoBytes, err := proto.Marshal(blobInfo)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encode))
}
c.PrivateKeyPassphraseEncrypted = protoBytes
if err := c.hmacPrivateKeyPassphrase(ctx, cipher); err != nil {
return errors.Wrap(ctx, err, op)
}
}
return nil
}
func (c *SshPrivateKeyCredential) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "static.(SshPrivateKeyCredential).decrypt"
if err := structwrapping.UnwrapStruct(ctx, cipher, c.SshPrivateKeyCredential, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
if len(c.PrivateKeyEncrypted) > 0 {
dec := new(wrapping.BlobInfo)
if err := proto.Unmarshal(c.PrivateKeyEncrypted, dec); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decode))
}
pt, err := cipher.Decrypt(ctx, dec)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
}
c.PrivateKey = pt
}
if len(c.PrivateKeyPassphraseEncrypted) > 0 {
dec := new(wrapping.BlobInfo)
if err := proto.Unmarshal(c.PrivateKeyPassphraseEncrypted, dec); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decode))
}
pt, err := cipher.Decrypt(ctx, dec)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
}
c.PrivateKeyPassphrase = pt
}
return nil
}
@@ -136,3 +202,16 @@ func (c *SshPrivateKeyCredential) hmacPrivateKey(ctx context.Context, cipher wra
c.PrivateKeyHmac = []byte(hm)
return nil
}
func (c *SshPrivateKeyCredential) hmacPrivateKeyPassphrase(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "static.(SshPrivateKeyCredential).hmacPrivateKeyPassphrase"
if cipher == nil {
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
}
hm, err := crypto.HmacSha256(ctx, c.PrivateKeyPassphrase, cipher, []byte(c.StoreId), nil)
if err != nil {
return errors.Wrap(ctx, err, op)
}
c.PrivateKeyPassphraseHmac = []byte(hm)
return nil
}
@@ -13,6 +13,7 @@ import (
"github.com/hashicorp/boundary/internal/libs/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh/testdata"
"google.golang.org/protobuf/testing/protocmp"
)
@@ -24,6 +25,8 @@ func TestSshPrivateKeyCredential_New(t *testing.T) {
rw := db.New(conn)
privKey := []byte(TestSshPrivateKeyPem)
ePrivKey := testdata.PEMEncryptedKeys[0].PEMBytes
ePrivKeyPass := testdata.PEMEncryptedKeys[0].EncryptionKey
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
cs := TestCredentialStore(t, conn, wrapper, prj.PublicId)
@@ -129,6 +132,33 @@ func TestSshPrivateKeyCredential_New(t *testing.T) {
},
},
},
{
name: "invalid-passphrase",
args: args{
username: "test-user",
sshPrivateKey: privKey,
storeId: cs.PublicId,
options: []Option{WithPrivateKeyPassphrase([]byte(ePrivKeyPass))},
},
wantAllocError: true,
},
{
name: "valid-with-passphrase",
args: args{
username: "test-user",
sshPrivateKey: ePrivKey,
storeId: cs.PublicId,
options: []Option{WithPrivateKeyPassphrase([]byte(ePrivKeyPass))},
},
want: &SshPrivateKeyCredential{
SshPrivateKeyCredential: &store.SshPrivateKeyCredential{
Username: "test-user",
PrivateKey: ePrivKey,
StoreId: cs.PublicId,
PrivateKeyPassphrase: []byte(ePrivKeyPass),
},
},
},
}
for _, tt := range tests {
@@ -184,12 +214,20 @@ func TestSshPrivateKeyCredential_New(t *testing.T) {
// KeyId is allocated via kms no need to validate in this test
tt.want.KeyId = got2.KeyId
got2.PrivateKeyEncrypted = nil
got2.PrivateKeyPassphraseEncrypted = nil
// encrypt also calculates the hmac, validate it is correct
hm, err := crypto.HmacSha256(ctx, got.PrivateKey, databaseWrapper, []byte(got.StoreId), nil)
require.NoError(err)
tt.want.PrivateKeyHmac = []byte(hm)
if len(got.PrivateKeyPassphrase) > 0 {
// encrypt also calculates the hmac, validate it is correct
hm, err = crypto.HmacSha256(ctx, got.PrivateKeyPassphrase, databaseWrapper, []byte(got.StoreId), nil)
require.NoError(err)
tt.want.PrivateKeyPassphraseHmac = []byte(hm)
}
assert.Empty(cmp.Diff(tt.want, got2.clone(), protocmp.Transform()))
})
}
+61 -7
View File
@@ -347,6 +347,19 @@ type SshPrivateKeyCredential struct {
// It must be set.
// @inject_tag: `gorm:"not_null"`
KeyId string `protobuf:"bytes,12,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"`
// private_key_passphrase is the plain-text of the passphrase of the SSH private
// key associated with the credential. We are not storing this plain-text
// private key passphrase in the database.
// @inject_tag: `gorm:"-"`
PrivateKeyPassphrase []byte `protobuf:"bytes,13,opt,name=private_key_passphrase,json=privateKeyPassphrase,proto3" json:"private_key_passphrase,omitempty" gorm:"-"`
// private_key_passphrase_encrypted is the ciphertext of the private key passphrase. It is stored in
// the database.
// @inject_tag: `gorm:"column:private_key_passphrase_encrypted;not_null"`
PrivateKeyPassphraseEncrypted []byte `protobuf:"bytes,14,opt,name=private_key_passphrase_encrypted,json=privateKeyPassphraseEncrypted,proto3" json:"private_key_passphrase_encrypted,omitempty" gorm:"column:private_key_passphrase_encrypted;not_null"`
// private_key_passphrase_hmac is a sha256-hmac of the unencrypted private key passphrase. It is recalculated
// everytime the private key passphrase is updated.
// @inject_tag: `gorm:"not_null"`
PrivateKeyPassphraseHmac []byte `protobuf:"bytes,15,opt,name=private_key_passphrase_hmac,json=privateKeyPassphraseHmac,proto3" json:"private_key_passphrase_hmac,omitempty" gorm:"not_null"`
}
func (x *SshPrivateKeyCredential) Reset() {
@@ -465,6 +478,27 @@ func (x *SshPrivateKeyCredential) GetKeyId() string {
return ""
}
func (x *SshPrivateKeyCredential) GetPrivateKeyPassphrase() []byte {
if x != nil {
return x.PrivateKeyPassphrase
}
return nil
}
func (x *SshPrivateKeyCredential) GetPrivateKeyPassphraseEncrypted() []byte {
if x != nil {
return x.PrivateKeyPassphraseEncrypted
}
return nil
}
func (x *SshPrivateKeyCredential) GetPrivateKeyPassphraseHmac() []byte {
if x != nil {
return x.PrivateKeyPassphraseHmac
}
return nil
}
var File_controller_storage_credential_static_store_v1_static_proto protoreflect.FileDescriptor
var file_controller_storage_credential_static_store_v1_static_proto_rawDesc = []byte{
@@ -543,7 +577,7 @@ var file_controller_storage_credential_static_store_v1_static_proto_rawDesc = []
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x52, 0x0c, 0x70, 0x61, 0x73,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x6d, 0x61, 0x63, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79,
0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64,
0x22, 0xa1, 0x05, 0x0a, 0x17, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b,
0x22, 0xe7, 0x07, 0x0a, 0x17, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b,
0x65, 0x79, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x0a, 0x09,
0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65,
@@ -585,12 +619,32 @@ var file_controller_storage_credential_static_store_v1_static_proto_rawDesc = []
0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x52, 0x0e, 0x70,
0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x48, 0x6d, 0x61, 0x63, 0x12, 0x15, 0x0a,
0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b,
0x65, 0x79, 0x49, 0x64, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75,
0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63,
0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63,
0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
0x65, 0x79, 0x49, 0x64, 0x12, 0x73, 0x0a, 0x16, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f,
0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x18, 0x0d,
0x20, 0x01, 0x28, 0x0c, 0x42, 0x3d, 0xc2, 0xdd, 0x29, 0x39, 0x0a, 0x14, 0x50, 0x72, 0x69, 0x76,
0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65,
0x12, 0x21, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69,
0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72,
0x61, 0x73, 0x65, 0x52, 0x14, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50,
0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x20, 0x70, 0x72, 0x69,
0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72,
0x61, 0x73, 0x65, 0x5f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x0e, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x1d, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50,
0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74,
0x65, 0x64, 0x12, 0x85, 0x01, 0x0a, 0x1b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b,
0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x68, 0x6d,
0x61, 0x63, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x46, 0xc2, 0xdd, 0x29, 0x42, 0x0a, 0x18,
0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68,
0x72, 0x61, 0x73, 0x65, 0x48, 0x6d, 0x61, 0x63, 0x12, 0x26, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62,
0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79,
0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x68, 0x6d, 0x61, 0x63,
0x52, 0x18, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73,
0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x48, 0x6d, 0x61, 0x63, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f,
0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2f,
0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f,
0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -13,34 +13,36 @@ type (
// extractFunc attempts to extract the username and private key
// from sd using the provided attribute names, using a known
// Vault data response format.
extractFunc func(sd data, usernameAttr, privateKeyAttr string) (string, credential.PrivateKey)
extractFunc func(sd data, usernameAttr, privateKeyAttr, passphraseAttr string) (string, credential.PrivateKey, []byte)
)
// Extract attempts to extract the values of the username and private key
// stored within the provided data using the given attribute names.
// Extract attempts to extract the values of the username, private key and optional
// passphrase stored within the provided data using the given attribute names.
//
// Extract does not return partial results, i.e. if one of the attributes
// were extracted but not the other ("", nil) will be returned.
func Extract(d data, usernameAttr, privateKeyAttr string) (string, credential.PrivateKey) {
func Extract(d data, usernameAttr, privateKeyAttr, passphraseAttr string) (string, credential.PrivateKey, []byte) {
for _, f := range []extractFunc{
defaultExtract,
kv2Extract,
} {
username, privateKey := f(d, usernameAttr, privateKeyAttr)
username, privateKey, passphrase := f(d, usernameAttr, privateKeyAttr, passphraseAttr)
if username != "" && privateKey != nil {
// got valid username and privateKey from secret
return username, privateKey
return username, privateKey, passphrase
}
}
return "", nil
return "", nil, nil
}
// defaultExtract looks for the usernameAttr and privateKeyAttr in the data map
func defaultExtract(sd data, usernameAttr, privateKeyAttr string) (username string, privateKey credential.PrivateKey) {
func defaultExtract(sd data, usernameAttr, privateKeyAttr, passphraseAttr string) (
username string, privateKey credential.PrivateKey, passphrase []byte,
) {
if sd == nil {
// nothing to do return early
return "", nil
return "", nil, nil
}
var u any
@@ -49,9 +51,8 @@ func defaultExtract(sd data, usernameAttr, privateKeyAttr string) (username stri
var err error
u, err = pointerstructure.Get(sd, usernameAttr)
if err != nil {
return "", nil
return "", nil, nil
}
default:
u = sd[usernameAttr]
}
@@ -59,24 +60,37 @@ func defaultExtract(sd data, usernameAttr, privateKeyAttr string) (username stri
username = u
}
var p any
var pk any
switch {
case strings.HasPrefix(privateKeyAttr, "/"):
var err error
p, err = pointerstructure.Get(sd, privateKeyAttr)
pk, err = pointerstructure.Get(sd, privateKeyAttr)
if err != nil {
return "", nil
return "", nil, nil
}
default:
p = sd[privateKeyAttr]
pk = sd[privateKeyAttr]
}
if p, ok := p.(string); ok {
if p, ok := pk.(string); ok {
privateKey = credential.PrivateKey(p)
}
return username, privateKey
var pass any
switch {
case strings.HasPrefix(passphraseAttr, "/"):
var err error
pass, err = pointerstructure.Get(sd, passphraseAttr)
if err != nil {
return "", nil, nil
}
default:
pass = sd[passphraseAttr]
}
if p, ok := pass.(string); ok {
passphrase = []byte(p)
}
return username, privateKey, passphrase
}
// kv2Extract looks for the the usernameAttr and privateKeyAttr in the embedded
@@ -89,10 +103,12 @@ func defaultExtract(sd data, usernameAttr, privateKeyAttr string) (username stri
// }
// If the format does not match, it returns ("", ""). See:
// https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1
func kv2Extract(sd data, usernameAttr, privateKeyAttr string) (username string, privateKey credential.PrivateKey) {
func kv2Extract(sd data, usernameAttr, privateKeyAttr, passphraseAttr string) (
username string, privateKey credential.PrivateKey, passphrase []byte,
) {
if sd == nil {
// nothing to do return early
return "", nil
return "", nil, nil
}
var data, metadata map[string]any
@@ -102,22 +118,22 @@ func kv2Extract(sd data, usernameAttr, privateKeyAttr string) (username string,
var ok bool
if data, ok = v.(map[string]any); !ok {
// data field should be of type map[string]any in KV-v2
return "", nil
return "", nil, nil
}
case "metadata":
var ok bool
if metadata, ok = v.(map[string]any); !ok {
// metadata field should be of type map[string]any in KV-v2
return "", nil
return "", nil, nil
}
default:
// secretData contains a non valid KV-v2 top level field
return "", nil
return "", nil, nil
}
}
if data == nil || metadata == nil {
// missing required KV-v2 field
return "", nil
return "", nil, nil
}
if u, ok := data[usernameAttr]; ok {
@@ -130,6 +146,11 @@ func kv2Extract(sd data, usernameAttr, privateKeyAttr string) (username string,
privateKey = credential.PrivateKey(p)
}
}
if p, ok := data[passphraseAttr]; ok {
if p, ok := p.(string); ok {
passphrase = []byte(p)
}
}
return username, privateKey
return username, privateKey, passphrase
}
@@ -15,13 +15,15 @@ func TestExtract(t *testing.T) {
rsaKey := testdata.PEMBytes["rsa"]
type args struct {
s data
uAttr string
pAttr string
s data
uAttr string
pkAttr string
pAttr string
}
type sshpk struct {
user string
privateKey credential.PrivateKey
passphrase []byte
}
tests := []struct {
name string
@@ -30,20 +32,20 @@ func TestExtract(t *testing.T) {
}{
{
name: "nil-input",
want: sshpk{user: "", privateKey: nil},
want: sshpk{user: "", privateKey: nil, passphrase: nil},
},
{
name: "no-input",
given: args{},
want: sshpk{user: "", privateKey: nil},
want: sshpk{user: "", privateKey: nil, passphrase: nil},
},
{
name: "no-secret",
given: args{
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
},
want: sshpk{user: "", privateKey: nil},
want: sshpk{user: "", privateKey: nil, passphrase: nil},
},
{
name: "no-match-username-secret",
@@ -52,10 +54,10 @@ func TestExtract(t *testing.T) {
"username-wrong": "user",
"private_key": string(edKey),
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
},
want: sshpk{user: "", privateKey: nil},
want: sshpk{user: "", privateKey: nil, passphrase: nil},
},
{
name: "no-match-private-key-secret",
@@ -64,10 +66,10 @@ func TestExtract(t *testing.T) {
"username": "user",
"private_key-wrong": string(edKey),
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
},
want: sshpk{user: "", privateKey: nil},
want: sshpk{user: "", privateKey: nil, passphrase: nil},
},
{
name: "valid-default",
@@ -76,22 +78,24 @@ func TestExtract(t *testing.T) {
"username": "user",
"private_key": string(edKey),
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
},
want: sshpk{user: "user", privateKey: edKey},
want: sshpk{user: "user", privateKey: edKey, passphrase: nil},
},
{
name: "valid-default-private-key-string",
name: "valid-default-with-passphrase",
given: args{
s: data{
"username": "user",
"private_key": string(edKey),
"passphrase": "my-pass",
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
pAttr: "passphrase",
},
want: sshpk{user: "user", privateKey: edKey},
want: sshpk{user: "user", privateKey: edKey, passphrase: []byte("my-pass")},
},
{
name: "no-match-username-secret-kv2",
@@ -103,10 +107,10 @@ func TestExtract(t *testing.T) {
"private_key": string(edKey),
},
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
},
want: sshpk{user: "", privateKey: nil},
want: sshpk{user: "", privateKey: nil, passphrase: nil},
},
{
name: "no-match-private-key-secret-kv2",
@@ -118,10 +122,10 @@ func TestExtract(t *testing.T) {
"private_key-wrong": string(edKey),
},
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
},
want: sshpk{user: "", privateKey: nil},
want: sshpk{user: "", privateKey: nil, passphrase: nil},
},
{
name: "valid-kv2",
@@ -133,25 +137,27 @@ func TestExtract(t *testing.T) {
"private_key": string(edKey),
},
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
},
want: sshpk{user: "user", privateKey: edKey},
want: sshpk{user: "user", privateKey: edKey, passphrase: nil},
},
{
name: "valid-kv2-private-key-string",
name: "valid-kv2-with-passphrase",
given: args{
s: data{
"metadata": map[string]interface{}{},
"data": map[string]interface{}{
"username": "user",
"private_key": string(edKey),
"passphrase": "my-pass",
},
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
pAttr: "passphrase",
},
want: sshpk{user: "user", privateKey: edKey},
want: sshpk{user: "user", privateKey: edKey, passphrase: []byte("my-pass")},
},
{
name: "no-metadata-kv2",
@@ -162,10 +168,10 @@ func TestExtract(t *testing.T) {
"private_key": string(edKey),
},
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
},
want: sshpk{user: "", privateKey: nil},
want: sshpk{user: "", privateKey: nil, passphrase: nil},
},
{
name: "invalid-metadata-kv2",
@@ -177,10 +183,10 @@ func TestExtract(t *testing.T) {
"private_key": string(edKey),
},
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
},
want: sshpk{user: "", privateKey: nil},
want: sshpk{user: "", privateKey: nil, passphrase: nil},
},
{
name: "invalid-field-kv2",
@@ -193,10 +199,10 @@ func TestExtract(t *testing.T) {
"private_key": string(edKey),
},
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
},
want: sshpk{user: "", privateKey: nil},
want: sshpk{user: "", privateKey: nil, passphrase: nil},
},
{
name: "valid-order-default-first",
@@ -210,10 +216,10 @@ func TestExtract(t *testing.T) {
"private_key": string(edKey),
},
},
uAttr: "username",
pAttr: "private_key",
uAttr: "username",
pkAttr: "private_key",
},
want: sshpk{user: "default-user", privateKey: rsaKey},
want: sshpk{user: "default-user", privateKey: rsaKey, passphrase: nil},
},
{
name: "default-user-json-pointer-pk",
@@ -224,10 +230,10 @@ func TestExtract(t *testing.T) {
"private_key": string(edKey),
},
},
uAttr: "username",
pAttr: "/testing/private_key",
uAttr: "username",
pkAttr: "/testing/private_key",
},
want: sshpk{user: "default-user", privateKey: edKey},
want: sshpk{user: "default-user", privateKey: edKey, passphrase: nil},
},
{
name: "default-pk-json-pointer-user",
@@ -238,13 +244,13 @@ func TestExtract(t *testing.T) {
"special": "not-so-special",
},
},
uAttr: "/testing/special",
pAttr: "private_key",
uAttr: "/testing/special",
pkAttr: "private_key",
},
want: sshpk{user: "not-so-special", privateKey: edKey},
want: sshpk{user: "not-so-special", privateKey: edKey, passphrase: nil},
},
{
name: "both-json-pointer",
name: "all-json-pointer",
given: args{
s: data{
"first-path": map[string]interface{}{
@@ -255,20 +261,25 @@ func TestExtract(t *testing.T) {
"testing": map[string]interface{}{
"private_key": string(edKey),
},
"hidden": map[string]interface{}{
"pass": "my-pass",
},
},
uAttr: "/first-path/deeper-path/my-special-user",
pAttr: "/testing/private_key",
uAttr: "/first-path/deeper-path/my-special-user",
pkAttr: "/testing/private_key",
pAttr: "/hidden/pass",
},
want: sshpk{user: "you-found-me", privateKey: edKey},
want: sshpk{user: "you-found-me", privateKey: edKey, passphrase: []byte("my-pass")},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
user, privateKey := Extract(tt.given.s, tt.given.uAttr, tt.given.pAttr)
user, privateKey, passphrase := Extract(tt.given.s, tt.given.uAttr, tt.given.pkAttr, tt.given.pAttr)
assert.Equal(tt.want.user, user)
assert.Equal(tt.want.privateKey, privateKey)
assert.Equal(tt.want.passphrase, passphrase)
})
}
}
@@ -110,14 +110,15 @@ type SshPrivateKeyOverride struct {
var _ MappingOverride = (*SshPrivateKeyOverride)(nil)
// NewSshPrivateKeyOverride creates a new in memory SshPrivateKeyOverride.
// WithOverrideUsernameAttribute and WithOverridePasswordAttribute are the
// only valid options. All other options are ignored.
// WithOverrideUsernameAttribute, WithOverridePrivateKeyAttribute and WithOverridePrivateKeyPassphraseAttribute
// are the only valid options. All other options are ignored.
func NewSshPrivateKeyOverride(opt ...Option) *SshPrivateKeyOverride {
opts := getOpts(opt...)
o := &SshPrivateKeyOverride{
SshPrivateKeyOverride: &store.SshPrivateKeyOverride{
UsernameAttribute: sanitize.String(opts.withOverrideUsernameAttribute),
PrivateKeyAttribute: sanitize.String(opts.withOverridePrivateKeyAttribute),
UsernameAttribute: sanitize.String(opts.withOverrideUsernameAttribute),
PrivateKeyAttribute: sanitize.String(opts.withOverridePrivateKeyAttribute),
PrivateKeyPassphraseAttribute: sanitize.String(opts.withOverridePrivateKeyPassphraseAttribute),
},
}
return o
@@ -147,6 +148,9 @@ func (o *SshPrivateKeyOverride) sanitize() {
if sentinel.Is(o.PrivateKeyAttribute) {
o.PrivateKeyAttribute = ""
}
if sentinel.Is(o.PrivateKeyPassphraseAttribute) {
o.PrivateKeyPassphraseAttribute = ""
}
}
// TableName returns the table name.
+13 -4
View File
@@ -29,10 +29,11 @@ type options struct {
withRequestBody []byte
withCredentialType credential.Type
withOverrideUsernameAttribute string
withOverridePasswordAttribute string
withOverridePrivateKeyAttribute string
withMappingOverride MappingOverride
withOverrideUsernameAttribute string
withOverridePasswordAttribute string
withOverridePrivateKeyAttribute string
withOverridePrivateKeyPassphraseAttribute string
withMappingOverride MappingOverride
}
func getDefaultOptions() options {
@@ -158,6 +159,14 @@ func WithOverridePrivateKeyAttribute(s string) Option {
}
}
// WithOverridePrivateKeyPassphraseAttribute provides the name of an attribute in the
// Data field of a Vault api.Secret that maps to a passphrase value.
func WithOverridePrivateKeyPassphraseAttribute(s string) Option {
return func(o *options) {
o.withOverridePrivateKeyPassphraseAttribute = s
}
}
// WithMappingOverride provides an optional mapping override to use for
// mapping the Data fields of a Vault api.Secret to a credential.
func WithMappingOverride(m MappingOverride) Option {
@@ -114,6 +114,13 @@ func Test_GetOpts(t *testing.T) {
testOpts.withOverridePrivateKeyAttribute = "test"
assert.Equal(t, opts, testOpts)
})
t.Run("WithOverridePrivateKeyPassphraseAttribute", func(t *testing.T) {
opts := getOpts(WithOverridePrivateKeyPassphraseAttribute("test"))
testOpts := getDefaultOptions()
assert.NotEqual(t, opts, testOpts)
testOpts.withOverridePrivateKeyPassphraseAttribute = "test"
assert.Equal(t, opts, testOpts)
})
t.Run("WithMappingOverride", func(t *testing.T) {
opts := getOpts(WithMappingOverride(unknownMapper(1)))
testOpts := getDefaultOptions()
+72 -64
View File
@@ -91,10 +91,12 @@ type sshPrivateKeyCred struct {
*baseCred
username string
privateKey credential.PrivateKey
passphrase []byte
}
func (c *sshPrivateKeyCred) Username() string { return c.username }
func (c *sshPrivateKeyCred) PrivateKey() credential.PrivateKey { return c.privateKey }
func (c *sshPrivateKeyCred) PrivateKeyPassphrase() []byte { return c.passphrase }
func baseToSshPriKey(ctx context.Context, bc *baseCred) (*sshPrivateKeyCred, error) {
switch {
@@ -106,14 +108,17 @@ func baseToSshPriKey(ctx context.Context, bc *baseCred) (*sshPrivateKeyCred, err
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type"))
}
uAttr, pAttr := bc.lib.UsernameAttribute, bc.lib.PrivateKeyAttribute
uAttr, pkAttr, pAttr := bc.lib.UsernameAttribute, bc.lib.PrivateKeyAttribute, bc.lib.PrivateKeyPassphraseAttribute
if uAttr == "" {
uAttr = "username"
}
if pAttr == "" {
pAttr = "private_key"
if pkAttr == "" {
pkAttr = "private_key"
}
username, pk := sshprivatekey.Extract(bc.secretData, uAttr, pAttr)
if pAttr == "" {
pAttr = "private_key_passphrase"
}
username, pk, pass := sshprivatekey.Extract(bc.secretData, uAttr, pkAttr, pAttr)
if username == "" || pk == nil {
return nil, errors.E(ctx, errors.WithCode(errors.VaultInvalidCredentialMapping))
}
@@ -122,6 +127,7 @@ func baseToSshPriKey(ctx context.Context, bc *baseCred) (*sshPrivateKeyCred, err
baseCred: bc,
username: username,
privateKey: pk,
passphrase: pass,
}, nil
}
@@ -130,72 +136,74 @@ var _ credential.Library = (*privateLibrary)(nil)
// A privateLibrary contains all the values needed to connect to Vault and
// retrieve credentials.
type privateLibrary struct {
PublicId string `gorm:"primary_key"`
StoreId string
CredType string `gorm:"column:credential_type"`
UsernameAttribute string
PasswordAttribute string
PrivateKeyAttribute string
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
ScopeId string
VaultPath string
HttpMethod string
HttpRequestBody []byte
VaultAddress string
Namespace string
CaCert []byte
TlsServerName string
TlsSkipVerify bool
WorkerFilter string
TokenHmac []byte
Token TokenSecret
CtToken []byte
TokenKeyId string
ClientCert []byte
ClientKey KeySecret
CtClientKey []byte
ClientKeyId string
Purpose credential.Purpose `gorm:"-"`
PublicId string `gorm:"primary_key"`
StoreId string
CredType string `gorm:"column:credential_type"`
UsernameAttribute string
PasswordAttribute string
PrivateKeyAttribute string
PrivateKeyPassphraseAttribute string
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
ScopeId string
VaultPath string
HttpMethod string
HttpRequestBody []byte
VaultAddress string
Namespace string
CaCert []byte
TlsServerName string
TlsSkipVerify bool
WorkerFilter string
TokenHmac []byte
Token TokenSecret
CtToken []byte
TokenKeyId string
ClientCert []byte
ClientKey KeySecret
CtClientKey []byte
ClientKeyId string
Purpose credential.Purpose `gorm:"-"`
}
func (pl *privateLibrary) clone() *privateLibrary {
// The 'append(a[:0:0], a...)' comes from
// https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F
return &privateLibrary{
PublicId: pl.PublicId,
StoreId: pl.StoreId,
CredType: pl.CredType,
UsernameAttribute: pl.UsernameAttribute,
PasswordAttribute: pl.PasswordAttribute,
PrivateKeyAttribute: pl.PrivateKeyAttribute,
Name: pl.Name,
Description: pl.Description,
CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp),
UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp),
Version: pl.Version,
ScopeId: pl.ScopeId,
VaultPath: pl.VaultPath,
HttpMethod: pl.HttpMethod,
HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...),
VaultAddress: pl.VaultAddress,
Namespace: pl.Namespace,
CaCert: append(pl.CaCert[:0:0], pl.CaCert...),
TlsServerName: pl.TlsServerName,
TlsSkipVerify: pl.TlsSkipVerify,
WorkerFilter: pl.WorkerFilter,
TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...),
Token: append(pl.Token[:0:0], pl.Token...),
CtToken: append(pl.CtToken[:0:0], pl.CtToken...),
TokenKeyId: pl.TokenKeyId,
ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...),
ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...),
CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...),
ClientKeyId: pl.ClientKeyId,
Purpose: pl.Purpose,
PublicId: pl.PublicId,
StoreId: pl.StoreId,
CredType: pl.CredType,
UsernameAttribute: pl.UsernameAttribute,
PasswordAttribute: pl.PasswordAttribute,
PrivateKeyAttribute: pl.PrivateKeyAttribute,
PrivateKeyPassphraseAttribute: pl.PrivateKeyPassphraseAttribute,
Name: pl.Name,
Description: pl.Description,
CreateTime: proto.Clone(pl.CreateTime).(*timestamp.Timestamp),
UpdateTime: proto.Clone(pl.UpdateTime).(*timestamp.Timestamp),
Version: pl.Version,
ScopeId: pl.ScopeId,
VaultPath: pl.VaultPath,
HttpMethod: pl.HttpMethod,
HttpRequestBody: append(pl.HttpRequestBody[:0:0], pl.HttpRequestBody...),
VaultAddress: pl.VaultAddress,
Namespace: pl.Namespace,
CaCert: append(pl.CaCert[:0:0], pl.CaCert...),
TlsServerName: pl.TlsServerName,
TlsSkipVerify: pl.TlsSkipVerify,
WorkerFilter: pl.WorkerFilter,
TokenHmac: append(pl.TokenHmac[:0:0], pl.TokenHmac...),
Token: append(pl.Token[:0:0], pl.Token...),
CtToken: append(pl.CtToken[:0:0], pl.CtToken...),
TokenKeyId: pl.TokenKeyId,
ClientCert: append(pl.ClientCert[:0:0], pl.ClientCert...),
ClientKey: append(pl.ClientKey[:0:0], pl.ClientKey...),
CtClientKey: append(pl.CtClientKey[:0:0], pl.CtClientKey...),
ClientKeyId: pl.ClientKeyId,
Purpose: pl.Purpose,
}
}
@@ -227,12 +227,30 @@ func TestRepository_getPrivateLibraries(t *testing.T) {
req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.BrokeredPurpose}
requests = append(requests, req)
}
{
opts := []Option{
WithCredentialType(credential.SshPrivateKeyType),
WithMappingOverride(NewSshPrivateKeyOverride(
WithOverridePrivateKeyPassphraseAttribute("test-passphrase"),
)),
}
libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...)
assert.NoError(err)
require.NotNil(libIn)
lib, err := repo.CreateCredentialLibrary(ctx, prj.GetPublicId(), libIn)
assert.NoError(err)
require.NotNil(lib)
libs[lib.GetPublicId()] = lib
req := credential.Request{SourceId: lib.GetPublicId(), Purpose: credential.BrokeredPurpose}
requests = append(requests, req)
}
{
opts := []Option{
WithCredentialType(credential.SshPrivateKeyType),
WithMappingOverride(NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("test-username"),
WithOverridePrivateKeyAttribute("test-private-key"),
WithOverridePrivateKeyPassphraseAttribute("test-passphrase"),
)),
}
libIn, err := NewCredentialLibrary(origStore.GetPublicId(), "/vault/path", opts...)
@@ -273,6 +291,7 @@ func TestRepository_getPrivateLibraries(t *testing.T) {
case *SshPrivateKeyOverride:
assert.Equal(w.UsernameAttribute, got.UsernameAttribute)
assert.Equal(w.PrivateKeyAttribute, got.PrivateKeyAttribute)
assert.Equal(w.PrivateKeyPassphraseAttribute, got.PrivateKeyPassphraseAttribute)
default:
assert.Fail("unknown mapping override type")
}
@@ -806,6 +825,24 @@ func TestBaseToSshPriKey(t *testing.T) {
privateKey: credential.PrivateKey("my-pk"),
},
},
{
name: "valid-default-attributes-with-passphrase",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.SshPrivateKeyType),
},
secretData: map[string]interface{}{
"username": "my-username",
"private_key": "my-pk",
"private_key_passphrase": "my-pass",
},
},
want: &sshPrivateKeyCred{
username: "my-username",
privateKey: credential.PrivateKey("my-pk"),
passphrase: []byte("my-pass"),
},
},
{
name: "valid-override-attributes",
given: &baseCred{
@@ -826,6 +863,30 @@ func TestBaseToSshPriKey(t *testing.T) {
privateKey: credential.PrivateKey("override-pk"),
},
},
{
name: "valid-override-attributes-with-passphrase",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.SshPrivateKeyType),
UsernameAttribute: "test-username",
PrivateKeyAttribute: "test-pk",
PrivateKeyPassphraseAttribute: "test-pass",
},
secretData: map[string]interface{}{
"username": "default-username",
"private_key": "default-pk",
"passphrase": "default-pass",
"test-username": "override-username",
"test-pk": "override-pk",
"test-pass": "override-pass",
},
},
want: &sshPrivateKeyCred{
username: "override-username",
privateKey: credential.PrivateKey("override-pk"),
passphrase: []byte("override-pass"),
},
},
{
name: "valid-default-username-override-pk",
given: &baseCred{
@@ -1018,6 +1079,27 @@ func TestBaseToSshPriKey(t *testing.T) {
privateKey: credential.PrivateKey("default-pk"),
},
},
{
name: "valid-kv2-default-attributes-with-passphrase",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.SshPrivateKeyType),
},
secretData: map[string]interface{}{
"metadata": map[string]interface{}{},
"data": map[string]interface{}{
"username": "default-username",
"private_key": "default-pk",
"private_key_passphrase": "default-pass",
},
},
},
want: &sshPrivateKeyCred{
username: "default-username",
privateKey: credential.PrivateKey("default-pk"),
passphrase: []byte("default-pass"),
},
},
{
name: "valid-kv2-override-attributes",
given: &baseCred{
@@ -1041,6 +1123,33 @@ func TestBaseToSshPriKey(t *testing.T) {
privateKey: credential.PrivateKey("override-pk"),
},
},
{
name: "valid-kv2-override-attributes-with-passphrase",
given: &baseCred{
lib: &privateLibrary{
CredType: string(credential.SshPrivateKeyType),
UsernameAttribute: "test-username",
PrivateKeyAttribute: "test-pk",
PrivateKeyPassphraseAttribute: "test-pass",
},
secretData: map[string]interface{}{
"metadata": map[string]interface{}{},
"data": map[string]interface{}{
"username": "default-username",
"private_key": "default-pk",
"passphrase": "default-pass",
"test-username": "override-username",
"test-pk": "override-pk",
"test-pass": "override-pass",
},
},
},
want: &sshPrivateKeyCred{
username: "override-username",
privateKey: credential.PrivateKey("override-pk"),
passphrase: []byte("override-pass"),
},
},
{
name: "valid-kv2-default-username-override-pk",
given: &baseCred{
@@ -345,20 +345,21 @@ func (r *Repository) LookupCredentialLibrary(ctx context.Context, publicId strin
// mapping overrides. It does not include encrypted data and is safe to
// return external to boundary.
type publicLibrary struct {
PublicId string `gorm:"primary_key"`
StoreId string
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
VaultPath string
HttpMethod string
HttpRequestBody []byte
CredentialType string
UsernameAttribute string
PasswordAttribute string
PrivateKeyAttribute string
PublicId string `gorm:"primary_key"`
StoreId string
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
VaultPath string
HttpMethod string
HttpRequestBody []byte
CredentialType string
UsernameAttribute string
PasswordAttribute string
PrivateKeyAttribute string
PrivateKeyPassphraseAttribute string
}
func allocPublicLibrary() *publicLibrary {
@@ -390,11 +391,12 @@ func (pl *publicLibrary) toCredentialLibrary() *CredentialLibrary {
cl.MappingOverride = up
}
case string(credential.SshPrivateKeyType):
if pl.UsernameAttribute != "" || pl.PrivateKeyAttribute != "" {
if pl.UsernameAttribute != "" || pl.PrivateKeyAttribute != "" || pl.PrivateKeyPassphraseAttribute != "" {
pk := allocSshPrivateKeyOverride()
pk.LibraryId = pl.PublicId
pk.UsernameAttribute = pl.UsernameAttribute
pk.PrivateKeyAttribute = pl.PrivateKeyAttribute
pk.PrivateKeyPassphraseAttribute = pl.PrivateKeyPassphraseAttribute
pk.sanitize()
cl.MappingOverride = pk
}
@@ -381,11 +381,37 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) {
},
},
{
name: "valid-ssh-private-keyt-credential-type-with-username-and-private-key-override",
name: "valid-ssh-private-key-credential-type-with-passphrase-override",
in: &CredentialLibrary{
MappingOverride: NewSshPrivateKeyOverride(
WithOverridePrivateKeyPassphraseAttribute("passtest"),
),
CredentialLibrary: &store.CredentialLibrary{
StoreId: cs.GetPublicId(),
HttpMethod: "GET",
VaultPath: "/some/path",
CredentialType: string(credential.SshPrivateKeyType),
},
},
want: &CredentialLibrary{
MappingOverride: NewSshPrivateKeyOverride(
WithOverridePrivateKeyPassphraseAttribute("passtest"),
),
CredentialLibrary: &store.CredentialLibrary{
StoreId: cs.GetPublicId(),
HttpMethod: "GET",
VaultPath: "/some/path",
CredentialType: string(credential.SshPrivateKeyType),
},
},
},
{
name: "valid-ssh-private-keyt-credential-type-with-all-override",
in: &CredentialLibrary{
MappingOverride: NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("utest"),
WithOverridePrivateKeyAttribute("ptest"),
WithOverridePrivateKeyAttribute("pktest"),
WithOverridePrivateKeyPassphraseAttribute("ptest"),
),
CredentialLibrary: &store.CredentialLibrary{
StoreId: cs.GetPublicId(),
@@ -397,7 +423,8 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) {
want: &CredentialLibrary{
MappingOverride: NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("utest"),
WithOverridePrivateKeyAttribute("ptest"),
WithOverridePrivateKeyAttribute("pktest"),
WithOverridePrivateKeyPassphraseAttribute("ptest"),
),
CredentialLibrary: &store.CredentialLibrary{
StoreId: cs.GetPublicId(),
@@ -456,6 +483,7 @@ func TestRepository_CreateCredentialLibrary(t *testing.T) {
require.True(ok)
assert.Equal(w.UsernameAttribute, g.UsernameAttribute)
assert.Equal(w.PrivateKeyAttribute, g.PrivateKeyAttribute)
assert.Equal(w.PrivateKeyPassphraseAttribute, g.PrivateKeyPassphraseAttribute)
// verify it was persisted in the database
override := allocSshPrivateKeyOverride()
@@ -1302,11 +1330,10 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) {
wantCount: 1,
},
{
name: "ssh-private-key-attributes-change-username-and-private-key-attributes",
name: "ssh-private-key-attributes-change-passphrase-attribute",
orig: &CredentialLibrary{
MappingOverride: NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("orig-username"),
WithOverridePrivateKeyAttribute("orig-private-key"),
WithOverridePrivateKeyPassphraseAttribute("orig-passphrase"),
),
CredentialLibrary: &store.CredentialLibrary{
HttpMethod: "GET",
@@ -1317,15 +1344,13 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) {
},
chgFn: changeMappingOverride(
NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("changed-username"),
WithOverridePrivateKeyAttribute("changed-private-key"),
WithOverridePrivateKeyPassphraseAttribute("changed-passphrase"),
),
),
masks: []string{"MappingOverride"},
want: &CredentialLibrary{
MappingOverride: NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("changed-username"),
WithOverridePrivateKeyAttribute("changed-private-key"),
WithOverridePrivateKeyPassphraseAttribute("changed-passphrase"),
),
CredentialLibrary: &store.CredentialLibrary{
HttpMethod: "GET",
@@ -1337,7 +1362,45 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) {
wantCount: 1,
},
{
name: "no-mapping-override-change-username-and-private-key-attributes",
name: "ssh-private-key-attributes-change-all-attributes",
orig: &CredentialLibrary{
MappingOverride: NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("orig-username"),
WithOverridePrivateKeyAttribute("orig-private-key"),
WithOverridePrivateKeyPassphraseAttribute("orig-passphrase"),
),
CredentialLibrary: &store.CredentialLibrary{
HttpMethod: "GET",
VaultPath: "/some/path",
Name: "test-ssh-private-key-repo",
CredentialType: string(credential.SshPrivateKeyType),
},
},
chgFn: changeMappingOverride(
NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("changed-username"),
WithOverridePrivateKeyAttribute("changed-private-key"),
WithOverridePrivateKeyPassphraseAttribute("changed-passphrase"),
),
),
masks: []string{"MappingOverride"},
want: &CredentialLibrary{
MappingOverride: NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("changed-username"),
WithOverridePrivateKeyAttribute("changed-private-key"),
WithOverridePrivateKeyPassphraseAttribute("changed-passphrase"),
),
CredentialLibrary: &store.CredentialLibrary{
HttpMethod: "GET",
VaultPath: "/some/path",
Name: "test-ssh-private-key-repo",
CredentialType: string(credential.SshPrivateKeyType),
},
},
wantCount: 1,
},
{
name: "no-mapping-override-change-all-attributes",
orig: &CredentialLibrary{
CredentialLibrary: &store.CredentialLibrary{
HttpMethod: "GET",
@@ -1350,6 +1413,7 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) {
NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("changed-username"),
WithOverridePrivateKeyAttribute("changed-private-key"),
WithOverridePrivateKeyPassphraseAttribute("changed-passphrase"),
),
),
masks: []string{"MappingOverride"},
@@ -1357,6 +1421,7 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) {
MappingOverride: NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("changed-username"),
WithOverridePrivateKeyAttribute("changed-private-key"),
WithOverridePrivateKeyPassphraseAttribute("changed-passphrase"),
),
CredentialLibrary: &store.CredentialLibrary{
HttpMethod: "GET",
@@ -1373,6 +1438,7 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) {
MappingOverride: NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("orig-username"),
WithOverridePrivateKeyAttribute("orig-private-key"),
WithOverridePrivateKeyPassphraseAttribute("orig-passphrase"),
),
CredentialLibrary: &store.CredentialLibrary{
HttpMethod: "GET",
@@ -1424,7 +1490,7 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) {
assert.Nil(got)
return
}
assert.NoError(err)
require.NoError(err)
assert.Empty(tt.orig.PublicId)
require.NotNil(got)
assertPublicId(t, CredentialLibraryPrefix, got.GetPublicId())
@@ -1466,6 +1532,7 @@ func TestRepository_UpdateCredentialLibrary(t *testing.T) {
require.True(ok)
assert.Equal(w.UsernameAttribute, g.UsernameAttribute)
assert.Equal(w.PrivateKeyAttribute, g.PrivateKeyAttribute)
assert.Equal(w.PrivateKeyPassphraseAttribute, g.PrivateKeyPassphraseAttribute)
default:
assert.Fail("Unknown mapping override")
}
@@ -1705,11 +1772,26 @@ func TestRepository_LookupCredentialLibrary(t *testing.T) {
},
},
{
name: "valid-ssh-private-key-credential-type-with-username-and-private-key-override",
name: "valid-ssh-private-key-credential-type-with-passphrase-override",
in: &CredentialLibrary{
MappingOverride: NewSshPrivateKeyOverride(
WithOverridePrivateKeyPassphraseAttribute("ptest"),
),
CredentialLibrary: &store.CredentialLibrary{
StoreId: cs.GetPublicId(),
HttpMethod: "GET",
VaultPath: "/some/path",
CredentialType: string(credential.SshPrivateKeyType),
},
},
},
{
name: "valid-ssh-private-key-credential-type-with-all-overrides",
in: &CredentialLibrary{
MappingOverride: NewSshPrivateKeyOverride(
WithOverrideUsernameAttribute("utest"),
WithOverridePrivateKeyAttribute("ptest"),
WithOverridePrivateKeyAttribute("pktest"),
WithOverridePrivateKeyPassphraseAttribute("ptest"),
),
CredentialLibrary: &store.CredentialLibrary{
StoreId: cs.GetPublicId(),
@@ -1756,6 +1838,7 @@ func TestRepository_LookupCredentialLibrary(t *testing.T) {
require.True(ok)
assert.Equal(w.UsernameAttribute, g.UsernameAttribute)
assert.Equal(w.PrivateKeyAttribute, g.PrivateKeyAttribute)
assert.Equal(w.PrivateKeyPassphraseAttribute, g.PrivateKeyPassphraseAttribute)
default:
assert.Fail("Unknown mapping override")
}
+28 -6
View File
@@ -904,6 +904,16 @@ type SshPrivateKeyOverride struct {
//
// @inject_tag: `gorm:"default:null"`
PrivateKeyAttribute string `protobuf:"bytes,3,opt,name=private_key_attribute,json=privateKeyAttribute,proto3" json:"private_key_attribute,omitempty" gorm:"default:null"`
// private_key_passphrase_attribute is the name of the attribute in the Data
// field of a Vault api.Secret that maps to a private key passphrase. If set,
// it overrides any default attribute names the system uses to find a
// private_key_passphrase attribute.
//
// See
// https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25
//
// @inject_tag: `gorm:"default:null"`
PrivateKeyPassphraseAttribute string `protobuf:"bytes,4,opt,name=private_key_passphrase_attribute,json=privateKeyPassphraseAttribute,proto3" json:"private_key_passphrase_attribute,omitempty" gorm:"default:null"`
}
func (x *SshPrivateKeyOverride) Reset() {
@@ -959,6 +969,13 @@ func (x *SshPrivateKeyOverride) GetPrivateKeyAttribute() string {
return ""
}
func (x *SshPrivateKeyOverride) GetPrivateKeyPassphraseAttribute() string {
if x != nil {
return x.PrivateKeyPassphraseAttribute
}
return ""
}
var File_controller_storage_credential_vault_store_v1_vault_proto protoreflect.FileDescriptor
var file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = []byte{
@@ -1171,7 +1188,7 @@ var file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = []by
0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x52, 0x11, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x74, 0x74, 0x72,
0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0x99, 0x01, 0x0a, 0x15, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69,
0x69, 0x62, 0x75, 0x74, 0x65, 0x22, 0xe2, 0x01, 0x0a, 0x15, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69,
0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12,
0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x49, 0x64, 0x12, 0x2d,
@@ -1181,11 +1198,16 @@ var file_controller_storage_credential_vault_store_v1_vault_proto_rawDesc = []by
0x15, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x61, 0x74, 0x74,
0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72,
0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74,
0x65, 0x42, 0x45, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61,
0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, 0x64,
0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x73, 0x74, 0x6f,
0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x65, 0x12, 0x47, 0x0a, 0x20, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79,
0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72,
0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1d, 0x70, 0x72, 0x69,
0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73,
0x65, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x42, 0x45, 0x5a, 0x43, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f,
0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x2f,
0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72,
0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -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 {
@@ -95,6 +95,7 @@ comment on view credential_vault_store_public is
'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.';
-- Replaces view from 39/02_vault_ssh_private_key_override.up.sql
-- Replaced in 42/01_ssh_private_key_passphrase.up.sql
create view credential_vault_library_private as
with
password_override (library_id, username_attribute, password_attribute) as (
@@ -151,6 +152,7 @@ comment on view credential_vault_library_private is
'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.';
-- Replaces view from 39/02_vault_ssh_private_key_override.up.sql
-- Replaced in 42/01_ssh_private_key_passphrase.up.sql
create view credential_vault_library_public as
select public_id,
store_id,
@@ -0,0 +1,106 @@
begin;
alter table credential_static_ssh_private_key_credential
add column private_key_passphrase_encrypted bytea,
add column private_key_passphrase_hmac bytea,
add constraint private_key_passphrase_both_null_or_not_null
check (
(private_key_passphrase_encrypted is null and private_key_passphrase_hmac is null)
or
(private_key_passphrase_encrypted is not null and private_key_passphrase_hmac is not null)
)
;
alter table credential_vault_library_ssh_private_key_mapping_override
add column private_key_passphrase_attribute wt_sentinel
default wt_to_sentinel('no override')
not null;
-- Replaces view from 41/01_worker_filter_vault_cred_store.up.sql
drop view credential_vault_library_public;
drop view credential_vault_library_private;
-- Replaces view from 41/01_worker_filter_vault_cred_store.up.sql
create view credential_vault_library_private as
with
password_override (library_id, username_attribute, password_attribute) as (
select library_id,
nullif(username_attribute, wt_to_sentinel('no override')),
nullif(password_attribute, wt_to_sentinel('no override'))
from credential_vault_library_username_password_mapping_override
),
ssh_private_key_override (library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute) as (
select library_id,
nullif(username_attribute, wt_to_sentinel('no override')),
nullif(private_key_attribute, wt_to_sentinel('no override')),
nullif(private_key_passphrase_attribute, wt_to_sentinel('no override'))
from credential_vault_library_ssh_private_key_mapping_override
)
select library.public_id as public_id,
library.store_id as store_id,
library.name as name,
library.description as description,
library.create_time as create_time,
library.update_time as update_time,
library.version as version,
library.vault_path as vault_path,
library.http_method as http_method,
library.http_request_body as http_request_body,
library.credential_type as credential_type,
store.scope_id as scope_id,
store.vault_address as vault_address,
store.namespace as namespace,
store.ca_cert as ca_cert,
store.tls_server_name as tls_server_name,
store.tls_skip_verify as tls_skip_verify,
store.worker_filter as worker_filter,
store.token_hmac as token_hmac,
store.ct_token as ct_token, -- encrypted
store.token_key_id as token_key_id,
store.client_cert as client_cert,
store.ct_client_key as ct_client_key, -- encrypted
store.client_key_id as client_key_id,
coalesce(upasso.username_attribute,sshpk.username_attribute)
as username_attribute,
upasso.password_attribute as password_attribute,
sshpk.private_key_attribute as private_key_attribute,
sshpk.private_key_passphrase_attribute as private_key_passphrase_attribute
from credential_vault_library library
join credential_vault_store_private store
on library.store_id = store.public_id
left join password_override upasso
on library.public_id = upasso.library_id
and store.token_status = 'current'
left join ssh_private_key_override sshpk
on library.public_id = sshpk.library_id
and store.token_status = 'current';
comment on view credential_vault_library_private is
'credential_vault_library_private is a view where each row contains a credential library and the credential library''s data needed to connect to Vault. '
'Each row may contain encrypted data. This view should not be used to retrieve data which will be returned external to boundary.';
-- Replaces view from 41/01_worker_filter_vault_cred_store.up.sql
-- Replaces view from 39/02_vault_ssh_private_key_override.up.sql
-- Replaced in 42/01_ssh_private_key_passphrase.up.sql
create view credential_vault_library_public as
select public_id,
store_id,
name,
description,
create_time,
update_time,
version,
vault_path,
http_method,
http_request_body,
credential_type,
worker_filter,
username_attribute,
password_attribute,
private_key_attribute,
private_key_passphrase_attribute
from credential_vault_library_private;
comment on view credential_vault_library_public is
'credential_vault_library_public is a view where each row contains a credential library and any of library''s credential mapping overrides. '
'No encrypted data is returned. This view can be used to retrieve data which will be returned external to boundary.';
commit;
@@ -393,9 +393,9 @@ begin;
('vl______wvl11', 'my_private_key');
insert into credential_vault_library_ssh_private_key_mapping_override
(library_id, username_attribute, private_key_attribute)
(library_id, username_attribute, private_key_attribute, private_key_passphrase_attribute)
values
('vl______wvl12', 'my_username', 'my_private_key');
('vl______wvl12', 'my_username', 'my_private_key', 'my_passphrase');
insert into target_credential_library
(target_id, credential_library_id, credential_purpose)
@@ -17,7 +17,7 @@ begin;
where library_id in ('vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12');
prepare select_private_libraries as
select public_id::text, credential_type::text, username_attribute::text, private_key_attribute::text
select public_id::text, credential_type::text, username_attribute::text, private_key_attribute::text, private_key_passphrase_attribute::text
from credential_vault_library_private
where public_id in ('vl______wvl2', 'vl______wvl8', 'vl______wvl9', 'vl______wvl10', 'vl______wvl11', 'vl______wvl12')
order by public_id;
@@ -25,12 +25,12 @@ begin;
select results_eq(
'select_private_libraries',
$$VALUES
('vl______wvl10', 'ssh_private_key', 'my_username', null),
('vl______wvl11', 'ssh_private_key', null, 'my_private_key'),
('vl______wvl12', 'ssh_private_key', 'my_username', 'my_private_key'),
('vl______wvl2', 'unspecified', null, null),
('vl______wvl8', 'ssh_private_key', null, null),
('vl______wvl9', 'ssh_private_key', null, null)$$
('vl______wvl10', 'ssh_private_key', 'my_username', null, null),
('vl______wvl11', 'ssh_private_key', null, 'my_private_key', null),
('vl______wvl12', 'ssh_private_key', 'my_username', 'my_private_key', 'my_passphrase'),
('vl______wvl2', 'unspecified', null, null, null),
('vl______wvl8', 'ssh_private_key', null, null, null),
('vl______wvl9', 'ssh_private_key', null, null, null)$$
);
-- validate the insert triggers
@@ -158,7 +158,8 @@ func (x *UsernamePassword) GetPassword() string {
return ""
}
// SshPrivateKey is a credential containing a username and a private key.
// SshPrivateKey is a credential containing a username a private key and an optional
// private key passphrase.
type SshPrivateKey struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -168,6 +169,8 @@ type SshPrivateKey struct {
Username string `protobuf:"bytes,10,opt,name=username,proto3" json:"username,omitempty"` // @gotags: `class:"public"`
// The private key of the credential
PrivateKey string `protobuf:"bytes,20,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty"` // @gotags: `class:"secret"`
// The optional passphrase of the private_key
PrivateKeyPassphrase string `protobuf:"bytes,30,opt,name=private_key_passphrase,json=privateKeyPassphrase,proto3" json:"private_key_passphrase,omitempty"` // @gotags: `class:"secret"`
}
func (x *SshPrivateKey) Reset() {
@@ -216,6 +219,13 @@ func (x *SshPrivateKey) GetPrivateKey() string {
return ""
}
func (x *SshPrivateKey) GetPrivateKeyPassphrase() string {
if x != nil {
return x.PrivateKeyPassphrase
}
return ""
}
var File_controller_servers_services_v1_credential_proto protoreflect.FileDescriptor
var file_controller_servers_services_v1_credential_proto_rawDesc = []byte{
@@ -243,18 +253,21 @@ var file_controller_servers_services_v1_credential_proto_rawDesc = []byte{
0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a,
0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x4c, 0x0a, 0x0d, 0x53, 0x73, 0x68,
0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73,
0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73,
0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74,
0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x69,
0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x51, 0x5a, 0x4f, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f,
0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72,
0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x82, 0x01, 0x0a, 0x0d, 0x53, 0x73,
0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x75,
0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75,
0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61,
0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72,
0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x16, 0x70, 0x72, 0x69, 0x76,
0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61,
0x73, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74,
0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x42, 0x51,
0x5a, 0x4f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73,
0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f,
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e,
0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x2f,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -135,4 +135,23 @@ message SshPrivateKeyAttributes {
that: "PrivateKeyHmac"
}
]; // @gotags: `class:"public"`
// Input only. The passphrase for the SSH private key associated with the credential.
google.protobuf.StringValue private_key_passphrase = 40 [
json_name = "private_key_passphrase",
(custom_options.v1.generate_sdk_option) = true,
(custom_options.v1.mask_mapping) = {
this: "attributes.private_key_passphrase"
that: "PrivateKeyPassphrase"
}
]; // @gotags: `class:"secret"`
// Output only. The hmac value of the SSH private key passphrase.
string private_key_passphrase_hmac = 50 [
json_name = "private_key_passphrase_hmac",
(custom_options.v1.mask_mapping) = {
this: "attributes.private_key_passphrase_hmac"
that: "PrivateKeyPassphraseHmac"
}
]; // @gotags: `class:"public"`
}
@@ -330,4 +330,7 @@ message SshPrivateKeyCredential {
// Private key of the credential
string private_key = 2; // @gotags: `class:"secret"`
// The optional passphrase of the private_key
string private_key_passphrase = 3; // @gotags: `class:"secret"`
}
@@ -23,11 +23,15 @@ message UsernamePassword {
string password = 20; // @gotags: `class:"secret"`
}
// SshPrivateKey is a credential containing a username and a private key.
// SshPrivateKey is a credential containing a username a private key and an optional
// private key passphrase.
message SshPrivateKey {
// The username of the credential
string username = 10; // @gotags: `class:"public"`
// The private key of the credential
string private_key = 20; // @gotags: `class:"secret"`
// The optional passphrase of the private_key
string private_key_passphrase = 30; // @gotags: `class:"secret"`
}
@@ -186,4 +186,26 @@ message SshPrivateKeyCredential {
// It must be set.
// @inject_tag: `gorm:"not_null"`
string key_id = 12;
// private_key_passphrase is the plain-text of the passphrase of the SSH private
// key associated with the credential. We are not storing this plain-text
// private key passphrase in the database.
// @inject_tag: `gorm:"-"`
bytes private_key_passphrase = 13 [(custom_options.v1.mask_mapping) = {
this: "PrivateKeyPassphrase"
that: "attributes.private_key_passphrase"
}];
// private_key_passphrase_encrypted is the ciphertext of the private key passphrase. It is stored in
// the database.
// @inject_tag: `gorm:"column:private_key_passphrase_encrypted;not_null"`
bytes private_key_passphrase_encrypted = 14;
// private_key_passphrase_hmac is a sha256-hmac of the unencrypted private key passphrase. It is recalculated
// everytime the private key passphrase is updated.
// @inject_tag: `gorm:"not_null"`
bytes private_key_passphrase_hmac = 15 [(custom_options.v1.mask_mapping) = {
this: "PrivateKeyPassphraseHmac"
that: "attributes.private_key_passphrase_hmac"
}];
}
@@ -376,4 +376,15 @@ message SshPrivateKeyOverride {
//
// @inject_tag: `gorm:"default:null"`
string private_key_attribute = 3;
// private_key_passphrase_attribute is the name of the attribute in the Data
// field of a Vault api.Secret that maps to a private key passphrase. If set,
// it overrides any default attribute names the system uses to find a
// private_key_passphrase attribute.
//
// See
// https://github.com/hashicorp/vault/blob/5e505ec039177e8212cbbab74ccb644c46e62e63/api/secret.go#L25
//
// @inject_tag: `gorm:"default:null"`
string private_key_passphrase_attribute = 4;
}
@@ -13,6 +13,7 @@ import (
"github.com/hashicorp/boundary/internal/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh/testdata"
)
func TestList(t *testing.T) {
@@ -152,6 +153,87 @@ func TestCrud(t *testing.T) {
assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode())
}
func TestCrudSpk(t *testing.T) {
assert, require := assert.New(t), require.New(t)
tc := controller.NewTestController(t, nil)
defer tc.Shutdown()
client := tc.Client()
token := tc.Token()
client.SetToken(token.Token)
_, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(token.UserId))
cs, err := credentialstores.NewClient(client).Create(tc.Context(), "static", proj.GetPublicId())
require.NoError(err)
require.NotNil(cs)
checkResource := func(step string, c *credentials.Credential, wantedName, wantedUser string, wantVersion uint32) {
assert.NotNil(c, "returned no resource", step)
assert.Equal(wantedName, c.Name, step)
gotUser, ok := c.Attributes["username"]
require.True(ok)
assert.Equal(wantedUser, gotUser, step)
assert.Equal(wantVersion, c.Version)
}
credClient := credentials.NewClient(client)
spk := string(testdata.PEMBytes["rsa"])
spkWithPass := string(testdata.PEMEncryptedKeys[0].PEMBytes)
pass := testdata.PEMEncryptedKeys[0].EncryptionKey
cred, err := credClient.Create(tc.Context(), credential.SshPrivateKeySubtype.String(), cs.Item.Id, credentials.WithName("foo"),
credentials.WithSshPrivateKeyCredentialUsername("user"),
credentials.WithSshPrivateKeyCredentialPrivateKey(spkWithPass),
credentials.WithSshPrivateKeyCredentialPrivateKeyPassphrase(pass))
require.NoError(err)
require.NotNil(cs)
checkResource("create", cred.Item, "foo", "user", 1)
// Validate passphrase hmac was set and passpharse is not set
passHmac, ok := cred.GetItem().Attributes["private_key_passphrase_hmac"].(string)
require.True(ok)
require.NotNil(passHmac)
cred, err = credClient.Read(tc.Context(), cred.Item.Id)
require.NoError(err)
require.NotNil(cs)
checkResource("read", cred.Item, "foo", "user", 1)
cred, err = credClient.Update(tc.Context(), cred.Item.Id, cred.Item.Version, credentials.WithName("bar"))
require.NoError(err)
require.NotNil(cs)
checkResource("update", cred.Item, "bar", "user", 2)
cred, err = credClient.Update(tc.Context(), cred.Item.Id, cred.Item.Version, credentials.WithSshPrivateKeyCredentialUsername("newuser"))
require.NoError(err)
require.NotNil(cs)
checkResource("update", cred.Item, "bar", "newuser", 3)
cred, err = credClient.Update(tc.Context(), cred.Item.Id, cred.Item.Version, credentials.DefaultName())
require.NoError(err)
require.NotNil(cs)
checkResource("update", cred.Item, "", "newuser", 4)
// Update to non-encrypted key
cred, err = credClient.Update(tc.Context(), cred.Item.Id, cred.Item.Version, credentials.WithSshPrivateKeyCredentialPrivateKey(spk))
require.NoError(err)
require.NotNil(cs)
checkResource("update", cred.Item, "", "newuser", 5)
// Validate passphrase hmac is no longer set
_, ok = cred.GetItem().Attributes["private_key_passphrase_hmac"].(string)
require.False(ok)
_, err = credClient.Delete(tc.Context(), cred.Item.Id)
assert.NoError(err)
_, err = credClient.Delete(tc.Context(), cred.Item.Id)
require.Error(err)
apiErr := api.AsServerError(err)
assert.NotNil(apiErr)
assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode())
}
func TestErrors(t *testing.T) {
assert, require := assert.New(t), require.New(t)
tc := controller.NewTestController(t, nil)
@@ -292,6 +292,10 @@ type SshPrivateKeyAttributes struct {
PrivateKey *wrapperspb.StringValue `protobuf:"bytes,20,opt,name=private_key,proto3" json:"private_key,omitempty" class:"secret"` // @gotags: `class:"secret"`
// Output only. The hmac value of the SSH private key.
PrivateKeyHmac string `protobuf:"bytes,30,opt,name=private_key_hmac,proto3" json:"private_key_hmac,omitempty" class:"public"` // @gotags: `class:"public"`
// Input only. The passphrase for the SSH private key associated with the credential.
PrivateKeyPassphrase *wrapperspb.StringValue `protobuf:"bytes,40,opt,name=private_key_passphrase,proto3" json:"private_key_passphrase,omitempty" class:"secret"` // @gotags: `class:"secret"`
// Output only. The hmac value of the SSH private key passphrase.
PrivateKeyPassphraseHmac string `protobuf:"bytes,50,opt,name=private_key_passphrase_hmac,proto3" json:"private_key_passphrase_hmac,omitempty" class:"public"` // @gotags: `class:"public"`
}
func (x *SshPrivateKeyAttributes) Reset() {
@@ -347,6 +351,20 @@ func (x *SshPrivateKeyAttributes) GetPrivateKeyHmac() string {
return ""
}
func (x *SshPrivateKeyAttributes) GetPrivateKeyPassphrase() *wrapperspb.StringValue {
if x != nil {
return x.PrivateKeyPassphrase
}
return nil
}
func (x *SshPrivateKeyAttributes) GetPrivateKeyPassphraseHmac() string {
if x != nil {
return x.PrivateKeyPassphraseHmac
}
return ""
}
var File_controller_api_resources_credentials_v1_credential_proto protoreflect.FileDescriptor
var file_controller_api_resources_credentials_v1_credential_proto_rawDesc = []byte{
@@ -450,7 +468,7 @@ var file_controller_api_resources_credentials_v1_credential_proto_rawDesc = []by
0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e,
0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x12, 0x0c, 0x50,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x6d, 0x61, 0x63, 0x52, 0x0d, 0x70, 0x61, 0x73,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x22, 0xc9, 0x02, 0x0a, 0x17, 0x53,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x22, 0xee, 0x04, 0x0a, 0x17, 0x53,
0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x74, 0x74, 0x72,
0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x61, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61,
0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
@@ -471,13 +489,31 @@ var file_controller_api_resources_credentials_v1_credential_proto_rawDesc = []by
0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f,
0x68, 0x6d, 0x61, 0x63, 0x12, 0x0e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79,
0x48, 0x6d, 0x61, 0x63, 0x52, 0x10, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65,
0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x42, 0x58, 0x5a, 0x56, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62,
0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f,
0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74,
0x69, 0x61, 0x6c, 0x73, 0x3b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x12, 0x97, 0x01, 0x0a, 0x16, 0x70, 0x72, 0x69, 0x76, 0x61,
0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73,
0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67,
0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x41, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x39, 0x0a,
0x21, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76,
0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61,
0x73, 0x65, 0x12, 0x14, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61,
0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x52, 0x16, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74,
0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65,
0x12, 0x88, 0x01, 0x0a, 0x1b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79,
0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x68, 0x6d, 0x61, 0x63,
0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x42, 0x46, 0xc2, 0xdd, 0x29, 0x42, 0x0a, 0x26, 0x61, 0x74,
0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65,
0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f,
0x68, 0x6d, 0x61, 0x63, 0x12, 0x18, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79,
0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x48, 0x6d, 0x61, 0x63, 0x52, 0x1b,
0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73,
0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x42, 0x58, 0x5a, 0x56, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63,
0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b,
0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x63, 0x72,
0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x3b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e,
0x74, 0x69, 0x61, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -515,11 +551,12 @@ var file_controller_api_resources_credentials_v1_credential_proto_depIdxs = []in
4, // 9: controller.api.resources.credentials.v1.UsernamePasswordAttributes.password:type_name -> google.protobuf.StringValue
4, // 10: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.username:type_name -> google.protobuf.StringValue
4, // 11: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key:type_name -> google.protobuf.StringValue
12, // [12:12] is the sub-list for method output_type
12, // [12:12] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
4, // 12: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key_passphrase:type_name -> google.protobuf.StringValue
13, // [13:13] is the sub-list for method output_type
13, // [13:13] is the sub-list for method input_type
13, // [13:13] is the sub-list for extension type_name
13, // [13:13] is the sub-list for extension extendee
0, // [0:13] is the sub-list for field type_name
}
func init() { file_controller_api_resources_credentials_v1_credential_proto_init() }
@@ -1166,6 +1166,8 @@ type SshPrivateKeyCredential struct {
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty" class:"sensitive"` // @gotags: `class:"sensitive"`
// Private key of the credential
PrivateKey string `protobuf:"bytes,2,opt,name=private_key,json=privateKey,proto3" json:"private_key,omitempty" class:"secret"` // @gotags: `class:"secret"`
// The optional passphrase of the private_key
PrivateKeyPassphrase string `protobuf:"bytes,3,opt,name=private_key_passphrase,json=privateKeyPassphrase,proto3" json:"private_key_passphrase,omitempty" class:"secret"` // @gotags: `class:"secret"`
}
func (x *SshPrivateKeyCredential) Reset() {
@@ -1214,6 +1216,13 @@ func (x *SshPrivateKeyCredential) GetPrivateKey() string {
return ""
}
func (x *SshPrivateKeyCredential) GetPrivateKeyPassphrase() string {
if x != nil {
return x.PrivateKeyPassphrase
}
return ""
}
var File_controller_api_resources_targets_v1_target_proto protoreflect.FileDescriptor
var file_controller_api_resources_targets_v1_target_proto_rawDesc = []byte{
@@ -1513,18 +1522,21 @@ var file_controller_api_resources_targets_v1_target_proto_rawDesc = []byte{
0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x56, 0x0a, 0x17, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76,
0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c,
0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b,
0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x42, 0x50, 0x5a,
0x4e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68,
0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73,
0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65,
0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f,
0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x3b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x8c, 0x01, 0x0a, 0x17, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69,
0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61,
0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a,
0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x34,
0x0a, 0x16, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61,
0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14,
0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68,
0x72, 0x61, 0x73, 0x65, 0x42, 0x50, 0x5a, 0x4e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75,
0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f,
0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x3b, 0x74,
0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (