2023-01-30 22:17:53 +00:00
// Copyright (c) HashiCorp, Inc.
2021-11-10 06:47:21 -05:00
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2021-06-22 22:26:26 -04:00
package vault
import (
"context"
2023-01-30 22:17:53 +00:00
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
2021-09-30 08:47:34 -04:00
"database/sql"
2023-01-30 22:17:53 +00:00
"encoding/base64"
"encoding/json"
"encoding/pem"
2021-06-22 22:26:26 -04:00
"fmt"
2023-01-30 22:17:53 +00:00
"regexp"
2021-06-22 22:26:26 -04:00
"strings"
2021-11-10 06:47:21 -05:00
"time"
2021-06-22 22:26:26 -04:00
2023-10-18 14:48:44 -07:00
"github.com/hashicorp/boundary/globals"
2021-06-22 22:26:26 -04:00
"github.com/hashicorp/boundary/internal/credential"
2022-07-19 10:31:53 -07:00
"github.com/hashicorp/boundary/internal/credential/vault/internal/sshprivatekey"
2022-06-27 14:03:12 -07:00
"github.com/hashicorp/boundary/internal/credential/vault/internal/usernamepassword"
2023-01-31 22:14:56 +00:00
"github.com/hashicorp/boundary/internal/db/sentinel"
2021-06-22 22:26:26 -04:00
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
2023-10-23 15:53:29 -07:00
"github.com/hashicorp/boundary/internal/types/resource"
2022-10-27 09:01:05 -04:00
"github.com/hashicorp/boundary/internal/util/template"
2022-03-16 17:04:07 -04:00
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
2021-11-10 06:47:21 -05:00
vault "github.com/hashicorp/vault/api"
2023-01-30 22:17:53 +00:00
"github.com/mikesmitty/edkey"
"golang.org/x/crypto/ssh"
2021-06-22 22:26:26 -04:00
"google.golang.org/protobuf/proto"
)
2021-11-10 06:47:21 -05:00
var _ credential . Dynamic = ( * baseCred )( nil )
type baseCred struct {
* Credential
2021-06-22 22:26:26 -04:00
2023-01-30 22:17:53 +00:00
lib issuingCredentialLibrary
2022-10-25 18:29:38 -05:00
secretData map [ string ] any
2021-06-22 22:26:26 -04:00
}
2021-11-10 06:47:21 -05:00
func ( bc * baseCred ) Secret () credential . SecretData { return bc . secretData }
func ( bc * baseCred ) Library () credential . Library { return bc . lib }
2023-01-30 22:17:53 +00:00
func ( bc * baseCred ) Purpose () credential . Purpose { return bc . lib . GetPurpose () }
2021-11-10 06:47:21 -05:00
func ( bc * baseCred ) getExpiration () time . Duration { return bc . expiration }
2023-01-09 12:06:43 -05:00
func ( bc * baseCred ) getCredential () * Credential { return bc . Credential }
2023-01-31 22:14:56 +00:00
func ( bc * baseCred ) isRevokable () bool { return bc . ExternalId != sentinel . ExternalIdNone }
2021-11-10 06:47:21 -05:00
// 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 () {
2023-10-18 14:48:44 -07:00
case globals . UsernamePasswordCredentialType :
2021-11-10 06:47:21 -05:00
return baseToUsrPass ( ctx , bc )
2023-10-18 14:48:44 -07:00
case globals . SshPrivateKeyCredentialType :
2022-07-19 10:31:53 -07:00
return baseToSshPriKey ( ctx , bc )
2021-11-10 06:47:21 -05:00
}
return bc , nil
}
2022-07-19 10:31:53 -07:00
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 }
2021-11-10 06:47:21 -05:00
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" ))
2023-10-18 14:48:44 -07:00
case bc . Library (). CredentialType () != globals . UsernamePasswordCredentialType :
2021-11-10 06:47:21 -05:00
return nil , errors . E ( ctx , errors . WithCode ( errors . InvalidParameter ), errors . WithMsg ( "invalid credential type" ))
}
2023-01-30 22:17:53 +00:00
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
2021-11-10 06:47:21 -05:00
if uAttr == "" {
uAttr = "username"
}
if pAttr == "" {
pAttr = "password"
}
2022-06-27 14:03:12 -07:00
username , password := usernamepassword . Extract ( bc . secretData , uAttr , pAttr )
2021-11-10 06:47:21 -05:00
if username == "" || password == "" {
return nil , errors . E ( ctx , errors . WithCode ( errors . VaultInvalidCredentialMapping ))
}
return & usrPassCred {
baseCred : bc ,
username : username ,
password : credential . Password ( password ),
}, nil
}
2021-06-22 22:26:26 -04:00
2022-07-19 10:31:53 -07:00
var _ credential . SshPrivateKey = ( * sshPrivateKeyCred )( nil )
type sshPrivateKeyCred struct {
* baseCred
username string
privateKey credential . PrivateKey
2022-08-08 15:15:03 -04:00
passphrase [] byte
2022-07-19 10:31:53 -07:00
}
func ( c * sshPrivateKeyCred ) Username () string { return c . username }
func ( c * sshPrivateKeyCred ) PrivateKey () credential . PrivateKey { return c . privateKey }
2022-08-08 15:15:03 -04:00
func ( c * sshPrivateKeyCred ) PrivateKeyPassphrase () [] byte { return c . passphrase }
2022-07-19 10:31:53 -07:00
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" ))
2023-10-18 14:48:44 -07:00
case bc . Library (). CredentialType () != globals . SshPrivateKeyCredentialType :
2022-07-19 10:31:53 -07:00
return nil , errors . E ( ctx , errors . WithCode ( errors . InvalidParameter ), errors . WithMsg ( "invalid credential type" ))
}
2023-01-30 22:17:53 +00:00
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
2022-07-19 10:31:53 -07:00
if uAttr == "" {
uAttr = "username"
}
2022-08-08 15:15:03 -04:00
if pkAttr == "" {
pkAttr = "private_key"
}
2022-07-19 10:31:53 -07:00
if pAttr == "" {
2022-08-08 15:15:03 -04:00
pAttr = "private_key_passphrase"
2022-07-19 10:31:53 -07:00
}
2022-08-08 15:15:03 -04:00
username , pk , pass := sshprivatekey . Extract ( bc . secretData , uAttr , pkAttr , pAttr )
2022-07-19 10:31:53 -07:00
if username == "" || pk == nil {
return nil , errors . E ( ctx , errors . WithCode ( errors . VaultInvalidCredentialMapping ))
}
return & sshPrivateKeyCred {
baseCred : bc ,
username : username ,
privateKey : pk ,
2022-08-08 15:15:03 -04:00
passphrase : pass ,
2022-07-19 10:31:53 -07:00
}, nil
}
2023-01-30 22:17:53 +00:00
type sshCertCred struct {
* sshPrivateKeyCred
certificate [] byte
}
func ( c * sshCertCred ) Certificate () [] byte { return c . certificate }
2023-01-09 12:06:43 -05:00
var _ credential . Library = ( * genericIssuingCredentialLibrary )( nil )
2021-06-22 22:26:26 -04:00
2023-01-09 12:06:43 -05:00
type issuingCredentialLibrary interface {
2023-01-30 22:17:53 +00:00
credential . Library
GetPurpose () credential . Purpose
2023-01-09 12:06:43 -05:00
retrieveCredential ( context . Context , errors . Op , ... credential . Option ) ( dynamicCred , error )
}
// A genericIssuingCredentialLibrary contains all the values needed to connect to Vault and
2021-11-10 06:47:21 -05:00
// retrieve credentials.
2023-01-09 12:06:43 -05:00
type genericIssuingCredentialLibrary struct {
PublicId string
2022-08-08 15:15:03 -04:00
StoreId string
Name string
Description string
CreateTime * timestamp . Timestamp
UpdateTime * timestamp . Timestamp
Version uint32
VaultPath string
HttpMethod string
HttpRequestBody [] byte
2023-01-09 12:06:43 -05:00
CredType string
2022-09-06 07:50:15 -07:00
ProjectId string
2022-08-08 15:15:03 -04:00
VaultAddress string
Namespace string
CaCert [] byte
TlsServerName string
TlsSkipVerify bool
WorkerFilter string
Token TokenSecret
CtToken [] byte
2022-09-06 07:50:15 -07:00
TokenHmac [] byte
2022-08-08 15:15:03 -04:00
TokenKeyId string
ClientCert [] byte
ClientKey KeySecret
CtClientKey [] byte
ClientKeyId string
2022-09-06 07:50:15 -07:00
UsernameAttribute string
PasswordAttribute string
PrivateKeyAttribute string
PrivateKeyPassphraseAttribute string
2023-01-09 12:06:43 -05:00
Purpose credential . Purpose
2023-10-24 12:44:51 -04:00
AdditionalValidPrincipals string
2021-06-22 22:26:26 -04:00
}
2023-01-09 12:06:43 -05:00
func ( pl * genericIssuingCredentialLibrary ) clone () * genericIssuingCredentialLibrary {
2021-06-22 22:26:26 -04:00
// The 'append(a[:0:0], a...)' comes from
// https://github.com/go101/go101/wiki/How-to-perfectly-clone-a-slice%3F
2023-01-09 12:06:43 -05:00
return & genericIssuingCredentialLibrary {
2022-08-08 15:15:03 -04:00
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 ,
2022-07-20 16:24:46 -07:00
ProjectId : pl . ProjectId ,
2022-08-08 15:15:03 -04:00
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 ,
2021-06-22 22:26:26 -04:00
}
}
2023-01-09 12:06:43 -05:00
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 }
2023-01-30 22:17:53 +00:00
func ( pl * genericIssuingCredentialLibrary ) GetPurpose () credential . Purpose { return pl . Purpose }
2021-06-22 22:26:26 -04:00
2023-10-18 14:48:44 -07:00
func ( pl * genericIssuingCredentialLibrary ) CredentialType () globals . CredentialType {
2021-11-10 06:47:21 -05:00
switch ct := pl . CredType ; ct {
case "" :
2023-10-18 14:48:44 -07:00
return globals . UnspecifiedCredentialType
2021-11-10 06:47:21 -05:00
default :
2023-10-18 14:48:44 -07:00
return globals . CredentialType ( ct )
2021-11-10 06:47:21 -05:00
}
}
2023-10-23 15:53:29 -07:00
// GetResourceType returns the resource type of the CredentialLibrary
func ( pl * genericIssuingCredentialLibrary ) GetResourceType () resource . Type {
return resource . CredentialLibrary
}
2023-01-09 12:06:43 -05:00
func ( pl * genericIssuingCredentialLibrary ) client ( ctx context . Context ) ( vaultClient , error ) {
const op = "vault.(genericIssuingCredentialLibrary).client"
2021-06-22 22:26:26 -04:00
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
}
2022-08-31 12:40:35 -04:00
client , err := vaultClientFactoryFn ( ctx , clientConfig , WithWorkerFilter ( pl . WorkerFilter ))
2021-06-22 22:26:26 -04:00
if err != nil {
2023-06-06 17:09:08 -04:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to create vault client" ))
2021-06-22 22:26:26 -04:00
}
return client , nil
}
2021-11-10 06:47:21 -05:00
type dynamicCred interface {
credential . Dynamic
getExpiration () time . Duration
2023-01-09 12:06:43 -05:00
getCredential () * Credential
2023-01-31 22:14:56 +00:00
isRevokable () bool
2021-11-10 06:47:21 -05:00
}
// retrieveCredential retrieves a dynamic credential from Vault for the
// given sessionId.
2022-10-27 09:01:05 -04:00
//
// Supported options: credential.WithTemplateData
2023-01-09 12:06:43 -05:00
func ( pl * genericIssuingCredentialLibrary ) retrieveCredential ( ctx context . Context , op errors . Op , opt ... credential . Option ) ( dynamicCred , error ) {
2022-10-27 09:01:05 -04:00
opts , err := credential . GetOpts ( opt ... )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
2021-11-10 06:47:21 -05:00
// Get the credential ID early. No need to get a secret from Vault
// if there is no way to save it in the database.
2023-06-06 17:09:08 -04:00
credId , err := newCredentialId ( ctx )
2021-11-10 06:47:21 -05:00
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
2022-08-25 14:37:22 -04:00
client , err := pl . client ( ctx )
2021-11-10 06:47:21 -05:00
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
var secret * vault . Secret
2022-10-27 09:01:05 -04:00
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 )
}
}
2021-11-10 06:47:21 -05:00
switch Method ( pl . HttpMethod ) {
case MethodGet :
2022-10-27 09:01:05 -04:00
secret , reqErr = client . get ( ctx , path )
2021-11-10 06:47:21 -05:00
case MethodPost :
2022-10-27 09:01:05 -04:00
secret , reqErr = client . post ( ctx , path , [] byte ( body ))
2021-11-10 06:47:21 -05:00
default :
return nil , errors . New ( ctx , errors . Internal , op , fmt . Sprintf ( "unknown http method: library: %s" , pl . PublicId ))
}
2022-10-27 09:01:05 -04:00
if reqErr != nil {
2021-11-10 06:47:21 -05:00
// TODO(mgaffney) 05/2021: detect if the error is because of an
// expired or invalid token
2022-10-27 09:01:05 -04:00
return nil , errors . Wrap ( ctx , reqErr , op )
2021-11-10 06:47:21 -05:00
}
if secret == nil {
return nil , errors . E ( ctx , errors . WithCode ( errors . VaultEmptySecret ), errors . WithOp ( op ))
}
leaseDuration := time . Duration ( secret . LeaseDuration ) * time . Second
2023-06-06 17:09:08 -04:00
cred , err := newCredential ( ctx , pl . GetPublicId (), secret . LeaseID , pl . TokenHmac , leaseDuration )
2021-11-10 06:47:21 -05:00
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 )
}
2021-06-22 22:26:26 -04:00
// TableName returns the table name for gorm.
2023-01-09 12:06:43 -05:00
func ( pl * genericIssuingCredentialLibrary ) TableName () string {
2022-09-06 07:50:15 -07:00
return "credential_vault_library_issue_credentials"
2021-06-22 22:26:26 -04:00
}
2023-01-09 12:06:43 -05:00
func ( r * Repository ) getIssueCredLibraries ( ctx context . Context , requests [] credential . Request ) ([] issuingCredentialLibrary , error ) {
2022-09-06 07:50:15 -07:00
const op = "vault.(Repository).getIssueCredLibraries"
2021-06-22 22:26:26 -04:00
2023-06-06 17:09:08 -04:00
mapper , err := newMapper ( ctx , requests )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
libIds := mapper . libIds ()
2021-09-30 08:47:34 -04:00
var inClauseSpots [] string
for i := 1 ; i < len ( libIds ) + 1 ; i ++ {
inClauseSpots = append ( inClauseSpots , fmt . Sprintf ( "@%d" , i ))
}
inClause := strings . Join ( inClauseSpots , "," )
2021-06-22 22:26:26 -04:00
2022-09-06 07:50:15 -07:00
query := fmt . Sprintf ( selectLibrariesQuery , inClause )
2021-06-22 22:26:26 -04:00
2022-10-25 18:29:38 -05:00
var params [] any
2021-09-30 08:47:34 -04:00
for idx , v := range libIds {
params = append ( params , sql . Named ( fmt . Sprintf ( "%d" , idx + 1 ), v ))
2021-06-22 22:26:26 -04:00
}
rows , err := r . reader . Query ( ctx , query , params )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "query failed" ))
2021-06-22 22:26:26 -04:00
}
defer rows . Close ()
2023-01-09 12:06:43 -05:00
var libs [] * privateCredentialLibraryAllTypes
2021-06-22 22:26:26 -04:00
for rows . Next () {
2023-01-09 12:06:43 -05:00
var lib privateCredentialLibraryAllTypes
2022-01-05 14:38:38 -05:00
if err := r . reader . ScanRows ( ctx , rows , & lib ); err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "scan row failed" ))
2021-06-22 22:26:26 -04:00
}
purps := mapper . get ( lib . GetPublicId ())
if len ( purps ) == 0 {
2021-07-29 18:52:56 -06:00
return nil , errors . E ( ctx , errors . WithCode ( errors . InvalidParameter ), errors . WithMsg ( "unknown library" ))
2021-06-22 22:26:26 -04:00
}
for _ , purp := range purps {
cp := lib . clone ()
cp . Purpose = purp
libs = append ( libs , cp )
}
}
2024-01-22 14:03:40 -05:00
if err := rows . Err (); err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "next row failed" ))
}
2021-06-22 22:26:26 -04:00
2023-01-09 12:06:43 -05:00
var decryptedLibs [] issuingCredentialLibrary
2021-06-22 22:26:26 -04:00
for _ , pl := range libs {
2022-07-20 16:24:46 -07:00
databaseWrapper , err := r . kms . GetWrapper ( ctx , pl . ProjectId , kms . KeyPurposeDatabase )
2021-06-22 22:26:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get database wrapper" ))
2021-06-22 22:26:26 -04:00
}
if err := pl . decrypt ( ctx , databaseWrapper ); err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2021-06-22 22:26:26 -04:00
}
2023-01-09 12:06:43 -05:00
decryptedLibs = append ( decryptedLibs , pl . toTypedIssuingCredentialLibrary ())
2021-06-22 22:26:26 -04:00
}
2023-01-09 12:06:43 -05:00
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:"-"`
2023-01-30 22:17:53 +00:00
KeyType string
KeyBits int
Username string
Ttl string
KeyId string
CriticalOptions [] byte
Extensions [] byte
CredLibType string
2023-10-24 12:44:51 -04:00
AdditionalValidPrincipals string
2023-01-09 12:06:43 -05:00
}
func ( pl * privateCredentialLibraryAllTypes ) GetPublicId () string { return pl . PublicId }
2023-10-23 15:53:29 -07:00
// GetResourceType returns the resource type of the CredentialLibrary
func ( pl * privateCredentialLibraryAllTypes ) GetResourceType () resource . Type {
return resource . CredentialLibrary
}
2023-01-09 12:06:43 -05:00
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 ,
2023-01-30 22:17:53 +00:00
KeyType : pl . KeyType ,
KeyBits : pl . KeyBits ,
Username : pl . Username ,
Ttl : pl . Ttl ,
KeyId : pl . KeyId ,
CriticalOptions : pl . CriticalOptions ,
Extensions : pl . Extensions ,
CredLibType : pl . CredLibType ,
2023-10-24 12:44:51 -04:00
AdditionalValidPrincipals : pl . AdditionalValidPrincipals ,
2023-01-09 12:06:43 -05:00
}
}
func ( pl * privateCredentialLibraryAllTypes ) toTypedIssuingCredentialLibrary () issuingCredentialLibrary {
2023-01-30 22:17:53 +00:00
switch pl . CredLibType {
case "ssh-signed-cert" :
return & sshCertIssuingCredentialLibrary {
2023-10-24 12:44:51 -04:00
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 ,
2023-01-30 22:17:53 +00:00
}
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 ,
2023-10-24 12:44:51 -04:00
AdditionalValidPrincipals : pl . AdditionalValidPrincipals ,
2023-01-30 22:17:53 +00:00
}
2023-01-09 12:06:43 -05:00
}
2021-06-22 22:26:26 -04:00
}
// 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
}
2023-06-06 17:09:08 -04:00
func newMapper ( ctx context . Context , requests [] credential . Request ) ( * requestMap , error ) {
2021-06-22 22:26:26 -04:00
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 {
2023-06-06 17:09:08 -04:00
return nil , errors . E ( ctx , errors . WithCode ( errors . InvalidParameter ), errors . WithMsg ( "duplicate library and purpose" ))
2021-06-22 22:26:26 -04:00
}
}
}
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 ]
}
2023-01-30 22:17:53 +00:00
type sshCertIssuingCredentialLibrary struct {
2023-10-24 12:44:51 -04:00
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
2023-01-30 22:17:53 +00:00
}
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
}
2023-10-18 14:48:44 -07:00
func ( lib * sshCertIssuingCredentialLibrary ) CredentialType () globals . CredentialType {
2023-01-30 22:17:53 +00:00
switch ct := lib . CredType ; ct {
case "" :
2023-10-18 14:48:44 -07:00
return globals . UnspecifiedCredentialType
2023-01-30 22:17:53 +00:00
default :
2023-10-18 14:48:44 -07:00
return globals . CredentialType ( ct )
2023-01-30 22:17:53 +00:00
}
}
2023-10-23 15:53:29 -07:00
// GetResourceType returns the resource type of the CredentialLibrary
func ( lib * sshCertIssuingCredentialLibrary ) GetResourceType () resource . Type {
return resource . CredentialLibrary
}
2023-01-30 22:17:53 +00:00
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 {
2023-06-06 17:09:08 -04:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to create vault client" ))
2023-01-30 22:17:53 +00:00
}
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.
2023-06-06 17:09:08 -04:00
credId , err := newCredentialId ( ctx )
2023-01-30 22:17:53 +00:00
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 )
}
2024-01-04 21:19:36 -05:00
// 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 )
}
}
2023-01-30 22:17:53 +00:00
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 )
}
}
2023-10-24 12:44:51 -04:00
validPrincipals := username
if lib . AdditionalValidPrincipals != "" {
validPrincipals = fmt . Sprintf ( "%s,%s" , username , lib . AdditionalValidPrincipals )
}
2023-01-30 22:17:53 +00:00
payload := sshCertVaultBody {
2023-10-24 12:44:51 -04:00
ValidPrincipals : validPrincipals ,
2023-01-30 22:17:53 +00:00
CertType : "user" ,
CriticalOptions : criticalOptions ,
Extensions : extensions ,
TTL : lib . Ttl ,
2024-01-04 21:19:36 -05:00
KeyId : keyId ,
2023-01-30 22:17:53 +00:00
}
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
2023-06-06 17:09:08 -04:00
cred , err := newCredential ( ctx , lib . GetPublicId (), secret . LeaseID , lib . TokenHmac , leaseDuration )
2023-01-30 22:17:53 +00:00
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
}