mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1421 lines
50 KiB
1421 lines
50 KiB
// 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"
|
|
|
|
ldapv3 "github.com/go-ldap/ldap/v3"
|
|
"github.com/hashicorp/boundary/globals"
|
|
"github.com/hashicorp/boundary/internal/credential"
|
|
"github.com/hashicorp/boundary/internal/credential/vault/internal/password"
|
|
"github.com/hashicorp/boundary/internal/credential/vault/internal/sshprivatekey"
|
|
"github.com/hashicorp/boundary/internal/credential/vault/internal/usernamepassword"
|
|
"github.com/hashicorp/boundary/internal/credential/vault/internal/usernamepassworddomain"
|
|
"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)
|
|
case globals.UsernamePasswordDomainCredentialType:
|
|
return baseToUsrPassDomain(ctx, bc)
|
|
case globals.PasswordCredentialType:
|
|
return baseToPass(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.UsernamePasswordDomain = (*usrPassDomainCred)(nil)
|
|
|
|
type usrPassDomainCred struct {
|
|
*baseCred
|
|
username string
|
|
password credential.Password
|
|
domain string
|
|
}
|
|
|
|
func (c *usrPassDomainCred) Username() string { return c.username }
|
|
func (c *usrPassDomainCred) Password() credential.Password { return c.password }
|
|
func (c *usrPassDomainCred) Domain() string { return c.domain }
|
|
|
|
func baseToUsrPassDomain(ctx context.Context, bc *baseCred) (*usrPassDomainCred, 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.UsernamePasswordDomainCredentialType:
|
|
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, dAttr := lib.UsernameAttribute, lib.PasswordAttribute, lib.DomainAttribute
|
|
if uAttr == "" {
|
|
uAttr = "username"
|
|
}
|
|
if pAttr == "" {
|
|
pAttr = "password"
|
|
}
|
|
if dAttr == "" {
|
|
dAttr = "domain"
|
|
}
|
|
username, password, domain := usernamepassworddomain.Extract(bc.secretData, uAttr, pAttr, dAttr)
|
|
if username == "" || password == "" || domain == "" {
|
|
return nil, errors.E(ctx, errors.WithCode(errors.VaultInvalidCredentialMapping))
|
|
}
|
|
|
|
return &usrPassDomainCred{
|
|
baseCred: bc,
|
|
username: username,
|
|
password: credential.Password(password),
|
|
domain: domain,
|
|
}, nil
|
|
}
|
|
|
|
var _ credential.PasswordOnly = (*passCred)(nil)
|
|
|
|
type passCred struct {
|
|
*baseCred
|
|
password credential.Password
|
|
}
|
|
|
|
func (c *passCred) Password() credential.Password { return c.password }
|
|
|
|
func baseToPass(ctx context.Context, bc *baseCred) (*passCred, 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.PasswordCredentialType:
|
|
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"))
|
|
}
|
|
|
|
pAttr := lib.PasswordAttribute
|
|
if pAttr == "" {
|
|
pAttr = "password"
|
|
}
|
|
password := password.Extract(bc.secretData, pAttr)
|
|
if password == "" {
|
|
return nil, errors.E(ctx, errors.WithCode(errors.VaultInvalidCredentialMapping))
|
|
}
|
|
|
|
return &passCred{
|
|
baseCred: bc,
|
|
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)
|
|
}
|
|
|
|
// genericIssuingCredentialLibrary is a subtype of
|
|
// privateCredentialLibraryAllTypes specifically for Vault generic credential
|
|
// libraries. It 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
|
|
DomainAttribute 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,
|
|
DomainAttribute: pl.DomainAttribute,
|
|
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)
|
|
}
|
|
|
|
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 type that interfaces with the database.
|
|
// It 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
|
|
DomainAttribute 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,
|
|
DomainAttribute: pl.DomainAttribute,
|
|
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,
|
|
}
|
|
case "ldap":
|
|
return &ldapIssuingCredentialLibrary{
|
|
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,
|
|
DomainAttribute: pl.DomainAttribute,
|
|
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]
|
|
}
|
|
|
|
// sshCertIssuingCredentialLibrary is a subtype of
|
|
// privateCredentialLibraryAllTypes specifically for Vault SSH Certificate
|
|
// credential libraries. It contains all the values needed to connect to Vault
|
|
// and retrieve credentials.
|
|
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
|
|
}
|
|
|
|
type ldapIssuingCredentialLibrary 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
|
|
}
|
|
|
|
// GetPublicId returns the credential library's id.
|
|
func (lib *ldapIssuingCredentialLibrary) GetPublicId() string { return lib.PublicId }
|
|
|
|
// GetStoreId returns the credential library's credential store id.
|
|
func (lib *ldapIssuingCredentialLibrary) GetStoreId() string { return lib.StoreId }
|
|
|
|
// GetName returns the credential library's name.
|
|
func (lib *ldapIssuingCredentialLibrary) GetName() string { return lib.Name }
|
|
|
|
// GetDescription returns the credential library's description.
|
|
func (lib *ldapIssuingCredentialLibrary) GetDescription() string { return lib.Description }
|
|
|
|
// GetVersion returns the credential library's version.
|
|
func (lib *ldapIssuingCredentialLibrary) GetVersion() uint32 { return lib.Version }
|
|
|
|
// GetPurpose returns the credential library's purpose.
|
|
func (lib *ldapIssuingCredentialLibrary) GetPurpose() credential.Purpose { return lib.Purpose }
|
|
|
|
// GetCreateTime returns the credential library's create time.
|
|
func (lib *ldapIssuingCredentialLibrary) GetCreateTime() *timestamp.Timestamp { return lib.CreateTime }
|
|
|
|
// GetUpdateTime returns the credential library's update time.
|
|
func (lib *ldapIssuingCredentialLibrary) GetUpdateTime() *timestamp.Timestamp { return lib.UpdateTime }
|
|
|
|
// CredentialType returns the credential library's issuing credential type.
|
|
func (lib *ldapIssuingCredentialLibrary) CredentialType() globals.CredentialType {
|
|
switch ct := lib.CredType; ct {
|
|
case "":
|
|
return globals.UnspecifiedCredentialType
|
|
default:
|
|
return globals.CredentialType(ct)
|
|
}
|
|
}
|
|
|
|
// GetResourceType returns the credential library's resource type.
|
|
func (lib *ldapIssuingCredentialLibrary) GetResourceType() resource.Type {
|
|
return resource.CredentialLibrary
|
|
}
|
|
|
|
// client produces a Vault API client using this credential library's data.
|
|
func (lib *ldapIssuingCredentialLibrary) 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
|
|
}
|
|
|
|
var vaultLdapPathRegexp = regexp.MustCompile(`^.+\/(static-cred|creds).+$`)
|
|
|
|
// retrieveCredential retrieves a dynamic LDAP credential from Vault for a
|
|
// specific session and connection.
|
|
func (lib *ldapIssuingCredentialLibrary) retrieveCredential(ctx context.Context, op errors.Op, _ ...credential.Option) (dynamicCred, error) {
|
|
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)
|
|
}
|
|
var secret *vault.Secret
|
|
|
|
secret, err = client.get(ctx, lib.VaultPath)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, 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, lib.GetPublicId(), secret.LeaseID, lib.TokenHmac, leaseDuration)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
cred.PublicId = credId
|
|
cred.IsRenewable = secret.Renewable
|
|
|
|
upd := &usrPassDomainCred{
|
|
baseCred: &baseCred{
|
|
Credential: cred,
|
|
lib: lib,
|
|
secretData: secret.Data,
|
|
},
|
|
}
|
|
|
|
username, ok := secret.Data["username"].(string)
|
|
if !ok {
|
|
return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a username or response was not in the expected format")
|
|
}
|
|
upd.username = username
|
|
|
|
pwData, ok := secret.Data["password"].(string)
|
|
if !ok {
|
|
return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a password or response was not in the expected format")
|
|
}
|
|
upd.password = credential.Password(pwData)
|
|
|
|
matches := vaultLdapPathRegexp.FindStringSubmatch(lib.VaultPath)
|
|
if len(matches) < 2 { // [0] is the vault path, [1] is static-cred or creds, if it exists.
|
|
return nil, errors.New(ctx, errors.InvalidParameter, op, "vault path was not in an expected format. expected path containing \"static-cred\" or \"creds\"")
|
|
}
|
|
switch matches[1] {
|
|
case "static-cred":
|
|
dn, ok := secret.Data["dn"].(string)
|
|
if !ok {
|
|
return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain a dn or response was not in the expected format")
|
|
}
|
|
domain, err := extractDomainFromDn(dn)
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
upd.domain = domain
|
|
case "creds":
|
|
var dns []string
|
|
dis, ok := secret.Data["distinguished_names"].([]any)
|
|
if ok {
|
|
for _, value := range dis {
|
|
if v, ok := value.(string); ok {
|
|
dns = append(dns, v)
|
|
} else {
|
|
return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret returned distinguished_names with non-string member")
|
|
}
|
|
}
|
|
} else {
|
|
return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret did not contain distinguished_names or response was not in the expected format")
|
|
}
|
|
if len(dns) == 0 {
|
|
return nil, errors.New(ctx, errors.VaultInvalidCredentialMapping, op, "vault secret returned empty distinguished_names")
|
|
}
|
|
|
|
// In the Vault LDAP secrets engine, no deduplication occurs and order
|
|
// is maintained from the LDIF statements, so the assumption here is
|
|
// that the first DN should correspond to the user's creation in the
|
|
// creation LDIF, which should correspond to the correct domain.
|
|
// https://developer.hashicorp.com/vault/docs/secrets/ldap#dynamic-credentials
|
|
domain, err := extractDomainFromDn(dns[0])
|
|
if err != nil {
|
|
return nil, errors.Wrap(ctx, err, op)
|
|
}
|
|
upd.domain = domain
|
|
}
|
|
|
|
return upd, nil
|
|
}
|
|
|
|
// extractDomainFromDn composes a domain name from all the "dc" attributes in an
|
|
// LDAP DN.
|
|
func extractDomainFromDn(dnStr string) (string, error) {
|
|
if dnStr == "" {
|
|
return "", fmt.Errorf("empty DN")
|
|
}
|
|
|
|
dn, err := ldapv3.ParseDN(dnStr)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse DN: %w", err)
|
|
}
|
|
|
|
var dcs []string
|
|
for _, rdn := range dn.RDNs {
|
|
for _, attr := range rdn.Attributes {
|
|
// "0.9.2342.19200300.100.1.25" is the OID for the "dc" attribute
|
|
// and it is a valid representation in a DN string.
|
|
// (https://docs.ldap.com/specs/rfc4519.txt)
|
|
if strings.EqualFold(attr.Type, "dc") || strings.EqualFold(attr.Type, "0.9.2342.19200300.100.1.25") {
|
|
dcs = append(dcs, attr.Value)
|
|
}
|
|
}
|
|
}
|
|
if len(dcs) == 0 {
|
|
return "", fmt.Errorf("no domain component (dc) attribute found in dn")
|
|
}
|
|
|
|
return strings.Join(dcs, "."), nil
|
|
}
|