mirror of
https://github.com/hashicorp/vault.git
synced 2025-09-01 12:01:10 +02:00
* add gosimport to make fmt and run it * move installation to tools.sh * correct weird spacing issue * Update Makefile Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com> * fix a weird issue --------- Co-authored-by: Nick Cabatoff <ncabatoff@hashicorp.com>
495 lines
16 KiB
Go
495 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/builtin/logical/pki/managed_key"
|
|
"github.com/hashicorp/vault/builtin/logical/pki/parsing"
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
"github.com/hashicorp/vault/sdk/helper/errutil"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
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
|
|
}
|