Files
threshold/internal/credential/vault/private_library.go
T
Jim 6a37c93d77 fix: properly handle rows.Err() (#4261)
* 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()
2024-01-22 14:03:40 -05:00

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
}