package authority

import (
	"context"
	"crypto"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"math/big"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/pkg/errors"
	"golang.org/x/crypto/ssh"

	"go.step.sm/crypto/jose"
	"go.step.sm/crypto/keyutil"
	"go.step.sm/crypto/pemutil"
	"go.step.sm/crypto/x509util"

	"github.com/smallstep/certificates/authority/config"
	"github.com/smallstep/certificates/authority/provisioner"
	casapi "github.com/smallstep/certificates/cas/apiv1"
	"github.com/smallstep/certificates/db"
	"github.com/smallstep/certificates/errs"
	"github.com/smallstep/certificates/webhook"
	"github.com/smallstep/nosql/database"
)

type tokenKey struct{}

// NewTokenContext adds the given token to the context.
func NewTokenContext(ctx context.Context, token string) context.Context {
	return context.WithValue(ctx, tokenKey{}, token)
}

// TokenFromContext returns the token from the given context.
func TokenFromContext(ctx context.Context) (token string, ok bool) {
	token, ok = ctx.Value(tokenKey{}).(string)
	return
}

// GetTLSOptions returns the tls options configured.
func (a *Authority) GetTLSOptions() *config.TLSOptions {
	return a.config.TLS
}

var (
	oidAuthorityKeyIdentifier            = asn1.ObjectIdentifier{2, 5, 29, 35}
	oidSubjectKeyIdentifier              = asn1.ObjectIdentifier{2, 5, 29, 14}
	oidExtensionIssuingDistributionPoint = asn1.ObjectIdentifier{2, 5, 29, 28}
)

func withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc {
	return func(crt *x509.Certificate, _ provisioner.SignOptions) error {
		if def == nil {
			return errors.New("default ASN1DN template cannot be nil")
		}
		if len(crt.Subject.Country) == 0 && def.Country != "" {
			crt.Subject.Country = append(crt.Subject.Country, def.Country)
		}
		if len(crt.Subject.Organization) == 0 && def.Organization != "" {
			crt.Subject.Organization = append(crt.Subject.Organization, def.Organization)
		}
		if len(crt.Subject.OrganizationalUnit) == 0 && def.OrganizationalUnit != "" {
			crt.Subject.OrganizationalUnit = append(crt.Subject.OrganizationalUnit, def.OrganizationalUnit)
		}
		if len(crt.Subject.Locality) == 0 && def.Locality != "" {
			crt.Subject.Locality = append(crt.Subject.Locality, def.Locality)
		}
		if len(crt.Subject.Province) == 0 && def.Province != "" {
			crt.Subject.Province = append(crt.Subject.Province, def.Province)
		}
		if len(crt.Subject.StreetAddress) == 0 && def.StreetAddress != "" {
			crt.Subject.StreetAddress = append(crt.Subject.StreetAddress, def.StreetAddress)
		}
		if crt.Subject.SerialNumber == "" && def.SerialNumber != "" {
			crt.Subject.SerialNumber = def.SerialNumber
		}
		if crt.Subject.CommonName == "" && def.CommonName != "" {
			crt.Subject.CommonName = def.CommonName
		}
		return nil
	}
}

// GetX509Signer returns a [crypto.Signer] implementation using the intermediate
// key.
//
// This method can return a [NotImplementedError] if the CA is configured with a
// Certificate Authority Service (CAS) that does not implement the
// CertificateAuthoritySigner interface.
//
// [NotImplementedError]: https://pkg.go.dev/github.com/smallstep/certificates/cas/apiv1#NotImplementedError
func (a *Authority) GetX509Signer() (crypto.Signer, error) {
	if s, ok := a.x509CAService.(casapi.CertificateAuthoritySigner); ok {
		return s.GetSigner()
	}
	return nil, casapi.NotImplementedError{}
}

// Sign creates a signed certificate from a certificate signing request. It
// creates a new context.Context, and calls into SignWithContext.
//
// Deprecated: Use authority.SignWithContext with an actual context.Context.
func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
	return a.SignWithContext(context.Background(), csr, signOpts, extraOpts...)
}

// SignWithContext creates a signed certificate from a certificate signing
// request, taking the provided context.Context.
func (a *Authority) SignWithContext(ctx context.Context, csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
	chain, prov, err := a.signX509(ctx, csr, signOpts, extraOpts...)
	a.meter.X509Signed(chain, prov, err)
	return chain, err
}

func (a *Authority) signX509(ctx context.Context, csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, provisioner.Interface, error) {
	var (
		certOptions    []x509util.Option
		certValidators []provisioner.CertificateValidator
		certModifiers  []provisioner.CertificateModifier
		certEnforcers  []provisioner.CertificateEnforcer
	)

	opts := []any{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)}
	if err := csr.CheckSignature(); err != nil {
		return nil, nil, errs.ApplyOptions(
			errs.BadRequestErr(err, "invalid certificate request"),
			opts...,
		)
	}

	// Set backdate with the configured value
	signOpts.Backdate = a.config.AuthorityConfig.Backdate.Duration

	var (
		prov       provisioner.Interface
		pInfo      *casapi.ProvisionerInfo
		attData    *provisioner.AttestationData
		webhookCtl webhookController
	)
	for _, op := range extraOpts {
		switch k := op.(type) {
		// Capture current provisioner
		case provisioner.Interface:
			prov = k
			pInfo = &casapi.ProvisionerInfo{
				ID:   prov.GetID(),
				Type: prov.GetType().String(),
				Name: prov.GetName(),
			}
		// Adds new options to NewCertificate
		case provisioner.CertificateOptions:
			certOptions = append(certOptions, k.Options(signOpts)...)

		// Validate the given certificate request.
		case provisioner.CertificateRequestValidator:
			if err := k.Valid(csr); err != nil {
				return nil, prov, errs.ApplyOptions(
					errs.ForbiddenErr(err, "error validating certificate request"),
					opts...,
				)
			}

		// Validates the unsigned certificate template.
		case provisioner.CertificateValidator:
			certValidators = append(certValidators, k)

		// Modifies a certificate before validating it.
		case provisioner.CertificateModifier:
			certModifiers = append(certModifiers, k)

		// Modifies a certificate after validating it.
		case provisioner.CertificateEnforcer:
			certEnforcers = append(certEnforcers, k)

		// Extra information from ACME attestations.
		case provisioner.AttestationData:
			attData = &k

		// Capture the provisioner's webhook controller
		case webhookController:
			webhookCtl = k

		default:
			return nil, prov, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]any{k}, opts...)...)
		}
	}

	if err := a.callEnrichingWebhooksX509(ctx, prov, webhookCtl, attData, csr); err != nil {
		return nil, prov, errs.ApplyOptions(
			errs.ForbiddenErr(err, "%s", err.Error()),
			errs.WithKeyVal("csr", csr),
			errs.WithKeyVal("signOptions", signOpts),
		)
	}

	crt, err := x509util.NewCertificate(csr, certOptions...)
	if err != nil {
		var te *x509util.TemplateError
		switch {
		case errors.As(err, &te):
			return nil, prov, errs.ApplyOptions(
				errs.BadRequestErr(err, "%s", err.Error()),
				errs.WithKeyVal("csr", csr),
				errs.WithKeyVal("signOptions", signOpts),
			)
		case strings.HasPrefix(err.Error(), "error unmarshaling certificate"):
			// explicitly check for unmarshaling errors, which are most probably caused by JSON template (syntax) errors
			return nil, prov, errs.InternalServerErr(templatingError(err),
				errs.WithKeyVal("csr", csr),
				errs.WithKeyVal("signOptions", signOpts),
				errs.WithMessage("error applying certificate template"),
			)
		default:
			return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign", opts...)
		}
	}

	// Certificate modifiers before validation
	leaf := crt.GetCertificate()

	// Set default subject
	if err := withDefaultASN1DN(a.config.AuthorityConfig.Template).Modify(leaf, signOpts); err != nil {
		return nil, prov, errs.ApplyOptions(
			errs.ForbiddenErr(err, "error creating certificate"),
			opts...,
		)
	}

	for _, m := range certModifiers {
		if err := m.Modify(leaf, signOpts); err != nil {
			return nil, prov, errs.ApplyOptions(
				errs.ForbiddenErr(err, "error creating certificate"),
				opts...,
			)
		}
	}

	// Certificate validation.
	for _, v := range certValidators {
		if err := v.Valid(leaf, signOpts); err != nil {
			return nil, prov, errs.ApplyOptions(
				errs.ForbiddenErr(err, "error validating certificate"),
				opts...,
			)
		}
	}

	// Certificate modifiers after validation
	for _, m := range certEnforcers {
		if err = m.Enforce(leaf); err != nil {
			return nil, prov, errs.ApplyOptions(
				errs.ForbiddenErr(err, "error creating certificate"),
				opts...,
			)
		}
	}

	// Process injected modifiers after validation
	for _, m := range a.x509Enforcers {
		if err = m.Enforce(leaf); err != nil {
			return nil, prov, errs.ApplyOptions(
				errs.ForbiddenErr(err, "error creating certificate"),
				opts...,
			)
		}
	}

	// Check if authority is allowed to sign the certificate
	if err = a.isAllowedToSignX509Certificate(leaf); err != nil {
		var ee *errs.Error
		if errors.As(err, &ee) {
			return nil, prov, errs.ApplyOptions(ee, opts...)
		}
		return nil, prov, errs.InternalServerErr(err,
			errs.WithKeyVal("csr", csr),
			errs.WithKeyVal("signOptions", signOpts),
			errs.WithMessage("error creating certificate"),
		)
	}

	// Send certificate to webhooks for authorization
	if err := a.callAuthorizingWebhooksX509(ctx, prov, webhookCtl, crt, leaf, attData); err != nil {
		return nil, prov, errs.ApplyOptions(
			errs.ForbiddenErr(err, "error creating certificate"),
			opts...,
		)
	}

	// Sign certificate
	lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))

	resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
		Template:    leaf,
		CSR:         csr,
		Lifetime:    lifetime,
		Backdate:    signOpts.Backdate,
		Provisioner: pInfo,
	})
	if err != nil {
		return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error creating certificate", opts...)
	}

	chain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...)

	// Wrap provisioner with extra information, if not nil
	if prov != nil {
		prov = wrapProvisioner(prov, attData)
	}

	// Store certificate in the db.
	if err := a.storeCertificate(prov, chain); err != nil && !errors.Is(err, db.ErrNotImplemented) {
		return nil, prov, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error storing certificate in db", opts...)
	}

	return chain, prov, nil
}

// isAllowedToSignX509Certificate checks if the Authority is allowed
// to sign the X.509 certificate.
func (a *Authority) isAllowedToSignX509Certificate(cert *x509.Certificate) error {
	if err := a.constraintsEngine.ValidateCertificate(cert); err != nil {
		return err
	}
	return a.policyEngine.IsX509CertificateAllowed(cert)
}

// AreSANsAllowed evaluates the provided sans against the
// authority X.509 policy.
func (a *Authority) AreSANsAllowed(_ context.Context, sans []string) error {
	return a.policyEngine.AreSANsAllowed(sans)
}

// Renew creates a new Certificate identical to the old certificate, except with
// a validity window that begins 'now'.
func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) {
	return a.RenewContext(context.Background(), oldCert, nil)
}

// Rekey is used for rekeying and renewing based on the public key. If the
// public key is 'nil' then it's assumed that the cert should be renewed using
// the existing public key. If the public key is not 'nil' then it's assumed
// that the cert should be rekeyed.
//
// For both Rekey and Renew all other attributes of the new certificate should
// match the old certificate. The exceptions are 'AuthorityKeyId' (which may
// have changed), 'SubjectKeyId' (different in case of rekey), and
// 'NotBefore/NotAfter' (the validity duration of the new certificate should be
// equal to the old one, but starting 'now').
func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
	return a.RenewContext(context.Background(), oldCert, pk)
}

// RenewContext creates a new certificate identical to the old one, but it can
// optionally replace the public key with the given one. When running on RA
// mode, it can only renew a certificate using a renew token instead.
//
// For both rekey and renew operations, all other attributes of the new
// certificate should match the old certificate. The exceptions are
// 'AuthorityKeyId' (which may have changed), 'SubjectKeyId' (different in case
// of rekey), and 'NotBefore/NotAfter' (the validity duration of the new
// certificate should be equal to the old one, but starting 'now').
func (a *Authority) RenewContext(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
	chain, prov, err := a.renewContext(ctx, oldCert, pk)
	if pk == nil {
		a.meter.X509Renewed(chain, prov, err)
	} else {
		a.meter.X509Rekeyed(chain, prov, err)
	}
	return chain, err
}

func (a *Authority) renewContext(ctx context.Context, oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, provisioner.Interface, error) {
	isRekey := (pk != nil)
	opts := []errs.Option{
		errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String()),
	}

	// Check step provisioner extensions
	prov, err := a.authorizeRenew(ctx, oldCert)
	if err != nil {
		return nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)
	}

	// Durations
	backdate := a.config.AuthorityConfig.Backdate.Duration
	duration := oldCert.NotAfter.Sub(oldCert.NotBefore)
	lifetime := duration - backdate

	// Create new certificate from previous values.
	// Issuer, NotBefore, NotAfter and SubjectKeyId will be set by the CAS.
	newCert := &x509.Certificate{
		RawSubject:                  oldCert.RawSubject,
		KeyUsage:                    oldCert.KeyUsage,
		UnhandledCriticalExtensions: oldCert.UnhandledCriticalExtensions,
		ExtKeyUsage:                 oldCert.ExtKeyUsage,
		UnknownExtKeyUsage:          oldCert.UnknownExtKeyUsage,
		BasicConstraintsValid:       oldCert.BasicConstraintsValid,
		IsCA:                        oldCert.IsCA,
		MaxPathLen:                  oldCert.MaxPathLen,
		MaxPathLenZero:              oldCert.MaxPathLenZero,
		OCSPServer:                  oldCert.OCSPServer,
		IssuingCertificateURL:       oldCert.IssuingCertificateURL,
		PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical,
		PermittedEmailAddresses:     oldCert.PermittedEmailAddresses,
		DNSNames:                    oldCert.DNSNames,
		EmailAddresses:              oldCert.EmailAddresses,
		IPAddresses:                 oldCert.IPAddresses,
		URIs:                        oldCert.URIs,
		PermittedDNSDomains:         oldCert.PermittedDNSDomains,
		ExcludedDNSDomains:          oldCert.ExcludedDNSDomains,
		PermittedIPRanges:           oldCert.PermittedIPRanges,
		ExcludedIPRanges:            oldCert.ExcludedIPRanges,
		ExcludedEmailAddresses:      oldCert.ExcludedEmailAddresses,
		PermittedURIDomains:         oldCert.PermittedURIDomains,
		ExcludedURIDomains:          oldCert.ExcludedURIDomains,
		CRLDistributionPoints:       oldCert.CRLDistributionPoints,
		PolicyIdentifiers:           oldCert.PolicyIdentifiers,
	}

	if isRekey {
		newCert.PublicKey = pk
	} else {
		newCert.PublicKey = oldCert.PublicKey
	}

	// Copy all extensions except:
	//
	//  1. Authority Key Identifier - This one might be different if we rotate
	//  the intermediate certificate and it will cause a TLS bad certificate
	//  error.
	//
	//  2. Subject Key Identifier, if rekey - For rekey, SubjectKeyIdentifier
	//  extension will be calculated for the new public key by
	//  x509util.CreateCertificate()
	for _, ext := range oldCert.Extensions {
		if ext.Id.Equal(oidAuthorityKeyIdentifier) {
			continue
		}
		if ext.Id.Equal(oidSubjectKeyIdentifier) && isRekey {
			newCert.SubjectKeyId = nil
			continue
		}
		newCert.ExtraExtensions = append(newCert.ExtraExtensions, ext)
	}

	// Check if the certificate is allowed to be renewed, name constraints might
	// change over time.
	//
	// TODO(hslatman,maraino): consider adding policies too and consider if
	// RenewSSH should check policies.
	if err = a.constraintsEngine.ValidateCertificate(newCert); err != nil {
		var ee *errs.Error
		switch {
		case errors.As(err, &ee):
			return nil, prov, errs.StatusCodeError(ee.StatusCode(), err, opts...)
		default:
			return nil, prov, errs.InternalServerErr(err,
				errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String()),
				errs.WithMessage("error renewing certificate"),
			)
		}
	}

	// The token can optionally be in the context. If the CA is running in RA
	// mode, this can be used to renew a certificate.
	token, _ := TokenFromContext(ctx)

	resp, err := a.x509CAService.RenewCertificate(&casapi.RenewCertificateRequest{
		Template: newCert,
		Lifetime: lifetime,
		Backdate: backdate,
		Token:    token,
	})
	if err != nil {
		return nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)
	}

	chain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...)

	if err = a.storeRenewedCertificate(oldCert, chain); err != nil && !errors.Is(err, db.ErrNotImplemented) {
		return nil, prov, errs.StatusCodeError(http.StatusInternalServerError, err, opts...)
	}

	return chain, prov, nil
}

// storeCertificate allows to use an extension of the db.AuthDB interface that
// can log the full chain of certificates.
//
// TODO: at some point we should replace the db.AuthDB interface to implement
// `StoreCertificate(...*x509.Certificate) error` instead of just
// `StoreCertificate(*x509.Certificate) error`.
func (a *Authority) storeCertificate(prov provisioner.Interface, fullchain []*x509.Certificate) error {
	type certificateChainStorer interface {
		StoreCertificateChain(provisioner.Interface, ...*x509.Certificate) error
	}
	type certificateChainSimpleStorer interface {
		StoreCertificateChain(...*x509.Certificate) error
	}

	// Store certificate in linkedca
	switch s := a.adminDB.(type) {
	case certificateChainStorer:
		return s.StoreCertificateChain(prov, fullchain...)
	case certificateChainSimpleStorer:
		return s.StoreCertificateChain(fullchain...)
	}

	// Store certificate in local db
	switch s := a.db.(type) {
	case certificateChainStorer:
		return s.StoreCertificateChain(prov, fullchain...)
	case certificateChainSimpleStorer:
		return s.StoreCertificateChain(fullchain...)
	case db.CertificateStorer:
		return s.StoreCertificate(fullchain[0])
	default:
		return nil
	}
}

// storeRenewedCertificate allows to use an extension of the db.AuthDB interface
// that can log if a certificate has been renewed or rekeyed.
//
// TODO: at some point we should implement this in the standard implementation.
func (a *Authority) storeRenewedCertificate(oldCert *x509.Certificate, fullchain []*x509.Certificate) error {
	type renewedCertificateChainStorer interface {
		StoreRenewedCertificate(*x509.Certificate, ...*x509.Certificate) error
	}

	// Store certificate in linkedca
	if s, ok := a.adminDB.(renewedCertificateChainStorer); ok {
		return s.StoreRenewedCertificate(oldCert, fullchain...)
	}

	// Store certificate in local db
	switch s := a.db.(type) {
	case renewedCertificateChainStorer:
		return s.StoreRenewedCertificate(oldCert, fullchain...)
	case db.CertificateStorer:
		return s.StoreCertificate(fullchain[0])
	default:
		return nil
	}
}

// RevokeOptions are the options for the Revoke API.
type RevokeOptions struct {
	Serial      string
	Reason      string
	ReasonCode  int
	PassiveOnly bool
	MTLS        bool
	ACME        bool
	Crt         *x509.Certificate
	OTT         string
}

// Revoke revokes a certificate.
//
// NOTE: Only supports passive revocation - prevent existing certificates from
// being renewed.
//
// TODO: Add OCSP and CRL support.
func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error {
	opts := []interface{}{
		errs.WithKeyVal("serialNumber", revokeOpts.Serial),
		errs.WithKeyVal("reasonCode", revokeOpts.ReasonCode),
		errs.WithKeyVal("reason", revokeOpts.Reason),
		errs.WithKeyVal("passiveOnly", revokeOpts.PassiveOnly),
		errs.WithKeyVal("MTLS", revokeOpts.MTLS),
		errs.WithKeyVal("ACME", revokeOpts.ACME),
		errs.WithKeyVal("context", provisioner.MethodFromContext(ctx).String()),
	}
	if revokeOpts.MTLS || revokeOpts.ACME {
		opts = append(opts, errs.WithKeyVal("certificate", base64.StdEncoding.EncodeToString(revokeOpts.Crt.Raw)))
	} else {
		opts = append(opts, errs.WithKeyVal("token", revokeOpts.OTT))
	}

	rci := &db.RevokedCertificateInfo{
		Serial:     revokeOpts.Serial,
		ReasonCode: revokeOpts.ReasonCode,
		Reason:     revokeOpts.Reason,
		MTLS:       revokeOpts.MTLS,
		ACME:       revokeOpts.ACME,
		RevokedAt:  time.Now().UTC(),
	}

	// For X509 CRLs attempt to get the expiration date of the certificate.
	if provisioner.MethodFromContext(ctx) == provisioner.RevokeMethod {
		if revokeOpts.Crt == nil {
			cert, err := a.db.GetCertificate(revokeOpts.Serial)
			if err == nil {
				rci.ExpiresAt = cert.NotAfter
			}
		} else {
			rci.ExpiresAt = revokeOpts.Crt.NotAfter
		}
	}

	// If not mTLS nor ACME, then get the TokenID of the token.
	if !revokeOpts.MTLS && !revokeOpts.ACME {
		token, err := jose.ParseSigned(revokeOpts.OTT)
		if err != nil {
			return errs.Wrap(http.StatusUnauthorized, err, "authority.Revoke; error parsing token", opts...)
		}

		// Get claims w/out verification.
		var claims Claims
		if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
			return errs.Wrap(http.StatusUnauthorized, err, "authority.Revoke", opts...)
		}

		// Verify that the serial in the token matches the serial from the request.
		if revokeOpts.Serial != claims.Subject {
			return errs.ApplyOptions(
				errs.Forbidden(
					"request serial number %q and token subject %q do not match",
					revokeOpts.Serial, claims.Subject,
				), opts...,
			)
		}

		// This method will also validate the audiences for JWK provisioners.
		p, err := a.LoadProvisionerByToken(token, &claims.Claims)
		if err != nil {
			return err
		}
		rci.ProvisionerID = p.GetID()
		rci.TokenID, err = p.GetTokenID(revokeOpts.OTT)
		if err != nil && !errors.Is(err, provisioner.ErrAllowTokenReuse) {
			return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke; could not get ID for token")
		}
		opts = append(opts,
			errs.WithKeyVal("provisionerID", rci.ProvisionerID),
			errs.WithKeyVal("tokenID", rci.TokenID),
		)
	} else if p, err := a.LoadProvisionerByCertificate(revokeOpts.Crt); err == nil {
		// Load the Certificate provisioner if one exists.
		rci.ProvisionerID = p.GetID()
		opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID))
	}

	failRevoke := func(err error) error {
		switch {
		case errors.Is(err, db.ErrNotImplemented):
			return errs.NotImplemented("authority.Revoke; no persistence layer configured", opts...)
		case errors.Is(err, db.ErrAlreadyExists):
			return errs.ApplyOptions(
				errs.BadRequest("certificate with serial number '%s' is already revoked", rci.Serial),
				opts...,
			)
		default:
			return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
		}
	}

	if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod {
		if err := a.revokeSSH(nil, rci); err != nil {
			return failRevoke(err)
		}
	} else {
		// Revoke an X.509 certificate using CAS. If the certificate is not
		// provided we will try to read it from the db. If the read fails we
		// won't throw an error as it will be responsibility of the CAS
		// implementation to require a certificate.
		var revokedCert *x509.Certificate
		if revokeOpts.Crt != nil {
			revokedCert = revokeOpts.Crt
		} else if rci.Serial != "" {
			revokedCert, _ = a.db.GetCertificate(rci.Serial)
		}

		// CAS operation, note that SoftCAS (default) is a noop.
		// The revoke happens when this is stored in the db.
		_, err := a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{
			Certificate:  revokedCert,
			SerialNumber: rci.Serial,
			Reason:       rci.Reason,
			ReasonCode:   rci.ReasonCode,
			PassiveOnly:  revokeOpts.PassiveOnly,
		})
		if err != nil {
			return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
		}

		// Save as revoked in the Db.
		if err := a.revoke(revokedCert, rci); err != nil {
			return failRevoke(err)
		}

		// Generate a new CRL so CRL requesters will always get an up-to-date
		// CRL whenever they request it.
		if a.config.CRL.IsEnabled() && a.config.CRL.GenerateOnRevoke {
			if err := a.GenerateCertificateRevocationList(); err != nil {
				return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
			}
		}
	}

	return nil
}

func (a *Authority) revoke(crt *x509.Certificate, rci *db.RevokedCertificateInfo) error {
	if lca, ok := a.adminDB.(interface {
		Revoke(*x509.Certificate, *db.RevokedCertificateInfo) error
	}); ok {
		return lca.Revoke(crt, rci)
	}
	return a.db.Revoke(rci)
}

func (a *Authority) revokeSSH(crt *ssh.Certificate, rci *db.RevokedCertificateInfo) error {
	if lca, ok := a.adminDB.(interface {
		RevokeSSH(*ssh.Certificate, *db.RevokedCertificateInfo) error
	}); ok {
		return lca.RevokeSSH(crt, rci)
	}
	return a.db.RevokeSSH(rci)
}

// CertificateRevocationListInfo contains a CRL in DER format and associated metadata.
type CertificateRevocationListInfo struct {
	Number    int64
	ExpiresAt time.Time
	Duration  time.Duration
	Data      []byte
}

// GetCertificateRevocationList will return the currently generated CRL from the DB, or a not implemented
// error if the underlying AuthDB does not support CRLs
func (a *Authority) GetCertificateRevocationList() (*CertificateRevocationListInfo, error) {
	if !a.config.CRL.IsEnabled() {
		return nil, errs.Wrap(http.StatusNotFound, errors.Errorf("Certificate Revocation Lists are not enabled"), "authority.GetCertificateRevocationList")
	}

	crlDB, ok := a.db.(db.CertificateRevocationListDB)
	if !ok {
		return nil, errs.Wrap(http.StatusNotImplemented, errors.Errorf("Database does not support Certificate Revocation Lists"), "authority.GetCertificateRevocationList")
	}

	crlInfo, err := crlDB.GetCRL()
	if err != nil {
		return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetCertificateRevocationList")
	}

	return &CertificateRevocationListInfo{
		Number:    crlInfo.Number,
		ExpiresAt: crlInfo.ExpiresAt,
		Duration:  crlInfo.Duration,
		Data:      crlInfo.DER,
	}, nil
}

// GenerateCertificateRevocationList generates a DER representation of a signed CRL and stores it in the
// database. Returns nil if CRL generation has been disabled in the config
func (a *Authority) GenerateCertificateRevocationList() error {
	if !a.config.CRL.IsEnabled() {
		return nil
	}

	crlDB, ok := a.db.(db.CertificateRevocationListDB)
	if !ok {
		return errors.Errorf("Database does not support CRL generation")
	}

	// some CAS may not implement the CRLGenerator interface, so check before we proceed
	caCRLGenerator, ok := a.x509CAService.(casapi.CertificateAuthorityCRLGenerator)
	if !ok {
		return errors.Errorf("CA does not support CRL Generation")
	}

	// use a mutex to ensure only one CRL is generated at a time to avoid
	// concurrency issues
	a.crlMutex.Lock()
	defer a.crlMutex.Unlock()

	crlInfo, err := crlDB.GetCRL()
	if err != nil && !database.IsErrNotFound(err) {
		return errors.Wrap(err, "could not retrieve CRL from database")
	}

	now := time.Now().Truncate(time.Second).UTC()
	revokedList, err := crlDB.GetRevokedCertificates()
	if err != nil {
		return errors.Wrap(err, "could not retrieve revoked certificates list from database")
	}

	// Number is a monotonically increasing integer (essentially the CRL version
	// number) that we need to keep track of and increase every time we generate
	// a new CRL
	var bn big.Int
	if crlInfo != nil {
		bn.SetInt64(crlInfo.Number + 1)
	}

	// Convert our database db.RevokedCertificateInfo types into the x509
	// representation ready for the CAS to sign it
	var revokedCertificateEntries []x509.RevocationListEntry
	skipExpiredTime := now.Add(-config.DefaultCRLExpiredDuration)
	for _, revokedCert := range *revokedList {
		// skip expired certificates
		if !revokedCert.ExpiresAt.IsZero() && revokedCert.ExpiresAt.Before(skipExpiredTime) {
			continue
		}

		var sn big.Int
		sn.SetString(revokedCert.Serial, 10)
		revokedCertificateEntries = append(revokedCertificateEntries, x509.RevocationListEntry{
			SerialNumber:   &sn,
			RevocationTime: revokedCert.RevokedAt,
			ReasonCode:     revokedCert.ReasonCode,
		})
	}

	var updateDuration time.Duration
	if a.config.CRL.CacheDuration != nil {
		updateDuration = a.config.CRL.CacheDuration.Duration
	} else if crlInfo != nil {
		updateDuration = crlInfo.Duration
	}

	// Create a RevocationList representation ready for the CAS to sign
	// TODO: allow SignatureAlgorithm to be specified?
	revocationList := x509.RevocationList{
		SignatureAlgorithm:        0,
		RevokedCertificateEntries: revokedCertificateEntries,
		Number:                    &bn,
		ThisUpdate:                now,
		NextUpdate:                now.Add(updateDuration),
	}

	// Set CRL IDP to config item, otherwise, leave as default
	var fullName string
	if a.config.CRL.IDPurl != "" {
		fullName = a.config.CRL.IDPurl
	} else {
		fullName = a.config.Audience("/1.0/crl")[0]
	}

	// Add distribution point.
	//
	// Note that this is currently using the port 443 by default.
	if b, err := marshalDistributionPoint(fullName); err == nil {
		revocationList.ExtraExtensions = []pkix.Extension{
			{Id: oidExtensionIssuingDistributionPoint, Critical: true, Value: b},
		}
	}

	certificateRevocationList, err := caCRLGenerator.CreateCRL(&casapi.CreateCRLRequest{RevocationList: &revocationList})
	if err != nil {
		return errors.Wrap(err, "could not create CRL")
	}

	// Create a new db.CertificateRevocationListInfo, which stores the new Number we just generated, the
	// expiry time, duration, and the DER-encoded CRL
	newCRLInfo := db.CertificateRevocationListInfo{
		Number:    bn.Int64(),
		ExpiresAt: revocationList.NextUpdate,
		DER:       certificateRevocationList.CRL,
		Duration:  updateDuration,
	}

	// Store the CRL in the database ready for retrieval by api endpoints
	err = crlDB.StoreCRL(&newCRLInfo)
	if err != nil {
		return errors.Wrap(err, "could not store CRL in database")
	}

	return nil
}

// GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server.
func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
	fatal := func(err error) (*tls.Certificate, error) {
		return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate")
	}

	// Generate default key.
	priv, err := keyutil.GenerateDefaultKey()
	if err != nil {
		return fatal(err)
	}
	signer, ok := priv.(crypto.Signer)
	if !ok {
		return fatal(errors.New("private key is not a crypto.Signer"))
	}

	// prepare the sans: IPv6 DNS hostname representations are converted to their IP representation
	sans := make([]string, len(a.config.DNSNames))
	for i, san := range a.config.DNSNames {
		if strings.HasPrefix(san, "[") && strings.HasSuffix(san, "]") {
			if ip := net.ParseIP(san[1 : len(san)-1]); ip != nil {
				san = ip.String()
			}
		}
		sans[i] = san
	}

	// Create initial certificate request.
	cr, err := x509util.CreateCertificateRequest(a.config.CommonName, sans, signer)
	if err != nil {
		return fatal(err)
	}

	// Generate certificate template directly from the certificate request.
	template, err := x509util.NewCertificate(cr)
	if err != nil {
		return fatal(err)
	}

	// Get x509 certificate template, set validity and sign it.
	now := time.Now()
	certTpl := template.GetCertificate()
	certTpl.NotBefore = now.Add(-1 * time.Minute)
	certTpl.NotAfter = now.Add(24 * time.Hour)

	// Policy and constraints require this fields to be set. At this moment they
	// are only present in the extra extension.
	certTpl.DNSNames = cr.DNSNames
	certTpl.IPAddresses = cr.IPAddresses
	certTpl.EmailAddresses = cr.EmailAddresses
	certTpl.URIs = cr.URIs

	// Fail if name constraints do not allow the server names.
	if err := a.constraintsEngine.ValidateCertificate(certTpl); err != nil {
		return fatal(err)
	}

	// Set the cert lifetime as follows:
	//   i) If the CA is not a StepCAS RA use 24h, else
	//  ii) if the CA is a StepCAS RA, leave the lifetime empty and
	//      let the provisioner of the CA decide the lifetime of the RA cert.
	var lifetime time.Duration
	if casapi.TypeOf(a.x509CAService) != casapi.StepCAS {
		lifetime = 24 * time.Hour
	}

	resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
		Template:       certTpl,
		CSR:            cr,
		Lifetime:       lifetime,
		Backdate:       1 * time.Minute,
		IsCAServerCert: true,
	})
	if err != nil {
		return fatal(err)
	}

	// Generate PEM blocks to create tls.Certificate
	pemBlocks := pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: resp.Certificate.Raw,
	})
	for _, crt := range resp.CertificateChain {
		pemBlocks = append(pemBlocks, pem.EncodeToMemory(&pem.Block{
			Type:  "CERTIFICATE",
			Bytes: crt.Raw,
		})...)
	}
	keyPEM, err := pemutil.Serialize(priv)
	if err != nil {
		return fatal(err)
	}

	tlsCrt, err := tls.X509KeyPair(pemBlocks, pem.EncodeToMemory(keyPEM))
	if err != nil {
		return fatal(err)
	}
	// Set leaf certificate
	tlsCrt.Leaf = resp.Certificate
	return &tlsCrt, nil
}

// RFC 5280, 5.2.5
type distributionPoint struct {
	DistributionPoint          distributionPointName `asn1:"optional,tag:0"`
	OnlyContainsUserCerts      bool                  `asn1:"optional,tag:1"`
	OnlyContainsCACerts        bool                  `asn1:"optional,tag:2"`
	OnlySomeReasons            asn1.BitString        `asn1:"optional,tag:3"`
	IndirectCRL                bool                  `asn1:"optional,tag:4"`
	OnlyContainsAttributeCerts bool                  `asn1:"optional,tag:5"`
}

type distributionPointName struct {
	FullName     []asn1.RawValue  `asn1:"optional,tag:0"`
	RelativeName pkix.RDNSequence `asn1:"optional,tag:1"`
}

/*
marshalDistributionPoint currently marshals only DP, citing spec
https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5:

	That is, if onlyContainsUserCerts, onlyContainsCACerts, indirectCRL, and
	onlyContainsAttributeCerts are all FALSE, then either the
	distributionPoint field or the onlySomeReasons field MUST be present.
*/
func marshalDistributionPoint(fullName string) ([]byte, error) {
	return asn1.Marshal(distributionPoint{
		DistributionPoint: distributionPointName{
			FullName: []asn1.RawValue{
				{Class: 2, Tag: 6, Bytes: []byte(fullName)},
			},
		},
	})
}

// templatingError tries to extract more information about the cause of
// an error related to (most probably) malformed template data and adds
// this to the error message.
func templatingError(err error) error {
	cause := errors.Cause(err)
	var (
		syntaxError *json.SyntaxError
		typeError   *json.UnmarshalTypeError
	)
	if errors.As(err, &syntaxError) {
		// offset is arguably not super clear to the user, but it's the best we can do here
		cause = fmt.Errorf("%w at offset %d", cause, syntaxError.Offset)
	} else if errors.As(err, &typeError) {
		// slightly rewriting the default error message to include the offset
		cause = fmt.Errorf("cannot unmarshal %s at offset %d into Go value of type %s", typeError.Value, typeError.Offset, typeError.Type)
	}
	return errors.Wrap(cause, "error applying certificate template")
}

func (a *Authority) callEnrichingWebhooksX509(ctx context.Context, prov provisioner.Interface, webhookCtl webhookController, attData *provisioner.AttestationData, csr *x509.CertificateRequest) (err error) {
	if webhookCtl == nil {
		return
	}
	defer func() { a.meter.X509WebhookEnriched(prov, err) }()

	var attested *webhook.AttestationData
	if attData != nil {
		attested = &webhook.AttestationData{
			PermanentIdentifier: attData.PermanentIdentifier,
		}
	}

	var whEnrichReq *webhook.RequestBody
	if whEnrichReq, err = webhook.NewRequestBody(
		webhook.WithX509CertificateRequest(csr),
		webhook.WithAttestationData(attested),
	); err == nil {
		err = webhookCtl.Enrich(ctx, whEnrichReq)
	}

	return
}

func (a *Authority) callAuthorizingWebhooksX509(ctx context.Context, prov provisioner.Interface, webhookCtl webhookController, cert *x509util.Certificate, leaf *x509.Certificate, attData *provisioner.AttestationData) (err error) {
	if webhookCtl == nil {
		return
	}
	defer func() { a.meter.X509WebhookAuthorized(prov, err) }()

	var attested *webhook.AttestationData
	if attData != nil {
		attested = &webhook.AttestationData{
			PermanentIdentifier: attData.PermanentIdentifier,
		}
	}

	var whAuthBody *webhook.RequestBody
	if whAuthBody, err = webhook.NewRequestBody(
		webhook.WithX509Certificate(cert, leaf),
		webhook.WithAttestationData(attested),
	); err == nil {
		err = webhookCtl.Authorize(ctx, whAuthBody)
	}

	return
}
