// 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 }