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:
45 files changed
+1921
-382
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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(×tamp.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()))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
+20
-4
@@ -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
|
||||
|
||||
+144
-15
@@ -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)
|
||||
|
||||
+7
-7
@@ -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 (
|
||||
|
||||
Reference in new issue
Block a user