Files
vault/builtin/logical/pki/issuing/issuers.go
Steven Clark cbf6dc2c4f PKI refactoring to start breaking apart monolith into sub-packages (#24406)
* PKI refactoring to start breaking apart monolith into sub-packages

 - This was broken down by commit within enterprise for ease of review
   but would be too difficult to bring back individual commits back
   to the CE repository. (they would be squashed anyways)
 - This change was created by exporting a patch of the enterprise PR
   and applying it to CE repository

* Fix TestBackend_OID_SANs to not be rely on map ordering
2023-12-07 09:22:53 -05:00

496 lines
16 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package issuing
import (
"context"
"crypto/x509"
"fmt"
"sort"
"strings"
"time"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/errutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/builtin/logical/pki/managed_key"
"github.com/hashicorp/vault/builtin/logical/pki/parsing"
)
const (
ReadOnlyUsage IssuerUsage = iota
IssuanceUsage IssuerUsage = 1 << iota
CRLSigningUsage IssuerUsage = 1 << iota
OCSPSigningUsage IssuerUsage = 1 << iota
)
const (
// When adding a new usage in the future, we'll need to create a usage
// mask field on the IssuerEntry and handle migrations to a newer mask,
// inferring a value for the new bits.
AllIssuerUsages = ReadOnlyUsage | IssuanceUsage | CRLSigningUsage | OCSPSigningUsage
DefaultRef = "default"
IssuerPrefix = "config/issuer/"
// Used as a quick sanity check for a reference id lookups...
uuidLength = 36
IssuerRefNotFound = IssuerID("not-found")
LatestIssuerVersion = 1
LegacyCertBundlePath = "config/ca_bundle"
LegacyBundleShimID = IssuerID("legacy-entry-shim-id")
LegacyBundleShimKeyID = KeyID("legacy-entry-shim-key-id")
)
type IssuerID string
func (p IssuerID) String() string {
return string(p)
}
type IssuerUsage uint
var namedIssuerUsages = map[string]IssuerUsage{
"read-only": ReadOnlyUsage,
"issuing-certificates": IssuanceUsage,
"crl-signing": CRLSigningUsage,
"ocsp-signing": OCSPSigningUsage,
}
func (i *IssuerUsage) ToggleUsage(usages ...IssuerUsage) {
for _, usage := range usages {
*i ^= usage
}
}
func (i IssuerUsage) HasUsage(usage IssuerUsage) bool {
return (i & usage) == usage
}
func (i IssuerUsage) Names() string {
var names []string
var builtUsage IssuerUsage
// Return the known set of usages in a sorted order to not have Terraform state files flipping
// saying values are different when it's the same list in a different order.
keys := make([]string, 0, len(namedIssuerUsages))
for k := range namedIssuerUsages {
keys = append(keys, k)
}
sort.Strings(keys)
for _, name := range keys {
usage := namedIssuerUsages[name]
if i.HasUsage(usage) {
names = append(names, name)
builtUsage.ToggleUsage(usage)
}
}
if i != builtUsage {
// Found some unknown usage, we should indicate this in the names.
names = append(names, fmt.Sprintf("unknown:%v", i^builtUsage))
}
return strings.Join(names, ",")
}
func NewIssuerUsageFromNames(names []string) (IssuerUsage, error) {
var result IssuerUsage
for index, name := range names {
usage, ok := namedIssuerUsages[name]
if !ok {
return ReadOnlyUsage, fmt.Errorf("unknown name for usage at index %v: %v", index, name)
}
result.ToggleUsage(usage)
}
return result, nil
}
type IssuerEntry struct {
ID IssuerID `json:"id"`
Name string `json:"name"`
KeyID KeyID `json:"key_id"`
Certificate string `json:"certificate"`
CAChain []string `json:"ca_chain"`
ManualChain []IssuerID `json:"manual_chain"`
SerialNumber string `json:"serial_number"`
LeafNotAfterBehavior certutil.NotAfterBehavior `json:"not_after_behavior"`
Usage IssuerUsage `json:"usage"`
RevocationSigAlg x509.SignatureAlgorithm `json:"revocation_signature_algorithm"`
Revoked bool `json:"revoked"`
RevocationTime int64 `json:"revocation_time"`
RevocationTimeUTC time.Time `json:"revocation_time_utc"`
AIAURIs *AiaConfigEntry `json:"aia_uris,omitempty"`
LastModified time.Time `json:"last_modified"`
Version uint `json:"version"`
}
func (i IssuerEntry) GetCertificate() (*x509.Certificate, error) {
cert, err := parsing.ParseCertificateFromBytes([]byte(i.Certificate))
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to parse certificate from issuer: %s: %v", err.Error(), i.ID)}
}
return cert, nil
}
func (i IssuerEntry) EnsureUsage(usage IssuerUsage) error {
// We want to spit out a nice error message about missing usages.
if i.Usage.HasUsage(usage) {
return nil
}
issuerRef := fmt.Sprintf("id:%v", i.ID)
if len(i.Name) > 0 {
issuerRef = fmt.Sprintf("%v / name:%v", issuerRef, i.Name)
}
// These usages differ at some point in time. We've gotta find the first
// usage that differs and return a logical-sounding error message around
// that difference.
for name, candidate := range namedIssuerUsages {
if usage.HasUsage(candidate) && !i.Usage.HasUsage(candidate) {
return fmt.Errorf("requested usage %v for issuer [%v] but only had usage %v", name, issuerRef, i.Usage.Names())
}
}
// Maybe we have an unnamed usage that's requested.
return fmt.Errorf("unknown delta between usages: %v -> %v / for issuer [%v]", usage.Names(), i.Usage.Names(), issuerRef)
}
func (i IssuerEntry) CanMaybeSignWithAlgo(algo x509.SignatureAlgorithm) error {
// Hack: Go isn't kind enough expose its lovely signatureAlgorithmDetails
// informational struct for our usage. However, we don't want to actually
// fetch the private key and attempt a signature with this algo (as we'll
// mint new, previously unsigned material in the process that could maybe
// be potentially abused if it leaks).
//
// So...
//
// ...we maintain our own mapping of cert.PKI<->sigAlgos. Notably, we
// exclude DSA support as the PKI engine has never supported DSA keys.
if algo == x509.UnknownSignatureAlgorithm {
// Special cased to indicate upgrade and letting Go automatically
// chose the correct value.
return nil
}
cert, err := i.GetCertificate()
if err != nil {
return fmt.Errorf("unable to parse issuer's potential signature algorithm types: %w", err)
}
switch cert.PublicKeyAlgorithm {
case x509.RSA:
switch algo {
case x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA,
x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS,
x509.SHA512WithRSAPSS:
return nil
}
case x509.ECDSA:
switch algo {
case x509.ECDSAWithSHA256, x509.ECDSAWithSHA384, x509.ECDSAWithSHA512:
return nil
}
case x509.Ed25519:
switch algo {
case x509.PureEd25519:
return nil
}
}
return fmt.Errorf("unable to use issuer of type %v to sign with %v key type", cert.PublicKeyAlgorithm.String(), algo.String())
}
func ResolveIssuerReference(ctx context.Context, s logical.Storage, reference string) (IssuerID, error) {
if reference == DefaultRef {
// Handle fetching the default issuer.
config, err := GetIssuersConfig(ctx, s)
if err != nil {
return IssuerID("config-error"), err
}
if len(config.DefaultIssuerId) == 0 {
return IssuerRefNotFound, fmt.Errorf("no default issuer currently configured")
}
return config.DefaultIssuerId, nil
}
// Lookup by a direct get first to see if our reference is an ID, this is quick and cached.
if len(reference) == uuidLength {
entry, err := s.Get(ctx, IssuerPrefix+reference)
if err != nil {
return IssuerID("issuer-read"), err
}
if entry != nil {
return IssuerID(reference), nil
}
}
// ... than to pull all issuers from storage.
issuers, err := ListIssuers(ctx, s)
if err != nil {
return IssuerID("list-error"), err
}
for _, issuerId := range issuers {
issuer, err := FetchIssuerById(ctx, s, issuerId)
if err != nil {
return IssuerID("issuer-read"), err
}
if issuer.Name == reference {
return issuer.ID, nil
}
}
// Otherwise, we must not have found the issuer.
return IssuerRefNotFound, errutil.UserError{Err: fmt.Sprintf("unable to find PKI issuer for reference: %v", reference)}
}
func ListIssuers(ctx context.Context, s logical.Storage) ([]IssuerID, error) {
strList, err := s.List(ctx, IssuerPrefix)
if err != nil {
return nil, err
}
issuerIds := make([]IssuerID, 0, len(strList))
for _, entry := range strList {
issuerIds = append(issuerIds, IssuerID(entry))
}
return issuerIds, nil
}
// FetchIssuerById returns an IssuerEntry based on issuerId, if none found an error is returned.
func FetchIssuerById(ctx context.Context, s logical.Storage, issuerId IssuerID) (*IssuerEntry, error) {
if len(issuerId) == 0 {
return nil, errutil.InternalError{Err: "unable to fetch pki issuer: empty issuer identifier"}
}
entry, err := s.Get(ctx, IssuerPrefix+issuerId.String())
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch pki issuer: %v", err)}
}
if entry == nil {
return nil, errutil.UserError{Err: fmt.Sprintf("pki issuer id %s does not exist", issuerId.String())}
}
var issuer IssuerEntry
if err := entry.DecodeJSON(&issuer); err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to decode pki issuer with id %s: %v", issuerId.String(), err)}
}
return upgradeIssuerIfRequired(&issuer), nil
}
func WriteIssuer(ctx context.Context, s logical.Storage, issuer *IssuerEntry) error {
issuerId := issuer.ID
if issuer.LastModified.IsZero() {
issuer.LastModified = time.Now().UTC()
}
json, err := logical.StorageEntryJSON(IssuerPrefix+issuerId.String(), issuer)
if err != nil {
return err
}
return s.Put(ctx, json)
}
func DeleteIssuer(ctx context.Context, s logical.Storage, id IssuerID) (bool, error) {
config, err := GetIssuersConfig(ctx, s)
if err != nil {
return false, err
}
wasDefault := false
if config.DefaultIssuerId == id {
wasDefault = true
// Overwrite the fetched default issuer as we're going to remove this
// entry.
config.fetchedDefault = IssuerID("")
config.DefaultIssuerId = IssuerID("")
if err := SetIssuersConfig(ctx, s, config); err != nil {
return wasDefault, err
}
}
return wasDefault, s.Delete(ctx, IssuerPrefix+id.String())
}
func upgradeIssuerIfRequired(issuer *IssuerEntry) *IssuerEntry {
// *NOTE*: Don't attempt to write out the issuer here as it may cause ErrReadOnly that will direct the
// request all the way up to the primary cluster which would be horrible for local cluster operations such
// as generating a leaf cert or a revoke.
// Also even though we could tell if we are the primary cluster's active node, we can't tell if we have the
// a full rw issuer lock, so it might not be safe to write.
if issuer.Version == LatestIssuerVersion {
return issuer
}
if issuer.Version == 0 {
// Upgrade at this step requires interrogating the certificate itself;
// if this decode fails, it indicates internal problems and the
// request will subsequently fail elsewhere. However, decoding this
// certificate is mildly expensive, so we only do it in the event of
// a Version 0 certificate.
cert, err := issuer.GetCertificate()
if err != nil {
return issuer
}
hadCRL := issuer.Usage.HasUsage(CRLSigningUsage)
// Remove CRL signing usage if it exists on the issuer but doesn't
// exist in the KU of the x509 certificate.
if hadCRL && (cert.KeyUsage&x509.KeyUsageCRLSign) == 0 {
issuer.Usage.ToggleUsage(CRLSigningUsage)
}
// Handle our new OCSPSigning usage flag for earlier versions. If we
// had it (prior to removing it in this upgrade), we'll add the OCSP
// flag since EKUs don't matter.
if hadCRL && !issuer.Usage.HasUsage(OCSPSigningUsage) {
issuer.Usage.ToggleUsage(OCSPSigningUsage)
}
}
issuer.Version = LatestIssuerVersion
return issuer
}
// FetchCAInfoByIssuerId will fetch the CA info, will return an error if no ca info exists for the given issuerId.
// This does support the loading using the legacyBundleShimID
func FetchCAInfoByIssuerId(ctx context.Context, s logical.Storage, mkv managed_key.PkiManagedKeyView, issuerId IssuerID, usage IssuerUsage) (*certutil.CAInfoBundle, error) {
entry, bundle, err := FetchCertBundleByIssuerId(ctx, s, issuerId, true)
if err != nil {
switch err.(type) {
case errutil.UserError:
return nil, err
case errutil.InternalError:
return nil, err
default:
return nil, errutil.InternalError{Err: fmt.Sprintf("error fetching CA info: %v", err)}
}
}
if err = entry.EnsureUsage(usage); err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("error while attempting to use issuer %v: %v", issuerId, err)}
}
parsedBundle, err := ParseCABundle(ctx, mkv, bundle)
if err != nil {
return nil, errutil.InternalError{Err: err.Error()}
}
if parsedBundle.Certificate == nil {
return nil, errutil.InternalError{Err: "stored CA information not able to be parsed"}
}
if parsedBundle.PrivateKey == nil {
return nil, errutil.UserError{Err: fmt.Sprintf("unable to fetch corresponding key for issuer %v; unable to use this issuer for signing", issuerId)}
}
caInfo := &certutil.CAInfoBundle{
ParsedCertBundle: *parsedBundle,
URLs: nil,
LeafNotAfterBehavior: entry.LeafNotAfterBehavior,
RevocationSigAlg: entry.RevocationSigAlg,
}
entries, err := GetAIAURLs(ctx, s, entry)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch AIA URL information: %v", err)}
}
caInfo.URLs = entries
return caInfo, nil
}
func ParseCABundle(ctx context.Context, mkv managed_key.PkiManagedKeyView, bundle *certutil.CertBundle) (*certutil.ParsedCertBundle, error) {
if bundle.PrivateKeyType == certutil.ManagedPrivateKey {
return managed_key.ParseManagedKeyCABundle(ctx, mkv, bundle)
}
return bundle.ToParsedCertBundle()
}
// FetchCertBundleByIssuerId builds a certutil.CertBundle from the specified issuer identifier,
// optionally loading the key or not. This method supports loading legacy
// bundles using the legacyBundleShimID issuerId, and if no entry is found will return an error.
func FetchCertBundleByIssuerId(ctx context.Context, s logical.Storage, id IssuerID, loadKey bool) (*IssuerEntry, *certutil.CertBundle, error) {
if id == LegacyBundleShimID {
// We have not completed the migration, or started a request in legacy mode, so
// attempt to load the bundle from the legacy location
issuer, bundle, err := GetLegacyCertBundle(ctx, s)
if err != nil {
return nil, nil, err
}
if issuer == nil || bundle == nil {
return nil, nil, errutil.UserError{Err: "no legacy cert bundle exists"}
}
return issuer, bundle, err
}
issuer, err := FetchIssuerById(ctx, s, id)
if err != nil {
return nil, nil, err
}
var bundle certutil.CertBundle
bundle.Certificate = issuer.Certificate
bundle.CAChain = issuer.CAChain
bundle.SerialNumber = issuer.SerialNumber
// Fetch the key if it exists. Sometimes we don't need the key immediately.
if loadKey && issuer.KeyID != KeyID("") {
key, err := FetchKeyById(ctx, s, issuer.KeyID)
if err != nil {
return nil, nil, err
}
bundle.PrivateKeyType = key.PrivateKeyType
bundle.PrivateKey = key.PrivateKey
}
return issuer, &bundle, nil
}
func GetLegacyCertBundle(ctx context.Context, s logical.Storage) (*IssuerEntry, *certutil.CertBundle, error) {
entry, err := s.Get(ctx, LegacyCertBundlePath)
if err != nil {
return nil, nil, err
}
if entry == nil {
return nil, nil, nil
}
cb := &certutil.CertBundle{}
err = entry.DecodeJSON(cb)
if err != nil {
return nil, nil, err
}
// Fake a storage entry with backwards compatibility in mind.
issuer := &IssuerEntry{
ID: LegacyBundleShimID,
KeyID: LegacyBundleShimKeyID,
Name: "legacy-entry-shim",
Certificate: cb.Certificate,
CAChain: cb.CAChain,
SerialNumber: cb.SerialNumber,
LeafNotAfterBehavior: certutil.ErrNotAfterBehavior,
}
issuer.Usage.ToggleUsage(AllIssuerUsages)
return issuer, cb, nil
}