| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package pki |
| |
| import ( |
| "crypto" |
| "crypto/ecdsa" |
| "crypto/ed25519" |
| "crypto/rsa" |
| "crypto/x509" |
| "crypto/x509/pkix" |
| "encoding/asn1" |
| "encoding/base64" |
| "encoding/hex" |
| "encoding/pem" |
| "errors" |
| "fmt" |
| "io" |
| "math/big" |
| "net" |
| "net/url" |
| "regexp" |
| "strconv" |
| "strings" |
| "time" |
| |
| "github.com/hashicorp/go-secure-stdlib/strutil" |
| "github.com/hashicorp/vault/sdk/framework" |
| "github.com/hashicorp/vault/sdk/helper/certutil" |
| "github.com/hashicorp/vault/sdk/helper/errutil" |
| "github.com/hashicorp/vault/sdk/logical" |
| "github.com/ryanuber/go-glob" |
| "golang.org/x/crypto/cryptobyte" |
| cbbasn1 "golang.org/x/crypto/cryptobyte/asn1" |
| "golang.org/x/net/idna" |
| ) |
| |
| type inputBundle struct { |
| role *roleEntry |
| req *logical.Request |
| apiData *framework.FieldData |
| } |
| |
| var ( |
| // labelRegex is a single label from a valid domain name and was extracted |
| // from hostnameRegex below for use in leftWildLabelRegex, without any |
| // label separators (`.`). |
| labelRegex = `([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])` |
| |
| // A note on hostnameRegex: although we set the StrictDomainName option |
| // when doing the idna conversion, this appears to only affect output, not |
| // input, so it will allow e.g. host^123.example.com straight through. So |
| // we still need to use this to check the output. |
| hostnameRegex = regexp.MustCompile(`^(\*\.)?(` + labelRegex + `\.)*` + labelRegex + `\.?$`) |
| |
| // Left Wildcard Label Regex is equivalent to a single domain label |
| // component from hostnameRegex above, but with additional wildcard |
| // characters added. There are four possibilities here: |
| // |
| // 1. Entire label is a wildcard, |
| // 2. Wildcard exists at the start, |
| // 3. Wildcard exists at the end, |
| // 4. Wildcard exists in the middle. |
| allWildRegex = `\*` |
| startWildRegex = `\*` + labelRegex |
| endWildRegex = labelRegex + `\*` |
| middleWildRegex = labelRegex + `\*` + labelRegex |
| leftWildLabelRegex = regexp.MustCompile(`^(` + allWildRegex + `|` + startWildRegex + `|` + endWildRegex + `|` + middleWildRegex + `)$`) |
| |
| // OIDs for X.509 certificate extensions used below. |
| oidExtensionSubjectAltName = []int{2, 5, 29, 17} |
| ) |
| |
| func getFormat(data *framework.FieldData) string { |
| format := data.Get("format").(string) |
| switch format { |
| case "pem": |
| case "der": |
| case "pem_bundle": |
| default: |
| format = "" |
| } |
| return format |
| } |
| |
| // fetchCAInfo will fetch the CA info, will return an error if no ca info exists, this does NOT support |
| // loading using the legacyBundleShimID and should be used with care. This should be called only once |
| // within the request path otherwise you run the risk of a race condition with the issuer migration on perf-secondaries. |
| func (sc *storageContext) fetchCAInfo(issuerRef string, usage issuerUsage) (*certutil.CAInfoBundle, error) { |
| bundle, _, err := sc.fetchCAInfoWithIssuer(issuerRef, usage) |
| return bundle, err |
| } |
| |
| func (sc *storageContext) fetchCAInfoWithIssuer(issuerRef string, usage issuerUsage) (*certutil.CAInfoBundle, issuerID, error) { |
| var issuerId issuerID |
| |
| if sc.Backend.useLegacyBundleCaStorage() { |
| // We have not completed the migration so attempt to load the bundle from the legacy location |
| sc.Backend.Logger().Info("Using legacy CA bundle as PKI migration has not completed.") |
| issuerId = legacyBundleShimID |
| } else { |
| var err error |
| issuerId, err = sc.resolveIssuerReference(issuerRef) |
| if err != nil { |
| // Usually a bad label from the user or mis-configured default. |
| return nil, IssuerRefNotFound, errutil.UserError{Err: err.Error()} |
| } |
| } |
| |
| bundle, err := sc.fetchCAInfoByIssuerId(issuerId, usage) |
| if err != nil { |
| return nil, IssuerRefNotFound, err |
| } |
| |
| return bundle, issuerId, nil |
| } |
| |
| // 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 (sc *storageContext) fetchCAInfoByIssuerId(issuerId issuerID, usage issuerUsage) (*certutil.CAInfoBundle, error) { |
| entry, bundle, err := sc.fetchCertBundleByIssuerId(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(sc.Context, sc.Backend, 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 := entry.GetAIAURLs(sc) |
| 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 fetchCertBySerialBigInt(sc *storageContext, prefix string, serial *big.Int) (*logical.StorageEntry, error) { |
| return fetchCertBySerial(sc, prefix, serialFromBigInt(serial)) |
| } |
| |
| // Allows fetching certificates from the backend; it handles the slightly |
| // separate pathing for CRL, and revoked certificates. |
| // |
| // Support for fetching CA certificates was removed, due to the new issuers |
| // changes. |
| func fetchCertBySerial(sc *storageContext, prefix, serial string) (*logical.StorageEntry, error) { |
| var path, legacyPath string |
| var err error |
| var certEntry *logical.StorageEntry |
| |
| hyphenSerial := normalizeSerial(serial) |
| colonSerial := strings.ReplaceAll(strings.ToLower(serial), "-", ":") |
| |
| switch { |
| // Revoked goes first as otherwise crl get hardcoded paths which fail if |
| // we actually want revocation info |
| case strings.HasPrefix(prefix, "revoked/"): |
| legacyPath = "revoked/" + colonSerial |
| path = "revoked/" + hyphenSerial |
| case serial == legacyCRLPath || serial == deltaCRLPath || serial == unifiedCRLPath || serial == unifiedDeltaCRLPath: |
| warnings, err := sc.Backend.crlBuilder.rebuildIfForced(sc) |
| if err != nil { |
| return nil, err |
| } |
| if len(warnings) > 0 { |
| msg := "During rebuild of CRL for cert fetch, got the following warnings:" |
| for index, warning := range warnings { |
| msg = fmt.Sprintf("%v\n %d. %v", msg, index+1, warning) |
| } |
| sc.Backend.Logger().Warn(msg) |
| } |
| |
| unified := serial == unifiedCRLPath || serial == unifiedDeltaCRLPath |
| path, err = sc.resolveIssuerCRLPath(defaultRef, unified) |
| if err != nil { |
| return nil, err |
| } |
| |
| if serial == deltaCRLPath || serial == unifiedDeltaCRLPath { |
| if sc.Backend.useLegacyBundleCaStorage() { |
| return nil, fmt.Errorf("refusing to serve delta CRL with legacy CA bundle") |
| } |
| |
| path += deltaCRLPathSuffix |
| } |
| default: |
| legacyPath = "certs/" + colonSerial |
| path = "certs/" + hyphenSerial |
| } |
| |
| certEntry, err = sc.Storage.Get(sc.Context, path) |
| if err != nil { |
| return nil, errutil.InternalError{Err: fmt.Sprintf("error fetching certificate %s: %s", serial, err)} |
| } |
| if certEntry != nil { |
| if certEntry.Value == nil || len(certEntry.Value) == 0 { |
| return nil, errutil.InternalError{Err: fmt.Sprintf("returned certificate bytes for serial %s were empty", serial)} |
| } |
| return certEntry, nil |
| } |
| |
| // If legacyPath is unset, it's going to be a CA or CRL; return immediately |
| if legacyPath == "" { |
| return nil, nil |
| } |
| |
| // Retrieve the old-style path. We disregard errors here because they |
| // always manifest on Windows, and thus the initial check for a revoked |
| // cert fails would return an error when the cert isn't revoked, preventing |
| // the happy path from working. |
| certEntry, _ = sc.Storage.Get(sc.Context, legacyPath) |
| if certEntry == nil { |
| return nil, nil |
| } |
| if certEntry.Value == nil || len(certEntry.Value) == 0 { |
| return nil, errutil.InternalError{Err: fmt.Sprintf("returned certificate bytes for serial %s were empty", serial)} |
| } |
| |
| // Update old-style paths to new-style paths |
| certEntry.Key = path |
| certsCounted := sc.Backend.certsCounted.Load() |
| if err = sc.Storage.Put(sc.Context, certEntry); err != nil { |
| return nil, errutil.InternalError{Err: fmt.Sprintf("error saving certificate with serial %s to new location", serial)} |
| } |
| if err = sc.Storage.Delete(sc.Context, legacyPath); err != nil { |
| // If we fail here, we have an extra (copy) of a cert in storage, add to metrics: |
| switch { |
| case strings.HasPrefix(prefix, "revoked/"): |
| sc.Backend.ifCountEnabledIncrementTotalRevokedCertificatesCount(certsCounted, path) |
| default: |
| sc.Backend.ifCountEnabledIncrementTotalCertificatesCount(certsCounted, path) |
| } |
| return nil, errutil.InternalError{Err: fmt.Sprintf("error deleting certificate with serial %s from old location", serial)} |
| } |
| |
| return certEntry, nil |
| } |
| |
| // Given a URI SAN, verify that it is allowed. |
| func validateURISAN(b *backend, data *inputBundle, uri string) bool { |
| valid := false |
| for _, allowed := range data.role.AllowedURISANs { |
| if data.role.AllowedURISANsTemplate { |
| isTemplate, _ := framework.ValidateIdentityTemplate(allowed) |
| if isTemplate && data.req.EntityID != "" { |
| tmpAllowed, err := framework.PopulateIdentityTemplate(allowed, data.req.EntityID, b.System()) |
| if err != nil { |
| continue |
| } |
| allowed = tmpAllowed |
| } |
| } |
| validURI := glob.Glob(allowed, uri) |
| if validURI { |
| valid = true |
| break |
| } |
| } |
| return valid |
| } |
| |
| // Validates a given common name, ensuring it's either an email or a hostname |
| // after validating it according to the role parameters, or disables |
| // validation altogether. |
| func validateCommonName(b *backend, data *inputBundle, name string) string { |
| isDisabled := len(data.role.CNValidations) == 1 && data.role.CNValidations[0] == "disabled" |
| if isDisabled { |
| return "" |
| } |
| |
| if validateNames(b, data, []string{name}) != "" { |
| return name |
| } |
| |
| // Validations weren't disabled, but the role lacked CN Validations, so |
| // don't restrict types. This case is hit in certain existing tests. |
| if len(data.role.CNValidations) == 0 { |
| return "" |
| } |
| |
| // If there's an at in the data, ensure email type validation is allowed. |
| // Otherwise, ensure hostname is allowed. |
| if strings.Contains(name, "@") { |
| var allowsEmails bool |
| for _, validation := range data.role.CNValidations { |
| if validation == "email" { |
| allowsEmails = true |
| break |
| } |
| } |
| if !allowsEmails { |
| return name |
| } |
| } else { |
| var allowsHostnames bool |
| for _, validation := range data.role.CNValidations { |
| if validation == "hostname" { |
| allowsHostnames = true |
| break |
| } |
| } |
| if !allowsHostnames { |
| return name |
| } |
| } |
| |
| return "" |
| } |
| |
| func isWildcardDomain(name string) bool { |
| // Per RFC 6125 Section 6.4.3, and explicitly contradicting the earlier |
| // RFC 2818 which no modern client will validate against, there are two |
| // main types of wildcards, each with a single wildcard specifier (`*`, |
| // functionally different from the `*` used as a glob from the |
| // AllowGlobDomains parsing path) in the left-most label: |
| // |
| // 1. Entire label is a single wildcard character (most common and |
| // well-supported), |
| // 2. Part of the label contains a single wildcard character (e.g. per |
| // RFC 6125: baz*.example.net, *baz.example.net, or b*z.example.net). |
| // |
| // We permit issuance of both but not the older RFC 2818 style under |
| // the new AllowWildcardCertificates option. However, anything with a |
| // glob character is technically a wildcard, though not a valid one. |
| |
| return strings.Contains(name, "*") |
| } |
| |
| func validateWildcardDomain(name string) (string, string, error) { |
| // See note in isWildcardDomain(...) about the definition of a wildcard |
| // domain. |
| var wildcardLabel string |
| var reducedName string |
| |
| if strings.Count(name, "*") > 1 { |
| // As mentioned above, only one wildcard character is permitted |
| // under RFC 6125 semantics. |
| return wildcardLabel, reducedName, fmt.Errorf("expected only one wildcard identifier in the given domain name") |
| } |
| |
| // Split the Common Name into two parts: a left-most label and the |
| // remaining segments (if present). |
| splitLabels := strings.SplitN(name, ".", 2) |
| if len(splitLabels) != 2 { |
| // We've been given a single-part domain name that consists |
| // entirely of a wildcard. This is a little tricky to handle, |
| // but EnforceHostnames validates both the wildcard-containing |
| // label and the reduced name, but _only_ the latter if it is |
| // non-empty. This allows us to still validate the only label |
| // component matches hostname expectations still. |
| wildcardLabel = splitLabels[0] |
| reducedName = "" |
| } else { |
| // We have a (at least) two label domain name. But before we can |
| // update our names, we need to validate the wildcard ended up |
| // in the segment we expected it to. While this is (kinda) |
| // validated under EnforceHostnames's leftWildLabelRegex, we |
| // still need to validate it in the non-enforced mode. |
| // |
| // By validated assumption above, we know there's strictly one |
| // wildcard in this domain so we only need to check the wildcard |
| // label or the reduced name (as one is equivalent to the other). |
| // Because we later assume reducedName _lacks_ wildcard segments, |
| // we validate that. |
| wildcardLabel = splitLabels[0] |
| reducedName = splitLabels[1] |
| if strings.Contains(reducedName, "*") { |
| return wildcardLabel, reducedName, fmt.Errorf("expected wildcard to only be present in left-most domain label") |
| } |
| } |
| |
| return wildcardLabel, reducedName, nil |
| } |
| |
| // Given a set of requested names for a certificate, verifies that all of them |
| // match the various toggles set in the role for controlling issuance. |
| // If one does not pass, it is returned in the string argument. |
| func validateNames(b *backend, data *inputBundle, names []string) string { |
| for _, name := range names { |
| // Previously, reducedName was called sanitizedName but this made |
| // little sense under the previous interpretation of wildcards, |
| // leading to two bugs in this implementation. We presently call it |
| // "reduced" to indicate that it is still untrusted input (potentially |
| // different from the bare Common Name entry we're validating), it |
| // might have been modified such as by the removal of wildcard labels |
| // or the email prefix. |
| reducedName := name |
| emailDomain := reducedName |
| wildcardLabel := "" |
| isEmail := false |
| isWildcard := false |
| |
| // If it has an @, assume it is an email address and separate out the |
| // user from the hostname portion so that we can act on the hostname. |
| // Note that this matches behavior from the alt_names parameter. If it |
| // ends up being problematic for users, I guess that could be separated |
| // into dns_names and email_names in the future to be explicit, but I |
| // don't think this is likely. |
| if strings.Contains(reducedName, "@") { |
| splitEmail := strings.Split(reducedName, "@") |
| if len(splitEmail) != 2 { |
| return name |
| } |
| reducedName = splitEmail[1] |
| emailDomain = splitEmail[1] |
| isEmail = true |
| } |
| |
| if isWildcardDomain(reducedName) { |
| // Regardless of later rejections below, this common name contains |
| // a wildcard character and is thus technically a wildcard name. |
| isWildcard = true |
| |
| // Additionally, if AllowWildcardCertificates is explicitly |
| // forbidden, it takes precedence over AllowAnyName, thus we should |
| // reject the name now. |
| // |
| // We expect the role to have been correctly migrated but guard for |
| // safety. |
| if data.role.AllowWildcardCertificates != nil && !*data.role.AllowWildcardCertificates { |
| return name |
| } |
| |
| // Check that this domain is well-formatted per RFC 6125. |
| var err error |
| wildcardLabel, reducedName, err = validateWildcardDomain(reducedName) |
| if err != nil { |
| return name |
| } |
| } |
| |
| // Email addresses using wildcard domain names do not make sense |
| // in a Common Name field. |
| if isEmail && isWildcard { |
| return name |
| } |
| |
| // AllowAnyName is checked after this because EnforceHostnames still |
| // applies when allowing any name. Also, we check the reduced name to |
| // ensure that we are not either checking a full email address or a |
| // wildcard prefix. |
| if data.role.EnforceHostnames { |
| if reducedName != "" { |
| // See note above about splitLabels having only one segment |
| // and setting reducedName to the empty string. |
| p := idna.New( |
| idna.StrictDomainName(true), |
| idna.VerifyDNSLength(true), |
| ) |
| converted, err := p.ToASCII(reducedName) |
| if err != nil { |
| return name |
| } |
| if !hostnameRegex.MatchString(converted) { |
| return name |
| } |
| } |
| |
| // When a wildcard is specified, we additionally need to validate |
| // the label with the wildcard is correctly formed. |
| if isWildcard && !leftWildLabelRegex.MatchString(wildcardLabel) { |
| return name |
| } |
| } |
| |
| // Self-explanatory, but validations from EnforceHostnames and |
| // AllowWildcardCertificates take precedence. |
| if data.role.AllowAnyName { |
| continue |
| } |
| |
| // The following blocks all work the same basic way: |
| // 1) If a role allows a certain class of base (localhost, token |
| // display name, role-configured domains), perform further tests |
| // |
| // 2) If there is a perfect match on either the sanitized name or it's an |
| // email address with a perfect match on the hostname portion, allow it |
| // |
| // 3) If subdomains are allowed, we check based on the sanitized name; |
| // note that if not a wildcard, will be equivalent to the email domain |
| // for email checks, and we already checked above for both a wildcard |
| // and email address being present in the same name |
| // 3a) First we check for a non-wildcard subdomain, as in <name>.<base> |
| // 3b) Then we check if it's a wildcard and the base domain is a match |
| // |
| // Variances are noted in-line |
| |
| if data.role.AllowLocalhost { |
| if reducedName == "localhost" || |
| reducedName == "localdomain" || |
| (isEmail && emailDomain == "localhost") || |
| (isEmail && emailDomain == "localdomain") { |
| continue |
| } |
| |
| if data.role.AllowSubdomains { |
| // It is possible, if unlikely, to have a subdomain of "localhost" |
| if strings.HasSuffix(reducedName, ".localhost") || |
| (isWildcard && reducedName == "localhost") { |
| continue |
| } |
| |
| // A subdomain of "localdomain" is also not entirely uncommon |
| if strings.HasSuffix(reducedName, ".localdomain") || |
| (isWildcard && reducedName == "localdomain") { |
| continue |
| } |
| } |
| } |
| |
| if data.role.AllowTokenDisplayName { |
| if name == data.req.DisplayName { |
| continue |
| } |
| |
| if data.role.AllowSubdomains { |
| if isEmail { |
| // If it's an email address, we need to parse the token |
| // display name in order to do a proper comparison of the |
| // subdomain |
| if strings.Contains(data.req.DisplayName, "@") { |
| splitDisplay := strings.Split(data.req.DisplayName, "@") |
| if len(splitDisplay) == 2 { |
| // Compare the sanitized name against the hostname |
| // portion of the email address in the broken |
| // display name |
| if strings.HasSuffix(reducedName, "."+splitDisplay[1]) { |
| continue |
| } |
| } |
| } |
| } |
| |
| if strings.HasSuffix(reducedName, "."+data.req.DisplayName) || |
| (isWildcard && reducedName == data.req.DisplayName) { |
| continue |
| } |
| } |
| } |
| |
| if len(data.role.AllowedDomains) > 0 { |
| valid := false |
| for _, currDomain := range data.role.AllowedDomains { |
| // If there is, say, a trailing comma, ignore it |
| if currDomain == "" { |
| continue |
| } |
| |
| if data.role.AllowedDomainsTemplate { |
| isTemplate, _ := framework.ValidateIdentityTemplate(currDomain) |
| if isTemplate && data.req.EntityID != "" { |
| tmpCurrDomain, err := framework.PopulateIdentityTemplate(currDomain, data.req.EntityID, b.System()) |
| if err != nil { |
| continue |
| } |
| |
| currDomain = tmpCurrDomain |
| } |
| } |
| |
| // First, allow an exact match of the base domain if that role flag |
| // is enabled |
| if data.role.AllowBareDomains && |
| (strings.EqualFold(name, currDomain) || |
| (isEmail && strings.EqualFold(emailDomain, currDomain))) { |
| valid = true |
| break |
| } |
| |
| if data.role.AllowSubdomains { |
| if strings.HasSuffix(reducedName, "."+currDomain) || |
| (isWildcard && strings.EqualFold(reducedName, currDomain)) { |
| valid = true |
| break |
| } |
| } |
| |
| if data.role.AllowGlobDomains && |
| strings.Contains(currDomain, "*") && |
| glob.Glob(currDomain, name) { |
| valid = true |
| break |
| } |
| } |
| |
| if valid { |
| continue |
| } |
| } |
| |
| return name |
| } |
| |
| return "" |
| } |
| |
| // validateOtherSANs checks if the values requested are allowed. If an OID |
| // isn't allowed, it will be returned as the first string. If a value isn't |
| // allowed, it will be returned as the second string. Empty strings + error |
| // means everything is okay. |
| func validateOtherSANs(data *inputBundle, requested map[string][]string) (string, string, error) { |
| if len(data.role.AllowedOtherSANs) == 1 && data.role.AllowedOtherSANs[0] == "*" { |
| // Anything is allowed |
| return "", "", nil |
| } |
| |
| allowed, err := parseOtherSANs(data.role.AllowedOtherSANs) |
| if err != nil { |
| return "", "", fmt.Errorf("error parsing role's allowed SANs: %w", err) |
| } |
| for oid, names := range requested { |
| for _, name := range names { |
| allowedNames, ok := allowed[oid] |
| if !ok { |
| return oid, "", nil |
| } |
| |
| valid := false |
| for _, allowedName := range allowedNames { |
| if glob.Glob(allowedName, name) { |
| valid = true |
| break |
| } |
| } |
| |
| if !valid { |
| return oid, name, nil |
| } |
| } |
| } |
| |
| return "", "", nil |
| } |
| |
| func parseOtherSANs(others []string) (map[string][]string, error) { |
| result := map[string][]string{} |
| for _, other := range others { |
| splitOther := strings.SplitN(other, ";", 2) |
| if len(splitOther) != 2 { |
| return nil, fmt.Errorf("expected a semicolon in other SAN %q", other) |
| } |
| splitType := strings.SplitN(splitOther[1], ":", 2) |
| if len(splitType) != 2 { |
| return nil, fmt.Errorf("expected a colon in other SAN %q", other) |
| } |
| switch { |
| case strings.EqualFold(splitType[0], "utf8"): |
| case strings.EqualFold(splitType[0], "utf-8"): |
| default: |
| return nil, fmt.Errorf("only utf8 other SANs are supported; found non-supported type in other SAN %q", other) |
| } |
| result[splitOther[0]] = append(result[splitOther[0]], splitType[1]) |
| } |
| |
| return result, nil |
| } |
| |
| // Returns bool stating whether the given UserId is Valid |
| func validateUserId(data *inputBundle, userId string) bool { |
| allowedList := data.role.AllowedUserIDs |
| |
| if len(allowedList) == 0 { |
| // Nothing is allowed. |
| return false |
| } |
| |
| if strutil.StrListContainsCaseInsensitive(allowedList, userId) { |
| return true |
| } |
| |
| for _, rolePattern := range allowedList { |
| if rolePattern == "" { |
| continue |
| } |
| |
| if strings.Contains(rolePattern, "*") && glob.Glob(rolePattern, userId) { |
| return true |
| } |
| } |
| |
| // No matches. |
| return false |
| } |
| |
| func validateSerialNumber(data *inputBundle, serialNumber string) string { |
| valid := false |
| if len(data.role.AllowedSerialNumbers) > 0 { |
| for _, currSerialNumber := range data.role.AllowedSerialNumbers { |
| if currSerialNumber == "" { |
| continue |
| } |
| |
| if (strings.Contains(currSerialNumber, "*") && |
| glob.Glob(currSerialNumber, serialNumber)) || |
| currSerialNumber == serialNumber { |
| valid = true |
| break |
| } |
| } |
| } |
| if !valid { |
| return serialNumber |
| } else { |
| return "" |
| } |
| } |
| |
| func generateCert(sc *storageContext, |
| input *inputBundle, |
| caSign *certutil.CAInfoBundle, |
| isCA bool, |
| randomSource io.Reader) (*certutil.ParsedCertBundle, []string, error, |
| ) { |
| ctx := sc.Context |
| b := sc.Backend |
| |
| if input.role == nil { |
| return nil, nil, errutil.InternalError{Err: "no role found in data bundle"} |
| } |
| |
| if input.role.KeyType == "rsa" && input.role.KeyBits < 2048 { |
| return nil, nil, errutil.UserError{Err: "RSA keys < 2048 bits are unsafe and not supported"} |
| } |
| |
| data, warnings, err := generateCreationBundle(b, input, caSign, nil) |
| if err != nil { |
| return nil, nil, err |
| } |
| if data.Params == nil { |
| return nil, nil, errutil.InternalError{Err: "nil parameters received from parameter bundle generation"} |
| } |
| |
| if isCA { |
| data.Params.IsCA = isCA |
| data.Params.PermittedDNSDomains = input.apiData.Get("permitted_dns_domains").([]string) |
| |
| if data.SigningBundle == nil { |
| // Generating a self-signed root certificate. Since we have no |
| // issuer entry yet, we default to the global URLs. |
| entries, err := getGlobalAIAURLs(ctx, sc.Storage) |
| if err != nil { |
| return nil, nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch AIA URL information: %v", err)} |
| } |
| |
| uris, err := entries.toURLEntries(sc, issuerID("")) |
| if err != nil { |
| // When generating root issuers, don't err on missing issuer |
| // ID; there is little value in including AIA info on a root, |
| // as this info would point back to itself; though RFC 5280 is |
| // a touch vague on this point, this seems to be consensus |
| // from public CAs such as DigiCert Global Root G3, ISRG Root |
| // X1, and others. |
| // |
| // This is a UX bug if we do err here, as it requires AIA |
| // templating to not include issuer id (a best practice for |
| // child certs issued from root and intermediate mounts |
| // however), and setting this before root generation (or, on |
| // root renewal) could cause problems. |
| if _, nonEmptyIssuerErr := entries.toURLEntries(sc, issuerID("empty-issuer-id")); nonEmptyIssuerErr != nil { |
| return nil, nil, errutil.InternalError{Err: fmt.Sprintf("unable to parse AIA URL information: %v\nUsing templated AIA URL's {{issuer_id}} field when generating root certificates is not supported.", err)} |
| } |
| |
| uris = &certutil.URLEntries{} |
| |
| msg := "When generating root CA, found global AIA configuration with issuer_id template unsuitable for root generation. This AIA configuration has been ignored. To include AIA on this root CA, set the global AIA configuration to not include issuer_id and instead to refer to a static issuer name." |
| warnings = append(warnings, msg) |
| } |
| |
| data.Params.URLs = uris |
| |
| if input.role.MaxPathLength == nil { |
| data.Params.MaxPathLength = -1 |
| } else { |
| data.Params.MaxPathLength = *input.role.MaxPathLength |
| } |
| } |
| } |
| |
| parsedBundle, err := generateCABundle(sc, input, data, randomSource) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| return parsedBundle, warnings, nil |
| } |
| |
| // N.B.: This is only meant to be used for generating intermediate CAs. |
| // It skips some sanity checks. |
| func generateIntermediateCSR(sc *storageContext, input *inputBundle, randomSource io.Reader) (*certutil.ParsedCSRBundle, []string, error) { |
| b := sc.Backend |
| |
| creation, warnings, err := generateCreationBundle(b, input, nil, nil) |
| if err != nil { |
| return nil, nil, err |
| } |
| if creation.Params == nil { |
| return nil, nil, errutil.InternalError{Err: "nil parameters received from parameter bundle generation"} |
| } |
| |
| addBasicConstraints := input.apiData != nil && input.apiData.Get("add_basic_constraints").(bool) |
| parsedBundle, err := generateCSRBundle(sc, input, creation, addBasicConstraints, randomSource) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| return parsedBundle, warnings, nil |
| } |
| |
| func signCert(b *backend, |
| data *inputBundle, |
| caSign *certutil.CAInfoBundle, |
| isCA bool, |
| useCSRValues bool) (*certutil.ParsedCertBundle, []string, error, |
| ) { |
| if data.role == nil { |
| return nil, nil, errutil.InternalError{Err: "no role found in data bundle"} |
| } |
| |
| csrString := data.apiData.Get("csr").(string) |
| if csrString == "" { |
| return nil, nil, errutil.UserError{Err: "\"csr\" is empty"} |
| } |
| |
| pemBlock, _ := pem.Decode([]byte(csrString)) |
| if pemBlock == nil { |
| return nil, nil, errutil.UserError{Err: "csr contains no data"} |
| } |
| csr, err := x509.ParseCertificateRequest(pemBlock.Bytes) |
| if err != nil { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf("certificate request could not be parsed: %v", err)} |
| } |
| |
| if csr.PublicKeyAlgorithm == x509.UnknownPublicKeyAlgorithm || csr.PublicKey == nil { |
| return nil, nil, errutil.UserError{Err: "Refusing to sign CSR with empty PublicKey. This usually means the SubjectPublicKeyInfo field has an OID not recognized by Go, such as 1.2.840.113549.1.1.10 for rsaPSS."} |
| } |
| |
| // This switch validates that the CSR key type matches the role and sets |
| // the value in the actualKeyType/actualKeyBits values. |
| actualKeyType := "" |
| actualKeyBits := 0 |
| |
| switch data.role.KeyType { |
| case "rsa": |
| // Verify that the key matches the role type |
| if csr.PublicKeyAlgorithm != x509.RSA { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "role requires keys of type %s", |
| data.role.KeyType)} |
| } |
| |
| pubKey, ok := csr.PublicKey.(*rsa.PublicKey) |
| if !ok { |
| return nil, nil, errutil.UserError{Err: "could not parse CSR's public key"} |
| } |
| |
| actualKeyType = "rsa" |
| actualKeyBits = pubKey.N.BitLen() |
| case "ec": |
| // Verify that the key matches the role type |
| if csr.PublicKeyAlgorithm != x509.ECDSA { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "role requires keys of type %s", |
| data.role.KeyType)} |
| } |
| pubKey, ok := csr.PublicKey.(*ecdsa.PublicKey) |
| if !ok { |
| return nil, nil, errutil.UserError{Err: "could not parse CSR's public key"} |
| } |
| |
| actualKeyType = "ec" |
| actualKeyBits = pubKey.Params().BitSize |
| case "ed25519": |
| // Verify that the key matches the role type |
| if csr.PublicKeyAlgorithm != x509.Ed25519 { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "role requires keys of type %s", |
| data.role.KeyType)} |
| } |
| |
| _, ok := csr.PublicKey.(ed25519.PublicKey) |
| if !ok { |
| return nil, nil, errutil.UserError{Err: "could not parse CSR's public key"} |
| } |
| |
| actualKeyType = "ed25519" |
| actualKeyBits = 0 |
| case "any": |
| // We need to compute the actual key type and key bits, to correctly |
| // validate minimums and SignatureBits below. |
| switch csr.PublicKeyAlgorithm { |
| case x509.RSA: |
| pubKey, ok := csr.PublicKey.(*rsa.PublicKey) |
| if !ok { |
| return nil, nil, errutil.UserError{Err: "could not parse CSR's public key"} |
| } |
| if pubKey.N.BitLen() < 2048 { |
| return nil, nil, errutil.UserError{Err: "RSA keys < 2048 bits are unsafe and not supported"} |
| } |
| |
| actualKeyType = "rsa" |
| actualKeyBits = pubKey.N.BitLen() |
| case x509.ECDSA: |
| pubKey, ok := csr.PublicKey.(*ecdsa.PublicKey) |
| if !ok { |
| return nil, nil, errutil.UserError{Err: "could not parse CSR's public key"} |
| } |
| |
| actualKeyType = "ec" |
| actualKeyBits = pubKey.Params().BitSize |
| case x509.Ed25519: |
| _, ok := csr.PublicKey.(ed25519.PublicKey) |
| if !ok { |
| return nil, nil, errutil.UserError{Err: "could not parse CSR's public key"} |
| } |
| |
| actualKeyType = "ed25519" |
| actualKeyBits = 0 |
| default: |
| return nil, nil, errutil.UserError{Err: "Unknown key type in CSR: " + csr.PublicKeyAlgorithm.String()} |
| } |
| default: |
| return nil, nil, errutil.InternalError{Err: fmt.Sprintf("unsupported key type value: %s", data.role.KeyType)} |
| } |
| |
| // Before validating key lengths, update our KeyBits/SignatureBits based |
| // on the actual CSR key type. |
| if data.role.KeyType == "any" { |
| // We update the value of KeyBits and SignatureBits here (from the |
| // role), using the specified key type. This allows us to convert |
| // the default value (0) for SignatureBits and KeyBits to a |
| // meaningful value. |
| // |
| // We ignore the role's original KeyBits value if the KeyType is any |
| // as legacy (pre-1.10) roles had default values that made sense only |
| // for RSA keys (key_bits=2048) and the older code paths ignored the role value |
| // set for KeyBits when KeyType was set to any. This also enforces the |
| // docs saying when key_type=any, we only enforce our specified minimums |
| // for signing operations |
| if data.role.KeyBits, data.role.SignatureBits, err = certutil.ValidateDefaultOrValueKeyTypeSignatureLength( |
| actualKeyType, 0, data.role.SignatureBits); err != nil { |
| return nil, nil, errutil.InternalError{Err: fmt.Sprintf("unknown internal error updating default values: %v", err)} |
| } |
| |
| // We're using the KeyBits field as a minimum value below, and P-224 is safe |
| // and a previously allowed value. However, the above call defaults |
| // to P-256 as that's a saner default than P-224 (w.r.t. generation), so |
| // override it here to allow 224 as the smallest size we permit. |
| if actualKeyType == "ec" { |
| data.role.KeyBits = 224 |
| } |
| } |
| |
| // At this point, data.role.KeyBits and data.role.SignatureBits should both |
| // be non-zero, for RSA and ECDSA keys. Validate the actualKeyBits based on |
| // the role's values. If the KeyType was any, and KeyBits was set to 0, |
| // KeyBits should be updated to 2048 unless some other value was chosen |
| // explicitly. |
| // |
| // This validation needs to occur regardless of the role's key type, so |
| // that we always validate both RSA and ECDSA key sizes. |
| if actualKeyType == "rsa" { |
| if actualKeyBits < data.role.KeyBits { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "role requires a minimum of a %d-bit key, but CSR's key is %d bits", |
| data.role.KeyBits, actualKeyBits)} |
| } |
| |
| if actualKeyBits < 2048 { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "Vault requires a minimum of a 2048-bit key, but CSR's key is %d bits", |
| actualKeyBits)} |
| } |
| } else if actualKeyType == "ec" { |
| if actualKeyBits < data.role.KeyBits { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "role requires a minimum of a %d-bit key, but CSR's key is %d bits", |
| data.role.KeyBits, |
| actualKeyBits)} |
| } |
| } |
| |
| creation, warnings, err := generateCreationBundle(b, data, caSign, csr) |
| if err != nil { |
| return nil, nil, err |
| } |
| if creation.Params == nil { |
| return nil, nil, errutil.InternalError{Err: "nil parameters received from parameter bundle generation"} |
| } |
| |
| creation.Params.IsCA = isCA |
| creation.Params.UseCSRValues = useCSRValues |
| |
| if isCA { |
| creation.Params.PermittedDNSDomains = data.apiData.Get("permitted_dns_domains").([]string) |
| } else { |
| for _, ext := range csr.Extensions { |
| if ext.Id.Equal(certutil.ExtensionBasicConstraintsOID) { |
| warnings = append(warnings, "specified CSR contained a Basic Constraints extension that was ignored during issuance") |
| } |
| } |
| } |
| |
| parsedBundle, err := certutil.SignCertificate(creation) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| return parsedBundle, warnings, nil |
| } |
| |
| // otherNameRaw describes a name related to a certificate which is not in one |
| // of the standard name formats. RFC 5280, 4.2.1.6: |
| // |
| // OtherName ::= SEQUENCE { |
| // type-id OBJECT IDENTIFIER, |
| // value [0] EXPLICIT ANY DEFINED BY type-id } |
| type otherNameRaw struct { |
| TypeID asn1.ObjectIdentifier |
| Value asn1.RawValue |
| } |
| |
| type otherNameUtf8 struct { |
| oid string |
| value string |
| } |
| |
| // ExtractUTF8String returns the UTF8 string contained in the Value, or an error |
| // if none is present. |
| func (oraw *otherNameRaw) extractUTF8String() (*otherNameUtf8, error) { |
| svalue := cryptobyte.String(oraw.Value.Bytes) |
| var outTag cbbasn1.Tag |
| var val cryptobyte.String |
| read := svalue.ReadAnyASN1(&val, &outTag) |
| |
| if read && outTag == asn1.TagUTF8String { |
| return &otherNameUtf8{oid: oraw.TypeID.String(), value: string(val)}, nil |
| } |
| return nil, fmt.Errorf("no UTF-8 string found in OtherName") |
| } |
| |
| func (o otherNameUtf8) String() string { |
| return fmt.Sprintf("%s;%s:%s", o.oid, "UTF-8", o.value) |
| } |
| |
| func getOtherSANsFromX509Extensions(exts []pkix.Extension) ([]otherNameUtf8, error) { |
| var ret []otherNameUtf8 |
| for _, ext := range exts { |
| if !ext.Id.Equal(oidExtensionSubjectAltName) { |
| continue |
| } |
| err := forEachSAN(ext.Value, func(tag int, data []byte) error { |
| if tag != 0 { |
| return nil |
| } |
| |
| var other otherNameRaw |
| _, err := asn1.UnmarshalWithParams(data, &other, "tag:0") |
| if err != nil { |
| return fmt.Errorf("could not parse requested other SAN: %w", err) |
| } |
| val, err := other.extractUTF8String() |
| if err != nil { |
| return err |
| } |
| ret = append(ret, *val) |
| return nil |
| }) |
| if err != nil { |
| return nil, err |
| } |
| } |
| |
| return ret, nil |
| } |
| |
| func forEachSAN(extension []byte, callback func(tag int, data []byte) error) error { |
| // RFC 5280, 4.2.1.6 |
| |
| // SubjectAltName ::= GeneralNames |
| // |
| // GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName |
| // |
| // GeneralName ::= CHOICE { |
| // otherName [0] OtherName, |
| // rfc822Name [1] IA5String, |
| // dNSName [2] IA5String, |
| // x400Address [3] ORAddress, |
| // directoryName [4] Name, |
| // ediPartyName [5] EDIPartyName, |
| // uniformResourceIdentifier [6] IA5String, |
| // iPAddress [7] OCTET STRING, |
| // registeredID [8] OBJECT IDENTIFIER } |
| var seq asn1.RawValue |
| rest, err := asn1.Unmarshal(extension, &seq) |
| if err != nil { |
| return err |
| } else if len(rest) != 0 { |
| return fmt.Errorf("x509: trailing data after X.509 extension") |
| } |
| if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 { |
| return asn1.StructuralError{Msg: "bad SAN sequence"} |
| } |
| |
| rest = seq.Bytes |
| for len(rest) > 0 { |
| var v asn1.RawValue |
| rest, err = asn1.Unmarshal(rest, &v) |
| if err != nil { |
| return err |
| } |
| |
| if err := callback(v.Tag, v.FullBytes); err != nil { |
| return err |
| } |
| } |
| |
| return nil |
| } |
| |
| // generateCreationBundle is a shared function that reads parameters supplied |
| // from the various endpoints and generates a CreationParameters with the |
| // parameters that can be used to issue or sign |
| func generateCreationBundle(b *backend, data *inputBundle, caSign *certutil.CAInfoBundle, csr *x509.CertificateRequest) (*certutil.CreationBundle, []string, error) { |
| // Read in names -- CN, DNS and email addresses |
| var cn string |
| var ridSerialNumber string |
| var warnings []string |
| dnsNames := []string{} |
| emailAddresses := []string{} |
| { |
| if csr != nil && data.role.UseCSRCommonName { |
| cn = csr.Subject.CommonName |
| } |
| if cn == "" { |
| cn = data.apiData.Get("common_name").(string) |
| if cn == "" && data.role.RequireCN { |
| return nil, nil, errutil.UserError{Err: `the common_name field is required, or must be provided in a CSR with "use_csr_common_name" set to true, unless "require_cn" is set to false`} |
| } |
| } |
| |
| ridSerialNumber = data.apiData.Get("serial_number").(string) |
| |
| // only take serial number from CSR if one was not supplied via API |
| if ridSerialNumber == "" && csr != nil { |
| ridSerialNumber = csr.Subject.SerialNumber |
| } |
| |
| if csr != nil && data.role.UseCSRSANs { |
| dnsNames = csr.DNSNames |
| emailAddresses = csr.EmailAddresses |
| } |
| |
| if cn != "" && !data.apiData.Get("exclude_cn_from_sans").(bool) { |
| if strings.Contains(cn, "@") { |
| // Note: emails are not disallowed if the role's email protection |
| // flag is false, because they may well be included for |
| // informational purposes; it is up to the verifying party to |
| // ensure that email addresses in a subject alternate name can be |
| // used for the purpose for which they are presented |
| emailAddresses = append(emailAddresses, cn) |
| } else { |
| // Only add to dnsNames if it's actually a DNS name but convert |
| // idn first |
| p := idna.New( |
| idna.StrictDomainName(true), |
| idna.VerifyDNSLength(true), |
| ) |
| converted, err := p.ToASCII(cn) |
| if err != nil { |
| return nil, nil, errutil.UserError{Err: err.Error()} |
| } |
| if hostnameRegex.MatchString(converted) { |
| dnsNames = append(dnsNames, converted) |
| } |
| } |
| } |
| |
| if csr == nil || !data.role.UseCSRSANs { |
| cnAltRaw, ok := data.apiData.GetOk("alt_names") |
| if ok { |
| cnAlt := strutil.ParseDedupAndSortStrings(cnAltRaw.(string), ",") |
| for _, v := range cnAlt { |
| if strings.Contains(v, "@") { |
| emailAddresses = append(emailAddresses, v) |
| } else { |
| // Only add to dnsNames if it's actually a DNS name but |
| // convert idn first |
| p := idna.New( |
| idna.StrictDomainName(true), |
| idna.VerifyDNSLength(true), |
| ) |
| converted, err := p.ToASCII(v) |
| if err != nil { |
| return nil, nil, errutil.UserError{Err: err.Error()} |
| } |
| if hostnameRegex.MatchString(converted) { |
| dnsNames = append(dnsNames, converted) |
| } |
| } |
| } |
| } |
| } |
| |
| // Check the CN. This ensures that the CN is checked even if it's |
| // excluded from SANs. |
| if cn != "" { |
| badName := validateCommonName(b, data, cn) |
| if len(badName) != 0 { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "common name %s not allowed by this role", badName)} |
| } |
| } |
| |
| if ridSerialNumber != "" { |
| badName := validateSerialNumber(data, ridSerialNumber) |
| if len(badName) != 0 { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "serial_number %s not allowed by this role", badName)} |
| } |
| } |
| |
| // Check for bad email and/or DNS names |
| badName := validateNames(b, data, dnsNames) |
| if len(badName) != 0 { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "subject alternate name %s not allowed by this role", badName)} |
| } |
| |
| badName = validateNames(b, data, emailAddresses) |
| if len(badName) != 0 { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "email address %s not allowed by this role", badName)} |
| } |
| } |
| |
| // otherSANsInput has the same format as the other_sans HTTP param in the |
| // Vault PKI API: it is a list of strings of the form <oid>;<type>:<value> |
| // where <type> must be UTF8/UTF-8. |
| var otherSANsInput []string |
| // otherSANs is the output of parseOtherSANs(otherSANsInput): its keys are |
| // the <oid> value, its values are of the form [<type>, <value>] |
| var otherSANs map[string][]string |
| if sans := data.apiData.Get("other_sans").([]string); len(sans) > 0 { |
| otherSANsInput = sans |
| } |
| if data.role.UseCSRSANs && csr != nil && len(csr.Extensions) > 0 { |
| others, err := getOtherSANsFromX509Extensions(csr.Extensions) |
| if err != nil { |
| return nil, nil, errutil.UserError{Err: fmt.Errorf("could not parse requested other SAN: %w", err).Error()} |
| } |
| for _, other := range others { |
| otherSANsInput = append(otherSANsInput, other.String()) |
| } |
| } |
| if len(otherSANsInput) > 0 { |
| requested, err := parseOtherSANs(otherSANsInput) |
| if err != nil { |
| return nil, nil, errutil.UserError{Err: fmt.Errorf("could not parse requested other SAN: %w", err).Error()} |
| } |
| badOID, badName, err := validateOtherSANs(data, requested) |
| switch { |
| case err != nil: |
| return nil, nil, errutil.UserError{Err: err.Error()} |
| case len(badName) > 0: |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "other SAN %s not allowed for OID %s by this role", badName, badOID)} |
| case len(badOID) > 0: |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "other SAN OID %s not allowed by this role", badOID)} |
| default: |
| otherSANs = requested |
| } |
| } |
| |
| // Get and verify any IP SANs |
| ipAddresses := []net.IP{} |
| { |
| if csr != nil && data.role.UseCSRSANs { |
| if len(csr.IPAddresses) > 0 { |
| if !data.role.AllowIPSANs { |
| return nil, nil, errutil.UserError{Err: "IP Subject Alternative Names are not allowed in this role, but was provided some via CSR"} |
| } |
| ipAddresses = csr.IPAddresses |
| } |
| } else { |
| ipAlt := data.apiData.Get("ip_sans").([]string) |
| if len(ipAlt) > 0 { |
| if !data.role.AllowIPSANs { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "IP Subject Alternative Names are not allowed in this role, but was provided %s", ipAlt)} |
| } |
| for _, v := range ipAlt { |
| parsedIP := net.ParseIP(v) |
| if parsedIP == nil { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf( |
| "the value %q is not a valid IP address", v)} |
| } |
| ipAddresses = append(ipAddresses, parsedIP) |
| } |
| } |
| } |
| } |
| |
| URIs := []*url.URL{} |
| { |
| if csr != nil && data.role.UseCSRSANs { |
| if len(csr.URIs) > 0 { |
| if len(data.role.AllowedURISANs) == 0 { |
| return nil, nil, errutil.UserError{ |
| Err: "URI Subject Alternative Names are not allowed in this role, but were provided via CSR", |
| } |
| } |
| |
| // validate uri sans |
| for _, uri := range csr.URIs { |
| valid := validateURISAN(b, data, uri.String()) |
| if !valid { |
| return nil, nil, errutil.UserError{ |
| Err: "URI Subject Alternative Names were provided via CSR which are not valid for this role", |
| } |
| } |
| |
| URIs = append(URIs, uri) |
| } |
| } |
| } else { |
| uriAlt := data.apiData.Get("uri_sans").([]string) |
| if len(uriAlt) > 0 { |
| if len(data.role.AllowedURISANs) == 0 { |
| return nil, nil, errutil.UserError{ |
| Err: "URI Subject Alternative Names are not allowed in this role, but were provided via the API", |
| } |
| } |
| |
| for _, uri := range uriAlt { |
| valid := validateURISAN(b, data, uri) |
| if !valid { |
| return nil, nil, errutil.UserError{ |
| Err: "URI Subject Alternative Names were provided via the API which are not valid for this role", |
| } |
| } |
| |
| parsedURI, err := url.Parse(uri) |
| if parsedURI == nil || err != nil { |
| return nil, nil, errutil.UserError{ |
| Err: fmt.Sprintf( |
| "the provided URI Subject Alternative Name %q is not a valid URI", uri), |
| } |
| } |
| |
| URIs = append(URIs, parsedURI) |
| } |
| } |
| } |
| } |
| |
| // Most of these could also be RemoveDuplicateStable, or even |
| // leave duplicates in, but OU is the one most likely to be duplicated. |
| subject := pkix.Name{ |
| CommonName: cn, |
| SerialNumber: ridSerialNumber, |
| Country: strutil.RemoveDuplicatesStable(data.role.Country, false), |
| Organization: strutil.RemoveDuplicatesStable(data.role.Organization, false), |
| OrganizationalUnit: strutil.RemoveDuplicatesStable(data.role.OU, false), |
| Locality: strutil.RemoveDuplicatesStable(data.role.Locality, false), |
| Province: strutil.RemoveDuplicatesStable(data.role.Province, false), |
| StreetAddress: strutil.RemoveDuplicatesStable(data.role.StreetAddress, false), |
| PostalCode: strutil.RemoveDuplicatesStable(data.role.PostalCode, false), |
| } |
| |
| // Get the TTL and verify it against the max allowed |
| notAfter, ttlWarnings, err := getCertificateNotAfter(b, data, caSign) |
| if err != nil { |
| return nil, warnings, err |
| } |
| warnings = append(warnings, ttlWarnings...) |
| |
| // Parse SKID from the request for cross-signing. |
| var skid []byte |
| { |
| if rawSKIDValue, ok := data.apiData.GetOk("skid"); ok { |
| // Handle removing common separators to make copy/paste from tool |
| // output easier. Chromium uses space, OpenSSL uses colons, and at |
| // one point, Vault had preferred dash as a separator for hex |
| // strings. |
| var err error |
| skidValue := rawSKIDValue.(string) |
| for _, separator := range []string{":", "-", " "} { |
| skidValue = strings.ReplaceAll(skidValue, separator, "") |
| } |
| |
| skid, err = hex.DecodeString(skidValue) |
| if err != nil { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf("cannot parse requested SKID value as hex: %v", err)} |
| } |
| } |
| } |
| |
| // Add UserIDs into the Subject, if the request type supports it. |
| if _, present := data.apiData.Schema["user_ids"]; present { |
| rawUserIDs := data.apiData.Get("user_ids").([]string) |
| |
| // Only take UserIDs from CSR if one was not supplied via API. |
| if len(rawUserIDs) == 0 && csr != nil { |
| for _, attr := range csr.Subject.Names { |
| if attr.Type.Equal(certutil.SubjectPilotUserIDAttributeOID) { |
| switch aValue := attr.Value.(type) { |
| case string: |
| rawUserIDs = append(rawUserIDs, aValue) |
| case []byte: |
| rawUserIDs = append(rawUserIDs, string(aValue)) |
| default: |
| return nil, nil, errutil.UserError{Err: "unknown type for user_id attribute in CSR's Subject"} |
| } |
| } |
| } |
| } |
| |
| // Check for bad userIDs and add to the subject. |
| if len(rawUserIDs) > 0 { |
| for _, value := range rawUserIDs { |
| if !validateUserId(data, value) { |
| return nil, nil, errutil.UserError{Err: fmt.Sprintf("user_id %v is not allowed by this role", value)} |
| } |
| |
| subject.ExtraNames = append(subject.ExtraNames, pkix.AttributeTypeAndValue{ |
| Type: certutil.SubjectPilotUserIDAttributeOID, |
| Value: value, |
| }) |
| } |
| } |
| } |
| |
| creation := &certutil.CreationBundle{ |
| Params: &certutil.CreationParameters{ |
| Subject: subject, |
| DNSNames: strutil.RemoveDuplicates(dnsNames, false), |
| EmailAddresses: strutil.RemoveDuplicates(emailAddresses, false), |
| IPAddresses: ipAddresses, |
| URIs: URIs, |
| OtherSANs: otherSANs, |
| KeyType: data.role.KeyType, |
| KeyBits: data.role.KeyBits, |
| SignatureBits: data.role.SignatureBits, |
| UsePSS: data.role.UsePSS, |
| NotAfter: notAfter, |
| KeyUsage: x509.KeyUsage(parseKeyUsages(data.role.KeyUsage)), |
| ExtKeyUsage: parseExtKeyUsages(data.role), |
| ExtKeyUsageOIDs: data.role.ExtKeyUsageOIDs, |
| PolicyIdentifiers: data.role.PolicyIdentifiers, |
| BasicConstraintsValidForNonCA: data.role.BasicConstraintsValidForNonCA, |
| NotBeforeDuration: data.role.NotBeforeDuration, |
| ForceAppendCaChain: caSign != nil, |
| SKID: skid, |
| }, |
| SigningBundle: caSign, |
| CSR: csr, |
| } |
| |
| // Don't deal with URLs or max path length if it's self-signed, as these |
| // normally come from the signing bundle |
| if caSign == nil { |
| return creation, warnings, nil |
| } |
| |
| // This will have been read in from the getGlobalAIAURLs function |
| creation.Params.URLs = caSign.URLs |
| |
| // If the max path length in the role is not nil, it was specified at |
| // generation time with the max_path_length parameter; otherwise derive it |
| // from the signing certificate |
| if data.role.MaxPathLength != nil { |
| creation.Params.MaxPathLength = *data.role.MaxPathLength |
| } else { |
| switch { |
| case caSign.Certificate.MaxPathLen < 0: |
| creation.Params.MaxPathLength = -1 |
| case caSign.Certificate.MaxPathLen == 0 && |
| caSign.Certificate.MaxPathLenZero: |
| // The signing function will ensure that we do not issue a CA cert |
| creation.Params.MaxPathLength = 0 |
| default: |
| // If this takes it to zero, we handle this case later if |
| // necessary |
| creation.Params.MaxPathLength = caSign.Certificate.MaxPathLen - 1 |
| } |
| } |
| |
| return creation, warnings, nil |
| } |
| |
| // getCertificateNotAfter compute a certificate's NotAfter date based on the mount ttl, role, signing bundle and input |
| // api data being sent. Returns a NotAfter time, a set of warnings or an error. |
| func getCertificateNotAfter(b *backend, data *inputBundle, caSign *certutil.CAInfoBundle) (time.Time, []string, error) { |
| var warnings []string |
| var maxTTL time.Duration |
| var notAfter time.Time |
| var err error |
| |
| ttl := time.Duration(data.apiData.Get("ttl").(int)) * time.Second |
| notAfterAlt := data.role.NotAfter |
| if notAfterAlt == "" { |
| notAfterAltRaw, ok := data.apiData.GetOk("not_after") |
| if ok { |
| notAfterAlt = notAfterAltRaw.(string) |
| } |
| } |
| if ttl > 0 && notAfterAlt != "" { |
| return time.Time{}, warnings, errutil.UserError{Err: "Either ttl or not_after should be provided. Both should not be provided in the same request."} |
| } |
| |
| if ttl == 0 && data.role.TTL > 0 { |
| ttl = data.role.TTL |
| } |
| |
| if data.role.MaxTTL > 0 { |
| maxTTL = data.role.MaxTTL |
| } |
| |
| if ttl == 0 { |
| ttl = b.System().DefaultLeaseTTL() |
| } |
| if maxTTL == 0 { |
| maxTTL = b.System().MaxLeaseTTL() |
| } |
| if ttl > maxTTL { |
| warnings = append(warnings, fmt.Sprintf("TTL %q is longer than permitted maxTTL %q, so maxTTL is being used", ttl, maxTTL)) |
| ttl = maxTTL |
| } |
| |
| if notAfterAlt != "" { |
| notAfter, err = time.Parse(time.RFC3339, notAfterAlt) |
| if err != nil { |
| return notAfter, warnings, errutil.UserError{Err: err.Error()} |
| } |
| } else { |
| notAfter = time.Now().Add(ttl) |
| } |
| if caSign != nil && notAfter.After(caSign.Certificate.NotAfter) { |
| // If it's not self-signed, verify that the issued certificate |
| // won't be valid past the lifetime of the CA certificate, and |
| // act accordingly. This is dependent based on the issuer's |
| // LeafNotAfterBehavior argument. |
| switch caSign.LeafNotAfterBehavior { |
| case certutil.PermitNotAfterBehavior: |
| // Explicitly do nothing. |
| case certutil.TruncateNotAfterBehavior: |
| notAfter = caSign.Certificate.NotAfter |
| case certutil.ErrNotAfterBehavior: |
| fallthrough |
| default: |
| return time.Time{}, warnings, errutil.UserError{Err: fmt.Sprintf( |
| "cannot satisfy request, as TTL would result in notAfter of %s that is beyond the expiration of the CA certificate at %s", notAfter.UTC().Format(time.RFC3339Nano), caSign.Certificate.NotAfter.UTC().Format(time.RFC3339Nano))} |
| } |
| } |
| return notAfter, warnings, nil |
| } |
| |
| func convertRespToPKCS8(resp *logical.Response) error { |
| privRaw, ok := resp.Data["private_key"] |
| if !ok { |
| return nil |
| } |
| priv, ok := privRaw.(string) |
| if !ok { |
| return fmt.Errorf("error converting response to pkcs8: could not parse original value as string") |
| } |
| |
| privKeyTypeRaw, ok := resp.Data["private_key_type"] |
| if !ok { |
| return fmt.Errorf("error converting response to pkcs8: %q not found in response", "private_key_type") |
| } |
| privKeyType, ok := privKeyTypeRaw.(certutil.PrivateKeyType) |
| if !ok { |
| return fmt.Errorf("error converting response to pkcs8: could not parse original type value as string") |
| } |
| |
| var keyData []byte |
| var pemUsed bool |
| var err error |
| var signer crypto.Signer |
| |
| block, _ := pem.Decode([]byte(priv)) |
| if block == nil { |
| keyData, err = base64.StdEncoding.DecodeString(priv) |
| if err != nil { |
| return fmt.Errorf("error converting response to pkcs8: error decoding original value: %w", err) |
| } |
| } else { |
| keyData = block.Bytes |
| pemUsed = true |
| } |
| |
| switch privKeyType { |
| case certutil.RSAPrivateKey: |
| signer, err = x509.ParsePKCS1PrivateKey(keyData) |
| case certutil.ECPrivateKey: |
| signer, err = x509.ParseECPrivateKey(keyData) |
| case certutil.Ed25519PrivateKey: |
| k, err := x509.ParsePKCS8PrivateKey(keyData) |
| if err != nil { |
| return fmt.Errorf("error converting response to pkcs8: error parsing previous key: %w", err) |
| } |
| signer = k.(crypto.Signer) |
| default: |
| return fmt.Errorf("unknown private key type %q", privKeyType) |
| } |
| if err != nil { |
| return fmt.Errorf("error converting response to pkcs8: error parsing previous key: %w", err) |
| } |
| |
| keyData, err = x509.MarshalPKCS8PrivateKey(signer) |
| if err != nil { |
| return fmt.Errorf("error converting response to pkcs8: error marshaling pkcs8 key: %w", err) |
| } |
| |
| if pemUsed { |
| block.Type = "PRIVATE KEY" |
| block.Bytes = keyData |
| resp.Data["private_key"] = strings.TrimSpace(string(pem.EncodeToMemory(block))) |
| } else { |
| resp.Data["private_key"] = base64.StdEncoding.EncodeToString(keyData) |
| } |
| |
| return nil |
| } |
| |
| func handleOtherCSRSANs(in *x509.CertificateRequest, sans map[string][]string) error { |
| certTemplate := &x509.Certificate{ |
| DNSNames: in.DNSNames, |
| IPAddresses: in.IPAddresses, |
| EmailAddresses: in.EmailAddresses, |
| URIs: in.URIs, |
| } |
| if err := handleOtherSANs(certTemplate, sans); err != nil { |
| return err |
| } |
| if len(certTemplate.ExtraExtensions) > 0 { |
| in.ExtraExtensions = append(in.ExtraExtensions, certTemplate.ExtraExtensions...) |
| } |
| return nil |
| } |
| |
| func handleOtherSANs(in *x509.Certificate, sans map[string][]string) error { |
| // If other SANs is empty we return which causes normal Go stdlib parsing |
| // of the other SAN types |
| if len(sans) == 0 { |
| return nil |
| } |
| |
| var rawValues []asn1.RawValue |
| |
| // We need to generate an IMPLICIT sequence for compatibility with OpenSSL |
| // -- it's an open question what the default for RFC 5280 actually is, see |
| // https://github.com/openssl/openssl/issues/5091 -- so we have to use |
| // cryptobyte because using the asn1 package's marshaling always produces |
| // an EXPLICIT sequence. Note that asn1 is way too magical according to |
| // agl, and cryptobyte is modeled after the CBB/CBS bits that agl put into |
| // boringssl. |
| for oid, vals := range sans { |
| for _, val := range vals { |
| var b cryptobyte.Builder |
| oidStr, err := stringToOid(oid) |
| if err != nil { |
| return err |
| } |
| b.AddASN1ObjectIdentifier(oidStr) |
| b.AddASN1(cbbasn1.Tag(0).ContextSpecific().Constructed(), func(b *cryptobyte.Builder) { |
| b.AddASN1(cbbasn1.UTF8String, func(b *cryptobyte.Builder) { |
| b.AddBytes([]byte(val)) |
| }) |
| }) |
| m, err := b.Bytes() |
| if err != nil { |
| return err |
| } |
| rawValues = append(rawValues, asn1.RawValue{Tag: 0, Class: 2, IsCompound: true, Bytes: m}) |
| } |
| } |
| |
| // If other SANs is empty we return which causes normal Go stdlib parsing |
| // of the other SAN types |
| if len(rawValues) == 0 { |
| return nil |
| } |
| |
| // Append any existing SANs, sans marshalling |
| rawValues = append(rawValues, marshalSANs(in.DNSNames, in.EmailAddresses, in.IPAddresses, in.URIs)...) |
| |
| // Marshal and add to ExtraExtensions |
| ext := pkix.Extension{ |
| // This is the defined OID for subjectAltName |
| Id: asn1.ObjectIdentifier(oidExtensionSubjectAltName), |
| } |
| var err error |
| ext.Value, err = asn1.Marshal(rawValues) |
| if err != nil { |
| return err |
| } |
| in.ExtraExtensions = append(in.ExtraExtensions, ext) |
| |
| return nil |
| } |
| |
| // Note: Taken from the Go source code since it's not public, and used in the |
| // modified function below (which also uses these consts upstream) |
| const ( |
| nameTypeOther = 0 |
| nameTypeEmail = 1 |
| nameTypeDNS = 2 |
| nameTypeURI = 6 |
| nameTypeIP = 7 |
| ) |
| |
| // Note: Taken from the Go source code since it's not public, plus changed to not marshal |
| // marshalSANs marshals a list of addresses into the contents of an X.509 |
| // SubjectAlternativeName extension. |
| func marshalSANs(dnsNames, emailAddresses []string, ipAddresses []net.IP, uris []*url.URL) []asn1.RawValue { |
| var rawValues []asn1.RawValue |
| for _, name := range dnsNames { |
| rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeDNS, Class: 2, Bytes: []byte(name)}) |
| } |
| for _, email := range emailAddresses { |
| rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeEmail, Class: 2, Bytes: []byte(email)}) |
| } |
| for _, rawIP := range ipAddresses { |
| // If possible, we always want to encode IPv4 addresses in 4 bytes. |
| ip := rawIP.To4() |
| if ip == nil { |
| ip = rawIP |
| } |
| rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeIP, Class: 2, Bytes: ip}) |
| } |
| for _, uri := range uris { |
| rawValues = append(rawValues, asn1.RawValue{Tag: nameTypeURI, Class: 2, Bytes: []byte(uri.String())}) |
| } |
| return rawValues |
| } |
| |
| func stringToOid(in string) (asn1.ObjectIdentifier, error) { |
| split := strings.Split(in, ".") |
| ret := make(asn1.ObjectIdentifier, 0, len(split)) |
| for _, v := range split { |
| i, err := strconv.Atoi(v) |
| if err != nil { |
| return nil, err |
| } |
| ret = append(ret, i) |
| } |
| return ret, nil |
| } |
| |
| func parseCertificateFromBytes(certBytes []byte) (*x509.Certificate, error) { |
| block, extra := pem.Decode(certBytes) |
| if block == nil { |
| return nil, errors.New("unable to parse certificate: invalid PEM") |
| } |
| if len(strings.TrimSpace(string(extra))) > 0 { |
| return nil, errors.New("unable to parse certificate: trailing PEM data") |
| } |
| |
| return x509.ParseCertificate(block.Bytes) |
| } |