mirror of
https://github.com/hashicorp/vault.git
synced 2025-09-01 12:01:10 +02:00
* 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
453 lines
16 KiB
Go
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
|
|
}
|