package authority

import (
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha1" //nolint:gosec // used to create the Subject Key Identifier by RFC 5280
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/pem"
	"errors"
	"fmt"
	"net/http"
	"reflect"
	"strings"
	"testing"
	"time"

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

	"github.com/smallstep/certificates/api/render"
	"github.com/smallstep/certificates/authority/config"
	"github.com/smallstep/certificates/authority/policy"
	"github.com/smallstep/certificates/authority/provisioner"
	"github.com/smallstep/certificates/cas/apiv1"
	"github.com/smallstep/certificates/cas/softcas"
	"github.com/smallstep/certificates/db"
	"github.com/smallstep/certificates/errs"
	"github.com/smallstep/nosql/database"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var (
	stepOIDRoot        = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64}
	stepOIDProvisioner = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 1)...)
)

const provisionerTypeJWK = 1

type stepProvisionerASN1 struct {
	Type         int
	Name         []byte
	CredentialID []byte
}

type certificateDurationEnforcer struct {
	NotBefore time.Time
	NotAfter  time.Time
}

func (m *certificateDurationEnforcer) Enforce(cert *x509.Certificate) error {
	cert.NotBefore = m.NotBefore
	cert.NotAfter = m.NotAfter
	return nil
}

type certificateChainDB struct {
	db.MockAuthDB
	MStoreCertificateChain func(provisioner.Interface, ...*x509.Certificate) error
}

func (d *certificateChainDB) StoreCertificateChain(p provisioner.Interface, certs ...*x509.Certificate) error {
	return d.MStoreCertificateChain(p, certs...)
}

func getDefaultIssuer(a *Authority) *x509.Certificate {
	return a.x509CAService.(*softcas.SoftCAS).CertificateChain[len(a.x509CAService.(*softcas.SoftCAS).CertificateChain)-1]
}

func getDefaultSigner(a *Authority) crypto.Signer {
	return a.x509CAService.(*softcas.SoftCAS).Signer
}

func generateCertificate(t *testing.T, commonName string, sans []string, opts ...interface{}) *x509.Certificate {
	t.Helper()

	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	require.NoError(t, err)

	cr, err := x509util.CreateCertificateRequest(commonName, sans, priv)
	require.NoError(t, err)

	template, err := x509util.NewCertificate(cr)
	require.NoError(t, err)

	cert := template.GetCertificate()
	for _, m := range opts {
		switch m := m.(type) {
		case provisioner.CertificateModifierFunc:
			err = m.Modify(cert, provisioner.SignOptions{})
			require.NoError(t, err)
		case signerFunc:
			cert, err = m(cert, priv.Public())
			require.NoError(t, err)
		default:
			require.Fail(t, "", "unknown type %T", m)
		}

	}
	return cert
}

func generateRootCertificate(t *testing.T) (*x509.Certificate, crypto.Signer) {
	t.Helper()
	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	require.NoError(t, err)

	cr, err := x509util.CreateCertificateRequest("TestRootCA", nil, priv)
	require.NoError(t, err)

	data := x509util.CreateTemplateData("TestRootCA", nil)
	template, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data))
	require.NoError(t, err)

	cert := template.GetCertificate()
	cert, err = x509util.CreateCertificate(cert, cert, priv.Public(), priv)
	require.NoError(t, err)
	return cert, priv
}

func generateIntermidiateCertificate(t *testing.T, issuer *x509.Certificate, signer crypto.Signer) (*x509.Certificate, crypto.Signer) {
	t.Helper()
	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	require.NoError(t, err)

	cr, err := x509util.CreateCertificateRequest("TestIntermediateCA", nil, priv)
	require.NoError(t, err)

	data := x509util.CreateTemplateData("TestIntermediateCA", nil)
	template, err := x509util.NewCertificate(cr, x509util.WithTemplate(x509util.DefaultRootTemplate, data))
	require.NoError(t, err)

	cert := template.GetCertificate()
	cert, err = x509util.CreateCertificate(cert, issuer, priv.Public(), signer)
	require.NoError(t, err)
	return cert, priv
}

func withSubject(sub pkix.Name) provisioner.CertificateModifierFunc {
	return func(crt *x509.Certificate, _ provisioner.SignOptions) error {
		crt.Subject = sub
		return nil
	}
}

func withProvisionerOID(name, kid string) provisioner.CertificateModifierFunc {
	return func(crt *x509.Certificate, _ provisioner.SignOptions) error {
		b, err := asn1.Marshal(stepProvisionerASN1{
			Type:         provisionerTypeJWK,
			Name:         []byte(name),
			CredentialID: []byte(kid),
		})
		if err != nil {
			return err
		}
		crt.ExtraExtensions = append(crt.ExtraExtensions, pkix.Extension{
			Id:       stepOIDProvisioner,
			Critical: false,
			Value:    b,
		})
		return nil
	}
}

func withNotBeforeNotAfter(notBefore, notAfter time.Time) provisioner.CertificateModifierFunc {
	return func(crt *x509.Certificate, _ provisioner.SignOptions) error {
		crt.NotBefore = notBefore
		crt.NotAfter = notAfter
		return nil
	}
}

type signerFunc func(crt *x509.Certificate, pub crypto.PublicKey) (*x509.Certificate, error)

func withSigner(issuer *x509.Certificate, signer crypto.Signer) signerFunc {
	return func(crt *x509.Certificate, pub crypto.PublicKey) (*x509.Certificate, error) {
		return x509util.CreateCertificate(crt, issuer, pub, signer)
	}
}

func getCSR(t *testing.T, priv interface{}, opts ...func(*x509.CertificateRequest)) *x509.CertificateRequest {
	_csr := &x509.CertificateRequest{
		Subject:  pkix.Name{CommonName: "smallstep test"},
		DNSNames: []string{"test.smallstep.com"},
	}
	for _, opt := range opts {
		opt(_csr)
	}
	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, _csr, priv)
	require.NoError(t, err)
	csr, err := x509.ParseCertificateRequest(csrBytes)
	require.NoError(t, err)
	return csr
}

func setExtraExtsCSR(exts []pkix.Extension) func(*x509.CertificateRequest) {
	return func(csr *x509.CertificateRequest) {
		csr.ExtraExtensions = exts
	}
}

func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) {
	b, err := x509.MarshalPKIXPublicKey(pub)
	if err != nil {
		return nil, fmt.Errorf("error marshaling public key: %w", err)
	}
	info := struct {
		Algorithm        pkix.AlgorithmIdentifier
		SubjectPublicKey asn1.BitString
	}{}
	if _, err = asn1.Unmarshal(b, &info); err != nil {
		return nil, fmt.Errorf("error unmarshaling public key: %w", err)
	}
	//nolint:gosec // used to create the Subject Key Identifier by RFC 5280
	hash := sha1.Sum(info.SubjectPublicKey.Bytes)
	return hash[:], nil
}

type basicConstraints struct {
	IsCA       bool `asn1:"optional"`
	MaxPathLen int  `asn1:"optional,default:-1"`
}

type testEnforcer struct {
	enforcer func(*x509.Certificate) error
}

func (e *testEnforcer) Enforce(cert *x509.Certificate) error {
	if e.enforcer != nil {
		return e.enforcer(cert)
	}
	return nil
}

func assertHasPrefix(t *testing.T, s, p string) bool {
	t.Helper()
	return assert.True(t, strings.HasPrefix(s, p), "%q is not a prefix of %q", p, s)
}

func TestAuthority_SignWithContext(t *testing.T) {
	pub, priv, err := keyutil.GenerateDefaultKeyPair()
	require.NoError(t, err)

	a := testAuthority(t)
	require.NoError(t, err)
	a.config.AuthorityConfig.Template = &ASN1DN{
		Country:       "Tazmania",
		Organization:  "Acme Co",
		Locality:      "Landscapes",
		Province:      "Sudden Cliffs",
		StreetAddress: "TNT",
		CommonName:    "test.smallstep.com",
	}

	nb := time.Now()
	signOpts := provisioner.SignOptions{
		NotBefore: provisioner.NewTimeDuration(nb),
		NotAfter:  provisioner.NewTimeDuration(nb.Add(time.Minute * 5)),
		Backdate:  1 * time.Minute,
	}

	// Create a token to get test extra opts.
	p := a.config.AuthorityConfig.Provisioners[1].(*provisioner.JWK)
	key, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
	require.NoError(t, err)
	token, err := generateToken("smallstep test", "step-cli", testAudiences.Sign[0], []string{"test.smallstep.com"}, time.Now(), key)
	require.NoError(t, err)
	ctx := provisioner.NewContextWithMethod(context.Background(), provisioner.SignMethod)
	extraOpts, err := a.Authorize(ctx, token)
	require.NoError(t, err)

	type signTest struct {
		auth            *Authority
		csr             *x509.CertificateRequest
		signOpts        provisioner.SignOptions
		extraOpts       []provisioner.SignOption
		notBefore       time.Time
		notAfter        time.Time
		extensionsCount int
		err             error
		code            int
	}
	tests := map[string]func(*testing.T) *signTest{
		"fail invalid signature": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			csr.Signature = []byte("foo")
			return &signTest{
				auth:      a,
				csr:       csr,
				extraOpts: extraOpts,
				signOpts:  signOpts,
				err:       errors.New("invalid certificate request"),
				code:      http.StatusBadRequest,
			}
		},
		"fail invalid extra option": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			csr.Raw = []byte("foo")
			return &signTest{
				auth:      a,
				csr:       csr,
				extraOpts: append(extraOpts, "42"),
				signOpts:  signOpts,
				err:       errors.New("authority.Sign; invalid extra option type string"),
				code:      http.StatusInternalServerError,
			}
		},
		"fail merge default ASN1DN": func(t *testing.T) *signTest {
			_a := testAuthority(t)
			_a.config.AuthorityConfig.Template = nil
			csr := getCSR(t, priv)
			return &signTest{
				auth:      _a,
				csr:       csr,
				extraOpts: extraOpts,
				signOpts:  signOpts,
				err:       errors.New("default ASN1DN template cannot be nil"),
				code:      http.StatusForbidden,
			}
		},
		"fail create cert": func(t *testing.T) *signTest {
			_a := testAuthority(t)
			_a.x509CAService.(*softcas.SoftCAS).Signer = nil
			csr := getCSR(t, priv)
			return &signTest{
				auth:      _a,
				csr:       csr,
				extraOpts: extraOpts,
				signOpts:  signOpts,
				err:       errors.New("authority.Sign; error creating certificate"),
				code:      http.StatusInternalServerError,
			}
		},
		"fail provisioner duration claim": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			_signOpts := provisioner.SignOptions{
				NotBefore: provisioner.NewTimeDuration(nb),
				NotAfter:  provisioner.NewTimeDuration(nb.Add(time.Hour * 25)),
			}
			return &signTest{
				auth:      a,
				csr:       csr,
				extraOpts: extraOpts,
				signOpts:  _signOpts,
				err:       errors.New("requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h1m0s"),
				code:      http.StatusForbidden,
			}
		},
		"fail validate sans when adding common name not in claims": func(t *testing.T) *signTest {
			csr := getCSR(t, priv, func(csr *x509.CertificateRequest) {
				csr.DNSNames = append(csr.DNSNames, csr.Subject.CommonName)
			})
			return &signTest{
				auth:      a,
				csr:       csr,
				extraOpts: extraOpts,
				signOpts:  signOpts,
				err:       errors.New("certificate request does not contain the valid DNS names - got [test.smallstep.com smallstep test], want [test.smallstep.com]"),
				code:      http.StatusForbidden,
			}
		},
		"fail rsa key too short": func(t *testing.T) *signTest {
			shortRSAKeyPEM := `-----BEGIN CERTIFICATE REQUEST-----
MIIBhDCB7gIBADAZMRcwFQYDVQQDEw5zbWFsbHN0ZXAgdGVzdDCBnzANBgkqhkiG
9w0BAQEFAAOBjQAwgYkCgYEA5JlgH99HvHHsCD6XTqqYj3bXU2oIlnYGoLVs7IJ4
k205rv5/YWky2gjdpIv0Tnaf3o57IJ891lB7GiyO5iHIEUv5N9dVzrdUboyzk2uZ
7JMMNB43CSLB2oNuwJjLeAM/yBzlhRnvpKjrNSfSV+cH54FXdnbFbcTFMStnjqKG
MeECAwEAAaAsMCoGCSqGSIb3DQEJDjEdMBswGQYDVR0RBBIwEIIOc21hbGxzdGVw
IHRlc3QwDQYJKoZIhvcNAQELBQADgYEAKwsbr8Zfcq05DgOoJ//cXMFK1SP8ktRU
N2++E8Ww0Tet9oyNRArqxxS/UyVio63D3wynzRAB25PFGpYG1cN4b81Gv/foFUT6
W5kR63lNVHBHgQmv5mA8YFsfrJHstaz5k727v2LMHEYIf5/3i16d5zhuxUoaPTYr
ZYtQ9Ot36qc=
-----END CERTIFICATE REQUEST-----`
			block, _ := pem.Decode([]byte(shortRSAKeyPEM))
			require.NoError(t, err)
			csr, err := x509.ParseCertificateRequest(block.Bytes)
			require.NoError(t, err)

			return &signTest{
				auth:      a,
				csr:       csr,
				extraOpts: extraOpts,
				signOpts:  signOpts,
				err:       errors.New("certificate request RSA key must be at least 2048 bits (256 bytes)"),
				code:      http.StatusForbidden,
			}
		},
		"fail store cert in db": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			_a := testAuthority(t)
			_a.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					return errors.New("force")
				},
			}
			return &signTest{
				auth:      _a,
				csr:       csr,
				extraOpts: extraOpts,
				signOpts:  signOpts,
				err:       errors.New("authority.Sign; error storing certificate in db: force"),
				code:      http.StatusInternalServerError,
			}
		},
		"fail custom template": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			testAuthority := testAuthority(t)
			p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
			if !ok {
				t.Fatal("provisioner not found")
			}
			p.(*provisioner.JWK).Options = &provisioner.Options{
				X509: &provisioner.X509Options{Template: `{{ fail "fail message" }}`},
			}
			testExtraOpts, err := testAuthority.Authorize(ctx, token)
			require.NoError(t, err)
			testAuthority.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, "smallstep test", crt.Subject.CommonName)
					return nil
				},
			}
			return &signTest{
				auth:      testAuthority,
				csr:       csr,
				extraOpts: testExtraOpts,
				signOpts:  signOpts,
				err:       errors.New("fail message"),
				code:      http.StatusBadRequest,
			}
		},
		"fail bad JSON syntax template file": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			testAuthority := testAuthority(t)
			p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
			if !ok {
				t.Fatal("provisioner not found")
			}
			p.(*provisioner.JWK).Options = &provisioner.Options{
				X509: &provisioner.X509Options{
					TemplateFile: "./testdata/templates/badjsonsyntax.tpl",
				},
			}
			testExtraOpts, err := testAuthority.Authorize(ctx, token)
			require.NoError(t, err)
			testAuthority.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, "smallstep test", crt.Subject.CommonName)
					return nil
				},
			}
			return &signTest{
				auth:      testAuthority,
				csr:       csr,
				extraOpts: testExtraOpts,
				signOpts:  signOpts,
				err:       errors.New("error applying certificate template: invalid character"),
				code:      http.StatusInternalServerError,
			}
		},
		"fail bad JSON value template file": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			testAuthority := testAuthority(t)
			p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
			if !ok {
				t.Fatal("provisioner not found")
			}
			p.(*provisioner.JWK).Options = &provisioner.Options{
				X509: &provisioner.X509Options{
					TemplateFile: "./testdata/templates/badjsonvalue.tpl",
				},
			}
			testExtraOpts, err := testAuthority.Authorize(ctx, token)
			require.NoError(t, err)
			testAuthority.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, "smallstep test", crt.Subject.CommonName)
					return nil
				},
			}
			return &signTest{
				auth:      testAuthority,
				csr:       csr,
				extraOpts: testExtraOpts,
				signOpts:  signOpts,
				err:       errors.New("error applying certificate template: cannot unmarshal"),
				code:      http.StatusInternalServerError,
			}
		},
		"fail with provisioner enforcer": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			aa := testAuthority(t)
			aa.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, "smallstep test", crt.Subject.CommonName)
					return nil
				},
			}

			return &signTest{
				auth: aa,
				csr:  csr,
				extraOpts: append(extraOpts, &testEnforcer{
					enforcer: func(crt *x509.Certificate) error { return fmt.Errorf("an error") },
				}),
				signOpts: signOpts,
				err:      errors.New("error creating certificate"),
				code:     http.StatusForbidden,
			}
		},
		"fail with custom enforcer": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			aa := testAuthority(t, WithX509Enforcers(&testEnforcer{
				enforcer: func(cert *x509.Certificate) error {
					return fmt.Errorf("an error")
				},
			}))
			aa.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, "smallstep test", crt.Subject.CommonName)
					return nil
				},
			}
			return &signTest{
				auth:      aa,
				csr:       csr,
				extraOpts: extraOpts,
				signOpts:  signOpts,
				err:       errors.New("error creating certificate"),
				code:      http.StatusForbidden,
			}
		},
		"fail with policy": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			aa := testAuthority(t)
			aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
			aa.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					fmt.Println(crt.Subject)
					assert.Equal(t, "smallstep test", crt.Subject.CommonName)
					return nil
				},
			}
			options := &policy.Options{
				X509: &policy.X509PolicyOptions{
					DeniedNames: &policy.X509NameOptions{
						DNSDomains: []string{"test.smallstep.com"},
					},
				},
			}
			engine, err := policy.New(options)
			require.NoError(t, err)
			aa.policyEngine = engine
			return &signTest{
				auth:            aa,
				csr:             csr,
				extraOpts:       extraOpts,
				signOpts:        signOpts,
				notBefore:       signOpts.NotBefore.Time().Truncate(time.Second),
				notAfter:        signOpts.NotAfter.Time().Truncate(time.Second),
				extensionsCount: 6,
				err:             errors.New("dns name \"test.smallstep.com\" not allowed"),
				code:            http.StatusForbidden,
			}
		},
		"fail enriching webhooks": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			csr.Raw = []byte("foo")
			return &signTest{
				auth:            a,
				csr:             csr,
				extensionsCount: 7,
				extraOpts: append(extraOpts, &mockWebhookController{
					enrichErr: provisioner.ErrWebhookDenied,
				}),
				signOpts: signOpts,
				err:      provisioner.ErrWebhookDenied,
				code:     http.StatusForbidden,
			}
		},
		"fail authorizing webhooks": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			csr.Raw = []byte("foo")
			return &signTest{
				auth:            a,
				csr:             csr,
				extensionsCount: 7,
				extraOpts: append(extraOpts, &mockWebhookController{
					authorizeErr: provisioner.ErrWebhookDenied,
				}),
				signOpts: signOpts,
				err:      provisioner.ErrWebhookDenied,
				code:     http.StatusForbidden,
			}
		},
		"fail with cnf": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)

			auth := testAuthority(t)
			auth.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
			auth.db = &db.MockAuthDB{
				MUseToken: func(id, tok string) (bool, error) {
					return true, nil
				},
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, crt.Subject.CommonName, "smallstep test")
					assert.Equal(t, crt.DNSNames, []string{"test.smallstep.com"})
					return nil
				},
			}

			// Create a token with cnf
			tok, err := generateCustomToken("smallstep test", "step-cli", testAudiences.Sign[0], key, nil, map[string]any{
				"sans": []string{"test.smallstep.com"},
				"cnf":  map[string]any{"x5rt#S256": "bad-fingerprint"},
			})
			require.NoError(t, err)

			opts, err := auth.Authorize(ctx, tok)
			require.NoError(t, err)

			return &signTest{
				auth:      auth,
				csr:       csr,
				extraOpts: opts,
				signOpts:  signOpts,
				notBefore: signOpts.NotBefore.Time().Truncate(time.Second),
				notAfter:  signOpts.NotAfter.Time().Truncate(time.Second),
				err:       errors.New(`certificate request fingerprint does not match "bad-fingerprint"`),
				code:      http.StatusForbidden,
			}
		},
		"ok": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			_a := testAuthority(t)
			_a.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, "smallstep test", crt.Subject.CommonName)
					return nil
				},
			}
			return &signTest{
				auth:            a,
				csr:             csr,
				extraOpts:       extraOpts,
				signOpts:        signOpts,
				notBefore:       signOpts.NotBefore.Time().Truncate(time.Second),
				notAfter:        signOpts.NotAfter.Time().Truncate(time.Second),
				extensionsCount: 6,
			}
		},
		"ok with enforced modifier": func(t *testing.T) *signTest {
			bcExt := pkix.Extension{}
			bcExt.Id = asn1.ObjectIdentifier{2, 5, 29, 19}
			bcExt.Critical = false
			bcExt.Value, err = asn1.Marshal(basicConstraints{IsCA: true, MaxPathLen: 4})
			require.NoError(t, err)

			csr := getCSR(t, priv, setExtraExtsCSR([]pkix.Extension{
				bcExt,
				{Id: stepOIDProvisioner, Value: []byte("foo")},
				{Id: []int{1, 1, 1}, Value: []byte("bar")}}))
			now := time.Now().UTC()
			//nolint:gocritic
			enforcedExtraOptions := append(extraOpts, &certificateDurationEnforcer{
				NotBefore: now,
				NotAfter:  now.Add(365 * 24 * time.Hour),
			})
			_a := testAuthority(t)
			_a.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, "smallstep test", crt.Subject.CommonName)
					return nil
				},
			}
			return &signTest{
				auth:            a,
				csr:             csr,
				extraOpts:       enforcedExtraOptions,
				signOpts:        signOpts,
				notBefore:       now.Truncate(time.Second),
				notAfter:        now.Add(365 * 24 * time.Hour).Truncate(time.Second),
				extensionsCount: 6,
			}
		},
		"ok with custom template": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			testAuthority := testAuthority(t)
			testAuthority.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
			p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
			if !ok {
				t.Fatal("provisioner not found")
			}
			p.(*provisioner.JWK).Options = &provisioner.Options{
				X509: &provisioner.X509Options{Template: `{
					"subject": {{toJson .Subject}},
					"dnsNames": {{ toJson .Insecure.CR.DNSNames }},
					"keyUsage": ["digitalSignature"],
					"extKeyUsage": ["serverAuth","clientAuth"]
				}`},
			}
			testExtraOpts, err := testAuthority.Authorize(ctx, token)
			require.NoError(t, err)
			testAuthority.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, "smallstep test", crt.Subject.CommonName)
					return nil
				},
			}
			return &signTest{
				auth:            testAuthority,
				csr:             csr,
				extraOpts:       testExtraOpts,
				signOpts:        signOpts,
				notBefore:       signOpts.NotBefore.Time().Truncate(time.Second),
				notAfter:        signOpts.NotAfter.Time().Truncate(time.Second),
				extensionsCount: 6,
			}
		},
		"ok with enriching webhook": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			testAuthority := testAuthority(t)
			testAuthority.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
			p, ok := testAuthority.provisioners.Load("step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc")
			if !ok {
				t.Fatal("provisioner not found")
			}
			p.(*provisioner.JWK).Options = &provisioner.Options{
				X509: &provisioner.X509Options{Template: `{
					"subject": {"commonName": {{ toJson .Webhooks.people.role }} },
					"dnsNames": {{ toJson .Insecure.CR.DNSNames }},
					"keyUsage": ["digitalSignature"],
					"extKeyUsage": ["serverAuth","clientAuth"]
				}`},
			}
			testExtraOpts, err := testAuthority.Authorize(ctx, token)
			require.NoError(t, err)
			testAuthority.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, "smallstep test", crt.Subject.CommonName)
					return nil
				},
			}
			for i, o := range testExtraOpts {
				if wc, ok := o.(*provisioner.WebhookController); ok {
					testExtraOpts[i] = &mockWebhookController{
						templateData: wc.TemplateData,
						respData:     map[string]any{"people": map[string]any{"role": "smallstep test"}},
					}
				}
			}
			return &signTest{
				auth:            testAuthority,
				csr:             csr,
				extraOpts:       testExtraOpts,
				signOpts:        signOpts,
				notBefore:       signOpts.NotBefore.Time().Truncate(time.Second),
				notAfter:        signOpts.NotAfter.Time().Truncate(time.Second),
				extensionsCount: 6,
			}
		},
		"ok/csr with no template critical SAN extension": func(t *testing.T) *signTest {
			csr := getCSR(t, priv, func(csr *x509.CertificateRequest) {
				csr.Subject = pkix.Name{}
			}, func(csr *x509.CertificateRequest) {
				csr.DNSNames = []string{"foo", "bar"}
			})
			now := time.Now().UTC()
			enforcedExtraOptions := []provisioner.SignOption{&certificateDurationEnforcer{
				NotBefore: now,
				NotAfter:  now.Add(365 * 24 * time.Hour),
			}}
			_a := testAuthority(t)
			_a.config.AuthorityConfig.Template = &ASN1DN{}
			_a.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, pkix.Name{}, crt.Subject)
					return nil
				},
			}
			return &signTest{
				auth:            _a,
				csr:             csr,
				extraOpts:       enforcedExtraOptions,
				signOpts:        provisioner.SignOptions{},
				notBefore:       now.Truncate(time.Second),
				notAfter:        now.Add(365 * 24 * time.Hour).Truncate(time.Second),
				extensionsCount: 5,
			}
		},
		"ok with custom enforcer": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			aa := testAuthority(t, WithX509Enforcers(&testEnforcer{
				enforcer: func(cert *x509.Certificate) error {
					cert.CRLDistributionPoints = []string{"http://ca.example.org/leaf.crl"}
					return nil
				},
			}))
			aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
			aa.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, "smallstep test", crt.Subject.CommonName)
					assert.Equal(t, []string{"http://ca.example.org/leaf.crl"}, crt.CRLDistributionPoints)
					return nil
				},
			}
			return &signTest{
				auth:            aa,
				csr:             csr,
				extraOpts:       extraOpts,
				signOpts:        signOpts,
				notBefore:       signOpts.NotBefore.Time().Truncate(time.Second),
				notAfter:        signOpts.NotAfter.Time().Truncate(time.Second),
				extensionsCount: 7,
			}
		},
		"ok with policy": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			aa := testAuthority(t)
			aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
			aa.db = &db.MockAuthDB{
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, crt.Subject.CommonName, "smallstep test")
					return nil
				},
			}
			options := &policy.Options{
				X509: &policy.X509PolicyOptions{
					AllowedNames: &policy.X509NameOptions{
						CommonNames: []string{"smallstep test"},
						DNSDomains:  []string{"*.smallstep.com"},
					},
				},
			}
			engine, err := policy.New(options)
			require.NoError(t, err)
			aa.policyEngine = engine
			return &signTest{
				auth:            aa,
				csr:             csr,
				extraOpts:       extraOpts,
				signOpts:        signOpts,
				notBefore:       signOpts.NotBefore.Time().Truncate(time.Second),
				notAfter:        signOpts.NotAfter.Time().Truncate(time.Second),
				extensionsCount: 6,
			}
		},
		"ok with attestation data": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			aa := testAuthority(t)
			aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
			aa.db = &certificateChainDB{
				MStoreCertificateChain: func(prov provisioner.Interface, certs ...*x509.Certificate) error {
					p, ok := prov.(attProvisioner)
					if assert.True(t, ok) {
						assert.Equal(t, &provisioner.AttestationData{
							PermanentIdentifier: "1234567890",
						}, p.AttestationData())
					}
					if assert.Len(t, certs, 2) {
						assert.Equal(t, "smallstep test", certs[0].Subject.CommonName)
						assert.Equal(t, "smallstep Intermediate CA", certs[1].Subject.CommonName)
					}
					return nil
				},
			}

			return &signTest{
				auth: aa,
				csr:  csr,
				extraOpts: append(extraOpts, provisioner.AttestationData{
					PermanentIdentifier: "1234567890",
				}),
				signOpts:        signOpts,
				notBefore:       signOpts.NotBefore.Time().Truncate(time.Second),
				notAfter:        signOpts.NotAfter.Time().Truncate(time.Second),
				extensionsCount: 6,
			}
		},
		"ok with cnf": func(t *testing.T) *signTest {
			csr := getCSR(t, priv)
			fingerprint, err := fingerprint.New(csr.Raw, crypto.SHA256, fingerprint.Base64RawURLFingerprint)
			require.NoError(t, err)

			auth := testAuthority(t)
			auth.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
			auth.db = &db.MockAuthDB{
				MUseToken: func(id, tok string) (bool, error) {
					return true, nil
				},
				MStoreCertificate: func(crt *x509.Certificate) error {
					assert.Equal(t, crt.Subject.CommonName, "smallstep test")
					assert.Equal(t, crt.DNSNames, []string{"test.smallstep.com"})
					return nil
				},
			}

			// Create a token with cnf
			tok, err := generateCustomToken("smallstep test", "step-cli", testAudiences.Sign[0], key, nil, map[string]any{
				"sans": []string{"test.smallstep.com"},
				"cnf":  map[string]any{"x5rt#S256": fingerprint},
			})
			require.NoError(t, err)

			opts, err := auth.Authorize(ctx, tok)
			require.NoError(t, err)

			return &signTest{
				auth:            auth,
				csr:             csr,
				extraOpts:       opts,
				signOpts:        signOpts,
				notBefore:       signOpts.NotBefore.Time().Truncate(time.Second),
				notAfter:        signOpts.NotAfter.Time().Truncate(time.Second),
				extensionsCount: 6,
			}
		},
	}

	for name, genTestCase := range tests {
		t.Run(name, func(t *testing.T) {
			tc := genTestCase(t)

			certChain, err := tc.auth.SignWithContext(context.Background(), tc.csr, tc.signOpts, tc.extraOpts...)
			if err != nil {
				if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
					assert.Nil(t, certChain)
					var sc render.StatusCodedError
					require.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
					assert.Equal(t, tc.code, sc.StatusCode())
					assertHasPrefix(t, err.Error(), tc.err.Error())

					var ctxErr *errs.Error
					require.True(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
					assert.Equal(t, tc.csr, ctxErr.Details["csr"])
					assert.Equal(t, tc.signOpts, ctxErr.Details["signOptions"])
				}
			} else {
				leaf := certChain[0]
				intermediate := certChain[1]
				if assert.Nil(t, tc.err) {
					assert.Equal(t, tc.notBefore, leaf.NotBefore)
					assert.Equal(t, tc.notAfter, leaf.NotAfter)
					tmplt := a.config.AuthorityConfig.Template
					if tc.csr.Subject.CommonName == "" {
						assert.Equal(t, pkix.Name{}, leaf.Subject)
					} else {
						assert.Equal(t, pkix.Name{
							Country:       []string{tmplt.Country},
							Organization:  []string{tmplt.Organization},
							Locality:      []string{tmplt.Locality},
							StreetAddress: []string{tmplt.StreetAddress},
							Province:      []string{tmplt.Province},
							CommonName:    "smallstep test",
						}.String(), leaf.Subject.String())
						assert.Equal(t, []string{"test.smallstep.com"}, leaf.DNSNames)
					}
					assert.Equal(t, intermediate.Subject, leaf.Issuer)
					assert.Equal(t, x509.ECDSAWithSHA256, leaf.SignatureAlgorithm)
					assert.Equal(t, x509.ECDSA, leaf.PublicKeyAlgorithm)
					assert.Equal(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, leaf.ExtKeyUsage)

					issuer := getDefaultIssuer(a)
					subjectKeyID, err := generateSubjectKeyID(pub)
					require.NoError(t, err)
					assert.Equal(t, subjectKeyID, leaf.SubjectKeyId)
					assert.Equal(t, issuer.SubjectKeyId, leaf.AuthorityKeyId)

					// Verify Provisioner OID
					found := 0
					for _, ext := range leaf.Extensions {
						switch {
						case ext.Id.Equal(stepOIDProvisioner):
							found++
							val := stepProvisionerASN1{}
							_, err := asn1.Unmarshal(ext.Value, &val)
							require.NoError(t, err)
							assert.Equal(t, provisionerTypeJWK, val.Type)
							assert.Equal(t, []byte(p.Name), val.Name)
							assert.Equal(t, []byte(p.Key.KeyID), val.CredentialID)

						// Basic Constraints
						case ext.Id.Equal(asn1.ObjectIdentifier([]int{2, 5, 29, 19})):
							val := basicConstraints{}
							_, err := asn1.Unmarshal(ext.Value, &val)
							require.NoError(t, err)
							assert.False(t, val.IsCA, false)
							assert.Equal(t, val.MaxPathLen, 0)

						// SAN extension
						case ext.Id.Equal(asn1.ObjectIdentifier([]int{2, 5, 29, 17})):
							if tc.csr.Subject.CommonName == "" {
								// Empty CSR subject test does not use any provisioner extensions.
								// So provisioner ID ext will be missing.
								found = 1
							}
						}
					}
					assert.Equal(t, found, 1)
					realIntermediate, err := x509.ParseCertificate(issuer.Raw)
					require.NoError(t, err)
					assert.Equal(t, realIntermediate, intermediate)
					assert.Len(t, leaf.Extensions, tc.extensionsCount)
				}
			}
		})
	}
}

func TestAuthority_Renew(t *testing.T) {
	a := testAuthority(t)
	a.config.AuthorityConfig.Template = &ASN1DN{
		Country:       "Tazmania",
		Organization:  "Acme Co",
		Locality:      "Landscapes",
		Province:      "Sudden Cliffs",
		StreetAddress: "TNT",
		CommonName:    "renew",
	}

	now := time.Now().UTC()
	nb1 := now.Add(-time.Minute * 7)
	na1 := now.Add(time.Hour)
	so := &provisioner.SignOptions{
		NotBefore: provisioner.NewTimeDuration(nb1),
		NotAfter:  provisioner.NewTimeDuration(na1),
	}

	issuer := getDefaultIssuer(a)
	signer := getDefaultSigner(a)

	cert := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
		withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
		withDefaultASN1DN(a.config.AuthorityConfig.Template),
		withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID),
		withSigner(issuer, signer))

	certExtraNames := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
		withSubject(pkix.Name{
			CommonName: "renew",
			ExtraNames: []pkix.AttributeTypeAndValue{
				{Type: asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25}, Value: "dc"},
			},
		}),
		withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
		withDefaultASN1DN(a.config.AuthorityConfig.Template),
		withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID),
		withSigner(issuer, signer))

	certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
		withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
		withDefaultASN1DN(a.config.AuthorityConfig.Template),
		withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID),
		withSigner(issuer, signer))

	type renewTest struct {
		auth *Authority
		cert *x509.Certificate
		err  error
		code int
	}
	tests := map[string]func() (*renewTest, error){
		"fail/create-cert": func() (*renewTest, error) {
			_a := testAuthority(t)
			_a.x509CAService.(*softcas.SoftCAS).Signer = nil
			return &renewTest{
				auth: _a,
				cert: cert,
				err:  errors.New("error creating certificate"),
				code: http.StatusInternalServerError,
			}, nil
		},
		"fail/unauthorized": func() (*renewTest, error) {
			return &renewTest{
				cert: certNoRenew,
				err:  errors.New("authority.authorizeRenew: renew is disabled for provisioner 'dev'"),
				code: http.StatusUnauthorized,
			}, nil
		},
		"fail/WithAuthorizeRenewFunc": func() (*renewTest, error) {
			aa := testAuthority(t, WithAuthorizeRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *x509.Certificate) error {
				return errs.Unauthorized("not authorized")
			}))
			aa.x509CAService = a.x509CAService
			aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
			return &renewTest{
				auth: aa,
				cert: cert,
				err:  errors.New("authority.authorizeRenew: not authorized"),
				code: http.StatusUnauthorized,
			}, nil
		},
		"ok": func() (*renewTest, error) {
			return &renewTest{
				auth: a,
				cert: cert,
			}, nil
		},
		"ok/WithExtraNames": func() (*renewTest, error) {
			return &renewTest{
				auth: a,
				cert: certExtraNames,
			}, nil
		},
		"ok/success-new-intermediate": func() (*renewTest, error) {
			rootCert, rootSigner := generateRootCertificate(t)
			intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner)

			_a := testAuthority(t)
			_a.x509CAService.(*softcas.SoftCAS).CertificateChain = []*x509.Certificate{intCert}
			_a.x509CAService.(*softcas.SoftCAS).Signer = intSigner
			return &renewTest{
				auth: _a,
				cert: cert,
			}, nil
		},
		"ok/WithAuthorizeRenewFunc": func() (*renewTest, error) {
			aa := testAuthority(t, WithAuthorizeRenewFunc(func(ctx context.Context, p *provisioner.Controller, cert *x509.Certificate) error {
				return nil
			}))
			aa.x509CAService = a.x509CAService
			aa.config.AuthorityConfig.Template = a.config.AuthorityConfig.Template
			return &renewTest{
				auth: aa,
				cert: cert,
			}, nil
		},
	}

	for name, genTestCase := range tests {
		t.Run(name, func(t *testing.T) {
			tc, err := genTestCase()
			require.NoError(t, err)

			var certChain []*x509.Certificate
			if tc.auth != nil {
				certChain, err = tc.auth.Renew(tc.cert)
			} else {
				certChain, err = a.Renew(tc.cert)
			}
			if err != nil {
				if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
					assert.Nil(t, certChain)
					var sc render.StatusCodedError
					require.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
					assert.Equal(t, tc.code, sc.StatusCode())
					assertHasPrefix(t, err.Error(), tc.err.Error())

					var ctxErr *errs.Error
					require.True(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
					assert.Equal(t, tc.cert.SerialNumber.String(), ctxErr.Details["serialNumber"])
				}
			} else {
				leaf := certChain[0]
				intermediate := certChain[1]
				if assert.Nil(t, tc.err) {
					assert.Equal(t, tc.cert.NotAfter.Sub(cert.NotBefore), leaf.NotAfter.Sub(leaf.NotBefore))

					assert.True(t, leaf.NotBefore.After(now.Add(-2*time.Minute)))
					assert.True(t, leaf.NotBefore.Before(now.Add(time.Minute)))

					expiry := now.Add(time.Minute * 7)
					assert.True(t, leaf.NotAfter.After(expiry.Add(-2*time.Minute)))
					assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour)))

					tmplt := a.config.AuthorityConfig.Template
					assert.Equal(t, tc.cert.RawSubject, leaf.RawSubject)
					assert.Equal(t, []string{tmplt.Country}, leaf.Subject.Country)
					assert.Equal(t, []string{tmplt.Organization}, leaf.Subject.Organization)
					assert.Equal(t, []string{tmplt.Locality}, leaf.Subject.Locality)
					assert.Equal(t, []string{tmplt.StreetAddress}, leaf.Subject.StreetAddress)
					assert.Equal(t, []string{tmplt.Province}, leaf.Subject.Province)
					assert.Equal(t, tmplt.CommonName, leaf.Subject.CommonName)

					assert.Equal(t, intermediate.Subject, leaf.Issuer)

					assert.Equal(t, x509.ECDSAWithSHA256, leaf.SignatureAlgorithm)
					assert.Equal(t, x509.ECDSA, leaf.PublicKeyAlgorithm)
					assert.Equal(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, leaf.ExtKeyUsage)
					assert.Equal(t, []string{"test.smallstep.com", "test"}, leaf.DNSNames)

					subjectKeyID, err := generateSubjectKeyID(leaf.PublicKey)
					require.NoError(t, err)
					assert.Equal(t, subjectKeyID, leaf.SubjectKeyId)

					// We did not change the intermediate before renewing.
					authIssuer := getDefaultIssuer(tc.auth)
					if issuer.SerialNumber == authIssuer.SerialNumber {
						assert.Equal(t, issuer.SubjectKeyId, leaf.AuthorityKeyId)
						// Compare extensions: they can be in a different order
						for _, ext1 := range tc.cert.Extensions {
							//skip SubjectKeyIdentifier
							if ext1.Id.Equal(oidSubjectKeyIdentifier) {
								continue
							}
							found := false
							for _, ext2 := range leaf.Extensions {
								if reflect.DeepEqual(ext1, ext2) {
									found = true
									break
								}
							}
							if !found {
								t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String())
							}
						}
					} else {
						// We did change the intermediate before renewing.
						assert.Equal(t, authIssuer.SubjectKeyId, leaf.AuthorityKeyId)
						// Compare extensions: they can be in a different order
						for _, ext1 := range tc.cert.Extensions {
							//skip SubjectKeyIdentifier
							if ext1.Id.Equal(oidSubjectKeyIdentifier) {
								continue
							}
							// The authority key id extension should be different b/c the intermediates are different.
							if ext1.Id.Equal(oidAuthorityKeyIdentifier) {
								for _, ext2 := range leaf.Extensions {
									assert.False(t, reflect.DeepEqual(ext1, ext2))
								}
								continue
							}
							found := false
							for _, ext2 := range leaf.Extensions {
								if reflect.DeepEqual(ext1, ext2) {
									found = true
									break
								}
							}
							if !found {
								t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String())
							}
						}
					}

					realIntermediate, err := x509.ParseCertificate(authIssuer.Raw)
					require.NoError(t, err)
					assert.Equal(t, realIntermediate, intermediate)
				}
			}
		})
	}
}

func TestAuthority_Rekey(t *testing.T) {
	pub, _, err := keyutil.GenerateDefaultKeyPair()
	require.NoError(t, err)

	a := testAuthority(t)
	a.config.AuthorityConfig.Template = &ASN1DN{
		Country:       "Tazmania",
		Organization:  "Acme Co",
		Locality:      "Landscapes",
		Province:      "Sudden Cliffs",
		StreetAddress: "TNT",
		CommonName:    "renew",
	}

	now := time.Now().UTC()
	nb1 := now.Add(-time.Minute * 7)
	na1 := now.Add(time.Hour)
	so := &provisioner.SignOptions{
		NotBefore: provisioner.NewTimeDuration(nb1),
		NotAfter:  provisioner.NewTimeDuration(na1),
	}

	issuer := getDefaultIssuer(a)
	signer := getDefaultSigner(a)

	cert := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
		withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
		withDefaultASN1DN(a.config.AuthorityConfig.Template),
		withProvisionerOID("Max", a.config.AuthorityConfig.Provisioners[0].(*provisioner.JWK).Key.KeyID),
		withSigner(issuer, signer))

	certNoRenew := generateCertificate(t, "renew", []string{"test.smallstep.com", "test"},
		withNotBeforeNotAfter(so.NotBefore.Time(), so.NotAfter.Time()),
		withDefaultASN1DN(a.config.AuthorityConfig.Template),
		withProvisionerOID("dev", a.config.AuthorityConfig.Provisioners[2].(*provisioner.JWK).Key.KeyID),
		withSigner(issuer, signer))

	type renewTest struct {
		auth *Authority
		cert *x509.Certificate
		pk   crypto.PublicKey
		err  error
		code int
	}
	tests := map[string]func() (*renewTest, error){
		"fail/create-cert": func() (*renewTest, error) {
			_a := testAuthority(t)
			_a.x509CAService.(*softcas.SoftCAS).Signer = nil
			return &renewTest{
				auth: _a,
				cert: cert,
				err:  errors.New("error creating certificate"),
				code: http.StatusInternalServerError,
			}, nil
		},
		"fail/unauthorized": func() (*renewTest, error) {
			return &renewTest{
				cert: certNoRenew,
				err:  errors.New("authority.authorizeRenew: renew is disabled for provisioner 'dev'"),
				code: http.StatusUnauthorized,
			}, nil
		},
		"ok/renew": func() (*renewTest, error) {
			return &renewTest{
				auth: a,
				cert: cert,
			}, nil
		},
		"ok/rekey": func() (*renewTest, error) {
			return &renewTest{
				auth: a,
				cert: cert,
				pk:   pub,
			}, nil
		},
		"ok/renew/success-new-intermediate": func() (*renewTest, error) {
			rootCert, rootSigner := generateRootCertificate(t)
			intCert, intSigner := generateIntermidiateCertificate(t, rootCert, rootSigner)

			_a := testAuthority(t)
			_a.x509CAService.(*softcas.SoftCAS).CertificateChain = []*x509.Certificate{intCert}
			_a.x509CAService.(*softcas.SoftCAS).Signer = intSigner
			return &renewTest{
				auth: _a,
				cert: cert,
			}, nil
		},
	}

	for name, genTestCase := range tests {
		t.Run(name, func(t *testing.T) {
			tc, err := genTestCase()
			require.NoError(t, err)

			var certChain []*x509.Certificate
			if tc.auth != nil {
				certChain, err = tc.auth.Rekey(tc.cert, tc.pk)
			} else {
				certChain, err = a.Rekey(tc.cert, tc.pk)
			}
			if err != nil {
				if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
					assert.Nil(t, certChain)
					var sc render.StatusCodedError
					require.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
					assert.Equal(t, tc.code, sc.StatusCode())
					assertHasPrefix(t, err.Error(), tc.err.Error())

					var ctxErr *errs.Error
					require.True(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
					assert.Equal(t, tc.cert.SerialNumber.String(), ctxErr.Details["serialNumber"])
				}
			} else {
				leaf := certChain[0]
				intermediate := certChain[1]
				if assert.Nil(t, tc.err) {
					assert.Equal(t, tc.cert.NotAfter.Sub(cert.NotBefore), leaf.NotAfter.Sub(leaf.NotBefore))

					assert.True(t, leaf.NotBefore.After(now.Add(-2*time.Minute)))
					assert.True(t, leaf.NotBefore.Before(now.Add(time.Minute)))

					expiry := now.Add(time.Minute * 7)
					assert.True(t, leaf.NotAfter.After(expiry.Add(-2*time.Minute)))
					assert.True(t, leaf.NotAfter.Before(expiry.Add(time.Hour)))

					tmplt := a.config.AuthorityConfig.Template
					assert.Equal(t, pkix.Name{
						Country:       []string{tmplt.Country},
						Organization:  []string{tmplt.Organization},
						Locality:      []string{tmplt.Locality},
						StreetAddress: []string{tmplt.StreetAddress},
						Province:      []string{tmplt.Province},
						CommonName:    tmplt.CommonName,
					}.String(), leaf.Subject.String())
					assert.Equal(t, intermediate.Subject, leaf.Issuer)

					assert.Equal(t, x509.ECDSAWithSHA256, leaf.SignatureAlgorithm)
					assert.Equal(t, x509.ECDSA, leaf.PublicKeyAlgorithm)
					assert.Equal(t, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, leaf.ExtKeyUsage)
					assert.Equal(t, []string{"test.smallstep.com", "test"}, leaf.DNSNames)

					// Test Public Key and SubjectKeyId
					expectedPK := tc.pk
					if tc.pk == nil {
						expectedPK = cert.PublicKey
					}
					assert.Equal(t, expectedPK, leaf.PublicKey)

					subjectKeyID, err := generateSubjectKeyID(expectedPK)
					require.NoError(t, err)
					assert.Equal(t, subjectKeyID, leaf.SubjectKeyId)
					if tc.pk == nil {
						assert.Equal(t, cert.SubjectKeyId, leaf.SubjectKeyId)
					}

					// We did not change the intermediate before renewing.
					authIssuer := getDefaultIssuer(tc.auth)
					if issuer.SerialNumber == authIssuer.SerialNumber {
						assert.Equal(t, issuer.SubjectKeyId, leaf.AuthorityKeyId)
						// Compare extensions: they can be in a different order
						for _, ext1 := range tc.cert.Extensions {
							//skip SubjectKeyIdentifier
							if ext1.Id.Equal(oidSubjectKeyIdentifier) {
								continue
							}
							found := false
							for _, ext2 := range leaf.Extensions {
								if reflect.DeepEqual(ext1, ext2) {
									found = true
									break
								}
							}
							if !found {
								t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String())
							}
						}
					} else {
						// We did change the intermediate before renewing.
						assert.Equal(t, authIssuer.SubjectKeyId, leaf.AuthorityKeyId)
						// Compare extensions: they can be in a different order
						for _, ext1 := range tc.cert.Extensions {
							//skip SubjectKeyIdentifier
							if ext1.Id.Equal(oidSubjectKeyIdentifier) {
								continue
							}
							// The authority key id extension should be different b/c the intermediates are different.
							if ext1.Id.Equal(oidAuthorityKeyIdentifier) {
								for _, ext2 := range leaf.Extensions {
									assert.False(t, reflect.DeepEqual(ext1, ext2))
								}
								continue
							}
							found := false
							for _, ext2 := range leaf.Extensions {
								if reflect.DeepEqual(ext1, ext2) {
									found = true
									break
								}
							}
							if !found {
								t.Errorf("x509 extension %s not found in renewed certificate", ext1.Id.String())
							}
						}
					}

					realIntermediate, err := x509.ParseCertificate(authIssuer.Raw)
					require.NoError(t, err)
					assert.Equal(t, realIntermediate, intermediate)
				}
			}
		})
	}
}

func TestAuthority_GetTLSOptions(t *testing.T) {
	type renewTest struct {
		auth *Authority
		opts *TLSOptions
	}
	tests := map[string]func() (*renewTest, error){
		"default": func() (*renewTest, error) {
			a := testAuthority(t)
			return &renewTest{auth: a, opts: &DefaultTLSOptions}, nil
		},
		"non-default": func() (*renewTest, error) {
			a := testAuthority(t)
			a.config.TLS = &TLSOptions{
				CipherSuites: CipherSuites{
					"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
					"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
				},
				MinVersion:    1.0,
				MaxVersion:    1.1,
				Renegotiation: true,
			}
			return &renewTest{auth: a, opts: a.config.TLS}, nil
		},
	}

	for name, genTestCase := range tests {
		t.Run(name, func(t *testing.T) {
			tc, err := genTestCase()
			require.NoError(t, err)

			opts := tc.auth.GetTLSOptions()
			assert.Equal(t, tc.opts, opts)
		})
	}
}

func TestAuthority_Revoke(t *testing.T) {
	reasonCode := 2
	reason := "bob was let go"
	validIssuer := "step-cli"
	validAudience := testAudiences.Revoke
	now := time.Now().UTC()

	jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
	require.NoError(t, err)

	sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
		(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
	require.NoError(t, err)

	a := testAuthority(t)

	tlsRevokeCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)

	type test struct {
		auth            *Authority
		ctx             context.Context
		opts            *RevokeOptions
		err             error
		code            int
		checkErrDetails func(err *errs.Error)
	}
	tests := map[string]func() test{
		"fail/token/authorizeRevoke error": func() test {
			return test{
				auth: a,
				ctx:  tlsRevokeCtx,
				opts: &RevokeOptions{
					OTT:        "foo",
					Serial:     "sn",
					ReasonCode: reasonCode,
					Reason:     reason,
				},
				err:  errors.New("authority.Revoke; error parsing token"),
				code: http.StatusUnauthorized,
			}
		},
		"fail/nil-db": func() test {
			cl := jose.Claims{
				Subject:   "sn",
				Issuer:    validIssuer,
				NotBefore: jose.NewNumericDate(now),
				Expiry:    jose.NewNumericDate(now.Add(time.Minute)),
				Audience:  validAudience,
				ID:        "44",
			}
			raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
			require.NoError(t, err)

			return test{
				auth: a,
				ctx:  tlsRevokeCtx,
				opts: &RevokeOptions{
					Serial:     "sn",
					ReasonCode: reasonCode,
					Reason:     reason,
					OTT:        raw,
				},
				err:  errors.New("authority.Revoke; no persistence layer configured"),
				code: http.StatusNotImplemented,
				checkErrDetails: func(err *errs.Error) {
					assert.Equal(t, raw, err.Details["token"])
					assert.Equal(t, "44", err.Details["tokenID"])
					assert.Equal(t, "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc", err.Details["provisionerID"])
				},
			}
		},
		"fail/db-revoke": func() test {
			_a := testAuthority(t, WithDatabase(&db.MockAuthDB{
				MUseToken: func(id, tok string) (bool, error) {
					return true, nil
				},
				MGetCertificate: func(sn string) (*x509.Certificate, error) {
					return nil, errors.New("not found")
				},
				Err: errors.New("force"),
			}))

			cl := jose.Claims{
				Subject:   "sn",
				Issuer:    validIssuer,
				NotBefore: jose.NewNumericDate(now),
				Expiry:    jose.NewNumericDate(now.Add(time.Minute)),
				Audience:  validAudience,
				ID:        "44",
			}
			raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
			require.NoError(t, err)

			return test{
				auth: _a,
				ctx:  tlsRevokeCtx,
				opts: &RevokeOptions{
					Serial:     "sn",
					ReasonCode: reasonCode,
					Reason:     reason,
					OTT:        raw,
				},
				err:  errors.New("authority.Revoke: force"),
				code: http.StatusInternalServerError,
				checkErrDetails: func(err *errs.Error) {
					assert.Equal(t, raw, err.Details["token"])
					assert.Equal(t, "44", err.Details["tokenID"])
					assert.Equal(t, "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc", err.Details["provisionerID"])
				},
			}
		},
		"fail/already-revoked": func() test {
			_a := testAuthority(t, WithDatabase(&db.MockAuthDB{
				MUseToken: func(id, tok string) (bool, error) {
					return true, nil
				},
				MGetCertificate: func(sn string) (*x509.Certificate, error) {
					return nil, errors.New("not found")
				},
				Err: db.ErrAlreadyExists,
			}))

			cl := jose.Claims{
				Subject:   "sn",
				Issuer:    validIssuer,
				NotBefore: jose.NewNumericDate(now),
				Expiry:    jose.NewNumericDate(now.Add(time.Minute)),
				Audience:  validAudience,
				ID:        "44",
			}
			raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
			require.NoError(t, err)

			return test{
				auth: _a,
				ctx:  tlsRevokeCtx,
				opts: &RevokeOptions{
					Serial:     "sn",
					ReasonCode: reasonCode,
					Reason:     reason,
					OTT:        raw,
				},
				err:  errors.New("certificate with serial number 'sn' is already revoked"),
				code: http.StatusBadRequest,
				checkErrDetails: func(err *errs.Error) {
					assert.Equal(t, raw, err.Details["token"])
					assert.Equal(t, "44", err.Details["tokenID"])
					assert.Equal(t, "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc", err.Details["provisionerID"])
				},
			}
		},
		"fail/serial-number": func() test {
			_a := testAuthority(t, WithDatabase(&db.MockAuthDB{
				MUseToken: func(id, tok string) (bool, error) {
					return true, nil
				},
				MGetCertificate: func(sn string) (*x509.Certificate, error) {
					return nil, errors.New("not found")
				},
			}))

			cl := jose.Claims{
				Subject:   "token-sn",
				Issuer:    validIssuer,
				NotBefore: jose.NewNumericDate(now),
				Expiry:    jose.NewNumericDate(now.Add(time.Minute)),
				Audience:  validAudience,
				ID:        "44",
			}
			raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
			require.NoError(t, err)
			return test{
				auth: _a,
				ctx:  tlsRevokeCtx,
				opts: &RevokeOptions{
					Serial:     "request-sn",
					ReasonCode: reasonCode,
					Reason:     reason,
					OTT:        raw,
				},
				err:  errors.New(`request serial number "request-sn" and token subject "token-sn" do not match`),
				code: http.StatusForbidden,
				checkErrDetails: func(err *errs.Error) {
					assert.Equal(t, raw, err.Details["token"])
				},
			}
		},
		"ok/token": func() test {
			_a := testAuthority(t, WithDatabase(&db.MockAuthDB{
				MUseToken: func(id, tok string) (bool, error) {
					return true, nil
				},
				MGetCertificate: func(sn string) (*x509.Certificate, error) {
					return nil, errors.New("not found")
				},
			}))

			cl := jose.Claims{
				Subject:   "sn",
				Issuer:    validIssuer,
				NotBefore: jose.NewNumericDate(now),
				Expiry:    jose.NewNumericDate(now.Add(time.Minute)),
				Audience:  validAudience,
				ID:        "44",
			}
			raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
			require.NoError(t, err)
			return test{
				auth: _a,
				ctx:  tlsRevokeCtx,
				opts: &RevokeOptions{
					Serial:     "sn",
					ReasonCode: reasonCode,
					Reason:     reason,
					OTT:        raw,
				},
			}
		},
		"ok/mTLS": func() test {
			_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))

			crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
			require.NoError(t, err)

			return test{
				auth: _a,
				ctx:  tlsRevokeCtx,
				opts: &RevokeOptions{
					Crt:        crt,
					Serial:     "102012593071130646873265215610956555026",
					ReasonCode: reasonCode,
					Reason:     reason,
					MTLS:       true,
				},
			}
		},
		"ok/mTLS-no-provisioner": func() test {
			_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))

			crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
			require.NoError(t, err)
			// Filter out provisioner extension.
			for i, ext := range crt.Extensions {
				if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 1}) {
					crt.Extensions = append(crt.Extensions[:i], crt.Extensions[i+1:]...)
					break
				}
			}

			return test{
				auth: _a,
				ctx:  tlsRevokeCtx,
				opts: &RevokeOptions{
					Crt:        crt,
					Serial:     "102012593071130646873265215610956555026",
					ReasonCode: reasonCode,
					Reason:     reason,
					MTLS:       true,
				},
			}
		},
		"ok/ACME": func() test {
			_a := testAuthority(t, WithDatabase(&db.MockAuthDB{}))

			crt, err := pemutil.ReadCertificate("./testdata/certs/foo.crt")
			require.NoError(t, err)

			return test{
				auth: _a,
				ctx:  tlsRevokeCtx,
				opts: &RevokeOptions{
					Crt:        crt,
					Serial:     "102012593071130646873265215610956555026",
					ReasonCode: reasonCode,
					Reason:     reason,
					ACME:       true,
				},
			}
		},
		"ok/ssh": func() test {
			a := testAuthority(t, WithDatabase(&db.MockAuthDB{
				MRevoke: func(rci *db.RevokedCertificateInfo) error {
					return errors.New("Revoke was called")
				},
				MRevokeSSH: func(rci *db.RevokedCertificateInfo) error {
					return nil
				},
			}))

			cl := jose.Claims{
				Subject:   "sn",
				Issuer:    validIssuer,
				NotBefore: jose.NewNumericDate(now),
				Expiry:    jose.NewNumericDate(now.Add(time.Minute)),
				Audience:  validAudience,
				ID:        "44",
			}
			raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
			require.NoError(t, err)
			return test{
				auth: a,
				ctx:  provisioner.NewContextWithMethod(context.Background(), provisioner.SSHRevokeMethod),
				opts: &RevokeOptions{
					Serial:     "sn",
					ReasonCode: reasonCode,
					Reason:     reason,
					OTT:        raw,
				},
			}
		},
	}
	for name, f := range tests {
		tc := f()
		t.Run(name, func(t *testing.T) {
			if err := tc.auth.Revoke(tc.ctx, tc.opts); err != nil {
				if assert.NotNil(t, tc.err, fmt.Sprintf("unexpected error: %s", err)) {
					var sc render.StatusCodedError
					require.True(t, errors.As(err, &sc), "error does not implement StatusCodedError interface")
					assert.Equal(t, tc.code, sc.StatusCode())
					assertHasPrefix(t, err.Error(), tc.err.Error())

					var ctxErr *errs.Error
					require.True(t, errors.As(err, &ctxErr), "error is not of type *errs.Error")
					assert.Equal(t, tc.opts.Serial, ctxErr.Details["serialNumber"])
					assert.Equal(t, tc.opts.ReasonCode, ctxErr.Details["reasonCode"])
					assert.Equal(t, tc.opts.Reason, ctxErr.Details["reason"])
					assert.Equal(t, tc.opts.MTLS, ctxErr.Details["MTLS"])
					assert.Equal(t, provisioner.RevokeMethod.String(), ctxErr.Details["context"])

					if tc.checkErrDetails != nil {
						tc.checkErrDetails(ctxErr)
					}
				}
			} else {
				assert.Nil(t, tc.err)
			}
		})
	}
}

func TestAuthority_constraints(t *testing.T) {
	ca, err := minica.New(
		minica.WithIntermediateTemplate(`{
				"subject": {{ toJson .Subject }},
				"keyUsage": ["certSign", "crlSign"],
				"basicConstraints": {
					"isCA": true,
					"maxPathLen": 0
				},
				"nameConstraints": {
					"critical": true,
					"permittedDNSDomains": ["internal.example.org"],
					"excludedDNSDomains": ["internal.example.com"],
					"permittedIPRanges": ["192.168.1.0/24", "192.168.2.1/32"],
					"excludedIPRanges": ["192.168.3.0/24", "192.168.4.0/28"],
					"permittedEmailAddresses": ["root@example.org", "example.org", ".acme.org"],
					"excludedEmailAddresses": ["root@example.com", "example.com", ".acme.com"],
					"permittedURIDomains": ["uuid.example.org", ".acme.org"],
					"excludedURIDomains": ["uuid.example.com", ".acme.com"]
				}
			}`),
	)
	if err != nil {
		t.Fatal(err)
	}

	auth, err := NewEmbedded(WithX509RootCerts(ca.Root), WithX509Signer(ca.Intermediate, ca.Signer))
	if err != nil {
		t.Fatal(err)
	}
	signer, err := keyutil.GenerateDefaultSigner()
	if err != nil {
		t.Fatal(err)
	}

	tests := []struct {
		name    string
		sans    []string
		wantErr bool
	}{
		{"ok dns", []string{"internal.example.org", "host.internal.example.org"}, false},
		{"ok ip", []string{"192.168.1.10", "192.168.2.1"}, false},
		{"ok email", []string{"root@example.org", "info@example.org", "info@www.acme.org"}, false},
		{"ok uri", []string{"https://uuid.example.org/b908d973-5167-4a62-abe3-6beda358d82a", "https://uuid.acme.org/1724aae1-1bb3-44fb-83c3-9a1a18df67c8"}, false},
		{"fail permitted dns", []string{"internal.acme.org"}, true},
		{"fail excluded dns", []string{"internal.example.com"}, true},
		{"fail permitted ips", []string{"192.168.2.10"}, true},
		{"fail excluded ips", []string{"192.168.3.1"}, true},
		{"fail permitted emails", []string{"root@acme.org"}, true},
		{"fail excluded emails", []string{"root@example.com"}, true},
		{"fail permitted uris", []string{"https://acme.org/uuid/7848819c-9d0b-4e12-bbff-cd66079a3444"}, true},
		{"fail excluded uris", []string{"https://uuid.example.com/d325eda7-6356-4d60-b8f6-3d64724afeb3"}, true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			csr, err := x509util.CreateCertificateRequest(tt.sans[0], tt.sans, signer)
			if err != nil {
				t.Fatal(err)
			}
			cert, err := ca.SignCSR(csr)
			if err != nil {
				t.Fatal(err)
			}

			data := x509util.CreateTemplateData(tt.sans[0], tt.sans)
			templateOption, err := provisioner.TemplateOptions(nil, data)
			if err != nil {
				t.Fatal(err)
			}

			_, err = auth.SignWithContext(context.Background(), csr, provisioner.SignOptions{}, templateOption)
			if (err != nil) != tt.wantErr {
				t.Errorf("Authority.SignWithContext() error = %v, wantErr %v", err, tt.wantErr)
			}

			_, err = auth.Renew(cert)
			if (err != nil) != tt.wantErr {
				t.Errorf("Authority.Renew() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func TestAuthority_CRL(t *testing.T) {
	reasonCode := 2
	reason := "bob was let go"
	validIssuer := "step-cli"
	validAudience := testAudiences.Revoke
	now := time.Now().UTC()
	jwk, err := jose.ReadKey("testdata/secrets/step_cli_key_priv.jwk", jose.WithPassword([]byte("pass")))
	require.NoError(t, err)
	sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
		(&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", jwk.KeyID))
	require.NoError(t, err)

	crlCtx := provisioner.NewContextWithMethod(context.Background(), provisioner.RevokeMethod)

	var crlStore db.CertificateRevocationListInfo
	var revokedList []db.RevokedCertificateInfo

	type test struct {
		auth               *Authority
		ctx                context.Context
		expected           []string
		expectedReasonCode *int
		err                error
	}
	tests := map[string]func() test{
		"fail/empty-crl": func() test {
			a := testAuthority(t, WithDatabase(&db.MockAuthDB{
				MUseToken: func(id, tok string) (bool, error) {
					return true, nil
				},
				MGetCertificate: func(sn string) (*x509.Certificate, error) {
					return nil, errors.New("not found")
				},
				MStoreCRL: func(i *db.CertificateRevocationListInfo) error {
					crlStore = *i
					return nil
				},
				MGetCRL: func() (*db.CertificateRevocationListInfo, error) {
					return nil, database.ErrNotFound
				},
				MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) {
					return &revokedList, nil
				},
				MRevoke: func(rci *db.RevokedCertificateInfo) error {
					revokedList = append(revokedList, *rci)
					return nil
				},
			}))
			a.config.CRL = &config.CRLConfig{
				Enabled: true,
			}

			return test{
				auth:     a,
				ctx:      crlCtx,
				expected: nil,
				err:      errors.New("authority.GetCertificateRevocationList: not found"),
			}
		},
		"ok/crl-full": func() test {
			a := testAuthority(t, WithDatabase(&db.MockAuthDB{
				MUseToken: func(id, tok string) (bool, error) {
					return true, nil
				},
				MGetCertificate: func(sn string) (*x509.Certificate, error) {
					return nil, errors.New("not found")
				},
				MStoreCRL: func(i *db.CertificateRevocationListInfo) error {
					crlStore = *i
					return nil
				},
				MGetCRL: func() (*db.CertificateRevocationListInfo, error) {
					return &crlStore, nil
				},
				MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) {
					return &revokedList, nil
				},
				MRevoke: func(rci *db.RevokedCertificateInfo) error {
					revokedList = append(revokedList, *rci)
					return nil
				},
			}))
			a.config.CRL = &config.CRLConfig{
				Enabled:          true,
				GenerateOnRevoke: true,
			}

			var ex []string

			for i := 0; i < 100; i++ {
				sn := fmt.Sprintf("%v", i)

				cl := jose.Claims{
					Subject:   sn,
					Issuer:    validIssuer,
					NotBefore: jose.NewNumericDate(now),
					Expiry:    jose.NewNumericDate(now.Add(time.Minute)),
					Audience:  validAudience,
					ID:        sn,
				}
				raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
				require.NoError(t, err)
				err = a.Revoke(crlCtx, &RevokeOptions{
					Serial:     sn,
					ReasonCode: reasonCode,
					Reason:     reason,
					OTT:        raw,
				})

				require.NoError(t, err)

				ex = append(ex, sn)
			}

			return test{
				auth:               a,
				ctx:                crlCtx,
				expected:           ex,
				expectedReasonCode: &reasonCode,
			}
		},
		"ok/crl-no-reason-code": func() test {
			var localRevokedList []db.RevokedCertificateInfo
			var localCRLStore db.CertificateRevocationListInfo
			a := testAuthority(t, WithDatabase(&db.MockAuthDB{
				MUseToken: func(id, tok string) (bool, error) {
					return true, nil
				},
				MGetCertificate: func(sn string) (*x509.Certificate, error) {
					return nil, errors.New("not found")
				},
				MStoreCRL: func(i *db.CertificateRevocationListInfo) error {
					localCRLStore = *i
					return nil
				},
				MGetCRL: func() (*db.CertificateRevocationListInfo, error) {
					return &localCRLStore, nil
				},
				MGetRevokedCertificates: func() (*[]db.RevokedCertificateInfo, error) {
					return &localRevokedList, nil
				},
				MRevoke: func(rci *db.RevokedCertificateInfo) error {
					localRevokedList = append(localRevokedList, *rci)
					return nil
				},
			}))
			a.config.CRL = &config.CRLConfig{
				Enabled:          true,
				GenerateOnRevoke: true,
			}

			var ex []string
			zeroReasonCode := 0

			for i := 0; i < 5; i++ {
				sn := fmt.Sprintf("%v", i)
				cl := jose.Claims{
					Subject:   sn,
					Issuer:    validIssuer,
					NotBefore: jose.NewNumericDate(now),
					Expiry:    jose.NewNumericDate(now.Add(time.Minute)),
					Audience:  validAudience,
					ID:        sn,
				}
				raw, err := jose.Signed(sig).Claims(cl).CompactSerialize()
				require.NoError(t, err)
				err = a.Revoke(crlCtx, &RevokeOptions{
					Serial:     sn,
					ReasonCode: zeroReasonCode,
					Reason:     reason,
					OTT:        raw,
				})
				require.NoError(t, err)
				ex = append(ex, sn)
			}

			return test{
				auth:               a,
				ctx:                crlCtx,
				expected:           ex,
				expectedReasonCode: &zeroReasonCode,
			}
		},
	}
	for name, f := range tests {
		tc := f()
		t.Run(name, func(t *testing.T) {
			crlInfo, err := tc.auth.GetCertificateRevocationList()
			if tc.err != nil {
				assert.EqualError(t, err, tc.err.Error())
				assert.Nil(t, crlInfo)
				return
			}

			crl, parseErr := x509.ParseRevocationList(crlInfo.Data)
			require.NoError(t, parseErr)

			var cmpList []string
			for _, c := range crl.RevokedCertificateEntries {
				cmpList = append(cmpList, c.SerialNumber.String())
				// ReasonCode 0 causes Go's x509 package to omit the reasonCode
				// extension entirely. Parsing it back yields 0 as the zero value
				// of the field, not from an explicit extension. This confirms the
				// zero-code path produces a well-formed CRL, not that the
				// extension round-trips.
				if tc.expectedReasonCode != nil {
					assert.Equal(t, *tc.expectedReasonCode, c.ReasonCode)
				}
			}

			assert.Equal(t, tc.expected, cmpList)
		})
	}
}

type notImplementedCAS struct{}

func (notImplementedCAS) CreateCertificate(req *apiv1.CreateCertificateRequest) (*apiv1.CreateCertificateResponse, error) {
	return nil, apiv1.NotImplementedError{}
}
func (notImplementedCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1.RenewCertificateResponse, error) {
	return nil, apiv1.NotImplementedError{}
}
func (notImplementedCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) {
	return nil, apiv1.NotImplementedError{}
}

func TestAuthority_GetX509Signer(t *testing.T) {
	auth := testAuthority(t)
	require.IsType(t, &softcas.SoftCAS{}, auth.x509CAService)
	signer := auth.x509CAService.(*softcas.SoftCAS).Signer
	require.NotNil(t, signer)

	tests := []struct {
		name      string
		authority *Authority
		want      crypto.Signer
		assertion assert.ErrorAssertionFunc
	}{
		{"ok", auth, signer, assert.NoError},
		{"fail", testAuthority(t, WithX509CAService(notImplementedCAS{})), nil, assert.Error},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := tt.authority.GetX509Signer()
			tt.assertion(t, err)
			assert.Equal(t, tt.want, got)
		})
	}
}
