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

453 lines
16 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package issuing
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/logical"
)
var (
DefaultRoleKeyUsages = []string{"DigitalSignature", "KeyAgreement", "KeyEncipherment"}
DefaultRoleEstKeyUsages = []string{}
DefaultRoleEstKeyUsageOids = []string{}
)
const (
DefaultRoleSignatureBits = 0
DefaultRoleUsePss = false
)
type RoleEntry struct {
LeaseMax string `json:"lease_max"`
Lease string `json:"lease"`
DeprecatedMaxTTL string `json:"max_ttl"`
DeprecatedTTL string `json:"ttl"`
TTL time.Duration `json:"ttl_duration"`
MaxTTL time.Duration `json:"max_ttl_duration"`
AllowLocalhost bool `json:"allow_localhost"`
AllowedBaseDomain string `json:"allowed_base_domain"`
AllowedDomainsOld string `json:"allowed_domains,omitempty"`
AllowedDomains []string `json:"allowed_domains_list"`
AllowedDomainsTemplate bool `json:"allowed_domains_template"`
AllowBaseDomain bool `json:"allow_base_domain"`
AllowBareDomains bool `json:"allow_bare_domains"`
AllowTokenDisplayName bool `json:"allow_token_displayname"`
AllowSubdomains bool `json:"allow_subdomains"`
AllowGlobDomains bool `json:"allow_glob_domains"`
AllowWildcardCertificates *bool `json:"allow_wildcard_certificates,omitempty"`
AllowAnyName bool `json:"allow_any_name"`
EnforceHostnames bool `json:"enforce_hostnames"`
AllowIPSANs bool `json:"allow_ip_sans"`
ServerFlag bool `json:"server_flag"`
ClientFlag bool `json:"client_flag"`
CodeSigningFlag bool `json:"code_signing_flag"`
EmailProtectionFlag bool `json:"email_protection_flag"`
UseCSRCommonName bool `json:"use_csr_common_name"`
UseCSRSANs bool `json:"use_csr_sans"`
KeyType string `json:"key_type"`
KeyBits int `json:"key_bits"`
UsePSS bool `json:"use_pss"`
SignatureBits int `json:"signature_bits"`
MaxPathLength *int `json:",omitempty"`
KeyUsageOld string `json:"key_usage,omitempty"`
KeyUsage []string `json:"key_usage_list"`
ExtKeyUsage []string `json:"extended_key_usage_list"`
OUOld string `json:"ou,omitempty"`
OU []string `json:"ou_list"`
OrganizationOld string `json:"organization,omitempty"`
Organization []string `json:"organization_list"`
Country []string `json:"country"`
Locality []string `json:"locality"`
Province []string `json:"province"`
StreetAddress []string `json:"street_address"`
PostalCode []string `json:"postal_code"`
GenerateLease *bool `json:"generate_lease,omitempty"`
NoStore bool `json:"no_store"`
RequireCN bool `json:"require_cn"`
CNValidations []string `json:"cn_validations"`
AllowedOtherSANs []string `json:"allowed_other_sans"`
AllowedSerialNumbers []string `json:"allowed_serial_numbers"`
AllowedUserIDs []string `json:"allowed_user_ids"`
AllowedURISANs []string `json:"allowed_uri_sans"`
AllowedURISANsTemplate bool `json:"allowed_uri_sans_template"`
PolicyIdentifiers []string `json:"policy_identifiers"`
ExtKeyUsageOIDs []string `json:"ext_key_usage_oids"`
BasicConstraintsValidForNonCA bool `json:"basic_constraints_valid_for_non_ca"`
NotBeforeDuration time.Duration `json:"not_before_duration"`
NotAfter string `json:"not_after"`
Issuer string `json:"issuer"`
// Name is only set when the role has been stored, on the fly roles have a blank name
Name string `json:"-"`
// WasModified indicates to callers if the returned entry is different than the persisted version
WasModified bool `json:"-"`
}
func (r *RoleEntry) ToResponseData() map[string]interface{} {
responseData := map[string]interface{}{
"ttl": int64(r.TTL.Seconds()),
"max_ttl": int64(r.MaxTTL.Seconds()),
"allow_localhost": r.AllowLocalhost,
"allowed_domains": r.AllowedDomains,
"allowed_domains_template": r.AllowedDomainsTemplate,
"allow_bare_domains": r.AllowBareDomains,
"allow_token_displayname": r.AllowTokenDisplayName,
"allow_subdomains": r.AllowSubdomains,
"allow_glob_domains": r.AllowGlobDomains,
"allow_wildcard_certificates": r.AllowWildcardCertificates,
"allow_any_name": r.AllowAnyName,
"allowed_uri_sans_template": r.AllowedURISANsTemplate,
"enforce_hostnames": r.EnforceHostnames,
"allow_ip_sans": r.AllowIPSANs,
"server_flag": r.ServerFlag,
"client_flag": r.ClientFlag,
"code_signing_flag": r.CodeSigningFlag,
"email_protection_flag": r.EmailProtectionFlag,
"use_csr_common_name": r.UseCSRCommonName,
"use_csr_sans": r.UseCSRSANs,
"key_type": r.KeyType,
"key_bits": r.KeyBits,
"signature_bits": r.SignatureBits,
"use_pss": r.UsePSS,
"key_usage": r.KeyUsage,
"ext_key_usage": r.ExtKeyUsage,
"ext_key_usage_oids": r.ExtKeyUsageOIDs,
"ou": r.OU,
"organization": r.Organization,
"country": r.Country,
"locality": r.Locality,
"province": r.Province,
"street_address": r.StreetAddress,
"postal_code": r.PostalCode,
"no_store": r.NoStore,
"allowed_other_sans": r.AllowedOtherSANs,
"allowed_serial_numbers": r.AllowedSerialNumbers,
"allowed_user_ids": r.AllowedUserIDs,
"allowed_uri_sans": r.AllowedURISANs,
"require_cn": r.RequireCN,
"cn_validations": r.CNValidations,
"policy_identifiers": r.PolicyIdentifiers,
"basic_constraints_valid_for_non_ca": r.BasicConstraintsValidForNonCA,
"not_before_duration": int64(r.NotBeforeDuration.Seconds()),
"not_after": r.NotAfter,
"issuer_ref": r.Issuer,
}
if r.MaxPathLength != nil {
responseData["max_path_length"] = r.MaxPathLength
}
if r.GenerateLease != nil {
responseData["generate_lease"] = r.GenerateLease
}
return responseData
}
var ErrRoleNotFound = errors.New("role not found")
// GetRole will load a role from storage based on the provided name and
// update its contents to the latest version if out of date. The WasUpdated field
// will be set to true if modifications were made indicating the caller should if
// possible write them back to disk. If the role is not found an ErrRoleNotFound
// will be returned as an error.
func GetRole(ctx context.Context, s logical.Storage, n string) (*RoleEntry, error) {
entry, err := s.Get(ctx, "role/"+n)
if err != nil {
return nil, fmt.Errorf("failed to load role %s: %w", n, err)
}
if entry == nil {
return nil, fmt.Errorf("%w: with name %s", ErrRoleNotFound, n)
}
var result RoleEntry
if err := entry.DecodeJSON(&result); err != nil {
return nil, fmt.Errorf("failed decoding role %s: %w", n, err)
}
// Migrate existing saved entries and save back if changed
modified := false
if len(result.DeprecatedTTL) == 0 && len(result.Lease) != 0 {
result.DeprecatedTTL = result.Lease
result.Lease = ""
modified = true
}
if result.TTL == 0 && len(result.DeprecatedTTL) != 0 {
parsed, err := parseutil.ParseDurationSecond(result.DeprecatedTTL)
if err != nil {
return nil, err
}
result.TTL = parsed
result.DeprecatedTTL = ""
modified = true
}
if len(result.DeprecatedMaxTTL) == 0 && len(result.LeaseMax) != 0 {
result.DeprecatedMaxTTL = result.LeaseMax
result.LeaseMax = ""
modified = true
}
if result.MaxTTL == 0 && len(result.DeprecatedMaxTTL) != 0 {
parsed, err := parseutil.ParseDurationSecond(result.DeprecatedMaxTTL)
if err != nil {
return nil, fmt.Errorf("failed parsing max_ttl field in %s: %w", n, err)
}
result.MaxTTL = parsed
result.DeprecatedMaxTTL = ""
modified = true
}
if result.AllowBaseDomain {
result.AllowBaseDomain = false
result.AllowBareDomains = true
modified = true
}
if result.AllowedDomainsOld != "" {
result.AllowedDomains = strings.Split(result.AllowedDomainsOld, ",")
result.AllowedDomainsOld = ""
modified = true
}
if result.AllowedBaseDomain != "" {
found := false
for _, v := range result.AllowedDomains {
if v == result.AllowedBaseDomain {
found = true
break
}
}
if !found {
result.AllowedDomains = append(result.AllowedDomains, result.AllowedBaseDomain)
}
result.AllowedBaseDomain = ""
modified = true
}
if result.AllowWildcardCertificates == nil {
// While not the most secure default, when AllowWildcardCertificates isn't
// explicitly specified in the stored Role, we automatically upgrade it to
// true to preserve compatibility with previous versions of Vault. Once this
// field is set, this logic will not be triggered any more.
result.AllowWildcardCertificates = new(bool)
*result.AllowWildcardCertificates = true
modified = true
}
// Upgrade generate_lease in role
if result.GenerateLease == nil {
// All the new roles will have GenerateLease always set to a Value. A
// nil Value indicates that this role needs an upgrade. Set it to
// `true` to not alter its current behavior.
result.GenerateLease = new(bool)
*result.GenerateLease = true
modified = true
}
// Upgrade key usages
if result.KeyUsageOld != "" {
result.KeyUsage = strings.Split(result.KeyUsageOld, ",")
result.KeyUsageOld = ""
modified = true
}
// Upgrade OU
if result.OUOld != "" {
result.OU = strings.Split(result.OUOld, ",")
result.OUOld = ""
modified = true
}
// Upgrade Organization
if result.OrganizationOld != "" {
result.Organization = strings.Split(result.OrganizationOld, ",")
result.OrganizationOld = ""
modified = true
}
// Set the issuer field to default if not set. We want to do this
// unconditionally as we should probably never have an empty issuer
// on a stored roles.
if len(result.Issuer) == 0 {
result.Issuer = DefaultRef
modified = true
}
// Update CN Validations to be the present default, "email,hostname"
if len(result.CNValidations) == 0 {
result.CNValidations = []string{"email", "hostname"}
modified = true
}
result.Name = n
result.WasModified = modified
return &result, nil
}
type RoleModifier func(r *RoleEntry)
func WithKeyUsage(keyUsages []string) RoleModifier {
return func(r *RoleEntry) {
r.KeyUsage = keyUsages
}
}
func WithExtKeyUsage(extKeyUsages []string) RoleModifier {
return func(r *RoleEntry) {
r.ExtKeyUsage = extKeyUsages
}
}
func WithExtKeyUsageOIDs(extKeyUsageOids []string) RoleModifier {
return func(r *RoleEntry) {
r.ExtKeyUsageOIDs = extKeyUsageOids
}
}
func WithSignatureBits(signatureBits int) RoleModifier {
return func(r *RoleEntry) {
r.SignatureBits = signatureBits
}
}
func WithUsePSS(usePss bool) RoleModifier {
return func(r *RoleEntry) {
r.UsePSS = usePss
}
}
func WithTTL(ttl time.Duration) RoleModifier {
return func(r *RoleEntry) {
r.TTL = ttl
}
}
func WithMaxTTL(ttl time.Duration) RoleModifier {
return func(r *RoleEntry) {
r.MaxTTL = ttl
}
}
func WithGenerateLease(genLease bool) RoleModifier {
return func(r *RoleEntry) {
*r.GenerateLease = genLease
}
}
func WithNotBeforeDuration(ttl time.Duration) RoleModifier {
return func(r *RoleEntry) {
r.NotBeforeDuration = ttl
}
}
func WithNoStore(noStore bool) RoleModifier {
return func(r *RoleEntry) {
r.NoStore = noStore
}
}
func WithIssuer(issuer string) RoleModifier {
return func(r *RoleEntry) {
if issuer == "" {
issuer = DefaultRef
}
r.Issuer = issuer
}
}
// SignVerbatimRole create a sign-verbatim role with no overrides. This will store
// the signed certificate, allowing any key type and Value from a role restriction.
func SignVerbatimRole() *RoleEntry {
return SignVerbatimRoleWithOpts()
}
// SignVerbatimRoleWithOpts create a sign-verbatim role with the normal defaults,
// but allowing any field to be tweaked based on the consumers needs.
func SignVerbatimRoleWithOpts(opts ...RoleModifier) *RoleEntry {
entry := &RoleEntry{
AllowLocalhost: true,
AllowAnyName: true,
AllowIPSANs: true,
AllowWildcardCertificates: new(bool),
EnforceHostnames: false,
KeyType: "any",
UseCSRCommonName: true,
UseCSRSANs: true,
AllowedOtherSANs: []string{"*"},
AllowedSerialNumbers: []string{"*"},
AllowedURISANs: []string{"*"},
AllowedUserIDs: []string{"*"},
CNValidations: []string{"disabled"},
GenerateLease: new(bool),
KeyUsage: DefaultRoleKeyUsages,
ExtKeyUsage: DefaultRoleEstKeyUsages,
ExtKeyUsageOIDs: DefaultRoleEstKeyUsageOids,
SignatureBits: DefaultRoleSignatureBits,
UsePSS: DefaultRoleUsePss,
}
*entry.AllowWildcardCertificates = true
*entry.GenerateLease = false
if opts != nil {
for _, opt := range opts {
if opt != nil {
opt(entry)
}
}
}
return entry
}
func ParseExtKeyUsagesFromRole(role *RoleEntry) certutil.CertExtKeyUsage {
var parsedKeyUsages certutil.CertExtKeyUsage
if role.ServerFlag {
parsedKeyUsages |= certutil.ServerAuthExtKeyUsage
}
if role.ClientFlag {
parsedKeyUsages |= certutil.ClientAuthExtKeyUsage
}
if role.CodeSigningFlag {
parsedKeyUsages |= certutil.CodeSigningExtKeyUsage
}
if role.EmailProtectionFlag {
parsedKeyUsages |= certutil.EmailProtectionExtKeyUsage
}
for _, k := range role.ExtKeyUsage {
switch strings.ToLower(strings.TrimSpace(k)) {
case "any":
parsedKeyUsages |= certutil.AnyExtKeyUsage
case "serverauth":
parsedKeyUsages |= certutil.ServerAuthExtKeyUsage
case "clientauth":
parsedKeyUsages |= certutil.ClientAuthExtKeyUsage
case "codesigning":
parsedKeyUsages |= certutil.CodeSigningExtKeyUsage
case "emailprotection":
parsedKeyUsages |= certutil.EmailProtectionExtKeyUsage
case "ipsecendsystem":
parsedKeyUsages |= certutil.IpsecEndSystemExtKeyUsage
case "ipsectunnel":
parsedKeyUsages |= certutil.IpsecTunnelExtKeyUsage
case "ipsecuser":
parsedKeyUsages |= certutil.IpsecUserExtKeyUsage
case "timestamping":
parsedKeyUsages |= certutil.TimeStampingExtKeyUsage
case "ocspsigning":
parsedKeyUsages |= certutil.OcspSigningExtKeyUsage
case "microsoftservergatedcrypto":
parsedKeyUsages |= certutil.MicrosoftServerGatedCryptoExtKeyUsage
case "netscapeservergatedcrypto":
parsedKeyUsages |= certutil.NetscapeServerGatedCryptoExtKeyUsage
}
}
return parsedKeyUsages
}