package pki import ( "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "fmt" "reflect" "strings" "time" "golang.org/x/crypto/ed25519" "github.com/hashicorp/vault/sdk/helper/certutil" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/errutil" "github.com/hashicorp/vault/sdk/logical" ) func pathGenerateRoot(b *backend) *framework.Path { return buildPathGenerateRoot(b, "root/generate/"+framework.GenericNameRegex("exported")) } func pathDeleteRoot(b *backend) *framework.Path { ret := &framework.Path{ Pattern: "root", Operations: map[logical.Operation]framework.OperationHandler{ logical.DeleteOperation: &framework.PathOperation{ Callback: b.pathCADeleteRoot, // Read more about why these flags are set in backend.go ForwardPerformanceStandby: true, ForwardPerformanceSecondary: true, }, }, HelpSynopsis: pathDeleteRootHelpSyn, HelpDescription: pathDeleteRootHelpDesc, } return ret } func (b *backend) pathCADeleteRoot(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { // Since we're planning on updating issuers here, grab the lock so we've // got a consistent view. b.issuersLock.Lock() defer b.issuersLock.Unlock() sc := b.makeStorageContext(ctx, req.Storage) if !b.useLegacyBundleCaStorage() { issuers, err := sc.listIssuers() if err != nil { return nil, err } keys, err := sc.listKeys() if err != nil { return nil, err } // Delete all issuers and keys. Ignore deleting the default since we're // explicitly deleting everything. for _, issuer := range issuers { if _, err = sc.deleteIssuer(issuer); err != nil { return nil, err } } for _, key := range keys { if _, err = sc.deleteKey(key); err != nil { return nil, err } } } // Delete legacy CA bundle and its backup, if any. if err := req.Storage.Delete(ctx, legacyCertBundlePath); err != nil { return nil, err } if err := req.Storage.Delete(ctx, legacyCertBundleBackupPath); err != nil { return nil, err } // Delete legacy CRL bundle. if err := req.Storage.Delete(ctx, legacyCRLPath); err != nil { return nil, err } // Return a warning about preferring to delete issuers and keys // explicitly versus deleting everything. resp := &logical.Response{} resp.AddWarning("DELETE /root deletes all keys and issuers; prefer the new DELETE /key/:key_ref and DELETE /issuer/:issuer_ref for finer granularity, unless removal of all keys and issuers is desired.") return resp, nil } func (b *backend) pathCAGenerateRoot(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { // Since we're planning on updating issuers here, grab the lock so we've // got a consistent view. b.issuersLock.Lock() defer b.issuersLock.Unlock() var err error if b.useLegacyBundleCaStorage() { return logical.ErrorResponse("Can not create root CA until migration has completed"), nil } sc := b.makeStorageContext(ctx, req.Storage) exported, format, role, errorResp := getGenerationParams(sc, data) if errorResp != nil { return errorResp, nil } maxPathLengthIface, ok := data.GetOk("max_path_length") if ok { maxPathLength := maxPathLengthIface.(int) role.MaxPathLength = &maxPathLength } issuerName, err := getIssuerName(sc, data) if err != nil { return logical.ErrorResponse(err.Error()), nil } // Handle the aliased path specifying the new issuer name as "next", but // only do it if its not in use. if strings.HasPrefix(req.Path, "root/rotate/") && len(issuerName) == 0 { // err is nil when the issuer name is in use. _, err = sc.resolveIssuerReference("next") if err != nil { issuerName = "next" } } keyName, err := getKeyName(sc, data) if err != nil { return logical.ErrorResponse(err.Error()), nil } input := &inputBundle{ req: req, apiData: data, role: role, } parsedBundle, warnings, err := generateCert(sc, input, nil, true, b.Backend.GetRandomReader()) if err != nil { switch err.(type) { case errutil.UserError: return logical.ErrorResponse(err.Error()), nil default: return nil, err } } cb, err := parsedBundle.ToCertBundle() if err != nil { return nil, fmt.Errorf("error converting raw cert bundle to cert bundle: %w", err) } resp := &logical.Response{ Data: map[string]interface{}{ "expiration": int64(parsedBundle.Certificate.NotAfter.Unix()), "serial_number": cb.SerialNumber, }, } if len(parsedBundle.Certificate.RawSubject) <= 2 { // Strictly a subject is a SEQUENCE of SETs of SEQUENCES. // // The outer SEQUENCE is preserved, having byte value 30 00. // // Because of the tag and the length encoding each taking up // at least one byte, it is impossible to have a non-empty // subject in two or fewer bytes. We're also not here to validate // our certificate's ASN.1 content, so let's just assume it holds // and move on. resp.AddWarning("This issuer certificate was generated without a Subject; this makes it likely that issuing leaf certs with this certificate will cause TLS validation libraries to reject this certificate.") } if len(parsedBundle.Certificate.OCSPServer) == 0 && len(parsedBundle.Certificate.IssuingCertificateURL) == 0 && len(parsedBundle.Certificate.CRLDistributionPoints) == 0 { // If the operator hasn't configured any of the URLs prior to // generating this issuer, we should add a warning to the response, // informing them they might want to do so prior to issuing leaves. resp.AddWarning("This mount hasn't configured any authority information access (AIA) fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls or the newly generated issuer with this information.") } switch format { case "pem": resp.Data["certificate"] = cb.Certificate resp.Data["issuing_ca"] = cb.Certificate if exported { resp.Data["private_key"] = cb.PrivateKey resp.Data["private_key_type"] = cb.PrivateKeyType } case "pem_bundle": resp.Data["issuing_ca"] = cb.Certificate if exported { resp.Data["private_key"] = cb.PrivateKey resp.Data["private_key_type"] = cb.PrivateKeyType resp.Data["certificate"] = fmt.Sprintf("%s\n%s", cb.PrivateKey, cb.Certificate) } else { resp.Data["certificate"] = cb.Certificate } case "der": resp.Data["certificate"] = base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes) resp.Data["issuing_ca"] = base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes) if exported { resp.Data["private_key"] = base64.StdEncoding.EncodeToString(parsedBundle.PrivateKeyBytes) resp.Data["private_key_type"] = cb.PrivateKeyType } default: return nil, fmt.Errorf("unsupported format argument: %s", format) } if data.Get("private_key_format").(string) == "pkcs8" { err = convertRespToPKCS8(resp) if err != nil { return nil, err } } // Store it as the CA bundle myIssuer, myKey, err := sc.writeCaBundle(cb, issuerName, keyName) if err != nil { return nil, err } resp.Data["issuer_id"] = myIssuer.ID resp.Data["issuer_name"] = myIssuer.Name resp.Data["key_id"] = myKey.ID resp.Data["key_name"] = myKey.Name // The one time that it is safe (and good) to copy the // SignatureAlgorithm field off the certificate (for the purposes of // detecting PSS support) is when we've freshly generated it AND it // is a root (exactly this endpoint). // // For intermediates, this doesn't hold (not this endpoint) as that // reflects the parent key's preferences. For imports, this doesn't // hold as the old system might've allowed other signature types that // the new system (whether Vault or a managed key) doesn't. // // Previously we did this conditionally on whether or not PSS was in // use. This is insufficient as some cloud KMS providers (namely, GCP) // restrict the key to a single signature algorithm! So e.g., a RSA 3072 // key MUST use SHA-384 as the hash algorithm. Thus we pull in the // RevocationSigAlg unconditionally on roots now. myIssuer.RevocationSigAlg = parsedBundle.Certificate.SignatureAlgorithm if err := sc.writeIssuer(myIssuer); err != nil { return nil, fmt.Errorf("unable to store PSS-updated issuer: %w", err) } // Also store it as just the certificate identified by serial number, so it // can be revoked key := "certs/" + normalizeSerial(cb.SerialNumber) certsCounted := b.certsCounted.Load() err = req.Storage.Put(ctx, &logical.StorageEntry{ Key: key, Value: parsedBundle.CertificateBytes, }) if err != nil { return nil, fmt.Errorf("unable to store certificate locally: %w", err) } b.ifCountEnabledIncrementTotalCertificatesCount(certsCounted, key) // Build a fresh CRL err = b.crlBuilder.rebuild(sc, true) if err != nil { return nil, err } if parsedBundle.Certificate.MaxPathLen == 0 { resp.AddWarning("Max path length of the generated certificate is zero. This certificate cannot be used to issue intermediate CA certificates.") } // Check whether we need to update our default issuer configuration. config, err := sc.getIssuersConfig() if err != nil { resp.AddWarning("Unable to fetch default issuers configuration to update default issuer if necessary: " + err.Error()) } else if config.DefaultFollowsLatestIssuer { if err := sc.updateDefaultIssuerId(myIssuer.ID); err != nil { resp.AddWarning("Unable to update this new root as the default issuer: " + err.Error()) } } resp = addWarnings(resp, warnings) return resp, nil } func (b *backend) pathIssuerSignIntermediate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { var err error issuerName := getIssuerRef(data) if len(issuerName) == 0 { return logical.ErrorResponse("missing issuer reference"), nil } format := getFormat(data) if format == "" { return logical.ErrorResponse( `The "format" path parameter must be "pem" or "der"`, ), nil } role := &roleEntry{ OU: data.Get("ou").([]string), Organization: data.Get("organization").([]string), Country: data.Get("country").([]string), Locality: data.Get("locality").([]string), Province: data.Get("province").([]string), StreetAddress: data.Get("street_address").([]string), PostalCode: data.Get("postal_code").([]string), TTL: time.Duration(data.Get("ttl").(int)) * time.Second, AllowLocalhost: true, AllowAnyName: true, AllowIPSANs: true, AllowWildcardCertificates: new(bool), EnforceHostnames: false, KeyType: "any", SignatureBits: data.Get("signature_bits").(int), UsePSS: data.Get("use_pss").(bool), AllowedOtherSANs: []string{"*"}, AllowedSerialNumbers: []string{"*"}, AllowedURISANs: []string{"*"}, NotAfter: data.Get("not_after").(string), NotBeforeDuration: time.Duration(data.Get("not_before_duration").(int)) * time.Second, CNValidations: []string{"disabled"}, } *role.AllowWildcardCertificates = true if cn := data.Get("common_name").(string); len(cn) == 0 { role.UseCSRCommonName = true } var caErr error sc := b.makeStorageContext(ctx, req.Storage) signingBundle, caErr := sc.fetchCAInfo(issuerName, IssuanceUsage) if caErr != nil { switch caErr.(type) { case errutil.UserError: return nil, errutil.UserError{Err: fmt.Sprintf( "could not fetch the CA certificate (was one set?): %s", caErr)} default: return nil, errutil.InternalError{Err: fmt.Sprintf( "error fetching CA certificate: %s", caErr)} } } // Since we are signing an intermediate, we explicitly want to override // the leaf NotAfterBehavior to permit issuing intermediates longer than // the life of this issuer. signingBundle.LeafNotAfterBehavior = certutil.PermitNotAfterBehavior useCSRValues := data.Get("use_csr_values").(bool) maxPathLengthIface, ok := data.GetOk("max_path_length") if ok { maxPathLength := maxPathLengthIface.(int) role.MaxPathLength = &maxPathLength } input := &inputBundle{ req: req, apiData: data, role: role, } parsedBundle, warnings, err := signCert(b, input, signingBundle, true, useCSRValues) if err != nil { switch err.(type) { case errutil.UserError: return logical.ErrorResponse(err.Error()), nil default: return nil, errutil.InternalError{Err: fmt.Sprintf( "error signing cert: %s", err)} } } if err := parsedBundle.Verify(); err != nil { return nil, fmt.Errorf("verification of parsed bundle failed: %w", err) } signingCB, err := signingBundle.ToCertBundle() if err != nil { return nil, fmt.Errorf("error converting raw signing bundle to cert bundle: %w", err) } cb, err := parsedBundle.ToCertBundle() if err != nil { return nil, fmt.Errorf("error converting raw cert bundle to cert bundle: %w", err) } resp := &logical.Response{ Data: map[string]interface{}{ "expiration": int64(parsedBundle.Certificate.NotAfter.Unix()), "serial_number": cb.SerialNumber, }, } if signingBundle.Certificate.NotAfter.Before(parsedBundle.Certificate.NotAfter) { resp.AddWarning("The expiration time for the signed certificate is after the CA's expiration time. If the new certificate is not treated as a root, validation paths with the certificate past the issuing CA's expiration time will fail.") } if len(parsedBundle.Certificate.RawSubject) <= 2 { // Strictly a subject is a SEQUENCE of SETs of SEQUENCES. // // The outer SEQUENCE is preserved, having byte value 30 00. // // Because of the tag and the length encoding each taking up // at least one byte, it is impossible to have a non-empty // subject in two or fewer bytes. We're also not here to validate // our certificate's ASN.1 content, so let's just assume it holds // and move on. resp.AddWarning("This issuer certificate was generated without a Subject; this makes it likely that issuing leaf certs with this certificate will cause TLS validation libraries to reject this certificate.") } if len(parsedBundle.Certificate.OCSPServer) == 0 && len(parsedBundle.Certificate.IssuingCertificateURL) == 0 && len(parsedBundle.Certificate.CRLDistributionPoints) == 0 { // If the operator hasn't configured any of the URLs prior to // generating this issuer, we should add a warning to the response, // informing them they might want to do so prior to issuing leaves. resp.AddWarning("This mount hasn't configured any authority information access (AIA) fields; this may make it harder for systems to find missing certificates in the chain or to validate revocation status of certificates. Consider updating /config/urls or the newly generated issuer with this information.") } caChain := append([]string{cb.Certificate}, cb.CAChain...) switch format { case "pem": resp.Data["certificate"] = cb.Certificate resp.Data["issuing_ca"] = signingCB.Certificate resp.Data["ca_chain"] = caChain case "pem_bundle": resp.Data["certificate"] = cb.ToPEMBundle() resp.Data["issuing_ca"] = signingCB.Certificate resp.Data["ca_chain"] = caChain case "der": resp.Data["certificate"] = base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes) resp.Data["issuing_ca"] = base64.StdEncoding.EncodeToString(signingBundle.CertificateBytes) var derCaChain []string derCaChain = append(derCaChain, base64.StdEncoding.EncodeToString(parsedBundle.CertificateBytes)) for _, caCert := range parsedBundle.CAChain { derCaChain = append(derCaChain, base64.StdEncoding.EncodeToString(caCert.Bytes)) } resp.Data["ca_chain"] = derCaChain default: return nil, fmt.Errorf("unsupported format argument: %s", format) } key := "certs/" + normalizeSerial(cb.SerialNumber) certsCounted := b.certsCounted.Load() err = req.Storage.Put(ctx, &logical.StorageEntry{ Key: key, Value: parsedBundle.CertificateBytes, }) if err != nil { return nil, fmt.Errorf("unable to store certificate locally: %w", err) } b.ifCountEnabledIncrementTotalCertificatesCount(certsCounted, key) if parsedBundle.Certificate.MaxPathLen == 0 { resp.AddWarning("Max path length of the signed certificate is zero. This certificate cannot be used to issue intermediate CA certificates.") } resp = addWarnings(resp, warnings) return resp, nil } func (b *backend) pathIssuerSignSelfIssued(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { var err error issuerName := getIssuerRef(data) if len(issuerName) == 0 { return logical.ErrorResponse("missing issuer reference"), nil } certPem := data.Get("certificate").(string) block, _ := pem.Decode([]byte(certPem)) if block == nil || len(block.Bytes) == 0 { return logical.ErrorResponse("certificate could not be PEM-decoded"), nil } certs, err := x509.ParseCertificates(block.Bytes) if err != nil { return logical.ErrorResponse(fmt.Sprintf("error parsing certificate: %s", err)), nil } if len(certs) != 1 { return logical.ErrorResponse(fmt.Sprintf("%d certificates found in PEM file, expected 1", len(certs))), nil } cert := certs[0] if !cert.IsCA { return logical.ErrorResponse("given certificate is not a CA certificate"), nil } if !reflect.DeepEqual(cert.Issuer, cert.Subject) { return logical.ErrorResponse("given certificate is not self-issued"), nil } var caErr error sc := b.makeStorageContext(ctx, req.Storage) signingBundle, caErr := sc.fetchCAInfo(issuerName, IssuanceUsage) if caErr != nil { switch caErr.(type) { case errutil.UserError: return nil, errutil.UserError{Err: fmt.Sprintf( "could not fetch the CA certificate (was one set?): %s", caErr)} default: return nil, errutil.InternalError{Err: fmt.Sprintf("error fetching CA certificate: %s", caErr)} } } signingCB, err := signingBundle.ToCertBundle() if err != nil { return nil, fmt.Errorf("error converting raw signing bundle to cert bundle: %w", err) } urls := &certutil.URLEntries{} if signingBundle.URLs != nil { urls = signingBundle.URLs } cert.IssuingCertificateURL = urls.IssuingCertificates cert.CRLDistributionPoints = urls.CRLDistributionPoints cert.OCSPServer = urls.OCSPServers // If the requested signature algorithm isn't the same as the signing certificate, and // the user has requested a cross-algorithm signature, reset the template's signing algorithm // to that of the signing key signingPubType, signingAlgorithm, err := publicKeyType(signingBundle.Certificate.PublicKey) if err != nil { return nil, fmt.Errorf("error determining signing certificate algorithm type: %e", err) } certPubType, _, err := publicKeyType(cert.PublicKey) if err != nil { return nil, fmt.Errorf("error determining template algorithm type: %e", err) } if signingPubType != certPubType { b, ok := data.GetOk("require_matching_certificate_algorithms") if !ok || !b.(bool) { cert.SignatureAlgorithm = signingAlgorithm } else { return nil, fmt.Errorf("signing certificate's public key algorithm (%s) does not match submitted certificate's (%s), and require_matching_certificate_algorithms is true", signingPubType.String(), certPubType.String()) } } newCert, err := x509.CreateCertificate(rand.Reader, cert, signingBundle.Certificate, cert.PublicKey, signingBundle.PrivateKey) if err != nil { return nil, fmt.Errorf("error signing self-issued certificate: %w", err) } if len(newCert) == 0 { return nil, fmt.Errorf("nil cert was created when signing self-issued certificate") } pemCert := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: newCert, }) return &logical.Response{ Data: map[string]interface{}{ "certificate": strings.TrimSpace(string(pemCert)), "issuing_ca": signingCB.Certificate, }, }, nil } // Adapted from similar code in https://github.com/golang/go/blob/4a4221e8187189adcc6463d2d96fe2e8da290132/src/crypto/x509/x509.go#L1342, // may need to be updated in the future. func publicKeyType(pub crypto.PublicKey) (pubType x509.PublicKeyAlgorithm, sigAlgo x509.SignatureAlgorithm, err error) { switch pub := pub.(type) { case *rsa.PublicKey: pubType = x509.RSA sigAlgo = x509.SHA256WithRSA case *ecdsa.PublicKey: pubType = x509.ECDSA switch pub.Curve { case elliptic.P224(), elliptic.P256(): sigAlgo = x509.ECDSAWithSHA256 case elliptic.P384(): sigAlgo = x509.ECDSAWithSHA384 case elliptic.P521(): sigAlgo = x509.ECDSAWithSHA512 default: err = errors.New("x509: unknown elliptic curve") } case ed25519.PublicKey: pubType = x509.Ed25519 sigAlgo = x509.PureEd25519 default: err = errors.New("x509: only RSA, ECDSA and Ed25519 keys supported") } return } const pathGenerateRootHelpSyn = ` Generate a new CA certificate and private key used for signing. ` const pathGenerateRootHelpDesc = ` See the API documentation for more information. ` const pathDeleteRootHelpSyn = ` Deletes the root CA key to allow a new one to be generated. ` const pathDeleteRootHelpDesc = ` See the API documentation for more information. `