mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-14 18:47:01 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
392 lines
11 KiB
Go
392 lines
11 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package database
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/helper/random"
|
|
"github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
"github.com/hashicorp/vault/sdk/helper/template"
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
// passwordGenerator generates password credentials.
|
|
// A zero value passwordGenerator is usable.
|
|
type passwordGenerator struct {
|
|
// PasswordPolicy is the named password policy used to generate passwords.
|
|
// If empty (default), a random string of 20 characters will be generated.
|
|
PasswordPolicy string `mapstructure:"password_policy,omitempty"`
|
|
}
|
|
|
|
// newPasswordGenerator returns a new passwordGenerator using the given config.
|
|
// Default values will be set on the returned passwordGenerator if not provided
|
|
// in the config.
|
|
func newPasswordGenerator(config map[string]interface{}) (passwordGenerator, error) {
|
|
var pg passwordGenerator
|
|
if err := mapstructure.WeakDecode(config, &pg); err != nil {
|
|
return pg, err
|
|
}
|
|
|
|
return pg, nil
|
|
}
|
|
|
|
// Generate generates a password credential using the configured password policy.
|
|
// Returns the generated password or an error.
|
|
func (pg passwordGenerator) generate(ctx context.Context, b *databaseBackend, wrapper databaseVersionWrapper) (string, error) {
|
|
if !wrapper.isV5() && !wrapper.isV4() {
|
|
return "", fmt.Errorf("no underlying database specified")
|
|
}
|
|
|
|
// The database plugin generates the password if its interface is v4
|
|
if wrapper.isV4() {
|
|
password, err := wrapper.v4.GenerateCredentials(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return password, nil
|
|
}
|
|
|
|
if pg.PasswordPolicy == "" {
|
|
return random.DefaultStringGenerator.Generate(ctx, b.GetRandomReader())
|
|
}
|
|
return b.System().GeneratePasswordFromPolicy(ctx, pg.PasswordPolicy)
|
|
}
|
|
|
|
// configMap returns the configuration of the passwordGenerator
|
|
// as a map from string to string.
|
|
func (pg passwordGenerator) configMap() (map[string]interface{}, error) {
|
|
config := make(map[string]interface{})
|
|
if err := mapstructure.WeakDecode(pg, &config); err != nil {
|
|
return nil, err
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// rsaKeyGenerator generates RSA key pair credentials.
|
|
// A zero value rsaKeyGenerator is usable.
|
|
type rsaKeyGenerator struct {
|
|
// Format is the output format of the generated private key.
|
|
// Options include: 'pkcs8' (default)
|
|
Format string `mapstructure:"format,omitempty"`
|
|
|
|
// KeyBits is the bit size of the RSA key to generate.
|
|
// Options include: 2048 (default), 3072, and 4096
|
|
KeyBits int `mapstructure:"key_bits,omitempty"`
|
|
}
|
|
|
|
// newRSAKeyGenerator returns a new rsaKeyGenerator using the given config.
|
|
// Default values will be set on the returned rsaKeyGenerator if not provided
|
|
// in the given config.
|
|
func newRSAKeyGenerator(config map[string]interface{}) (rsaKeyGenerator, error) {
|
|
var kg rsaKeyGenerator
|
|
if err := mapstructure.WeakDecode(config, &kg); err != nil {
|
|
return kg, err
|
|
}
|
|
|
|
switch strings.ToLower(kg.Format) {
|
|
case "":
|
|
kg.Format = "pkcs8"
|
|
case "pkcs8":
|
|
default:
|
|
return kg, fmt.Errorf("invalid format: %v", kg.Format)
|
|
}
|
|
|
|
switch kg.KeyBits {
|
|
case 0:
|
|
kg.KeyBits = 2048
|
|
case 2048, 3072, 4096:
|
|
default:
|
|
return kg, fmt.Errorf("invalid key_bits: %v", kg.KeyBits)
|
|
}
|
|
|
|
return kg, nil
|
|
}
|
|
|
|
// Generate generates an RSA key pair. Returns a PEM-encoded, PKIX marshaled
|
|
// public key and a PEM-encoded private key marshaled into the configuration
|
|
// format (in that order) or an error.
|
|
func (kg *rsaKeyGenerator) generate(r io.Reader) ([]byte, []byte, error) {
|
|
reader := rand.Reader
|
|
if r != nil {
|
|
reader = r
|
|
}
|
|
|
|
var keyBits int
|
|
switch kg.KeyBits {
|
|
case 0:
|
|
keyBits = 2048
|
|
case 2048, 3072, 4096:
|
|
keyBits = kg.KeyBits
|
|
default:
|
|
return nil, nil, fmt.Errorf("invalid key_bits: %v", kg.KeyBits)
|
|
}
|
|
|
|
key, err := rsa.GenerateKey(reader, keyBits)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
public, err := x509.MarshalPKIXPublicKey(key.Public())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var private []byte
|
|
switch strings.ToLower(kg.Format) {
|
|
case "", "pkcs8":
|
|
private, err = x509.MarshalPKCS8PrivateKey(key)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
default:
|
|
return nil, nil, fmt.Errorf("invalid format: %v", kg.Format)
|
|
}
|
|
|
|
publicBlock := &pem.Block{
|
|
Type: "PUBLIC KEY",
|
|
Bytes: public,
|
|
}
|
|
privateBlock := &pem.Block{
|
|
Type: "PRIVATE KEY",
|
|
Bytes: private,
|
|
}
|
|
|
|
return pem.EncodeToMemory(publicBlock), pem.EncodeToMemory(privateBlock), nil
|
|
}
|
|
|
|
// configMap returns the configuration of the rsaKeyGenerator
|
|
// as a map from string to string.
|
|
func (kg rsaKeyGenerator) configMap() (map[string]interface{}, error) {
|
|
config := make(map[string]interface{})
|
|
if err := mapstructure.WeakDecode(kg, &config); err != nil {
|
|
return nil, err
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
type ClientCertificateGenerator struct {
|
|
// CommonNameTemplate is username template to be used for the client certificate common name.
|
|
CommonNameTemplate string `mapstructure:"common_name_template,omitempty"`
|
|
|
|
// CAPrivateKey is the PEM-encoded private key for the given ca_cert.
|
|
CAPrivateKey string `mapstructure:"ca_private_key,omitempty"`
|
|
|
|
// CACert is the PEM-encoded CA certificate.
|
|
CACert string `mapstructure:"ca_cert,omitempty"`
|
|
|
|
// KeyType specifies the desired key type.
|
|
// Options include: 'rsa', 'ed25519', 'ec'.
|
|
KeyType string `mapstructure:"key_type,omitempty"`
|
|
|
|
// KeyBits is the number of bits to use for the generated keys.
|
|
// Options include: with key_type=rsa, 2048 (default), 3072, 4096;
|
|
// With key_type=ec, allowed values are: 224, 256 (default), 384, 521;
|
|
// Ignored with key_type=ed25519.
|
|
KeyBits int `mapstructure:"key_bits,omitempty"`
|
|
|
|
// SignatureBits is the number of bits to use in the signature algorithm.
|
|
// Options include: 256 (default), 384, 512.
|
|
SignatureBits int `mapstructure:"signature_bits,omitempty"`
|
|
|
|
parsedCABundle *certutil.ParsedCertBundle
|
|
cnProducer template.StringTemplate
|
|
}
|
|
|
|
// newClientCertificateGenerator returns a new ClientCertificateGenerator
|
|
// using the given config. Default values will be set on the returned
|
|
// ClientCertificateGenerator if not provided in the config.
|
|
func newClientCertificateGenerator(config map[string]interface{}) (ClientCertificateGenerator, error) {
|
|
var cg ClientCertificateGenerator
|
|
if err := mapstructure.WeakDecode(config, &cg); err != nil {
|
|
return cg, err
|
|
}
|
|
|
|
switch cg.KeyType {
|
|
case "rsa":
|
|
switch cg.KeyBits {
|
|
case 0:
|
|
cg.KeyBits = 2048
|
|
case 2048, 3072, 4096:
|
|
default:
|
|
return cg, fmt.Errorf("invalid key_bits")
|
|
}
|
|
case "ec":
|
|
switch cg.KeyBits {
|
|
case 0:
|
|
cg.KeyBits = 256
|
|
case 224, 256, 384, 521:
|
|
default:
|
|
return cg, fmt.Errorf("invalid key_bits")
|
|
}
|
|
case "ed25519":
|
|
// key_bits ignored
|
|
default:
|
|
return cg, fmt.Errorf("invalid key_type")
|
|
}
|
|
|
|
switch cg.SignatureBits {
|
|
case 0:
|
|
cg.SignatureBits = 256
|
|
case 256, 384, 512:
|
|
default:
|
|
return cg, fmt.Errorf("invalid signature_bits")
|
|
}
|
|
|
|
if cg.CommonNameTemplate == "" {
|
|
return cg, fmt.Errorf("missing required common_name_template")
|
|
}
|
|
|
|
// Validate the common name template
|
|
t, err := template.NewTemplate(template.Template(cg.CommonNameTemplate))
|
|
if err != nil {
|
|
return cg, fmt.Errorf("failed to create template: %w", err)
|
|
}
|
|
|
|
_, err = t.Generate(dbplugin.UsernameMetadata{})
|
|
if err != nil {
|
|
return cg, fmt.Errorf("invalid common_name_template: %w", err)
|
|
}
|
|
cg.cnProducer = t
|
|
|
|
if cg.CACert == "" {
|
|
return cg, fmt.Errorf("missing required ca_cert")
|
|
}
|
|
if cg.CAPrivateKey == "" {
|
|
return cg, fmt.Errorf("missing required ca_private_key")
|
|
}
|
|
parsedBundle, err := certutil.ParsePEMBundle(strings.Join([]string{cg.CACert, cg.CAPrivateKey}, "\n"))
|
|
if err != nil {
|
|
return cg, err
|
|
}
|
|
if parsedBundle.PrivateKey == nil {
|
|
return cg, fmt.Errorf("private key not found in the PEM bundle")
|
|
}
|
|
if parsedBundle.PrivateKeyType == certutil.UnknownPrivateKey {
|
|
return cg, fmt.Errorf("unknown private key found in the PEM bundle")
|
|
}
|
|
if parsedBundle.Certificate == nil {
|
|
return cg, fmt.Errorf("certificate not found in the PEM bundle")
|
|
}
|
|
if !parsedBundle.Certificate.IsCA {
|
|
return cg, fmt.Errorf("the given certificate is not marked for CA use")
|
|
}
|
|
if !parsedBundle.Certificate.BasicConstraintsValid {
|
|
return cg, fmt.Errorf("the given certificate does not meet basic constraints for CA use")
|
|
}
|
|
|
|
certBundle, err := parsedBundle.ToCertBundle()
|
|
if err != nil {
|
|
return cg, fmt.Errorf("error converting raw values into cert bundle: %w", err)
|
|
}
|
|
|
|
parsedCABundle, err := certBundle.ToParsedCertBundle()
|
|
if err != nil {
|
|
return cg, fmt.Errorf("failed to parse cert bundle: %w", err)
|
|
}
|
|
cg.parsedCABundle = parsedCABundle
|
|
|
|
return cg, nil
|
|
}
|
|
|
|
func (cg *ClientCertificateGenerator) generate(r io.Reader, expiration time.Time, userMeta dbplugin.UsernameMetadata) (*certutil.CertBundle, string, error) {
|
|
commonName, err := cg.cnProducer.Generate(userMeta)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
// Set defaults
|
|
keyBits := cg.KeyBits
|
|
signatureBits := cg.SignatureBits
|
|
switch cg.KeyType {
|
|
case "rsa":
|
|
if keyBits == 0 {
|
|
keyBits = 2048
|
|
}
|
|
if signatureBits == 0 {
|
|
signatureBits = 256
|
|
}
|
|
case "ec":
|
|
if keyBits == 0 {
|
|
keyBits = 256
|
|
}
|
|
if signatureBits == 0 {
|
|
if keyBits == 224 {
|
|
signatureBits = 256
|
|
} else {
|
|
signatureBits = keyBits
|
|
}
|
|
}
|
|
case "ed25519":
|
|
// key_bits ignored
|
|
if signatureBits == 0 {
|
|
signatureBits = 256
|
|
}
|
|
}
|
|
|
|
subject := pkix.Name{
|
|
CommonName: commonName,
|
|
// Additional subject DN options intentionally omitted for now
|
|
}
|
|
|
|
creation := &certutil.CreationBundle{
|
|
Params: &certutil.CreationParameters{
|
|
Subject: subject,
|
|
KeyType: cg.KeyType,
|
|
KeyBits: cg.KeyBits,
|
|
SignatureBits: cg.SignatureBits,
|
|
NotAfter: expiration,
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: certutil.ClientAuthExtKeyUsage,
|
|
BasicConstraintsValidForNonCA: false,
|
|
NotBeforeDuration: 30 * time.Second,
|
|
URLs: &certutil.URLEntries{
|
|
IssuingCertificates: []string{},
|
|
CRLDistributionPoints: []string{},
|
|
OCSPServers: []string{},
|
|
},
|
|
},
|
|
SigningBundle: &certutil.CAInfoBundle{
|
|
ParsedCertBundle: *cg.parsedCABundle,
|
|
URLs: &certutil.URLEntries{
|
|
IssuingCertificates: []string{},
|
|
CRLDistributionPoints: []string{},
|
|
OCSPServers: []string{},
|
|
},
|
|
},
|
|
}
|
|
|
|
parsedClientBundle, err := certutil.CreateCertificateWithRandomSource(creation, r)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("unable to generate client certificate: %w", err)
|
|
}
|
|
|
|
cb, err := parsedClientBundle.ToCertBundle()
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("error converting raw cert bundle to cert bundle: %w", err)
|
|
}
|
|
|
|
return cb, subject.String(), nil
|
|
}
|
|
|
|
// configMap returns the configuration of the ClientCertificateGenerator
|
|
// as a map from string to string.
|
|
func (cg ClientCertificateGenerator) configMap() (map[string]interface{}, error) {
|
|
config := make(map[string]interface{})
|
|
if err := mapstructure.WeakDecode(cg, &config); err != nil {
|
|
return nil, err
|
|
}
|
|
return config, nil
|
|
}
|