// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1

package pki

import (
	"context"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/hashicorp/vault/builtin/logical/pki/issuing"
	"github.com/hashicorp/vault/builtin/logical/pki/observe"
	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/helper/certutil"
	"github.com/hashicorp/vault/sdk/logical"
)

func pathListIssuers(b *backend) *framework.Path {
	return &framework.Path{
		Pattern: "issuers/?$",

		DisplayAttrs: &framework.DisplayAttributes{
			OperationPrefix: operationPrefixPKI,
			OperationSuffix: "issuers",
		},

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.ListOperation: &framework.PathOperation{
				Callback: b.pathListIssuersHandler,
				Responses: map[int][]framework.Response{
					http.StatusOK: {{
						Description: "OK",
						Fields: map[string]*framework.FieldSchema{
							"keys": {
								Type:        framework.TypeStringSlice,
								Description: `A list of keys`,
								Required:    true,
							},
							"key_info": {
								Type:        framework.TypeMap,
								Description: `Key info with issuer name`,
								Required:    false,
							},
						},
					}},
				},
			},
		},

		HelpSynopsis:    pathListIssuersHelpSyn,
		HelpDescription: pathListIssuersHelpDesc,
	}
}

func (b *backend) pathListIssuersHandler(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
	if b.UseLegacyBundleCaStorage() {
		return logical.ErrorResponse("Can not list issuers until migration has completed"), nil
	}

	var responseKeys []string
	responseInfo := make(map[string]interface{})

	sc := b.makeStorageContext(ctx, req.Storage)
	entries, err := sc.listIssuers()
	if err != nil {
		return nil, err
	}

	config, err := sc.getIssuersConfig()
	if err != nil {
		return nil, err
	}

	// For each issuer, we need not only the identifier (as returned by
	// listIssuers), but also the name of the issuer. This means we have to
	// fetch the actual issuer object as well.
	for _, identifier := range entries {
		issuer, err := sc.fetchIssuerById(identifier)
		if err != nil {
			return nil, err
		}

		responseKeys = append(responseKeys, string(identifier))
		responseInfo[string(identifier)] = map[string]interface{}{
			"issuer_name":   issuer.Name,
			"is_default":    identifier == config.DefaultIssuerId,
			"serial_number": issuer.SerialNumber,

			// While nominally this could be considered sensitive information
			// to be returned on an unauthed endpoint, there's two mitigating
			// circumstances:
			//
			// 1. Key IDs are purely random numbers generated by Vault and
			//    have no relationship to the actual key material.
			// 2. They also don't _do_ anything by themselves. There is no
			//    modification of KeyIDs allowed, you need to be authenticated
			//    to Vault to understand what they mean, you _essentially_
			//    get the same information from looking at/comparing various
			//    cert's SubjectPublicKeyInfo field, and there's the `default`
			//    reference that anyone with issuer generation capabilities
			//    can use even if they can't access any of the other /key/*
			//    endpoints.
			//
			// So all in all, exposing this value is not a security risk and
			// is otherwise beneficial for the UI, hence its inclusion.
			"key_id": issuer.KeyID,
		}
	}

	return logical.ListResponseWithInfo(responseKeys, responseInfo), nil
}

const (
	pathListIssuersHelpSyn  = `Fetch a list of CA certificates.`
	pathListIssuersHelpDesc = `
This endpoint allows listing of known issuing certificates, returning
their identifier and their name (if set).
`
)

func pathGetIssuer(b *backend) *framework.Path {
	pattern := "issuer/" + framework.GenericNameRegex(issuerRefParam) + "$"

	displayAttrs := &framework.DisplayAttributes{
		OperationPrefix: operationPrefixPKI,
		OperationSuffix: "issuer",
	}

	return buildPathIssuer(b, pattern, displayAttrs)
}

func pathGetUnauthedIssuer(b *backend) *framework.Path {
	pattern := "issuer/" + framework.GenericNameRegex(issuerRefParam) + "/(json|der|pem)$"

	displayAttrs := &framework.DisplayAttributes{
		OperationPrefix: operationPrefixPKI,
		OperationSuffix: "issuer-json|issuer-der|issuer-pem",
	}

	return buildPathGetIssuer(b, pattern, displayAttrs)
}

func buildPathIssuer(b *backend, pattern string, displayAttrs *framework.DisplayAttributes) *framework.Path {
	fields := map[string]*framework.FieldSchema{}
	fields = addIssuerRefNameFields(fields)

	// Fields for updating issuer.
	fields["manual_chain"] = &framework.FieldSchema{
		Type: framework.TypeCommaStringSlice,
		Description: `Chain of issuer references to use to build this
issuer's computed CAChain field, when non-empty.`,
	}
	fields["leaf_not_after_behavior"] = &framework.FieldSchema{
		Type: framework.TypeString,
		Description: `Behavior of leaf's NotAfter fields: "err" to error
if the computed NotAfter date exceeds that of this issuer; "truncate" to
silently truncate to that of this issuer; or "permit" to allow this
issuance to succeed (with NotAfter exceeding that of an issuer). Note that
not all values will results in certificates that can be validated through
the entire validity period. It is suggested to use "truncate" for
intermediate CAs and "permit" only for root CAs.`,
		Default: "err",
	}
	fields["usage"] = &framework.FieldSchema{
		Type: framework.TypeCommaStringSlice,
		Description: `Comma-separated list (or string slice) of usages for
this issuer; valid values are "read-only", "issuing-certificates",
"crl-signing", and "ocsp-signing". Multiple values may be specified. Read-only
is implicit and always set.`,
		Default: []string{"read-only", "issuing-certificates", "crl-signing", "ocsp-signing"},
	}
	fields["revocation_signature_algorithm"] = &framework.FieldSchema{
		Type: framework.TypeString,
		Description: `Which x509.SignatureAlgorithm name to use for
signing CRLs. This parameter allows differentiation between PKCS#1v1.5
and PSS keys and choice of signature hash algorithm. The default (empty
string) value is for Go to select the signature algorithm. This can fail
if the underlying key does not support the requested signature algorithm,
which may not be known at modification time (such as with PKCS#11 managed
RSA keys).`,
		Default: "",
	}
	fields["issuing_certificates"] = &framework.FieldSchema{
		Type: framework.TypeCommaStringSlice,
		Description: `Comma-separated list of URLs to be used
for the issuing certificate attribute. See also RFC 5280 Section 4.2.2.1.`,
	}
	fields["crl_distribution_points"] = &framework.FieldSchema{
		Type: framework.TypeCommaStringSlice,
		Description: `Comma-separated list of URLs to be used
for the CRL distribution points attribute. See also RFC 5280 Section 4.2.1.13.`,
	}
	fields["delta_crl_distribution_points"] = &framework.FieldSchema{
		Type: framework.TypeCommaStringSlice,
		Description: `Comma-separated list of URLs to be used
for the Delta CRL distribution points attribute, also known as Freshest CRL
distribution points attribute. See also RFC 5280 Section 4.2.1.15.`,
	}
	fields["ocsp_servers"] = &framework.FieldSchema{
		Type: framework.TypeCommaStringSlice,
		Description: `Comma-separated list of URLs to be used
for the OCSP servers attribute. See also RFC 5280 Section 4.2.2.1.`,
	}
	fields["enable_aia_url_templating"] = &framework.FieldSchema{
		Type: framework.TypeBool,
		Description: `Whether or not to enabling templating of the
above AIA fields. When templating is enabled the special values '{{issuer_id}}',
'{{cluster_path}}', '{{cluster_aia_path}}' are available, but the addresses are
not checked for URL validity until issuance time. Using '{{cluster_path}}'
requires /config/cluster's 'path' member to be set on all PR Secondary clusters
and using '{{cluster_aia_path}}' requires /config/cluster's 'aia_path' member
to be set on all PR secondary clusters.`,
		Default: false,
	}
	addEntPathIssuerFields(fields)
	updateIssuerFields := issuerResponseFields(false)

	updateIssuerSchema := map[int][]framework.Response{
		http.StatusOK: {{
			Description: "OK",
			Fields:      updateIssuerFields,
		}},
	}

	return &framework.Path{
		// Returns a JSON entry.
		Pattern:      pattern,
		DisplayAttrs: displayAttrs,
		Fields:       fields,

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.ReadOperation: &framework.PathOperation{
				Callback:  b.pathGetIssuer,
				Responses: updateIssuerSchema,
			},
			logical.UpdateOperation: &framework.PathOperation{
				Callback:  b.pathUpdateIssuer,
				Responses: updateIssuerSchema,

				// Read more about why these flags are set in backend.go.
				ForwardPerformanceStandby:   true,
				ForwardPerformanceSecondary: true,
			},
			logical.DeleteOperation: &framework.PathOperation{
				Callback: b.pathDeleteIssuer,
				Responses: map[int][]framework.Response{
					http.StatusNoContent: {{
						Description: "No Content",
					}},
				},
				// Read more about why these flags are set in backend.go.
				ForwardPerformanceStandby:   true,
				ForwardPerformanceSecondary: true,
			},
			logical.PatchOperation: &framework.PathOperation{
				Callback:  b.pathPatchIssuer,
				Responses: updateIssuerSchema,
				// Read more about why these flags are set in backend.go.
				ForwardPerformanceStandby:   true,
				ForwardPerformanceSecondary: true,
			},
		},

		HelpSynopsis:    pathGetIssuerHelpSyn,
		HelpDescription: pathGetIssuerHelpDesc,
	}
}

func issuerResponseFields(required bool) map[string]*framework.FieldSchema {
	fields := map[string]*framework.FieldSchema{
		"issuer_id": {
			Type:        framework.TypeString,
			Description: `Issuer Id`,
			Required:    required,
		},
		"issuer_name": {
			Type:        framework.TypeString,
			Description: `Issuer Name`,
			Required:    required,
		},
		"key_id": {
			Type:        framework.TypeString,
			Description: `Key Id`,
			Required:    required,
		},
		"certificate": {
			Type:        framework.TypeString,
			Description: `Certificate`,
			Required:    required,
		},
		"manual_chain": {
			Type:        framework.TypeStringSlice,
			Description: `Manual Chain`,
			Required:    required,
		},
		"ca_chain": {
			Type:        framework.TypeStringSlice,
			Description: `CA Chain`,
			Required:    required,
		},
		"leaf_not_after_behavior": {
			Type:        framework.TypeString,
			Description: `Leaf Not After Behavior`,
			Required:    required,
		},
		"usage": {
			Type:        framework.TypeString,
			Description: `Usage`,
			Required:    required,
		},
		"revocation_signature_algorithm": {
			Type:        framework.TypeString,
			Description: `Revocation Signature Alogrithm`,
			Required:    required,
		},
		"revoked": {
			Type:        framework.TypeBool,
			Description: `Revoked`,
			Required:    required,
		},
		"revocation_time": {
			Type:        framework.TypeInt,
			Description: `Revocation time`,
			Required:    required,
		},
		"revocation_time_rfc3339": {
			Type:        framework.TypeString,
			Description: `Revocation time RFC 3339 formatted`,
			Required:    required,
		},
		"issuing_certificates": {
			Type:        framework.TypeStringSlice,
			Description: `Issuing Certificates`,
			Required:    required,
		},
		"crl_distribution_points": {
			Type:        framework.TypeStringSlice,
			Description: `CRL Distribution Points`,
			Required:    required,
		},
		"delta_crl_distribution_points": {
			Type:        framework.TypeStringSlice,
			Description: `Delta CRL Distribution Points`,
			Required:    required,
		},
		"ocsp_servers": {
			Type:        framework.TypeStringSlice,
			Description: `OCSP Servers`,
			Required:    required,
		},
		"enable_aia_url_templating": {
			Type:        framework.TypeBool,
			Description: `Whether or not templating is enabled for AIA fields`,
			Required:    required,
		},
	}

	addEntPathIssuerResponseFields(fields)

	return fields
}

func buildPathGetIssuer(b *backend, pattern string, displayAttrs *framework.DisplayAttributes) *framework.Path {
	fields := map[string]*framework.FieldSchema{}
	fields = addIssuerRefField(fields)

	getIssuerSchema := map[int][]framework.Response{
		http.StatusNotModified: {{
			Description: "Not Modified",
		}},
		http.StatusOK: {{
			Description: "OK",
			Fields: map[string]*framework.FieldSchema{
				"issuer_id": {
					Type:        framework.TypeString,
					Description: `Issuer Id`,
					Required:    true,
				},
				"issuer_name": {
					Type:        framework.TypeString,
					Description: `Issuer Name`,
					Required:    true,
				},
				"certificate": {
					Type:        framework.TypeString,
					Description: `Certificate`,
					Required:    true,
				},
				"ca_chain": {
					Type:        framework.TypeStringSlice,
					Description: `CA Chain`,
					Required:    true,
				},
			},
		}},
	}

	return &framework.Path{
		// Returns a JSON entry.
		Pattern:      pattern,
		DisplayAttrs: displayAttrs,
		Fields:       fields,

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.ReadOperation: &framework.PathOperation{
				Callback:  b.pathGetIssuer,
				Responses: getIssuerSchema,
			},
		},

		HelpSynopsis:    pathGetIssuerHelpSyn,
		HelpDescription: pathGetIssuerHelpDesc,
	}
}

func (b *backend) pathGetIssuer(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
	// Handle raw issuers first.
	if strings.HasSuffix(req.Path, "/der") || strings.HasSuffix(req.Path, "/pem") || strings.HasSuffix(req.Path, "/json") {
		return b.pathGetRawIssuer(ctx, req, data)
	}

	if b.UseLegacyBundleCaStorage() {
		return logical.ErrorResponse("Can not get issuer until migration has completed"), nil
	}

	issuerName := GetIssuerRef(data)
	if len(issuerName) == 0 {
		return logical.ErrorResponse("missing issuer reference"), nil
	}

	sc := b.makeStorageContext(ctx, req.Storage)
	ref, err := sc.resolveIssuerReference(issuerName)
	if err != nil {
		return nil, err
	}
	if ref == "" {
		return logical.ErrorResponse("unable to resolve issuer id for reference: " + issuerName), nil
	}

	issuer, err := sc.fetchIssuerById(ref)
	if err != nil {
		return nil, err
	}

	b.pkiObserver.RecordPKIObservation(ctx, req, observe.ObservationTypePKIIssuerRead,
		observe.NewAdditionalPKIMetadata("issuer_name", issuer.Name),
		observe.NewAdditionalPKIMetadata("issuer_id", issuer.ID),
		observe.NewAdditionalPKIMetadata("last_modified", issuer.LastModified.String()),
		observe.NewAdditionalPKIMetadata("revoked", issuer.Revoked))

	return respondReadIssuer(issuer)
}

func respondReadIssuer(issuer *issuing.IssuerEntry) (*logical.Response, error) {
	var respManualChain []string
	for _, entity := range issuer.ManualChain {
		respManualChain = append(respManualChain, string(entity))
	}

	revSigAlgStr, present := certutil.InvSignatureAlgorithmNames[issuer.RevocationSigAlg]
	if !present {
		revSigAlgStr = issuer.RevocationSigAlg.String()
		if issuer.RevocationSigAlg == x509.UnknownSignatureAlgorithm {
			revSigAlgStr = ""
		}
	}

	data := map[string]interface{}{
		"issuer_id":                      issuer.ID,
		"issuer_name":                    issuer.Name,
		"key_id":                         issuer.KeyID,
		"certificate":                    issuer.Certificate,
		"manual_chain":                   respManualChain,
		"ca_chain":                       issuer.CAChain,
		"leaf_not_after_behavior":        issuer.LeafNotAfterBehavior.String(),
		"usage":                          issuer.Usage.Names(),
		"revocation_signature_algorithm": revSigAlgStr,
		"revoked":                        issuer.Revoked,
		"issuing_certificates":           []string{},
		"crl_distribution_points":        []string{},
		"delta_crl_distribution_points":  []string{},
		"ocsp_servers":                   []string{},
	}
	setEntIssuerData(data, issuer)

	if issuer.Revoked {
		data["revocation_time"] = issuer.RevocationTime
		data["revocation_time_rfc3339"] = issuer.RevocationTimeUTC.Format(time.RFC3339Nano)
	}

	if issuer.AIAURIs != nil {
		data["issuing_certificates"] = issuer.AIAURIs.IssuingCertificates
		data["crl_distribution_points"] = issuer.AIAURIs.CRLDistributionPoints
		data["delta_crl_distribution_points"] = issuer.AIAURIs.DeltaCRLDistributionPoints
		data["ocsp_servers"] = issuer.AIAURIs.OCSPServers
		data["enable_aia_url_templating"] = issuer.AIAURIs.EnableTemplating
	} else {
		data["enable_aia_url_templating"] = false
	}

	response := &logical.Response{
		Data: data,
	}

	if issuer.RevocationSigAlg == x509.SHA256WithRSAPSS || issuer.RevocationSigAlg == x509.SHA384WithRSAPSS || issuer.RevocationSigAlg == x509.SHA512WithRSAPSS {
		response.AddWarning("Issuer uses a PSS Revocation Signature Algorithm. This algorithm will be downgraded to PKCS#1v1.5 signature scheme on OCSP responses, due to limitations in the OCSP library.")
	}

	return response, nil
}

func (b *backend) pathUpdateIssuer(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()

	if b.UseLegacyBundleCaStorage() {
		return logical.ErrorResponse("Can not update issuer until migration has completed"), nil
	}

	issuerName := GetIssuerRef(data)
	if len(issuerName) == 0 {
		return logical.ErrorResponse("missing issuer reference"), nil
	}

	sc := b.makeStorageContext(ctx, req.Storage)
	ref, err := sc.resolveIssuerReference(issuerName)
	if err != nil {
		return nil, err
	}
	if ref == "" {
		return logical.ErrorResponse("unable to resolve issuer id for reference: " + issuerName), nil
	}

	issuer, err := sc.fetchIssuerById(ref)
	if err != nil {
		return nil, err
	}

	newName, err := getIssuerName(sc, data)
	if err != nil && err != errIssuerNameInUse {
		// If the error is name already in use, and the new name is the
		// old name for this issuer, we're not actually updating the
		// issuer name (or causing a conflict) -- so don't err out. Other
		// errs should still be surfaced, however.
		return logical.ErrorResponse(err.Error()), nil
	}
	if err == errIssuerNameInUse && issuer.Name != newName {
		// When the new name is in use but isn't this name, throw an error.
		return logical.ErrorResponse(err.Error()), nil
	}
	if len(newName) > 0 && !nameMatcher.MatchString(newName) {
		return logical.ErrorResponse("new key name outside of valid character limits"), nil
	}

	newPath := data.Get("manual_chain").([]string)
	rawLeafBehavior := data.Get("leaf_not_after_behavior").(string)
	var newLeafBehavior certutil.NotAfterBehavior
	switch rawLeafBehavior {
	case "err":
		newLeafBehavior = certutil.ErrNotAfterBehavior
	case "always_enforce_err":
		newLeafBehavior = certutil.AlwaysEnforceErr
	case "truncate":
		newLeafBehavior = certutil.TruncateNotAfterBehavior
	case "permit":
		newLeafBehavior = certutil.PermitNotAfterBehavior
	default:
		return logical.ErrorResponse("Unknown value for field `leaf_not_after_behavior`. Possible values are `err`, `truncate`, and `permit`."), nil
	}

	rawUsage := data.Get("usage").([]string)
	newUsage, err := issuing.NewIssuerUsageFromNames(rawUsage)
	if err != nil {
		return logical.ErrorResponse(fmt.Sprintf("Unable to parse specified usages: %v - valid values are %v", rawUsage, issuing.AllIssuerUsages.Names())), nil
	}

	// Revocation signature algorithm changes
	revSigAlgStr := data.Get("revocation_signature_algorithm").(string)
	revSigAlg, present := certutil.SignatureAlgorithmNames[strings.ToLower(revSigAlgStr)]
	if !present && revSigAlgStr != "" {
		var knownAlgos []string
		for algoName := range certutil.SignatureAlgorithmNames {
			knownAlgos = append(knownAlgos, algoName)
		}

		return logical.ErrorResponse(fmt.Sprintf("Unknown signature algorithm value: %v - valid values are %v", revSigAlg, strings.Join(knownAlgos, ", "))), nil
	} else if revSigAlgStr == "" {
		revSigAlg = x509.UnknownSignatureAlgorithm
	}
	if err := issuer.CanMaybeSignWithAlgo(revSigAlg); err != nil {
		return nil, err
	}

	// AIA access changes
	enableTemplating := data.Get("enable_aia_url_templating").(bool)
	issuerCertificates := data.Get("issuing_certificates").([]string)
	if badURL := issuing.ValidateURLs(issuerCertificates); !enableTemplating && badURL != "" {
		return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter issuing_certificates: %s", badURL)), nil
	}
	crlDistributionPoints := data.Get("crl_distribution_points").([]string)
	if badURL := issuing.ValidateURLs(crlDistributionPoints); !enableTemplating && badURL != "" {
		return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter crl_distribution_points: %s", badURL)), nil
	}
	deltaCRLDistributionPoints := data.Get("delta_crl_distribution_points").([]string)
	if badURL := issuing.ValidateURLs(deltaCRLDistributionPoints); !enableTemplating && badURL != "" {
		return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter delta_crl_distribution_points: %s", badURL)), nil
	}
	ocspServers := data.Get("ocsp_servers").([]string)
	if badURL := issuing.ValidateURLs(ocspServers); !enableTemplating && badURL != "" {
		return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter ocsp_servers: %s", badURL)), nil
	}

	modified := false

	var oldName string
	if newName != issuer.Name {
		oldName = issuer.Name
		issuer.Name = newName
		issuer.LastModified = time.Now().UTC()
		// See note in updateDefaultIssuerId about why this is necessary.
		b.CrlBuilder().invalidateCRLBuildTime()
		b.CrlBuilder().flushCRLBuildTimeInvalidation(sc)
		modified = true
	}

	if newLeafBehavior != issuer.LeafNotAfterBehavior {
		issuer.LeafNotAfterBehavior = newLeafBehavior
		modified = true
	}

	if newUsage != issuer.Usage {
		if issuer.Revoked && newUsage.HasUsage(issuing.IssuanceUsage) {
			// Forbid allowing cert signing on its usage.
			return logical.ErrorResponse("This issuer was revoked; unable to modify its usage to include certificate signing again. Reissue this certificate (preferably with a new key) and modify that entry instead."), nil
		}

		// Ensure we deny adding CRL usage if the bits are missing from the
		// cert itself.
		cert, err := issuer.GetCertificate()
		if err != nil {
			return nil, fmt.Errorf("unable to parse issuer's certificate: %w", err)
		}
		if (cert.KeyUsage&x509.KeyUsageCRLSign) == 0 && newUsage.HasUsage(issuing.CRLSigningUsage) {
			return logical.ErrorResponse("This issuer's underlying certificate lacks the CRLSign KeyUsage value; unable to set CRLSigningUsage on this issuer as a result."), nil
		}

		issuer.Usage = newUsage
		modified = true
	}

	if revSigAlg != issuer.RevocationSigAlg {
		issuer.RevocationSigAlg = revSigAlg
		modified = true
	}

	if issuer.AIAURIs == nil && (len(issuerCertificates) > 0 || len(crlDistributionPoints) > 0 || len(ocspServers) > 0 || len(deltaCRLDistributionPoints) > 0) {
		issuer.AIAURIs = &issuing.AiaConfigEntry{}
	}
	if issuer.AIAURIs != nil {
		// Associative mapping from data source to destination on the
		// backing issuer object.
		type aiaPair struct {
			Source *[]string
			Dest   *[]string
		}
		pairs := []aiaPair{
			{
				Source: &issuerCertificates,
				Dest:   &issuer.AIAURIs.IssuingCertificates,
			},
			{
				Source: &crlDistributionPoints,
				Dest:   &issuer.AIAURIs.CRLDistributionPoints,
			},
			{
				Source: &ocspServers,
				Dest:   &issuer.AIAURIs.OCSPServers,
			},
			{
				Source: &deltaCRLDistributionPoints,
				Dest:   &issuer.AIAURIs.DeltaCRLDistributionPoints,
			},
		}

		// For each pair, if it is different on the object, update it.
		for _, pair := range pairs {
			if isStringArrayDifferent(*pair.Source, *pair.Dest) {
				*pair.Dest = *pair.Source
				modified = true
			}
		}
		if enableTemplating != issuer.AIAURIs.EnableTemplating {
			issuer.AIAURIs.EnableTemplating = enableTemplating
			modified = true
		}

		// If no AIA URLs exist on the issuer, set the AIA URLs entry to nil
		// to ease usage later.
		if len(issuer.AIAURIs.IssuingCertificates) == 0 && len(issuer.AIAURIs.CRLDistributionPoints) == 0 && len(issuer.AIAURIs.OCSPServers) == 0 && len(issuer.AIAURIs.DeltaCRLDistributionPoints) == 0 {
			issuer.AIAURIs = nil
		}
	}

	var updatedIssuanceValidations bool
	if updateEntIssuerFields(issuer, data, false) {
		modified = true
		updatedIssuanceValidations = true
	}

	// Updating the chain should be the last modification as there's a chance
	// it'll write it out to disk for us. We'd hate to then modify the issuer
	// again and write it a second time.
	var updateChain bool
	var constructedChain []issuing.IssuerID
	for index, newPathRef := range newPath {
		// Allow self for the first entry.
		if index == 0 && newPathRef == "self" {
			newPathRef = string(ref)
		}

		resolvedId, err := sc.resolveIssuerReference(newPathRef)
		if err != nil {
			return nil, err
		}

		if index == 0 && resolvedId != ref {
			return logical.ErrorResponse(fmt.Sprintf("expected first cert in chain to be a self-reference, but was: %v/%v", newPathRef, resolvedId)), nil
		}

		constructedChain = append(constructedChain, resolvedId)
		if len(issuer.ManualChain) < len(constructedChain) || constructedChain[index] != issuer.ManualChain[index] {
			updateChain = true
		}
	}

	if len(issuer.ManualChain) != len(constructedChain) {
		updateChain = true
	}

	if updateChain {
		oldChain := issuer.ManualChain
		issuer.ManualChain = constructedChain

		// Building the chain will write the issuer to disk; no need to do it
		// twice.
		modified = false
		err = sc.rebuildIssuersChains(issuer)
		if err != nil {
			return nil, err
		}

		if issuer.Usage.HasUsage(issuing.IssuanceUsage) {
			// Issuer has been saved by building the chain above
			err = b.issueSignEmptyCert(ctx, req, issuer.ID.String())
			if err != nil {
				issuer.ManualChain = oldChain
				newErr := sc.rebuildIssuersChains(issuer)
				if newErr != nil {
					return logical.ErrorResponse("error reverting bad chain update, state unknown: %v, \ninitial error: %v", newErr.Error(), err.Error()), nil
				}
				return logical.ErrorResponse("other changes to issuer may be persisted.  Error setting manual chain, issuer would be unusuable with this chain: %v", err), nil
			} else {
				updatedIssuanceValidations = false
			}
		}

	}

	if modified {
		err = sc.writeIssuer(issuer)
		if err != nil {
			return nil, err
		}
	}

	response, err := respondReadIssuer(issuer)
	if err != nil {
		return response, err
	}
	if newName != oldName {
		addWarningOnDereferencing(sc, oldName, response)
	}
	if issuer.AIAURIs != nil && issuer.AIAURIs.EnableTemplating {
		_, aiaErr := ToURLEntries(sc, issuer.ID, issuer.AIAURIs)
		if aiaErr != nil {
			response.AddWarning(fmt.Sprintf("issuance may fail: %v\n\nConsider setting the cluster-local address if it is not already set.", aiaErr))
		}
	}
	if (issuer.AIAURIs != nil && issuer.AIAURIs.DeltaCRLDistributionPoints != nil && len(issuer.AIAURIs.DeltaCRLDistributionPoints) > 0) &&
		(issuer.AIAURIs.CRLDistributionPoints == nil || len(issuer.AIAURIs.CRLDistributionPoints) == 0) {
		response.AddWarning(fmt.Sprintf("delta crl distribution points were set: %v but no base crl distribution point was set, consider setting base crl distribution point.", strings.Join(issuer.AIAURIs.DeltaCRLDistributionPoints, ", ")))
	}
	if updatedIssuanceValidations {
		warning := checkIssuer(issuer, ctx, req, b)
		if warning != "" {
			response.AddWarning(warning)
		}
	}

	b.pkiObserver.RecordPKIObservation(ctx, req, observe.ObservationTypePKIIssuerWrite,
		observe.NewAdditionalPKIMetadata("issuer_name", issuer.Name),
		observe.NewAdditionalPKIMetadata("issuer_id", issuer.ID),
		observe.NewAdditionalPKIMetadata("last_modified", issuer.LastModified.String()),
		observe.NewAdditionalPKIMetadata("revoked", issuer.Revoked))

	return response, err
}

func (b *backend) pathPatchIssuer(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()

	if b.UseLegacyBundleCaStorage() {
		return logical.ErrorResponse("Can not patch issuer until migration has completed"), nil
	}

	// First we fetch the issuer
	issuerName := GetIssuerRef(data)
	if len(issuerName) == 0 {
		return logical.ErrorResponse("missing issuer reference"), nil
	}

	sc := b.makeStorageContext(ctx, req.Storage)
	ref, err := sc.resolveIssuerReference(issuerName)
	if err != nil {
		return nil, err
	}
	if ref == "" {
		return logical.ErrorResponse("unable to resolve issuer id for reference: " + issuerName), nil
	}

	issuer, err := sc.fetchIssuerById(ref)
	if err != nil {
		return nil, err
	}

	// Now We are Looking at What (Might) Have Changed
	modified := false

	// Name Changes First
	_, ok := data.GetOk("issuer_name") // Don't check for conflicts if we aren't updating the name
	var oldName string
	var newName string
	if ok {
		newName, err = getIssuerName(sc, data)
		if err != nil && err != errIssuerNameInUse && err != errIssuerNameIsEmpty {
			// If the error is name already in use, and the new name is the
			// old name for this issuer, we're not actually updating the
			// issuer name (or causing a conflict) -- so don't err out. Other
			// errs should still be surfaced, however.
			return logical.ErrorResponse(err.Error()), nil
		}
		if err == errIssuerNameInUse && issuer.Name != newName {
			// When the new name is in use but isn't this name, throw an error.
			return logical.ErrorResponse(err.Error()), nil
		}
		if len(newName) > 0 && !nameMatcher.MatchString(newName) {
			return logical.ErrorResponse("new key name outside of valid character limits"), nil
		}
		if newName != issuer.Name {
			oldName = issuer.Name
			issuer.Name = newName
			issuer.LastModified = time.Now().UTC()
			// See note in updateDefaultIssuerId about why this is necessary.
			b.CrlBuilder().invalidateCRLBuildTime()
			b.CrlBuilder().flushCRLBuildTimeInvalidation(sc)
			modified = true
		}
	}

	// Leaf Not After Changes
	rawLeafBehaviorData, ok := data.GetOk("leaf_not_after_behavior")
	if ok {
		rawLeafBehavior := rawLeafBehaviorData.(string)
		var newLeafBehavior certutil.NotAfterBehavior
		switch rawLeafBehavior {
		case "err":
			newLeafBehavior = certutil.ErrNotAfterBehavior
		case "always_enforce_err":
			newLeafBehavior = certutil.AlwaysEnforceErr
		case "truncate":
			newLeafBehavior = certutil.TruncateNotAfterBehavior
		case "permit":
			newLeafBehavior = certutil.PermitNotAfterBehavior
		default:
			return logical.ErrorResponse("Unknown value for field `leaf_not_after_behavior`. Possible values are `err`, `truncate`, and `permit`."), nil
		}
		if newLeafBehavior != issuer.LeafNotAfterBehavior {
			issuer.LeafNotAfterBehavior = newLeafBehavior
			modified = true
		}
	}

	// Usage Changes
	rawUsageData, ok := data.GetOk("usage")
	if ok {
		rawUsage := rawUsageData.([]string)
		newUsage, err := issuing.NewIssuerUsageFromNames(rawUsage)
		if err != nil {
			return logical.ErrorResponse(fmt.Sprintf("Unable to parse specified usages: %v - valid values are %v", rawUsage, issuing.AllIssuerUsages.Names())), nil
		}
		if newUsage != issuer.Usage {
			if issuer.Revoked && newUsage.HasUsage(issuing.IssuanceUsage) {
				// Forbid allowing cert signing on its usage.
				return logical.ErrorResponse("This issuer was revoked; unable to modify its usage to include certificate signing again. Reissue this certificate (preferably with a new key) and modify that entry instead."), nil
			}

			cert, err := issuer.GetCertificate()
			if err != nil {
				return nil, fmt.Errorf("unable to parse issuer's certificate: %w", err)
			}
			if (cert.KeyUsage&x509.KeyUsageCRLSign) == 0 && newUsage.HasUsage(issuing.CRLSigningUsage) {
				return logical.ErrorResponse("This issuer's underlying certificate lacks the CRLSign KeyUsage value; unable to set CRLSigningUsage on this issuer as a result."), nil
			}

			issuer.Usage = newUsage
			modified = true
		}
	}

	// Revocation signature algorithm changes
	rawRevSigAlg, ok := data.GetOk("revocation_signature_algorithm")
	if ok {
		revSigAlgStr := rawRevSigAlg.(string)
		revSigAlg, present := certutil.SignatureAlgorithmNames[strings.ToLower(revSigAlgStr)]
		if !present && revSigAlgStr != "" {
			var knownAlgos []string
			for algoName := range certutil.SignatureAlgorithmNames {
				knownAlgos = append(knownAlgos, algoName)
			}

			return logical.ErrorResponse(fmt.Sprintf("Unknown signature algorithm value: %v - valid values are %v", revSigAlg, strings.Join(knownAlgos, ", "))), nil
		} else if revSigAlgStr == "" {
			revSigAlg = x509.UnknownSignatureAlgorithm
		}

		if err := issuer.CanMaybeSignWithAlgo(revSigAlg); err != nil {
			return nil, err
		}

		if revSigAlg != issuer.RevocationSigAlg {
			issuer.RevocationSigAlg = revSigAlg
			modified = true
		}
	}

	// AIA access changes.
	if issuer.AIAURIs == nil {
		issuer.AIAURIs = &issuing.AiaConfigEntry{}
	}

	// Associative mapping from data source to destination on the
	// backing issuer object. For PATCH requests, we use the source
	// data parameter as we still need to validate them and process
	// it into a string list.
	type aiaPair struct {
		Source string
		Dest   *[]string
	}
	pairs := []aiaPair{
		{
			Source: "issuing_certificates",
			Dest:   &issuer.AIAURIs.IssuingCertificates,
		},
		{
			Source: "crl_distribution_points",
			Dest:   &issuer.AIAURIs.CRLDistributionPoints,
		},
		{
			Source: "delta_crl_distribution_points",
			Dest:   &issuer.AIAURIs.DeltaCRLDistributionPoints,
		},
		{
			Source: "ocsp_servers",
			Dest:   &issuer.AIAURIs.OCSPServers,
		},
	}

	if enableTemplatingRaw, ok := data.GetOk("enable_aia_url_templating"); ok {
		enableTemplating := enableTemplatingRaw.(bool)
		if enableTemplating != issuer.AIAURIs.EnableTemplating {
			issuer.AIAURIs.EnableTemplating = enableTemplating
			modified = true
		}
	}

	// For each pair, if it is different on the object, update it.
	for _, pair := range pairs {
		rawURLsValue, ok := data.GetOk(pair.Source)
		if ok {
			urlsValue := rawURLsValue.([]string)
			if badURL := issuing.ValidateURLs(urlsValue); !issuer.AIAURIs.EnableTemplating && badURL != "" {
				return logical.ErrorResponse(fmt.Sprintf("invalid URL found in Authority Information Access (AIA) parameter %v: %s", pair.Source, badURL)), nil
			}

			if isStringArrayDifferent(urlsValue, *pair.Dest) {
				modified = true
				*pair.Dest = urlsValue
			}
		}
	}

	// If no AIA URLs exist on the issuer, set the AIA URLs entry to nil to
	// ease usage later.
	if len(issuer.AIAURIs.IssuingCertificates) == 0 && len(issuer.AIAURIs.CRLDistributionPoints) == 0 && len(issuer.AIAURIs.DeltaCRLDistributionPoints) == 0 && len(issuer.AIAURIs.OCSPServers) == 0 {
		issuer.AIAURIs = nil
	}

	updatedIssuanceValidations := false
	if updateEntIssuerFields(issuer, data, true) {
		modified = true
		updatedIssuanceValidations = true
	}

	// Manual Chain Changes
	newPathData, ok := data.GetOk("manual_chain")
	if ok {
		newPath := newPathData.([]string)
		var updateChain bool
		var constructedChain []issuing.IssuerID
		for index, newPathRef := range newPath {
			// Allow self for the first entry.
			if index == 0 && newPathRef == "self" {
				newPathRef = string(ref)
			}

			resolvedId, err := sc.resolveIssuerReference(newPathRef)
			if err != nil {
				return nil, err
			}

			if index == 0 && resolvedId != ref {
				return logical.ErrorResponse(fmt.Sprintf("expected first cert in chain to be a self-reference, but was: %v/%v", newPathRef, resolvedId)), nil
			}

			constructedChain = append(constructedChain, resolvedId)
			if len(issuer.ManualChain) < len(constructedChain) || constructedChain[index] != issuer.ManualChain[index] {
				updateChain = true
			}
		}

		if len(issuer.ManualChain) != len(constructedChain) {
			updateChain = true
		}

		if updateChain {
			oldChain := issuer.ManualChain
			issuer.ManualChain = constructedChain

			// Building the chain will write the issuer to disk; no need to do it
			// twice.
			modified = false
			err = sc.rebuildIssuersChains(issuer)
			if err != nil {
				return nil, err
			}
			// If this issuer is supposed to be issuing certificates, test that will work
			if issuer.Usage.HasUsage(issuing.IssuanceUsage) {
				// Issuer has been saved by building the chain above
				err = b.issueSignEmptyCert(ctx, req, issuer.Name)
				if err != nil {
					issuer.ManualChain = oldChain
					newErr := sc.rebuildIssuersChains(issuer)
					if newErr != nil {
						return logical.ErrorResponse("error reverting bad chain update, state unknown: %v, \ninitial error: %v", newErr.Error(), err.Error()), nil
					}
					return logical.ErrorResponse("other changes to issuer may be persisted.  Error setting manual chain, issuer would be unusuable with this chain: %v", err), nil
				} else {
					updatedIssuanceValidations = false
				}
			}

		}
	}

	if modified {
		err := sc.writeIssuer(issuer)
		if err != nil {
			return nil, err
		}
	}

	response, err := respondReadIssuer(issuer)
	if err != nil {
		return nil, err
	}
	if newName != oldName {
		addWarningOnDereferencing(sc, oldName, response)
	}
	if updatedIssuanceValidations {
		warning := checkIssuer(issuer, ctx, req, b)
		if warning != "" {
			response.AddWarning(warning)
		}
	}
	if issuer.AIAURIs != nil && issuer.AIAURIs.EnableTemplating {
		_, aiaErr := ToURLEntries(sc, issuer.ID, issuer.AIAURIs)
		if aiaErr != nil {
			response.AddWarning(fmt.Sprintf("issuance may fail: %v\n\nConsider setting the cluster-local address if it is not already set.", aiaErr))
		}
	}
	if (issuer.AIAURIs != nil && issuer.AIAURIs.DeltaCRLDistributionPoints != nil && len(issuer.AIAURIs.DeltaCRLDistributionPoints) > 0) &&
		(issuer.AIAURIs.CRLDistributionPoints == nil || len(issuer.AIAURIs.CRLDistributionPoints) == 0) {
		response.AddWarning(fmt.Sprintf("delta crl distribution points were set: %v but no base crl distribution point was set, consider setting base crl distribution point.", strings.Join(issuer.AIAURIs.DeltaCRLDistributionPoints, ", ")))
	}

	b.pkiObserver.RecordPKIObservation(ctx, req, observe.ObservationTypePKIIssuerPatch,
		observe.NewAdditionalPKIMetadata("issuer_name", issuer.Name),
		observe.NewAdditionalPKIMetadata("issuer_id", issuer.ID),
		observe.NewAdditionalPKIMetadata("last_modified", issuer.LastModified.String()),
		observe.NewAdditionalPKIMetadata("revoked", issuer.Revoked))

	return response, err
}

// checkIssuer looks at an issuer that has already been written, and returns a warning if it is not functional.
func checkIssuer(issuer *issuing.IssuerEntry, ctx context.Context, req *logical.Request, b *backend) (warning string) {
	if issuer.Usage.HasUsage(issuing.IssuanceUsage) {
		err := b.issueSignEmptyCert(ctx, req, issuer.ID.String())
		if err != nil {
			return fmt.Sprintf("warning: issuer with issuance usage %s cannot issue certificates with this configuration: %v", issuer.ID.String(), err)
		}
	}
	return ""
}

func (b *backend) pathGetRawIssuer(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
	if b.UseLegacyBundleCaStorage() {
		return logical.ErrorResponse("Can not get issuer until migration has completed"), nil
	}

	issuerName := GetIssuerRef(data)
	if len(issuerName) == 0 {
		return logical.ErrorResponse("missing issuer reference"), nil
	}

	sc := b.makeStorageContext(ctx, req.Storage)
	ref, err := sc.resolveIssuerReference(issuerName)
	if err != nil {
		return nil, err
	}
	if ref == "" {
		return logical.ErrorResponse("unable to resolve issuer id for reference: " + issuerName), nil
	}

	issuer, err := sc.fetchIssuerById(ref)
	if err != nil {
		return nil, err
	}

	var contentType string
	var certificate []byte

	response := &logical.Response{}
	ret, err := sendNotModifiedResponseIfNecessary(&IfModifiedSinceHelper{req: req, reqType: ifModifiedCA, issuerRef: ref}, sc, response)
	if err != nil {
		return nil, err
	}
	if ret {
		return response, nil
	}

	certificate = []byte(issuer.Certificate)

	if strings.HasSuffix(req.Path, "/pem") {
		contentType = "application/pem-certificate-chain"
	} else if strings.HasSuffix(req.Path, "/der") {
		contentType = "application/pkix-cert"
	}

	if strings.HasSuffix(req.Path, "/der") {
		pemBlock, _ := pem.Decode(certificate)
		if pemBlock == nil {
			return nil, err
		}

		certificate = pemBlock.Bytes
	}

	statusCode := 200
	if len(certificate) == 0 {
		statusCode = 204
	}

	if strings.HasSuffix(req.Path, "/pem") || strings.HasSuffix(req.Path, "/der") {
		return &logical.Response{
			Data: map[string]interface{}{
				logical.HTTPContentType: contentType,
				logical.HTTPRawBody:     certificate,
				logical.HTTPStatusCode:  statusCode,
			},
		}, nil
	} else {
		return &logical.Response{
			Data: map[string]interface{}{
				"certificate": string(certificate),
				"ca_chain":    issuer.CAChain,
				"issuer_id":   issuer.ID,
				"issuer_name": issuer.Name,
			},
		}, nil
	}
}

func (b *backend) pathDeleteIssuer(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()

	if b.UseLegacyBundleCaStorage() {
		return logical.ErrorResponse("Can not delete issuer until migration has completed"), nil
	}

	issuerName := GetIssuerRef(data)
	if len(issuerName) == 0 {
		return logical.ErrorResponse("missing issuer reference"), nil
	}

	sc := b.makeStorageContext(ctx, req.Storage)
	ref, err := sc.resolveIssuerReference(issuerName)
	if err != nil {
		// Return as if we deleted it if we fail to lookup the issuer.
		if ref == issuing.IssuerRefNotFound {
			return &logical.Response{}, nil
		}
		return nil, err
	}

	response := &logical.Response{}

	issuer, err := sc.fetchIssuerById(ref)
	if err != nil {
		return nil, err
	}
	if issuer.Name != "" {
		addWarningOnDereferencing(sc, issuer.Name, response)
	}
	addWarningOnDereferencing(sc, string(issuer.ID), response)

	wasDefault, err := sc.deleteIssuer(ref)
	if err != nil {
		return nil, err
	}
	if wasDefault {
		response.AddWarning(fmt.Sprintf("Deleted issuer %v (via issuer_ref %v); this was configured as the default issuer. Operations without an explicit issuer will not work until a new default is configured.", ref, issuerName))
		addWarningOnDereferencing(sc, defaultRef, response)
	}

	// Since we've deleted an issuer, the chains might've changed. Call the
	// rebuild code. We shouldn't technically err (as the issuer was deleted
	// successfully), but log a warning (and to the response) if this fails.
	if err := sc.rebuildIssuersChains(nil); err != nil {
		msg := fmt.Sprintf("Failed to rebuild remaining issuers' chains: %v", err)
		b.Logger().Error(msg)
		response.AddWarning(msg)
	}

	// Finally, we need to rebuild both the local and the unified CRLs. This
	// will free up any now unnecessary space used in both the CRL config
	// and for the underlying CRL.
	warnings, err := b.CrlBuilder().Rebuild(sc, true)
	if err != nil {
		return nil, err
	}

	for index, warning := range warnings {
		response.AddWarning(fmt.Sprintf("Warning %d during CRL rebuild: %v", index+1, warning))
	}

	b.pkiObserver.RecordPKIObservation(ctx, req, observe.ObservationTypePKIIssuerDelete,
		observe.NewAdditionalPKIMetadata("issuer_name", issuer.Name),
		observe.NewAdditionalPKIMetadata("issuer_id", issuer.ID),
		observe.NewAdditionalPKIMetadata("was_default", wasDefault))

	return response, nil
}

func addWarningOnDereferencing(sc *storageContext, name string, resp *logical.Response) {
	timeout, inUseBy, err := sc.checkForRolesReferencing(name)
	if err != nil || timeout {
		if inUseBy == 0 {
			resp.AddWarning(fmt.Sprint("Unable to check if any roles referenced this issuer by ", name))
		} else {
			resp.AddWarning(fmt.Sprint("The name ", name, " was in use by at least ", inUseBy, " roles"))
		}
	} else {
		if inUseBy > 0 {
			resp.AddWarning(fmt.Sprint(inUseBy, " roles reference ", name))
		}
	}
}

const (
	pathGetIssuerHelpSyn  = `Fetch a single issuer certificate.`
	pathGetIssuerHelpDesc = `
This allows fetching information associated with the underlying issuer
certificate.

:ref can be either the literal value "default", in which case /config/issuers
will be consulted for the present default issuer, an identifier of an issuer,
or its assigned name value.

Use /issuer/:ref/der or /issuer/:ref/pem to return just the certificate in
raw DER or PEM form, without the JSON structure of /issuer/:ref.

Writing to /issuer/:ref allows updating of the name field associated with
the certificate.
`
)

func pathGetIssuerCRL(b *backend) *framework.Path {
	pattern := "issuer/" + framework.GenericNameRegex(issuerRefParam) + "/crl(/pem|/der|/delta(/pem|/der)?)?"

	displayAttrs := &framework.DisplayAttributes{
		OperationPrefix: operationPrefixPKIIssuer,
		OperationSuffix: "crl|crl-pem|crl-der|crl-delta|crl-delta-pem|crl-delta-der",
	}

	return buildPathGetIssuerCRL(b, pattern, displayAttrs)
}

func pathGetIssuerUnifiedCRL(b *backend) *framework.Path {
	pattern := "issuer/" + framework.GenericNameRegex(issuerRefParam) + "/unified-crl(/pem|/der|/delta(/pem|/der)?)?"

	displayAttrs := &framework.DisplayAttributes{
		OperationPrefix: operationPrefixPKIIssuer,
		OperationSuffix: "unified-crl|unified-crl-pem|unified-crl-der|unified-crl-delta|unified-crl-delta-pem|unified-crl-delta-der",
	}

	return buildPathGetIssuerCRL(b, pattern, displayAttrs)
}

func buildPathGetIssuerCRL(b *backend, pattern string, displayAttrs *framework.DisplayAttributes) *framework.Path {
	fields := map[string]*framework.FieldSchema{}
	fields = addIssuerRefNameFields(fields)

	return &framework.Path{
		// Returns raw values.
		Pattern:      pattern,
		DisplayAttrs: displayAttrs,
		Fields:       fields,

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.ReadOperation: &framework.PathOperation{
				Callback: b.pathGetIssuerCRL,
				Responses: map[int][]framework.Response{
					http.StatusOK: {{
						Description: "OK",
						Fields: map[string]*framework.FieldSchema{
							"crl": {
								Type:     framework.TypeString,
								Required: false,
							},
						},
					}},
				},
			},
		},

		HelpSynopsis:    pathGetIssuerCRLHelpSyn,
		HelpDescription: pathGetIssuerCRLHelpDesc,
	}
}

func (b *backend) pathGetIssuerCRL(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
	if b.UseLegacyBundleCaStorage() {
		return logical.ErrorResponse("Can not get issuer's CRL until migration has completed"), nil
	}

	issuerName := GetIssuerRef(data)
	if len(issuerName) == 0 {
		return logical.ErrorResponse("missing issuer reference"), nil
	}

	sc := b.makeStorageContext(ctx, req.Storage)
	warnings, err := b.CrlBuilder().RebuildIfForced(sc)
	if err != nil {
		return nil, err
	}
	if len(warnings) > 0 {
		// Since this is a fetch of a specific CRL, this most likely comes
		// from an automated system of some sort; these warnings would be
		// ignored and likely meaningless. Log them instead.
		msg := "During rebuild of CRL on issuer CRL fetch, got the following warnings:"
		for index, warning := range warnings {
			msg = fmt.Sprintf("%v\n %d. %v", msg, index+1, warning)
		}
		b.Logger().Warn(msg)
	}

	var certificate []byte
	var contentType string

	isUnified := strings.Contains(req.Path, "unified")
	isDelta := strings.Contains(req.Path, "delta")

	response := &logical.Response{}
	var crlType ifModifiedReqType = ifModifiedCRL

	if !isUnified && isDelta {
		crlType = ifModifiedDeltaCRL
	} else if isUnified && !isDelta {
		crlType = ifModifiedUnifiedCRL
	} else if isUnified && isDelta {
		crlType = ifModifiedUnifiedDeltaCRL
	}

	ret, err := sendNotModifiedResponseIfNecessary(&IfModifiedSinceHelper{req: req, reqType: crlType}, sc, response)
	if err != nil {
		return nil, err
	}
	if ret {
		return response, nil
	}

	crlPath, err := issuing.ResolveIssuerCRLPath(sc.GetContext(), sc.GetStorage(), sc.UseLegacyBundleCaStorage(), issuerName, isUnified)
	if err != nil {
		return nil, err
	}

	if strings.Contains(req.Path, "delta") {
		crlPath += deltaCRLPathSuffix
	}

	crlEntry, err := req.Storage.Get(ctx, crlPath)
	if err != nil {
		return nil, err
	}

	if crlEntry != nil && len(crlEntry.Value) > 0 {
		certificate = []byte(crlEntry.Value)
	}

	if strings.HasSuffix(req.Path, "/der") {
		contentType = "application/pkix-crl"
	} else if strings.HasSuffix(req.Path, "/pem") {
		contentType = "application/x-pem-file"
	}

	if !strings.HasSuffix(req.Path, "/der") {
		// Rather return an empty response rather than an empty PEM blob.
		// We build this PEM block for both the JSON and PEM endpoints.
		if len(certificate) > 0 {
			pemBlock := pem.Block{
				Type:  "X509 CRL",
				Bytes: certificate,
			}

			certificate = pem.EncodeToMemory(&pemBlock)
		}
	}

	statusCode := 200
	if len(certificate) == 0 {
		statusCode = 204
	}

	if strings.HasSuffix(req.Path, "/der") || strings.HasSuffix(req.Path, "/pem") {
		return &logical.Response{
			Data: map[string]interface{}{
				logical.HTTPContentType: contentType,
				logical.HTTPRawBody:     certificate,
				logical.HTTPStatusCode:  statusCode,
			},
		}, nil
	}

	return &logical.Response{
		Data: map[string]interface{}{
			"crl": string(certificate),
		},
	}, nil
}

const (
	pathGetIssuerCRLHelpSyn  = `Fetch an issuer's Certificate Revocation Log (CRL).`
	pathGetIssuerCRLHelpDesc = `
This allows fetching the specified issuer's CRL. Note that this is different
than the legacy path (/crl and /certs/crl) in that this is per-issuer and not
just the default issuer's CRL.

Two issuers will have the same CRL if they have the same key material and if
they have the same Subject value.

:ref can be either the literal value "default", in which case /config/issuers
will be consulted for the present default issuer, an identifier of an issuer,
or its assigned name value.

 - /issuer/:ref/crl is JSON encoded and contains a PEM CRL,
 - /issuer/:ref/crl/pem contains the PEM-encoded CRL,
 - /issuer/:ref/crl/DER contains the raw DER-encoded (binary) CRL.
`
)
