* fix (auth): properly handle rows.Err() * fix (auth/ldap): properly handle rows.Err() * fix (auth/oidc): properly handle rows.Err() * fix (auth/password): properly handle rows.Err() * fix (authtoken): properly handle rows.Err() * fix (credential): properly handle rows.Err() * fix (credential/static): properly handle rows.Err() * fix (credential/vault): properly handle rows.Err() * fix (db): properly handle rows.Err() * fix (db/schema): properly handle rows.Err() * fix (db/migrations): properly handle rows.Err() * fix (host): properly handle rows.Err() * fix (host/plugin): properly handle rows.Err() * fix (host/static): properly handle rows.Err() * fix (iam): properly handle rows.Err() * fix (kms): properly handle rows.Err() * fix (oplog): properly handle rows.Err() * fix (pagination/purge): properly handle rows.Err() * fix (plugin): properly handle rows.Err() * fix (job/scheduler): properly handle rows.Err() * fix (server): properly handle rows.Err() * fix (session): properly handle rows.Err() * fix (target): properly handle rows.Err()
1052 lines
37 KiB
Go
1052 lines
37 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package vault
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/globals"
|
|
"github.com/hashicorp/boundary/internal/credential"
|
|
"github.com/hashicorp/boundary/internal/credential/vault/internal/sshprivatekey"
|
|
"github.com/hashicorp/boundary/internal/credential/vault/internal/usernamepassword"
|
|
"github.com/hashicorp/boundary/internal/db/sentinel"
|
|
"github.com/hashicorp/boundary/internal/db/timestamp"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/kms"
|
|
"github.com/hashicorp/boundary/internal/types/resource"
|
|
"github.com/hashicorp/boundary/internal/util/template"
|
|
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
|
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
|
|
vault "github.com/hashicorp/vault/api"
|
|
"github.com/mikesmitty/edkey"
|
|
"golang.org/x/crypto/ssh"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
var _ credential.Dynamic = (*baseCred)(nil)
|
|
|
|
type baseCred struct {
|
|
*Credential
|
|
|
|
lib issuingCredentialLibrary
|
|
secretData map[string]any
|
|
}
|
|
|
|
func (bc *baseCred) Secret() credential.SecretData { return bc.secretData }
|
|
func (bc *baseCred) Library() credential.Library { return bc.lib }
|
|
func (bc *baseCred) Purpose() credential.Purpose { return bc.lib.GetPurpose() }
|
|
func (bc *baseCred) getExpiration() time.Duration { return bc.expiration }
|
|
func (bc *baseCred) getCredential() *Credential { return bc.Credential }
|
|
func (bc *baseCred) isRevokable() bool { return bc.ExternalId != sentinel.ExternalIdNone }
|
|
|
|
// convert converts bc to a specific credential type if bc is not
|
|
// UnspecifiedType.
|
|
func convert(ctx context.Context, bc *baseCred) (dynamicCred, error) {
|
|
switch bc.Library().CredentialType() {
|
|
case globals.UsernamePasswordCredentialType:
|
|
return baseToUsrPass(ctx, bc)
|
|
case globals.SshPrivateKeyCredentialType:
|
|
return baseToSshPriKey(ctx, bc)
|
|
}
|
|
return bc, nil
|
|
}
|
|
|
|
var _ credential.UsernamePassword = (*usrPassCred)(nil)
|
|
|
|
type usrPassCred struct {
|
|
*baseCred
|
|
username string
|
|
password credential.Password
|
|
}
|
|
|
|
func (c *usrPassCred) Username() string { return c.username }
|
|
func (c *usrPassCred) Password() credential.Password { return c.password }
|
|
|
|
func baseToUsrPass(ctx context.Context, bc *baseCred) (*usrPassCred, error) {
|
|
switch {
|
|
case bc == nil:
|
|
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred"))
|
|
case bc.lib == nil:
|
|
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred.lib"))
|
|
case bc.Library().CredentialType() != globals.UsernamePasswordCredentialType:
|
|
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type"))
|
|
}
|
|
|
|
lib, ok := bc.lib.(*genericIssuingCredentialLibrary)
|
|
if !ok {
|
|
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("baseCred.lib is not of type genericIssuingCredentialLibrary"))
|
|
}
|
|
|
|
uAttr, pAttr := lib.UsernameAttribute, lib.PasswordAttribute
|
|
if uAttr == "" {
|
|
uAttr = "username"
|
|
}
|
|
if pAttr == "" {
|
|
pAttr = "password"
|
|
}
|
|
username, password := usernamepassword.Extract(bc.secretData, uAttr, pAttr)
|
|
if username == "" || password == "" {
|
|
return nil, errors.E(ctx, errors.WithCode(errors.VaultInvalidCredentialMapping))
|
|
}
|
|
|
|
return &usrPassCred{
|
|
baseCred: bc,
|
|
username: username,
|
|
password: credential.Password(password),
|
|
}, nil
|
|
}
|
|
|
|
var _ credential.SshPrivateKey = (*sshPrivateKeyCred)(nil)
|
|
|
|
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 {
|
|
case bc == nil:
|
|
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred"))
|
|
case bc.lib == nil:
|
|
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("nil baseCred.lib"))
|
|
case bc.Library().CredentialType() != globals.SshPrivateKeyCredentialType:
|
|
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("invalid credential type"))
|
|
}
|
|
|
|
lib, ok := bc.lib.(*genericIssuingCredentialLibrary)
|
|
if !ok {
|
|
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("baseCred.lib is not of type genericIssuingCredentialLibrary"))
|
|
}
|
|
|
|
uAttr, pkAttr, pAttr := lib.UsernameAttribute, lib.PrivateKeyAttribute, lib.PrivateKeyPassphraseAttribute
|
|
if uAttr == "" {
|
|
uAttr = "username"
|
|
}
|
|
if pkAttr == "" {
|
|
pkAttr = "private_key"
|
|
}
|
|
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))
|
|
}
|
|
|
|
return &sshPrivateKeyCred{
|
|
baseCred: bc,
|
|
username: username,
|
|
privateKey: pk,
|
|
passphrase: pass,
|
|
}, nil
|
|
}
|
|
|
|
type sshCertCred struct {
|
|
*sshPrivateKeyCred
|
|
certificate []byte
|
|
}
|
|
|
|
func (c *sshCertCred) Certificate() []byte { return c.certificate }
|
|
|
|
var _ credential.Library = (*genericIssuingCredentialLibrary)(nil)
|
|
|
|
type issuingCredentialLibrary interface {
|
|
credential.Library
|
|
GetPurpose() credential.Purpose
|
|
retrieveCredential(context.Context, errors.Op, ...credential.Option) (dynamicCred, error)
|
|
}
|
|
|
|
// A genericIssuingCredentialLibrary contains all the values needed to connect to Vault and
|
|
// retrieve credentials.
|
|
type genericIssuingCredentialLibrary struct {
|
|
PublicId string
|
|
StoreId string
|
|
Name string
|
|
Description string
|
|
CreateTime *timestamp.Timestamp
|
|
UpdateTime *timestamp.Timestamp
|
|
Version uint32
|
|
VaultPath string
|
|
HttpMethod string
|
|
HttpRequestBody []byte
|
|
CredType string
|
|
ProjectId string
|
|
VaultAddress string
|
|
Namespace string
|
|
CaCert []byte
|
|
TlsServerName string
|
|
TlsSkipVerify bool
|
|
WorkerFilter string
|
|
Token TokenSecret
|
|
CtToken []byte
|
|
TokenHmac []byte
|
|
TokenKeyId string
|
|
ClientCert []byte
|
|
ClientKey KeySecret
|
|
CtClientKey []byte
|
|
ClientKeyId string
|
|
UsernameAttribute string
|
|
PasswordAttribute string
|
|
PrivateKeyAttribute string
|
|
PrivateKeyPassphraseAttribute string
|
|
Purpose credential.Purpose
|
|
AdditionalValidPrincipals string
|
|
}
|
|
|
|
func (pl *genericIssuingCredentialLibrary) clone() *genericIssuingCredentialLibrary {
|
|
// The 'append(a[:0:0], a...)' comes from
|
|
// https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F
|
|
return &genericIssuingCredentialLibrary{
|
|
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,
|
|
ProjectId: pl.ProjectId,
|
|
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,
|
|
}
|
|
}
|
|
|
|
func (pl *genericIssuingCredentialLibrary) GetPublicId() string { return pl.PublicId }
|
|
func (pl *genericIssuingCredentialLibrary) GetStoreId() string { return pl.StoreId }
|
|
func (pl *genericIssuingCredentialLibrary) GetName() string { return pl.Name }
|
|
func (pl *genericIssuingCredentialLibrary) GetDescription() string { return pl.Description }
|
|
func (pl *genericIssuingCredentialLibrary) GetVersion() uint32 { return pl.Version }
|
|
func (pl *genericIssuingCredentialLibrary) GetCreateTime() *timestamp.Timestamp { return pl.CreateTime }
|
|
func (pl *genericIssuingCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { return pl.UpdateTime }
|
|
func (pl *genericIssuingCredentialLibrary) GetPurpose() credential.Purpose { return pl.Purpose }
|
|
|
|
func (pl *genericIssuingCredentialLibrary) CredentialType() globals.CredentialType {
|
|
switch ct := pl.CredType; ct {
|
|
case "":
|
|
return globals.UnspecifiedCredentialType
|
|
default:
|
|
return globals.CredentialType(ct)
|
|
}
|
|
}
|
|
|
|
// GetResourceType returns the resource type of the CredentialLibrary
|
|
func (pl *genericIssuingCredentialLibrary) GetResourceType() resource.Type {
|
|
return resource.CredentialLibrary
|
|
}
|
|
|
|
func (pl *genericIssuingCredentialLibrary) client(ctx context.Context) (vaultClient, error) {
|
|
const op = "vault.(genericIssuingCredentialLibrary).client"
|
|
clientConfig := &clientConfig{
|
|
Addr: pl.VaultAddress,
|
|
Token: pl.Token,
|
|
CaCert: pl.CaCert,
|
|
TlsServerName: pl.TlsServerName,
|
|
TlsSkipVerify: pl.TlsSkipVerify,
|
|
Namespace: pl.Namespace,
|
|
}
|
|
|
|
if pl.ClientKey != nil {
|
|
clientConfig.ClientCert = pl.ClientCert
|
|
clientConfig.ClientKey = pl.ClientKey
|
|
}
|
|
|
|
client, err := vaultClientFactoryFn(ctx, clientConfig, WithWorkerFilter(pl.WorkerFilter))
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create vault client"))
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
type dynamicCred interface {
|
|
credential.Dynamic
|
|
getExpiration() time.Duration
|
|
getCredential() *Credential
|
|
isRevokable() bool
|
|
}
|
|
|
|
// retrieveCredential retrieves a dynamic credential from Vault for the
|
|
// given sessionId.
|
|
//
|
|
// Supported options: credential.WithTemplateData
|
|
func (pl *genericIssuingCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, opt ...credential.Option) (dynamicCred, error) {
|
|
opts, err := credential.GetOpts(opt...)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// Get the credential ID early. No need to get a secret from Vault
|
|
// if there is no way to save it in the database.
|
|
credId, err := newCredentialId(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
client, err := pl.client(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
var secret *vault.Secret
|
|
var reqErr error
|
|
|
|
// Template the path
|
|
path := pl.VaultPath
|
|
if path != "" {
|
|
parsedTmpl, err := template.New(ctx, path)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
path, err = parsedTmpl.Generate(ctx, opts.WithTemplateData)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
// Template the body
|
|
body := string(pl.HttpRequestBody)
|
|
if body != "" {
|
|
parsedTmpl, err := template.New(ctx, body)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
body, err = parsedTmpl.Generate(ctx, opts.WithTemplateData)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
switch Method(pl.HttpMethod) {
|
|
case MethodGet:
|
|
secret, reqErr = client.get(ctx, path)
|
|
case MethodPost:
|
|
secret, reqErr = client.post(ctx, path, []byte(body))
|
|
default:
|
|
return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("unknown http method: library: %s", pl.PublicId))
|
|
}
|
|
|
|
if reqErr != nil {
|
|
// TODO(mgaffney) 05/2021: detect if the error is because of an
|
|
// expired or invalid token
|
|
return nil, errors.Wrap(ctx, reqErr, op)
|
|
}
|
|
if secret == nil {
|
|
return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op))
|
|
}
|
|
|
|
leaseDuration := time.Duration(secret.LeaseDuration) * time.Second
|
|
cred, err := newCredential(ctx, pl.GetPublicId(), secret.LeaseID, pl.TokenHmac, leaseDuration)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
cred.PublicId = credId
|
|
cred.IsRenewable = secret.Renewable
|
|
|
|
dCred := &baseCred{
|
|
Credential: cred,
|
|
lib: pl,
|
|
secretData: secret.Data,
|
|
}
|
|
return convert(ctx, dCred)
|
|
}
|
|
|
|
// TableName returns the table name for gorm.
|
|
func (pl *genericIssuingCredentialLibrary) TableName() string {
|
|
return "credential_vault_library_issue_credentials"
|
|
}
|
|
|
|
func (r *Repository) getIssueCredLibraries(ctx context.Context, requests []credential.Request) ([]issuingCredentialLibrary, error) {
|
|
const op = "vault.(Repository).getIssueCredLibraries"
|
|
|
|
mapper, err := newMapper(ctx, requests)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
libIds := mapper.libIds()
|
|
var inClauseSpots []string
|
|
for i := 1; i < len(libIds)+1; i++ {
|
|
inClauseSpots = append(inClauseSpots, fmt.Sprintf("@%d", i))
|
|
}
|
|
inClause := strings.Join(inClauseSpots, ",")
|
|
|
|
query := fmt.Sprintf(selectLibrariesQuery, inClause)
|
|
|
|
var params []any
|
|
for idx, v := range libIds {
|
|
params = append(params, sql.Named(fmt.Sprintf("%d", idx+1), v))
|
|
}
|
|
rows, err := r.reader.Query(ctx, query, params)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("query failed"))
|
|
}
|
|
defer rows.Close()
|
|
|
|
var libs []*privateCredentialLibraryAllTypes
|
|
for rows.Next() {
|
|
var lib privateCredentialLibraryAllTypes
|
|
if err := r.reader.ScanRows(ctx, rows, &lib); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("scan row failed"))
|
|
}
|
|
purps := mapper.get(lib.GetPublicId())
|
|
if len(purps) == 0 {
|
|
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("unknown library"))
|
|
}
|
|
for _, purp := range purps {
|
|
cp := lib.clone()
|
|
cp.Purpose = purp
|
|
libs = append(libs, cp)
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("next row failed"))
|
|
}
|
|
|
|
var decryptedLibs []issuingCredentialLibrary
|
|
for _, pl := range libs {
|
|
databaseWrapper, err := r.kms.GetWrapper(ctx, pl.ProjectId, kms.KeyPurposeDatabase)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
|
|
}
|
|
|
|
if err := pl.decrypt(ctx, databaseWrapper); err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
decryptedLibs = append(decryptedLibs, pl.toTypedIssuingCredentialLibrary())
|
|
}
|
|
|
|
return decryptedLibs, nil
|
|
}
|
|
|
|
// privateCredentialLibraryAllTypes is a clone of genericIssuingCredentialLibrary. Contains
|
|
// all the values needed to connect to Vault and retrieve credentials.
|
|
type privateCredentialLibraryAllTypes 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
|
|
CredType string `gorm:"column:credential_type"`
|
|
ProjectId string
|
|
VaultAddress string
|
|
Namespace string
|
|
CaCert []byte
|
|
TlsServerName string
|
|
TlsSkipVerify bool
|
|
WorkerFilter string
|
|
Token TokenSecret
|
|
CtToken []byte
|
|
TokenHmac []byte
|
|
TokenKeyId string
|
|
ClientCert []byte
|
|
ClientKey KeySecret
|
|
CtClientKey []byte
|
|
ClientKeyId string
|
|
UsernameAttribute string
|
|
PasswordAttribute string
|
|
PrivateKeyAttribute string
|
|
PrivateKeyPassphraseAttribute string
|
|
Purpose credential.Purpose `gorm:"-"`
|
|
KeyType string
|
|
KeyBits int
|
|
Username string
|
|
Ttl string
|
|
KeyId string
|
|
CriticalOptions []byte
|
|
Extensions []byte
|
|
CredLibType string
|
|
AdditionalValidPrincipals string
|
|
}
|
|
|
|
func (pl *privateCredentialLibraryAllTypes) GetPublicId() string { return pl.PublicId }
|
|
|
|
// GetResourceType returns the resource type of the CredentialLibrary
|
|
func (pl *privateCredentialLibraryAllTypes) GetResourceType() resource.Type {
|
|
return resource.CredentialLibrary
|
|
}
|
|
|
|
func (pl *privateCredentialLibraryAllTypes) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
|
const op = "vault.(privateCredentialLibraryAllTypes).decrypt"
|
|
|
|
if pl.CtToken != nil {
|
|
type ptk struct {
|
|
Token []byte `wrapping:"pt,token_data"`
|
|
CtToken []byte `wrapping:"ct,token_data"`
|
|
}
|
|
ptkv := &ptk{
|
|
CtToken: pl.CtToken,
|
|
}
|
|
if err := structwrapping.UnwrapStruct(ctx, cipher, ptkv, nil); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("token"))
|
|
}
|
|
pl.Token = ptkv.Token
|
|
}
|
|
|
|
if pl.CtClientKey != nil && pl.ClientCert != nil {
|
|
type pck struct {
|
|
Key []byte `wrapping:"pt,key_data"`
|
|
CtKey []byte `wrapping:"ct,key_data"`
|
|
}
|
|
pckv := &pck{
|
|
CtKey: pl.CtClientKey,
|
|
}
|
|
if err := structwrapping.UnwrapStruct(ctx, cipher, pckv, nil); err != nil {
|
|
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("client certificate"))
|
|
}
|
|
pl.ClientKey = pckv.Key
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (pl *privateCredentialLibraryAllTypes) clone() *privateCredentialLibraryAllTypes {
|
|
// The 'append(a[:0:0], a...)' comes from
|
|
// https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F
|
|
return &privateCredentialLibraryAllTypes{
|
|
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,
|
|
ProjectId: pl.ProjectId,
|
|
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,
|
|
KeyType: pl.KeyType,
|
|
KeyBits: pl.KeyBits,
|
|
Username: pl.Username,
|
|
Ttl: pl.Ttl,
|
|
KeyId: pl.KeyId,
|
|
CriticalOptions: pl.CriticalOptions,
|
|
Extensions: pl.Extensions,
|
|
CredLibType: pl.CredLibType,
|
|
AdditionalValidPrincipals: pl.AdditionalValidPrincipals,
|
|
}
|
|
}
|
|
|
|
func (pl *privateCredentialLibraryAllTypes) toTypedIssuingCredentialLibrary() issuingCredentialLibrary {
|
|
switch pl.CredLibType {
|
|
case "ssh-signed-cert":
|
|
return &sshCertIssuingCredentialLibrary{
|
|
PublicId: pl.PublicId,
|
|
StoreId: pl.StoreId,
|
|
CredType: pl.CredType,
|
|
Username: pl.Username,
|
|
Name: pl.Name,
|
|
Description: pl.Description,
|
|
CreateTime: pl.CreateTime,
|
|
UpdateTime: pl.UpdateTime,
|
|
Version: pl.Version,
|
|
ProjectId: pl.ProjectId,
|
|
VaultPath: pl.VaultPath,
|
|
VaultAddress: pl.VaultAddress,
|
|
Namespace: pl.Namespace,
|
|
CaCert: pl.CaCert,
|
|
TlsServerName: pl.TlsServerName,
|
|
TlsSkipVerify: pl.TlsSkipVerify,
|
|
WorkerFilter: pl.WorkerFilter,
|
|
TokenHmac: pl.TokenHmac,
|
|
Token: pl.Token,
|
|
CtToken: pl.CtToken,
|
|
TokenKeyId: pl.TokenKeyId,
|
|
ClientCert: pl.ClientCert,
|
|
ClientKey: pl.ClientKey,
|
|
CtClientKey: pl.CtClientKey,
|
|
ClientKeyId: pl.ClientKeyId,
|
|
Purpose: pl.Purpose,
|
|
KeyType: pl.KeyType,
|
|
KeyBits: pl.KeyBits,
|
|
Ttl: pl.Ttl,
|
|
KeyId: pl.KeyId,
|
|
CriticalOptions: pl.CriticalOptions,
|
|
Extensions: pl.Extensions,
|
|
AdditionalValidPrincipals: pl.AdditionalValidPrincipals,
|
|
}
|
|
default:
|
|
return &genericIssuingCredentialLibrary{
|
|
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: pl.CreateTime,
|
|
UpdateTime: pl.UpdateTime,
|
|
Version: pl.Version,
|
|
ProjectId: pl.ProjectId,
|
|
VaultPath: pl.VaultPath,
|
|
HttpMethod: pl.HttpMethod,
|
|
HttpRequestBody: pl.HttpRequestBody,
|
|
VaultAddress: pl.VaultAddress,
|
|
Namespace: pl.Namespace,
|
|
CaCert: pl.CaCert,
|
|
TlsServerName: pl.TlsServerName,
|
|
TlsSkipVerify: pl.TlsSkipVerify,
|
|
WorkerFilter: pl.WorkerFilter,
|
|
TokenHmac: pl.TokenHmac,
|
|
Token: pl.Token,
|
|
CtToken: pl.CtToken,
|
|
TokenKeyId: pl.TokenKeyId,
|
|
ClientCert: pl.ClientCert,
|
|
ClientKey: pl.ClientKey,
|
|
CtClientKey: pl.CtClientKey,
|
|
ClientKeyId: pl.ClientKeyId,
|
|
Purpose: pl.Purpose,
|
|
AdditionalValidPrincipals: pl.AdditionalValidPrincipals,
|
|
}
|
|
}
|
|
}
|
|
|
|
// requestMap takes a slice of credential requests and provides of list of
|
|
// library IDs with duplicate IDs removed. It also provides a way to lookup
|
|
// the list of credential purposes for a particular library ID.
|
|
//
|
|
// A single library can be used to retrieve multiple credentials as long as
|
|
// each credential is for a different purpose. When retrieving the private
|
|
// libraries from the database, a list of library IDs with duplicates
|
|
// removed is needed. When requesting credentials from vault, any library
|
|
// being used for multiple purposes needs to be duplicated with the purpose
|
|
// so multiple requests are made to vault using the same library.
|
|
type requestMap struct {
|
|
ids map[string][]credential.Purpose
|
|
}
|
|
|
|
func newMapper(ctx context.Context, requests []credential.Request) (*requestMap, error) {
|
|
ids := make(map[string][]credential.Purpose, len(requests))
|
|
for _, req := range requests {
|
|
if purps, ok := ids[req.SourceId]; ok {
|
|
for _, purp := range purps {
|
|
if purp == req.Purpose {
|
|
return nil, errors.E(ctx, errors.WithCode(errors.InvalidParameter), errors.WithMsg("duplicate library and purpose"))
|
|
}
|
|
}
|
|
}
|
|
ids[req.SourceId] = append(ids[req.SourceId], req.Purpose)
|
|
}
|
|
return &requestMap{
|
|
ids: ids,
|
|
}, nil
|
|
}
|
|
|
|
func (m *requestMap) libIds() []string {
|
|
var ids []string
|
|
for id := range m.ids {
|
|
ids = append(ids, id)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func (m *requestMap) get(libraryId string) []credential.Purpose {
|
|
return m.ids[libraryId]
|
|
}
|
|
|
|
type sshCertIssuingCredentialLibrary struct {
|
|
PublicId string
|
|
StoreId string
|
|
Name string
|
|
Description string
|
|
CreateTime *timestamp.Timestamp
|
|
UpdateTime *timestamp.Timestamp
|
|
Version uint32
|
|
VaultPath string
|
|
CredType string
|
|
ProjectId string
|
|
VaultAddress string
|
|
Namespace string
|
|
CaCert []byte
|
|
TlsServerName string
|
|
TlsSkipVerify bool
|
|
WorkerFilter string
|
|
Token TokenSecret
|
|
CtToken []byte
|
|
TokenHmac []byte
|
|
TokenKeyId string
|
|
ClientCert []byte
|
|
ClientKey KeySecret
|
|
CtClientKey []byte
|
|
ClientKeyId string
|
|
Username string
|
|
KeyType string
|
|
KeyBits int
|
|
KeyId string
|
|
Ttl string
|
|
CriticalOptions []byte
|
|
Extensions []byte
|
|
Purpose credential.Purpose
|
|
AdditionalValidPrincipals string
|
|
}
|
|
|
|
func (lib *sshCertIssuingCredentialLibrary) GetPublicId() string { return lib.PublicId }
|
|
func (lib *sshCertIssuingCredentialLibrary) GetStoreId() string { return lib.StoreId }
|
|
func (lib *sshCertIssuingCredentialLibrary) GetName() string { return lib.Name }
|
|
func (lib *sshCertIssuingCredentialLibrary) GetDescription() string { return lib.Description }
|
|
func (lib *sshCertIssuingCredentialLibrary) GetVersion() uint32 { return lib.Version }
|
|
func (lib *sshCertIssuingCredentialLibrary) GetPurpose() credential.Purpose { return lib.Purpose }
|
|
func (lib *sshCertIssuingCredentialLibrary) GetCreateTime() *timestamp.Timestamp {
|
|
return lib.CreateTime
|
|
}
|
|
|
|
func (lib *sshCertIssuingCredentialLibrary) GetUpdateTime() *timestamp.Timestamp {
|
|
return lib.UpdateTime
|
|
}
|
|
|
|
func (lib *sshCertIssuingCredentialLibrary) CredentialType() globals.CredentialType {
|
|
switch ct := lib.CredType; ct {
|
|
case "":
|
|
return globals.UnspecifiedCredentialType
|
|
default:
|
|
return globals.CredentialType(ct)
|
|
}
|
|
}
|
|
|
|
// GetResourceType returns the resource type of the CredentialLibrary
|
|
func (lib *sshCertIssuingCredentialLibrary) GetResourceType() resource.Type {
|
|
return resource.CredentialLibrary
|
|
}
|
|
|
|
func (lib *sshCertIssuingCredentialLibrary) client(ctx context.Context) (vaultClient, error) {
|
|
const op = "vault.(genericIssuingCredentialLibrary).client"
|
|
clientConfig := &clientConfig{
|
|
Addr: lib.VaultAddress,
|
|
Token: lib.Token,
|
|
CaCert: lib.CaCert,
|
|
TlsServerName: lib.TlsServerName,
|
|
TlsSkipVerify: lib.TlsSkipVerify,
|
|
Namespace: lib.Namespace,
|
|
}
|
|
|
|
if lib.ClientKey != nil {
|
|
clientConfig.ClientCert = lib.ClientCert
|
|
clientConfig.ClientKey = lib.ClientKey
|
|
}
|
|
|
|
client, err := vaultClientFactoryFn(ctx, clientConfig, WithWorkerFilter(lib.WorkerFilter))
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create vault client"))
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
func generatePublicPrivateKeys(ctx context.Context, keyType string, keyBits int) (string, []byte, error) {
|
|
const op = "vault.generatePublicPrivateKeys"
|
|
pemBlock := pem.Block{}
|
|
var sshKey ssh.PublicKey
|
|
|
|
switch keyType {
|
|
case KeyTypeRsa:
|
|
pemBlock.Type = "RSA PRIVATE KEY" // these values are copied from the crypto ssh library in ssh/keys.go
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, keyBits)
|
|
if err != nil {
|
|
return "", nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
if sshKey, err = ssh.NewPublicKey(&key.PublicKey); err != nil {
|
|
return "", nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
pemBlock.Bytes = x509.MarshalPKCS1PrivateKey(key)
|
|
|
|
case KeyTypeEd25519:
|
|
pemBlock.Type = "OPENSSH PRIVATE KEY" // these values are copied from the crypto ssh library in ssh/keys.go
|
|
|
|
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return "", nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
if sshKey, err = ssh.NewPublicKey(pubKey); err != nil {
|
|
return "", nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
if pemBlock.Bytes = edkey.MarshalED25519PrivateKey(privKey); pemBlock.Bytes == nil {
|
|
return "", nil, errors.New(ctx, errors.Encode, op, "failed to marshal ed25519 private key")
|
|
}
|
|
|
|
case KeyTypeEcdsa:
|
|
pemBlock.Type = "EC PRIVATE KEY" // these values are copied from the crypto ssh library in ssh/keys.go
|
|
|
|
var curve elliptic.Curve
|
|
switch keyBits {
|
|
case 256:
|
|
curve = elliptic.P256()
|
|
case 384:
|
|
curve = elliptic.P384()
|
|
case 521:
|
|
curve = elliptic.P521()
|
|
default:
|
|
return "", nil, errors.New(ctx, errors.InvalidParameter, op, "invalid KeyBits. when KeyType=ecdsa, KeyBits must be one of: 256, 384, or 521")
|
|
}
|
|
|
|
key, err := ecdsa.GenerateKey(curve, rand.Reader)
|
|
if err != nil {
|
|
return "", nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
if sshKey, err = ssh.NewPublicKey(&key.PublicKey); err != nil {
|
|
return "", nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
if pemBlock.Bytes, err = x509.MarshalECPrivateKey(key); err != nil {
|
|
return "", nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
default:
|
|
return "", nil, errors.New(ctx, errors.InvalidParameter, op, "invalid KeyType, must be one of: \"rsa\", \"ed25519\", or \"ecdsa\"")
|
|
}
|
|
|
|
publicKey := base64.StdEncoding.EncodeToString(sshKey.Marshal())
|
|
privateKey := pem.EncodeToMemory(&pemBlock)
|
|
if privateKey == nil {
|
|
return "", nil, errors.New(ctx, errors.Encode, op, "failed to encode private key to PEM format")
|
|
}
|
|
return publicKey, privateKey, nil
|
|
}
|
|
|
|
type sshCertVaultBody struct {
|
|
KeyType string `json:"key_type,omitempty"` // must be "rsa", "ed25519", or "ecdsa"
|
|
KeyBits int `json:"key_bits,omitempty"` // with key_type=rsa, allowed values are: 2048 (default), 3072, or 4096; with key_type=ecdsa, allowed values are: 256 (default), 384, or 521; ignored with key_type=ed25519
|
|
PublicKey string `json:"public_key,omitempty"`
|
|
TTL string `json:"ttl,omitempty"`
|
|
ValidPrincipals string `json:"valid_principals,omitempty"` // this needs to be "generated" off of the username provided in config
|
|
CertType string `json:"cert_type,omitempty"` // this should always be "user"
|
|
KeyId string `json:"key_id,omitempty"` // this will be loaded directly from lib
|
|
CriticalOptions map[string]string `json:"critical_options,omitempty"` // this will be loaded directly from lib
|
|
Extensions map[string]string `json:"extensions,omitempty"` // this will be loaded directly from lib
|
|
}
|
|
|
|
var vaultPathRegexp = regexp.MustCompile(`^.+\/(sign|issue)\/[^\/\\\s]+$`)
|
|
|
|
// retrieveCredential retrieves a dynamic connection credential from Vault
|
|
// for a specific session and connection.
|
|
//
|
|
// Supported options: credential.WithTemplateData
|
|
func (lib *sshCertIssuingCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, opt ...credential.Option) (dynamicCred, error) {
|
|
opts, err := credential.GetOpts(opt...)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// Get the credential ID early. No need to get a secret from Vault
|
|
// if there is no way to save it in the database.
|
|
credId, err := newCredentialId(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
client, err := lib.client(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// build as much of the payload as we can, since sign/issue share many attributes
|
|
// Template the username
|
|
tplate, err := template.New(ctx, lib.Username)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
username, err := tplate.Generate(ctx, opts.WithTemplateData)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
// Template the key ID if it's not empty
|
|
keyId := lib.KeyId
|
|
if keyId != "" {
|
|
tplate, err = template.New(ctx, lib.KeyId)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
keyId, err = tplate.Generate(ctx, opts.WithTemplateData)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
var criticalOptions map[string]string
|
|
if lib.CriticalOptions != nil {
|
|
if json.Unmarshal(lib.CriticalOptions, &criticalOptions) != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
var extensions map[string]string
|
|
if lib.Extensions != nil {
|
|
if json.Unmarshal(lib.Extensions, &extensions) != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
}
|
|
|
|
validPrincipals := username
|
|
if lib.AdditionalValidPrincipals != "" {
|
|
validPrincipals = fmt.Sprintf("%s,%s", username, lib.AdditionalValidPrincipals)
|
|
}
|
|
|
|
payload := sshCertVaultBody{
|
|
ValidPrincipals: validPrincipals,
|
|
CertType: "user",
|
|
CriticalOptions: criticalOptions,
|
|
Extensions: extensions,
|
|
TTL: lib.Ttl,
|
|
KeyId: keyId,
|
|
}
|
|
|
|
var privateKey credential.PrivateKey
|
|
var secret *vault.Secret
|
|
|
|
match := vaultPathRegexp.FindStringSubmatch(lib.VaultPath)
|
|
if len(match) < 2 {
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "vault path was not in an expected format. expected path containing \"sign\" or \"issue\"")
|
|
}
|
|
|
|
// by definition, if match exists, then match[1] == "sign" or "issue"
|
|
switch match[1] {
|
|
case "sign":
|
|
payload.PublicKey, privateKey, err = generatePublicPrivateKeys(ctx, lib.KeyType, lib.KeyBits)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
secret, err = client.post(ctx, lib.VaultPath, body)
|
|
if err != nil {
|
|
// TODO(mgaffney) 05/2021: detect if the error is because of an
|
|
// expired or invalid token
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
if secret == nil {
|
|
return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op))
|
|
}
|
|
|
|
case "issue":
|
|
payload.KeyBits = lib.KeyBits
|
|
if lib.KeyType == KeyTypeEcdsa {
|
|
// this is a special case where internal to boundary, we refer to the crypto
|
|
// library name, but vault refers to it simply as "ec" for "Elliptic Curve"
|
|
payload.KeyType = "ec"
|
|
} else {
|
|
payload.KeyType = lib.KeyType
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
|
|
secret, err = client.post(ctx, lib.VaultPath, body)
|
|
if err != nil {
|
|
// TODO(mgaffney) 05/2021: detect if the error is because of an
|
|
// expired or invalid token
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
if secret == nil {
|
|
return nil, errors.E(ctx, errors.WithCode(errors.VaultEmptySecret), errors.WithOp(op))
|
|
}
|
|
|
|
pk, ok := secret.Data["private_key"].(string)
|
|
if !ok {
|
|
return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a private key or response was not in the expected format")
|
|
}
|
|
|
|
privateKey = []byte(pk)
|
|
}
|
|
|
|
leaseDuration := time.Duration(secret.LeaseDuration) * time.Second
|
|
cred, err := newCredential(ctx, lib.GetPublicId(), secret.LeaseID, lib.TokenHmac, leaseDuration)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
cred.PublicId = credId
|
|
cred.IsRenewable = secret.Renewable // possibly set to just false
|
|
|
|
// same location for both
|
|
cert, ok := secret.Data["signed_key"].(string)
|
|
if !ok {
|
|
return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a signed key or response was not in the expected format")
|
|
}
|
|
|
|
return &sshCertCred{
|
|
sshPrivateKeyCred: &sshPrivateKeyCred{
|
|
baseCred: &baseCred{
|
|
Credential: cred,
|
|
lib: lib,
|
|
secretData: secret.Data,
|
|
},
|
|
username: username,
|
|
privateKey: privateKey,
|
|
},
|
|
certificate: []byte(cert),
|
|
}, nil
|
|
}
|