Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for client cert credential type #20425

Merged
merged 27 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ced6698
poc: client cert credential type
austingebauer Jan 31, 2023
5fb44b5
Merge branch 'main' into db-secrets-cred-type-client-cert
Zlaticanin Apr 28, 2023
5789f0c
go mod tidy
Zlaticanin Apr 28, 2023
fca9dc0
fix typo
Zlaticanin May 22, 2023
c07c680
fix newUserReqToProto
austingebauer May 23, 2023
6012557
add changelog
Zlaticanin May 23, 2023
03d7e53
merge
Zlaticanin May 23, 2023
a154483
add newline
Zlaticanin May 23, 2023
d3e939b
fix changelog
Zlaticanin May 23, 2023
ed0a58a
add test for the client cert generator
Zlaticanin May 24, 2023
f358b4c
Fix formatting
Zlaticanin May 24, 2023
1a69695
unset signing bundle URLs
austingebauer May 24, 2023
3e22a33
set BasicConstraintsValidForNonCA to false
austingebauer May 24, 2023
76dc836
backdate cert by 30s
austingebauer May 24, 2023
3289292
remove empty creation params URLs
austingebauer May 24, 2023
02ef08e
check cert BasicConstraintsValid
austingebauer May 24, 2023
d342fa5
set default key bits in newClientCertificateGenerator
austingebauer May 24, 2023
948e33c
fix client cert gen test with default values
Zlaticanin May 24, 2023
91197ef
Add default for key_type
Zlaticanin May 25, 2023
801daa8
fix default key_type
Zlaticanin May 25, 2023
9acc816
update test with default key type
Zlaticanin May 25, 2023
bf987a6
update test
Zlaticanin May 25, 2023
249de1a
Update changelog/20425.txt
Zlaticanin May 26, 2023
3610ede
set default key bits and sig bits
austingebauer May 26, 2023
7bcacf0
remove the default for key type ad fix the test
Zlaticanin May 26, 2023
0abd0a4
make fmt + add comments for each exported field
Zlaticanin May 26, 2023
df9efcd
restart test
Zlaticanin May 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions builtin/logical/database/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import (
"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"
)

Expand Down Expand Up @@ -170,3 +175,207 @@ func (kg rsaKeyGenerator) configMap() (map[string]interface{}, error) {
}
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: is it possible to write a test for this? Understand if it is hard to do with the SDK 🙏🏼

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll create a separate task for this, it will require some more time

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,
},
SigningBundle: &certutil.CAInfoBundle{
ParsedCertBundle: *cg.parsedCABundle,
},
}

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
}
Loading