open-vault/builtin/logical/pki/path_root.go

648 lines
22 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package pki
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"net/http"
"reflect"
"strings"
"time"
"golang.org/x/crypto/ed25519"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/errutil"
"github.com/hashicorp/vault/sdk/logical"
)
func pathGenerateRoot(b *backend) *framework.Path {
pattern := "root/generate/" + framework.GenericNameRegex("exported")
displayAttrs := &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
OperationVerb: "generate",
OperationSuffix: "root",
}
return buildPathGenerateRoot(b, pattern, displayAttrs)
}
func pathDeleteRoot(b *backend) *framework.Path {
ret := &framework.Path{
Pattern: "root",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixPKI,
OperationSuffix: "root",
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.DeleteOperation: &framework.PathOperation{
Callback: b.pathCADeleteRoot,
Responses: map[int][]framework.Response{
http.StatusOK: {{
Description: "OK",
}},
},
// Read more about why these flags are set in backend.go
ForwardPerformanceStandby: true,
ForwardPerformanceSecondary: true,
},
},
HelpSynopsis: pathDeleteRootHelpSyn,
HelpDescription: pathDeleteRootHelpDesc,
}
return ret
}
func (b *backend) pathCADeleteRoot(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
// Since we're planning on updating issuers here, grab the lock so we've
// got a consistent view.
b.issuersLock.Lock()
defer b.issuersLock.Unlock()
sc := b.makeStorageContext(ctx, req.Storage)
if !b.useLegacyBundleCaStorage() {
issuers, err := sc.listIssuers()
if err != nil {
return nil, err
}
keys, err := sc.listKeys()
if err != nil {
return nil, err
}
// Delete all issuers and keys. Ignore deleting the default since we're
// explicitly deleting everything.
for _, issuer := range issuers {
if _, err = sc.deleteIssuer(issuer); err != nil {
return nil, err
}
}
for _, key := range keys {
if _, err = sc.deleteKey(key); err != nil {
return nil, err
}
}
}
// Delete legacy CA bundle and its backup, if any.
if err := req.Storage.Delete(ctx, legacyCertBundlePath); err != nil {
return nil, err
}
if err := req.Storage.Delete(ctx, legacyCertBundleBackupPath); err != nil {
return nil, err
}
// Delete legacy CRL bundle.
if err := req.Storage.Delete(ctx, legacyCRLPath); err != nil {
return nil, err
}
// Return a warning about preferring to delete issuers and keys
// explicitly versus deleting everything.
resp := &logical.Response{}
resp.AddWarning("DELETE /root deletes all keys and issuers; prefer the new DELETE /key/:key_ref and DELETE /issuer/:issuer_ref for finer granularity, unless removal of all keys and issuers is desired.")
return resp, nil
}
func (b *backend) pathCAGenerateRoot(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Since we're planning on updating issuers here, grab the lock so we've
// got a consistent view.
b.issuersLock.Lock()
defer b.issuersLock.Unlock()
var err error
if b.useLegacyBundleCaStorage() {
return logical.ErrorResponse("Can not create root CA until migration has completed"), nil
}
sc := b.makeStorageContext(ctx, req.Storage)
exported, format, role, errorResp := getGenerationParams(sc, data)
if errorResp != nil {
return errorResp, nil
}
maxPathLengthIface, ok := data.GetOk("max_path_length")
if ok {
maxPathLength := maxPathLengthIface.(int)
role.MaxPathLength = &maxPathLength
}
issuerName, err := getIssuerName(sc, data)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
// Handle the aliased path specifying the new issuer name as "next", but
// only do it if its not in use.
if strings.HasPrefix(req.Path, "root/rotate/") && len(issuerName) == 0 {
// err is nil when the issuer name is in use.
_, err = sc.resolveIssuerReference("next")
if err != nil {
issuerName = "next"
}
}
keyName, err := getKeyName(sc, data)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
input := &inputBundle{
req: req,
apiData: data,
role: role,
}
parsedBundle, warnings, err := generateCert(sc, input, nil, true, b.Backend.GetRandomReader())
if err != nil {
switch err.(type) {
case errutil.UserError:
return logical.ErrorResponse(err.Error()), nil
default:
return nil, err
}
}
cb, err := parsedBundle.ToCertBundle()
if err != nil {
return nil, fmt.Errorf("error converting raw cert bundle to cert bundle: %w", err)
}
resp := &logical.Response{
Data: map[string]interface{}{
"expiration": int64(parsedBundle.Certificate.NotAfter.Unix()),
"serial_number": cb.SerialNumber,
},
}
if len(parsedBundle.Certificate.RawSubject) <= 2 {
// Strictly a subject is a SEQUENCE of SETs of SEQUENCES.
//
// The outer SEQUENCE is preserved, having byte value 30 00.
//
// Because of the tag and the length encoding each taking up
// at least one byte, it is impossible to have a non-empty
// subject in two or fewer bytes. We're also not here to validate
// our certificate's ASN.1 content, so let's just assume it holds
// and move on.
resp.AddWarning("This issuer certificate was generated without a Subject; this makes it likely that issuing leaf certs with this certificate will cause TLS validation libraries to reject this certificate.")
}
if len(parsedBundle.Certificate.OCSPServer) == 0 && len(parsedBundle.Certificate.IssuingCertificateURL) == 0 && len(parsedBundle.Certificate.CRLDistributionPoints) == 0 {
// If the operator hasn't configured any of the URLs prior to
// generating this issuer, we should add a warning to the response,
// informing them they might want to do so prior to issuing leaves.
resp.AddWarning("This mount hasn't configured any authority information access (AIA) fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls or the newly generated issuer with this information.")
}
switch format {
case "pem":
resp.Data["certificate"] = cb.Certificate
resp.Data["issuing_ca"] = cb.Certificate
if exported {
resp.Data["private_key"] = cb.PrivateKey
resp.Data["private_key_type"] = cb.PrivateKeyType
}
case "pem_bundle":
resp.Data["issuing_ca"] = cb.Certificate
if exported {
resp.Data["private_key"] = cb.PrivateKey
resp.Data["private_key_type"] = cb.PrivateKeyType
resp.Data["certificate"] = fmt.Sprintf("%s\n%s", cb.PrivateKey, cb.Certificate)
} else {
resp.Data["certificate"] = cb.Certificate
}
case "der":
resp.Data["certificate"] = base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes)
resp.Data["issuing_ca"] = base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes)
if exported {
resp.Data["private_key"] = base64.StdEncoding.EncodeToString(parsedBundle.PrivateKeyBytes)
resp.Data["private_key_type"] = cb.PrivateKeyType
}
default:
return nil, fmt.Errorf("unsupported format argument: %s", format)
}
if data.Get("private_key_format").(string) == "pkcs8" {
err = convertRespToPKCS8(resp)
if err != nil {
return nil, err
}
}
// Store it as the CA bundle
myIssuer, myKey, err := sc.writeCaBundle(cb, issuerName, keyName)
if err != nil {
return nil, err
}
resp.Data["issuer_id"] = myIssuer.ID
resp.Data["issuer_name"] = myIssuer.Name
resp.Data["key_id"] = myKey.ID
resp.Data["key_name"] = myKey.Name
// The one time that it is safe (and good) to copy the
// SignatureAlgorithm field off the certificate (for the purposes of
// detecting PSS support) is when we've freshly generated it AND it
// is a root (exactly this endpoint).
//
// For intermediates, this doesn't hold (not this endpoint) as that
// reflects the parent key's preferences. For imports, this doesn't
// hold as the old system might've allowed other signature types that
// the new system (whether Vault or a managed key) doesn't.
//
// Previously we did this conditionally on whether or not PSS was in
// use. This is insufficient as some cloud KMS providers (namely, GCP)
// restrict the key to a single signature algorithm! So e.g., a RSA 3072
// key MUST use SHA-384 as the hash algorithm. Thus we pull in the
// RevocationSigAlg unconditionally on roots now.
myIssuer.RevocationSigAlg = parsedBundle.Certificate.SignatureAlgorithm
if err := sc.writeIssuer(myIssuer); err != nil {
return nil, fmt.Errorf("unable to store PSS-updated issuer: %w", err)
}
// Also store it as just the certificate identified by serial number, so it
// can be revoked
key := "certs/" + normalizeSerial(cb.SerialNumber)
certsCounted := b.certsCounted.Load()
err = req.Storage.Put(ctx, &logical.StorageEntry{
Key: key,
Value: parsedBundle.CertificateBytes,
})
if err != nil {
return nil, fmt.Errorf("unable to store certificate locally: %w", err)
}
b.ifCountEnabledIncrementTotalCertificatesCount(certsCounted, key)
// Build a fresh CRL
warnings, err = b.crlBuilder.rebuild(sc, true)
if err != nil {
return nil, err
}
for index, warning := range warnings {
resp.AddWarning(fmt.Sprintf("Warning %d during CRL rebuild: %v", index+1, warning))
}
if parsedBundle.Certificate.MaxPathLen == 0 {
resp.AddWarning("Max path length of the generated certificate is zero. This certificate cannot be used to issue intermediate CA certificates.")
}
// Check whether we need to update our default issuer configuration.
config, err := sc.getIssuersConfig()
if err != nil {
resp.AddWarning("Unable to fetch default issuers configuration to update default issuer if necessary: " + err.Error())
} else if config.DefaultFollowsLatestIssuer {
if err := sc.updateDefaultIssuerId(myIssuer.ID); err != nil {
resp.AddWarning("Unable to update this new root as the default issuer: " + err.Error())
}
}
resp = addWarnings(resp, warnings)
return resp, nil
}
func (b *backend) pathIssuerSignIntermediate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
var err error
issuerName := getIssuerRef(data)
if len(issuerName) == 0 {
return logical.ErrorResponse("missing issuer reference"), nil
}
format := getFormat(data)
if format == "" {
return logical.ErrorResponse(
`The "format" path parameter must be "pem" or "der"`,
), nil
}
role := &roleEntry{
OU: data.Get("ou").([]string),
Organization: data.Get("organization").([]string),
Country: data.Get("country").([]string),
Locality: data.Get("locality").([]string),
Province: data.Get("province").([]string),
StreetAddress: data.Get("street_address").([]string),
PostalCode: data.Get("postal_code").([]string),
TTL: time.Duration(data.Get("ttl").(int)) * time.Second,
AllowLocalhost: true,
AllowAnyName: true,
AllowIPSANs: true,
AllowWildcardCertificates: new(bool),
EnforceHostnames: false,
KeyType: "any",
SignatureBits: data.Get("signature_bits").(int),
UsePSS: data.Get("use_pss").(bool),
AllowedOtherSANs: []string{"*"},
AllowedSerialNumbers: []string{"*"},
AllowedURISANs: []string{"*"},
NotAfter: data.Get("not_after").(string),
NotBeforeDuration: time.Duration(data.Get("not_before_duration").(int)) * time.Second,
CNValidations: []string{"disabled"},
}
*role.AllowWildcardCertificates = true
if cn := data.Get("common_name").(string); len(cn) == 0 {
role.UseCSRCommonName = true
}
var caErr error
sc := b.makeStorageContext(ctx, req.Storage)
signingBundle, caErr := sc.fetchCAInfo(issuerName, IssuanceUsage)
if caErr != nil {
switch caErr.(type) {
case errutil.UserError:
return nil, errutil.UserError{Err: fmt.Sprintf(
"could not fetch the CA certificate (was one set?): %s", caErr)}
default:
return nil, errutil.InternalError{Err: fmt.Sprintf(
"error fetching CA certificate: %s", caErr)}
}
}
// Since we are signing an intermediate, we explicitly want to override
// the leaf NotAfterBehavior to permit issuing intermediates longer than
// the life of this issuer.
signingBundle.LeafNotAfterBehavior = certutil.PermitNotAfterBehavior
useCSRValues := data.Get("use_csr_values").(bool)
maxPathLengthIface, ok := data.GetOk("max_path_length")
if ok {
maxPathLength := maxPathLengthIface.(int)
role.MaxPathLength = &maxPathLength
}
input := &inputBundle{
req: req,
apiData: data,
role: role,
}
parsedBundle, warnings, err := signCert(b, input, signingBundle, true, useCSRValues)
if err != nil {
switch err.(type) {
case errutil.UserError:
return logical.ErrorResponse(err.Error()), nil
default:
return nil, errutil.InternalError{Err: fmt.Sprintf(
"error signing cert: %s", err)}
}
}
if err := parsedBundle.Verify(); err != nil {
return nil, fmt.Errorf("verification of parsed bundle failed: %w", err)
}
signingCB, err := signingBundle.ToCertBundle()
if err != nil {
return nil, fmt.Errorf("error converting raw signing bundle to cert bundle: %w", err)
}
cb, err := parsedBundle.ToCertBundle()
if err != nil {
return nil, fmt.Errorf("error converting raw cert bundle to cert bundle: %w", err)
}
resp := &logical.Response{
Data: map[string]interface{}{
"expiration": int64(parsedBundle.Certificate.NotAfter.Unix()),
"serial_number": cb.SerialNumber,
},
}
if signingBundle.Certificate.NotAfter.Before(parsedBundle.Certificate.NotAfter) {
resp.AddWarning("The expiration time for the signed certificate is after the CA's expiration time. If the new certificate is not treated as a root, validation paths with the certificate past the issuing CA's expiration time will fail.")
}
if len(parsedBundle.Certificate.RawSubject) <= 2 {
// Strictly a subject is a SEQUENCE of SETs of SEQUENCES.
//
// The outer SEQUENCE is preserved, having byte value 30 00.
//
// Because of the tag and the length encoding each taking up
// at least one byte, it is impossible to have a non-empty
// subject in two or fewer bytes. We're also not here to validate
// our certificate's ASN.1 content, so let's just assume it holds
// and move on.
resp.AddWarning("This issuer certificate was generated without a Subject; this makes it likely that issuing leaf certs with this certificate will cause TLS validation libraries to reject this certificate.")
}
if len(parsedBundle.Certificate.OCSPServer) == 0 && len(parsedBundle.Certificate.IssuingCertificateURL) == 0 && len(parsedBundle.Certificate.CRLDistributionPoints) == 0 {
// If the operator hasn't configured any of the URLs prior to
// generating this issuer, we should add a warning to the response,
// informing them they might want to do so prior to issuing leaves.
resp.AddWarning("This mount hasn't configured any authority information access (AIA) fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls or the newly generated issuer with this information.")
}
caChain := append([]string{cb.Certificate}, cb.CAChain...)
switch format {
case "pem":
resp.Data["certificate"] = cb.Certificate
resp.Data["issuing_ca"] = signingCB.Certificate
resp.Data["ca_chain"] = caChain
case "pem_bundle":
resp.Data["certificate"] = cb.ToPEMBundle()
resp.Data["issuing_ca"] = signingCB.Certificate
resp.Data["ca_chain"] = caChain
case "der":
resp.Data["certificate"] = base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes)
resp.Data["issuing_ca"] = base64.StdEncoding.EncodeToString(signingBundle.CertificateBytes)
var derCaChain []string
derCaChain = append(derCaChain, base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes))
for _, caCert := range parsedBundle.CAChain {
derCaChain = append(derCaChain, base64.StdEncoding.EncodeToString(caCert.Bytes))
}
resp.Data["ca_chain"] = derCaChain
default:
return nil, fmt.Errorf("unsupported format argument: %s", format)
}
key := "certs/" + normalizeSerial(cb.SerialNumber)
certsCounted := b.certsCounted.Load()
err = req.Storage.Put(ctx, &logical.StorageEntry{
Key: key,
Value: parsedBundle.CertificateBytes,
})
if err != nil {
return nil, fmt.Errorf("unable to store certificate locally: %w", err)
}
b.ifCountEnabledIncrementTotalCertificatesCount(certsCounted, key)
if parsedBundle.Certificate.MaxPathLen == 0 {
resp.AddWarning("Max path length of the signed certificate is zero. This certificate cannot be used to issue intermediate CA certificates.")
}
resp = addWarnings(resp, warnings)
return resp, nil
}
func (b *backend) pathIssuerSignSelfIssued(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
var err error
issuerName := getIssuerRef(data)
if len(issuerName) == 0 {
return logical.ErrorResponse("missing issuer reference"), nil
}
certPem := data.Get("certificate").(string)
block, _ := pem.Decode([]byte(certPem))
if block == nil || len(block.Bytes) == 0 {
return logical.ErrorResponse("certificate could not be PEM-decoded"), nil
}
certs, err := x509.ParseCertificates(block.Bytes)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("error parsing certificate: %s", err)), nil
}
if len(certs) != 1 {
return logical.ErrorResponse(fmt.Sprintf("%d certificates found in PEM file, expected 1", len(certs))), nil
}
cert := certs[0]
if !cert.IsCA {
return logical.ErrorResponse("given certificate is not a CA certificate"), nil
}
if !reflect.DeepEqual(cert.Issuer, cert.Subject) {
return logical.ErrorResponse("given certificate is not self-issued"), nil
}
var caErr error
sc := b.makeStorageContext(ctx, req.Storage)
signingBundle, caErr := sc.fetchCAInfo(issuerName, IssuanceUsage)
if caErr != nil {
switch caErr.(type) {
case errutil.UserError:
return nil, errutil.UserError{Err: fmt.Sprintf(
"could not fetch the CA certificate (was one set?): %s", caErr)}
default:
return nil, errutil.InternalError{Err: fmt.Sprintf("error fetching CA certificate: %s", caErr)}
}
}
signingCB, err := signingBundle.ToCertBundle()
if err != nil {
return nil, fmt.Errorf("error converting raw signing bundle to cert bundle: %w", err)
}
urls := &certutil.URLEntries{}
if signingBundle.URLs != nil {
urls = signingBundle.URLs
}
cert.IssuingCertificateURL = urls.IssuingCertificates
cert.CRLDistributionPoints = urls.CRLDistributionPoints
cert.OCSPServer = urls.OCSPServers
// If the requested signature algorithm isn't the same as the signing certificate, and
// the user has requested a cross-algorithm signature, reset the template's signing algorithm
// to that of the signing key
signingPubType, signingAlgorithm, err := publicKeyType(signingBundle.Certificate.PublicKey)
if err != nil {
return nil, fmt.Errorf("error determining signing certificate algorithm type: %e", err)
}
certPubType, _, err := publicKeyType(cert.PublicKey)
if err != nil {
return nil, fmt.Errorf("error determining template algorithm type: %e", err)
}
if signingPubType != certPubType {
b, ok := data.GetOk("require_matching_certificate_algorithms")
if !ok || !b.(bool) {
cert.SignatureAlgorithm = signingAlgorithm
} else {
return nil, fmt.Errorf("signing certificate's public key algorithm (%s) does not match submitted certificate's (%s), and require_matching_certificate_algorithms is true",
signingPubType.String(), certPubType.String())
}
}
newCert, err := x509.CreateCertificate(rand.Reader, cert, signingBundle.Certificate, cert.PublicKey, signingBundle.PrivateKey)
if err != nil {
return nil, fmt.Errorf("error signing self-issued certificate: %w", err)
}
if len(newCert) == 0 {
return nil, fmt.Errorf("nil cert was created when signing self-issued certificate")
}
pemCert := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: newCert,
})
return &logical.Response{
Data: map[string]interface{}{
"certificate": strings.TrimSpace(string(pemCert)),
"issuing_ca": signingCB.Certificate,
},
}, nil
}
// Adapted from similar code in https://github.com/golang/go/blob/4a4221e8187189adcc6463d2d96fe2e8da290132/src/crypto/x509/x509.go#L1342,
// may need to be updated in the future.
func publicKeyType(pub crypto.PublicKey) (pubType x509.PublicKeyAlgorithm, sigAlgo x509.SignatureAlgorithm, err error) {
switch pub := pub.(type) {
case *rsa.PublicKey:
pubType = x509.RSA
sigAlgo = x509.SHA256WithRSA
case *ecdsa.PublicKey:
pubType = x509.ECDSA
switch pub.Curve {
case elliptic.P224(), elliptic.P256():
sigAlgo = x509.ECDSAWithSHA256
case elliptic.P384():
sigAlgo = x509.ECDSAWithSHA384
case elliptic.P521():
sigAlgo = x509.ECDSAWithSHA512
default:
err = errors.New("x509: unknown elliptic curve")
}
case ed25519.PublicKey:
pubType = x509.Ed25519
sigAlgo = x509.PureEd25519
default:
err = errors.New("x509: only RSA, ECDSA and Ed25519 keys supported")
}
return
}
const pathGenerateRootHelpSyn = `
Generate a new CA certificate and private key used for signing.
`
const pathGenerateRootHelpDesc = `
See the API documentation for more information.
`
const pathDeleteRootHelpSyn = `
Deletes the root CA key to allow a new one to be generated.
`
const pathDeleteRootHelpDesc = `
See the API documentation for more information.
`