package acme

import (
	"bytes"
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"math/big"
	"net"
	"net/http"
	"net/http/httptest"
	"reflect"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/fxamacker/cbor/v2"
	"github.com/smallstep/certificates/authority/config"
	"github.com/smallstep/certificates/authority/provisioner"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.step.sm/crypto/jose"
	"go.step.sm/crypto/keyutil"
	"go.step.sm/crypto/minica"
	"go.step.sm/crypto/x509util"
)

type mockClient struct {
	get       func(url string) (*http.Response, error)
	lookupTxt func(name string) ([]string, error)
	tlsDial   func(network, addr string, config *tls.Config) (*tls.Conn, error)
}

func (m *mockClient) Get(url string) (*http.Response, error)  { return m.get(url) }
func (m *mockClient) LookupTxt(name string) ([]string, error) { return m.lookupTxt(name) }
func (m *mockClient) TLSDial(network, addr string, tlsConfig *tls.Config) (*tls.Conn, error) {
	return m.tlsDial(network, addr, tlsConfig)
}

func fatalError(t *testing.T, err error) {
	t.Helper()
	if err != nil {
		t.Fatal(err)
	}
}

func mustNonAttestationProvisioner(t *testing.T) Provisioner {
	t.Helper()

	prov := &provisioner.ACME{
		Type:       "ACME",
		Name:       "acme",
		Challenges: []provisioner.ACMEChallenge{provisioner.HTTP_01},
	}
	if err := prov.Init(provisioner.Config{
		Claims: config.GlobalProvisionerClaims,
	}); err != nil {
		t.Fatal(err)
	}
	prov.AttestationFormats = []provisioner.ACMEAttestationFormat{"bogus-format"} // results in no attestation formats enabled
	return prov
}

func mustAttestationProvisioner(t *testing.T, roots []byte) Provisioner {
	t.Helper()

	prov := &provisioner.ACME{
		Type:             "ACME",
		Name:             "acme",
		Challenges:       []provisioner.ACMEChallenge{provisioner.DEVICE_ATTEST_01},
		AttestationRoots: roots,
	}
	if err := prov.Init(provisioner.Config{
		Claims: config.GlobalProvisionerClaims,
	}); err != nil {
		t.Fatal(err)
	}
	return prov
}

func mustAccountAndKeyAuthorization(t *testing.T, token string) (*jose.JSONWebKey, string) {
	t.Helper()

	jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
	fatalError(t, err)

	keyAuth, err := KeyAuthorization(token, jwk)
	fatalError(t, err)
	return jwk, keyAuth
}

func mustAttestApple(t *testing.T, nonce string) ([]byte, *x509.Certificate, *x509.Certificate) {
	t.Helper()

	ca, err := minica.New()
	fatalError(t, err)

	signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	fatalError(t, err)

	nonceSum := sha256.Sum256([]byte(nonce))
	leaf, err := ca.Sign(&x509.Certificate{
		Subject:   pkix.Name{CommonName: "attestation cert"},
		PublicKey: signer.Public(),
		ExtraExtensions: []pkix.Extension{
			{Id: oidAppleSerialNumber, Value: []byte("serial-number")},
			{Id: oidAppleUniqueDeviceIdentifier, Value: []byte("udid")},
			{Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte("16.0")},
			{Id: oidAppleNonce, Value: nonceSum[:]},
		},
	})
	fatalError(t, err)

	attObj, err := cbor.Marshal(struct {
		Format       string                 `json:"fmt"`
		AttStatement map[string]interface{} `json:"attStmt,omitempty"`
	}{
		Format: "apple",
		AttStatement: map[string]interface{}{
			"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
		},
	})
	fatalError(t, err)

	payload, err := json.Marshal(struct {
		AttObj string `json:"attObj"`
	}{
		AttObj: base64.RawURLEncoding.EncodeToString(attObj),
	})
	fatalError(t, err)

	return payload, leaf, ca.Root
}

func mustAttestYubikey(t *testing.T, _, keyAuthorization string, serial int) ([]byte, *x509.Certificate, *x509.Certificate) {
	ca, err := minica.New()
	fatalError(t, err)

	keyAuthSum := sha256.Sum256([]byte(keyAuthorization))

	signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	fatalError(t, err)
	sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
	fatalError(t, err)
	cborSig, err := cbor.Marshal(sig)
	fatalError(t, err)

	serialNumber, err := asn1.Marshal(serial)
	fatalError(t, err)

	leaf, err := ca.Sign(&x509.Certificate{
		Subject:   pkix.Name{CommonName: "attestation cert"},
		PublicKey: signer.Public(),
		ExtraExtensions: []pkix.Extension{
			{Id: oidYubicoSerialNumber, Value: serialNumber},
		},
	})
	fatalError(t, err)

	attObj, err := cbor.Marshal(struct {
		Format       string                 `json:"fmt"`
		AttStatement map[string]interface{} `json:"attStmt,omitempty"`
	}{
		Format: "step",
		AttStatement: map[string]interface{}{
			"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
			"alg": -7,
			"sig": cborSig,
		},
	})
	fatalError(t, err)

	payload, err := json.Marshal(struct {
		AttObj string `json:"attObj"`
	}{
		AttObj: base64.RawURLEncoding.EncodeToString(attObj),
	})
	fatalError(t, err)

	return payload, leaf, ca.Root
}

func Test_storeError(t *testing.T) {
	type test struct {
		ch          *Challenge
		db          DB
		markInvalid bool
		err         *Error
	}
	err := NewError(ErrorMalformedType, "foo")
	tests := map[string]func(t *testing.T) test{
		"fail/db.UpdateChallenge-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusValid,
			}
			return test{
				ch: ch,
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusValid, updch.Status)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"fail/db.UpdateChallenge-acme-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusValid,
			}
			return test{
				ch: ch,
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusValid, updch.Status)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return NewError(ErrorMalformedType, "bar")
					},
				},
				err: NewError(ErrorMalformedType, "failure saving error to acme challenge: bar"),
			}
		},
		"ok": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusValid,
			}
			return test{
				ch: ch,
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusValid, updch.Status)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
			}
		},
		"ok/mark-invalid": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusValid,
			}
			return test{
				ch: ch,
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusInvalid, updch.Status)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				markInvalid: true,
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)
			if err := storeError(context.Background(), tc.db, tc.ch, tc.markInvalid, err); err != nil {
				if assert.Error(t, tc.err) {
					var k *Error
					if errors.As(err, &k) {
						assert.Equal(t, tc.err.Type, k.Type)
						assert.Equal(t, tc.err.Detail, k.Detail)
						assert.Equal(t, tc.err.Status, k.Status)
						assert.Equal(t, tc.err.Err.Error(), k.Err.Error())
					} else {
						assert.Fail(t, "unexpected error type")
					}
				}
			} else {
				assert.Nil(t, tc.err)
			}
		})
	}
}

func TestKeyAuthorization(t *testing.T) {
	type test struct {
		token string
		jwk   *jose.JSONWebKey
		exp   string
		err   *Error
	}
	tests := map[string]func(t *testing.T) test{
		"fail/jwk-thumbprint-error": func(t *testing.T) test {
			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)
			jwk.Key = "foo"
			return test{
				token: "1234",
				jwk:   jwk,
				err:   NewErrorISE("error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'"),
			}
		},
		"ok": func(t *testing.T) test {
			token := "1234"
			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)
			thumbprint, err := jwk.Thumbprint(crypto.SHA256)
			require.NoError(t, err)
			encPrint := base64.RawURLEncoding.EncodeToString(thumbprint)
			return test{
				token: token,
				jwk:   jwk,
				exp:   fmt.Sprintf("%s.%s", token, encPrint),
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)
			if ka, err := KeyAuthorization(tc.token, tc.jwk); err != nil {
				if assert.Error(t, tc.err) {
					var k *Error
					if errors.As(err, &k) {
						assert.Equal(t, tc.err.Type, k.Type)
						assert.Equal(t, tc.err.Detail, k.Detail)
						assert.Equal(t, tc.err.Status, k.Status)
						assert.Equal(t, tc.err.Err.Error(), k.Err.Error())
					} else {
						assert.Fail(t, "unexpected error type")
					}
				}
			} else {
				if assert.Nil(t, tc.err) {
					assert.Equal(t, tc.exp, ka)
				}
			}
		})
	}
}

func TestChallenge_Validate(t *testing.T) {
	type test struct {
		ch      *Challenge
		vc      Client
		jwk     *jose.JSONWebKey
		db      DB
		srv     *httptest.Server
		payload []byte
		ctx     context.Context
		err     *Error
	}
	tests := map[string]func(t *testing.T) test{
		"ok/already-valid": func(t *testing.T) test {
			ch := &Challenge{
				Status: StatusValid,
			}
			return test{
				ch: ch,
			}
		},
		"fail/already-invalid": func(t *testing.T) test {
			ch := &Challenge{
				Status: StatusInvalid,
			}
			return test{
				ch: ch,
			}
		},
		"fail/unexpected-type": func(t *testing.T) test {
			ch := &Challenge{
				Status: StatusPending,
				Type:   "foo",
			}
			return test{
				ch:  ch,
				err: NewErrorISE("unexpected challenge type 'foo'"),
			}
		},
		"fail/http-01": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Status: StatusPending,
				Type:   "http-01",
				Token:  "token",
				Value:  "zap.internal",
			}

			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, ChallengeType("http-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorConnectionType, "error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s: force", ch.Token)
						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/http-01": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Status: StatusPending,
				Type:   "http-01",
				Token:  "token",
				Value:  "zap.internal",
			}

			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, ChallengeType("http-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorConnectionType, "error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s: force", ch.Token)
						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
			}
		},
		"ok/http-01-insecure": func(t *testing.T) test {
			t.Cleanup(func() {
				InsecurePortHTTP01 = 0
			})

			ch := &Challenge{
				ID:     "chID",
				Status: StatusPending,
				Type:   "http-01",
				Token:  "token",
				Value:  "zap.internal",
			}

			InsecurePortHTTP01 = 8080

			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, ChallengeType("http-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorConnectionType, "error doing http GET for url http://zap.internal:8080/.well-known/acme-challenge/%s: force", ch.Token)
						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
			}
		},
		"fail/dns-01": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Type:   "dns-01",
				Status: StatusPending,
				Token:  "token",
				Value:  "zap.internal",
			}

			return test{
				ch: ch,
				vc: &mockClient{
					lookupTxt: func(url string) ([]string, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, ChallengeType("dns-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorDNSType, "error looking up TXT records for domain %s: force", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/dns-01": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Type:   "dns-01",
				Status: StatusPending,
				Token:  "token",
				Value:  "zap.internal",
			}

			return test{
				ch: ch,
				vc: &mockClient{
					lookupTxt: func(url string) ([]string, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, ChallengeType("dns-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorDNSType, "error looking up TXT records for domain %s: force", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
			}
		},
		"fail/tls-alpn-01": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Type:   "tls-alpn-01",
				Status: StatusPending,
				Value:  "zap.internal",
			}
			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443: force", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/tls-alpn-01": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Type:   "tls-alpn-01",
				Status: StatusPending,
				Value:  "zap.internal",
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusValid, updch.Status)
						assert.Nil(t, updch.Error)
						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"ok/tls-alpn-01-insecure": func(t *testing.T) test {
			t.Cleanup(func() {
				InsecurePortTLSALPN01 = 0
			})

			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Type:   "tls-alpn-01",
				Status: StatusPending,
				Value:  "zap.internal",
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value)
			require.NoError(t, err)

			l, err := net.Listen("tcp", "127.0.0.1:0")
			if err != nil {
				if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
					t.Fatalf("failed to listen on a port: %v", err)
				}
			}
			_, port, err := net.SplitHostPort(l.Addr().String())
			if err != nil {
				t.Fatalf("failed to split host port: %v", err)
			}

			// Use an insecure port
			InsecurePortTLSALPN01, err = strconv.Atoi(port)
			if err != nil {
				t.Fatalf("failed to convert port to int: %v", err)
			}

			srv, tlsDial := newTestTLSALPNServer(cert, func(srv *httptest.Server) {
				srv.Listener.Close()
				srv.Listener = l
			})
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusValid, updch.Status)
						assert.Nil(t, updch.Error)
						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"fail/device-attest-01": func(t *testing.T) test {
			payload, err := json.Marshal(struct {
				Error string `json:"error"`
			}{
				Error: "an error",
			})
			assert.NoError(t, err)
			return test{
				ch: &Challenge{
					ID:              "chID",
					AuthorizationID: "azID",
					Token:           "token",
					Type:            "device-attest-01",
					Status:          StatusPending,
					Value:           "12345678",
				},
				payload: payload,
				db: &MockDB{
					MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
						assert.Equal(t, "azID", id)
						return &Authorization{ID: "azID"}, nil
					},
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
						assert.Equal(t, "12345678", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "payload contained error: an error")

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)

						return errors.New("force")
					},
				},
				err: NewError(ErrorServerInternalType, "failure saving error to acme challenge: force"),
			}
		},
		"ok/device-attest-01": func(t *testing.T) test {
			jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
			payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 1234)

			caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
			ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))

			return test{
				ch: &Challenge{
					ID:              "chID",
					AuthorizationID: "azID",
					Token:           "token",
					Type:            "device-attest-01",
					Status:          StatusPending,
					Value:           "1234",
				},
				payload: payload,
				ctx:     ctx,
				jwk:     jwk,
				db: &MockDB{
					MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
						assert.Equal(t, "azID", id)
						return &Authorization{ID: "azID"}, nil
					},
					MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
						fingerprint, err := keyutil.Fingerprint(leaf.PublicKey)
						assert.NoError(t, err)
						assert.Equal(t, "azID", az.ID)
						assert.Equal(t, fingerprint, az.Fingerprint)
						return nil
					},
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusValid, updch.Status)
						assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
						assert.Equal(t, "1234", updch.Value)

						return nil
					},
				},
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)

			if tc.srv != nil {
				defer tc.srv.Close()
			}

			ctx := tc.ctx
			if ctx == nil {
				ctx = context.Background()
			}
			ctx = NewClientContext(ctx, tc.vc)
			if err := tc.ch.Validate(ctx, tc.db, tc.jwk, tc.payload); err != nil {
				if assert.Error(t, tc.err) {
					var k *Error
					if errors.As(err, &k) {
						assert.Equal(t, tc.err.Type, k.Type)
						assert.Equal(t, tc.err.Detail, k.Detail)
						assert.Equal(t, tc.err.Status, k.Status)
						assert.Equal(t, tc.err.Err.Error(), k.Err.Error())
					} else {
						assert.Fail(t, "unexpected error type")
					}
				}
			} else {
				assert.Nil(t, tc.err)
			}
		})
	}
}

type errReader int

func (errReader) Read([]byte) (int, error) {
	return 0, errors.New("force")
}
func (errReader) Close() error {
	return nil
}

func TestHTTP01Validate(t *testing.T) {
	type test struct {
		vc  Client
		ch  *Challenge
		jwk *jose.JSONWebKey
		db  DB
		err *Error
	}
	tests := map[string]func(t *testing.T) test{
		"fail/http-get-error-store-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusPending,
			}

			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorConnectionType, "error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s: force", ch.Token)
						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/http-get-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusPending,
			}

			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorConnectionType, "error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s: force", ch.Token)
						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
			}
		},
		"fail/http-get->=400-store-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusPending,
			}

			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return &http.Response{
							StatusCode: http.StatusBadRequest,
							Body:       errReader(0),
						}, nil
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorConnectionType, "error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s with status code 400", ch.Token)
						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/http-get->=400": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusPending,
			}

			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return &http.Response{
							StatusCode: http.StatusBadRequest,
							Body:       errReader(0),
						}, nil
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorConnectionType, "error doing http GET for url http://zap.internal/.well-known/acme-challenge/%s with status code 400", ch.Token)
						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
			}
		},
		"fail/read-body": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusPending,
			}

			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return &http.Response{
							Body: errReader(0),
						}, nil
					},
				},
				err: NewErrorISE("error reading response body for url http://zap.internal/.well-known/acme-challenge/%s: force", ch.Token),
			}
		},
		"fail/key-auth-gen-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusPending,
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)
			jwk.Key = "foo"
			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return &http.Response{
							Body: io.NopCloser(bytes.NewBufferString("foo")),
						}, nil
					},
				},
				jwk: jwk,
				err: NewErrorISE("error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'"),
			}
		},
		"ok/key-auth-mismatch": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusPending,
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return &http.Response{
							Body: io.NopCloser(bytes.NewBufferString("foo")),
						}, nil
					},
				},
				jwk: jwk,
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusInvalid, updch.Status)

						err := NewError(ErrorRejectedIdentifierType,
							"keyAuthorization does not match; expected %s, but got foo", expKeyAuth)
						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
			}
		},
		"fail/key-auth-mismatch-store-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusPending,
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return &http.Response{
							Body: io.NopCloser(bytes.NewBufferString("foo")),
						}, nil
					},
				},
				jwk: jwk,
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusInvalid, updch.Status)

						err := NewError(ErrorRejectedIdentifierType,
							"keyAuthorization does not match; expected %s, but got foo", expKeyAuth)
						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"fail/update-challenge-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusPending,
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return &http.Response{
							Body: io.NopCloser(bytes.NewBufferString(expKeyAuth)),
						}, nil
					},
				},
				jwk: jwk,
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusValid, updch.Status)
						assert.Nil(t, updch.Error)

						va, err := time.Parse(time.RFC3339, updch.ValidatedAt)
						require.NoError(t, err)
						now := clock.Now()
						assert.True(t, va.Add(-time.Minute).Before(now))
						assert.True(t, va.Add(time.Minute).After(now))

						return errors.New("force")
					},
				},
				err: NewErrorISE("error updating challenge: force"),
			}
		},
		"ok": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  "zap.internal",
				Status: StatusPending,
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			return test{
				ch: ch,
				vc: &mockClient{
					get: func(url string) (*http.Response, error) {
						return &http.Response{
							Body: io.NopCloser(bytes.NewBufferString(expKeyAuth)),
						}, nil
					},
				},
				jwk: jwk,
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Equal(t, StatusValid, updch.Status)
						assert.Nil(t, updch.Error)

						va, err := time.Parse(time.RFC3339, updch.ValidatedAt)
						require.NoError(t, err)
						now := clock.Now()
						assert.True(t, va.Add(-time.Minute).Before(now))
						assert.True(t, va.Add(time.Minute).After(now))
						return nil
					},
				},
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)
			ctx := NewClientContext(context.Background(), tc.vc)
			if err := http01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
				if assert.Error(t, tc.err) {
					var k *Error
					if errors.As(err, &k) {
						assert.Equal(t, tc.err.Type, k.Type)
						assert.Equal(t, tc.err.Detail, k.Detail)
						assert.Equal(t, tc.err.Status, k.Status)
						assert.Equal(t, tc.err.Err.Error(), k.Err.Error())
					} else {
						assert.Fail(t, "unexpected error type")
					}
				}
			} else {
				assert.Nil(t, tc.err)
			}
		})
	}
}

func TestDNS01Validate(t *testing.T) {
	fulldomain := "*.zap.internal"
	domain := strings.TrimPrefix(fulldomain, "*.")
	type test struct {
		vc  Client
		ch  *Challenge
		jwk *jose.JSONWebKey
		db  DB
		err *Error
	}
	tests := map[string]func(t *testing.T) test{
		"fail/lookupTXT-store-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  fulldomain,
				Status: StatusPending,
			}

			return test{
				ch: ch,
				vc: &mockClient{
					lookupTxt: func(url string) ([]string, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, fulldomain, updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorDNSType, "error looking up TXT records for domain %s: force", domain)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/lookupTXT-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  fulldomain,
				Status: StatusPending,
			}

			return test{
				ch: ch,
				vc: &mockClient{
					lookupTxt: func(url string) ([]string, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, fulldomain, updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorDNSType, "error looking up TXT records for domain %s: force", domain)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
			}
		},
		"fail/key-auth-gen-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  fulldomain,
				Status: StatusPending,
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)
			jwk.Key = "foo"

			return test{
				ch: ch,
				vc: &mockClient{
					lookupTxt: func(url string) ([]string, error) {
						return []string{"foo"}, nil
					},
				},
				jwk: jwk,
				err: NewErrorISE("error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'"),
			}
		},
		"fail/key-auth-mismatch-store-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  fulldomain,
				Status: StatusPending,
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)

			return test{
				ch: ch,
				vc: &mockClient{
					lookupTxt: func(url string) ([]string, error) {
						return []string{"foo", "bar"}, nil
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, fulldomain, updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorRejectedIdentifierType, "keyAuthorization does not match; expected %s, but got %s", expKeyAuth, []string{"foo", "bar"})

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				jwk: jwk,
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/key-auth-mismatch-store-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  fulldomain,
				Status: StatusPending,
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)

			return test{
				ch: ch,
				vc: &mockClient{
					lookupTxt: func(url string) ([]string, error) {
						return []string{"foo", "bar"}, nil
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, fulldomain, updch.Value)
						assert.Equal(t, StatusPending, updch.Status)

						err := NewError(ErrorRejectedIdentifierType, "keyAuthorization does not match; expected %s, but got %s", expKeyAuth, []string{"foo", "bar"})

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				jwk: jwk,
			}
		},
		"fail/update-challenge-error": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  fulldomain,
				Status: StatusPending,
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			h := sha256.Sum256([]byte(expKeyAuth))
			expected := base64.RawURLEncoding.EncodeToString(h[:])

			return test{
				ch: ch,
				vc: &mockClient{
					lookupTxt: func(url string) ([]string, error) {
						return []string{"foo", expected}, nil
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, fulldomain, ch.Value)
						assert.Equal(t, StatusValid, updch.Status)
						assert.Nil(t, updch.Error)

						va, err := time.Parse(time.RFC3339, updch.ValidatedAt)
						require.NoError(t, err)
						now := clock.Now()
						assert.True(t, va.Add(-time.Minute).Before(now))
						assert.True(t, va.Add(time.Minute).After(now))

						return errors.New("force")
					},
				},
				jwk: jwk,
				err: NewErrorISE("error updating challenge: force"),
			}
		},
		"ok": func(t *testing.T) test {
			ch := &Challenge{
				ID:     "chID",
				Token:  "token",
				Value:  fulldomain,
				Status: StatusPending,
			}

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			h := sha256.Sum256([]byte(expKeyAuth))
			expected := base64.RawURLEncoding.EncodeToString(h[:])

			return test{
				ch: ch,
				vc: &mockClient{
					lookupTxt: func(url string) ([]string, error) {
						return []string{"foo", expected}, nil
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, fulldomain, updch.Value)
						assert.Equal(t, StatusValid, updch.Status)
						assert.Nil(t, updch.Error)

						va, err := time.Parse(time.RFC3339, updch.ValidatedAt)
						require.NoError(t, err)
						now := clock.Now()
						assert.True(t, va.Add(-time.Minute).Before(now))
						assert.True(t, va.Add(time.Minute).After(now))

						return nil
					},
				},
				jwk: jwk,
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)
			ctx := NewClientContext(context.Background(), tc.vc)
			if err := dns01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
				if assert.Error(t, tc.err) {
					var k *Error
					if errors.As(err, &k) {
						assert.Equal(t, tc.err.Type, k.Type)
						assert.Equal(t, tc.err.Detail, k.Detail)
						assert.Equal(t, tc.err.Status, k.Status)
						assert.Equal(t, tc.err.Err.Error(), k.Err.Error())
					} else {
						assert.Fail(t, "unexpected error type")
					}
				}
			} else {
				assert.Nil(t, tc.err)
			}
		})
	}
}

type tlsDialer func(network, addr string, config *tls.Config) (conn *tls.Conn, err error)

func newTestTLSALPNServer(validationCert *tls.Certificate, opts ...func(*httptest.Server)) (*httptest.Server, tlsDialer) {
	srv := httptest.NewUnstartedServer(http.NewServeMux())

	srv.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){
		"acme-tls/1": func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
			// no-op
		},
		"http/1.1": func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
			panic("unexpected http/1.1 next proto")
		},
	}

	srv.TLS = &tls.Config{
		GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
			if len(hello.SupportedProtos) == 1 && hello.SupportedProtos[0] == "acme-tls/1" {
				return validationCert, nil
			}
			return nil, nil
		},
		NextProtos: []string{
			"acme-tls/1",
			"http/1.1",
		},
	}

	// Apply options
	for _, fn := range opts {
		fn(srv)
	}

	srv.Listener = tls.NewListener(srv.Listener, srv.TLS)
	//srv.Config.ErrorLog = log.New(ioutil.Discard, "", 0) // hush

	return srv, func(network, addr string, config *tls.Config) (conn *tls.Conn, err error) {
		return tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", srv.Listener.Addr().String(), config)
	}
}

// noopConn is a mock net.Conn that does nothing.
type noopConn struct{}

func (c *noopConn) Read(_ []byte) (n int, err error)  { return 0, io.EOF }
func (c *noopConn) Write(_ []byte) (n int, err error) { return 0, io.EOF }
func (c *noopConn) Close() error                      { return nil }
func (c *noopConn) LocalAddr() net.Addr               { return &net.IPAddr{IP: net.IPv4zero, Zone: ""} }
func (c *noopConn) RemoteAddr() net.Addr              { return &net.IPAddr{IP: net.IPv4zero, Zone: ""} }
func (c *noopConn) SetDeadline(time.Time) error       { return nil }
func (c *noopConn) SetReadDeadline(time.Time) error   { return nil }
func (c *noopConn) SetWriteDeadline(time.Time) error  { return nil }

func newTLSALPNValidationCert(keyAuthHash []byte, obsoleteOID, critical bool, names ...string) (*tls.Certificate, error) {
	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return nil, err
	}

	certTemplate := &x509.Certificate{
		SerialNumber: big.NewInt(1337),
		Subject: pkix.Name{
			Organization: []string{"Test"},
		},
		NotBefore:             time.Now(),
		NotAfter:              time.Now().AddDate(0, 0, 1),
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
		DNSNames:              names,
	}

	if keyAuthHash != nil {
		oid := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
		if obsoleteOID {
			oid = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}
		}

		keyAuthHashEnc, _ := asn1.Marshal(keyAuthHash)

		certTemplate.ExtraExtensions = []pkix.Extension{
			{
				Id:       oid,
				Critical: critical,
				Value:    keyAuthHashEnc,
			},
		}
	}

	cert, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, privateKey.Public(), privateKey)
	if err != nil {
		return nil, err
	}

	return &tls.Certificate{
		PrivateKey:  privateKey,
		Certificate: [][]byte{cert},
	}, nil
}

func TestTLSALPN01Validate(t *testing.T) {
	makeTLSCh := func() *Challenge {
		return &Challenge{
			ID:     "chID",
			Token:  "token",
			Type:   "tls-alpn-01",
			Status: StatusPending,
			Value:  "zap.internal",
		}
	}
	type test struct {
		vc  Client
		ch  *Challenge
		jwk *jose.JSONWebKey
		db  DB
		srv *httptest.Server
		err *Error
	}
	tests := map[string]func(t *testing.T) test{
		"fail/tlsDial-store-error": func(t *testing.T) test {
			ch := makeTLSCh()
			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusPending, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443: force", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/tlsDial-error": func(t *testing.T) test {
			ch := makeTLSCh()
			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
						return nil, errors.New("force")
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusPending, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443: force", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
			}
		},
		"ok/tlsDial-timeout": func(t *testing.T) test {
			ch := makeTLSCh()

			srv, tlsDial := newTestTLSALPNServer(nil)
			// srv.Start() - do not start server to cause timeout

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusPending, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorConnectionType, "error doing TLS dial for %v:443: context deadline exceeded", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				srv: srv,
			}
		},
		"ok/no-certificates-error": func(t *testing.T) test {
			ch := makeTLSCh()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
						return tls.Client(&noopConn{}, config), nil
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "tls-alpn-01 challenge for %v resulted in no certificates", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
			}
		},
		"fail/no-certificates-store-error": func(t *testing.T) test {
			ch := makeTLSCh()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
						return tls.Client(&noopConn{}, config), nil
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "tls-alpn-01 challenge for %v resulted in no certificates", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/error-no-protocol": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			srv := httptest.NewTLSServer(nil)

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
						return tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", srv.Listener.Addr().String(), config)
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge")

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"fail/no-protocol-store-error": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			srv := httptest.NewTLSServer(nil)

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
						return tls.DialWithDialer(&net.Dialer{Timeout: time.Second}, "tcp", srv.Listener.Addr().String(), config)
					},
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "cannot negotiate ALPN acme-tls/1 protocol for tls-alpn-01 challenge")

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				srv: srv,
				jwk: jwk,
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/no-names-nor-ips-error": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"fail/no-names-store-error": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				srv: srv,
				jwk: jwk,
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/too-many-names-error": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value, "other.internal")
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"ok/wrong-name": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, "other.internal")
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: leaf certificate must contain a single IP address or DNS name, %v", ch.Value)

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"fail/key-auth-gen-error": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))
			jwk.Key = "foo"

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				srv: srv,
				jwk: jwk,
				err: NewErrorISE("error generating JWK thumbprint: go-jose/go-jose: unknown key type 'string'"),
			}
		},
		"ok/error-no-extension": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			cert, err := newTLSALPNValidationCert(nil, false, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: missing acmeValidationV1 extension")

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"fail/no-extension-store-error": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			cert, err := newTLSALPNValidationCert(nil, false, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: missing acmeValidationV1 extension")

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				srv: srv,
				jwk: jwk,
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/error-extension-not-critical": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, false, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: acmeValidationV1 extension not critical")

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"fail/extension-not-critical-store-error": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, false, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: acmeValidationV1 extension not critical")

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				srv: srv,
				jwk: jwk,
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/error-malformed-extension": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			cert, err := newTLSALPNValidationCert([]byte{1, 2, 3}, false, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: malformed acmeValidationV1 extension value")

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"fail/malformed-extension-store-error": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			cert, err := newTLSALPNValidationCert([]byte{1, 2, 3}, false, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: malformed acmeValidationV1 extension value")

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				srv: srv,
				jwk: jwk,
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/error-keyauth-mismatch": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))
			incorrectTokenHash := sha256.Sum256([]byte("mismatched"))

			cert, err := newTLSALPNValidationCert(incorrectTokenHash[:], false, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: "+
							"expected acmeValidationV1 extension value %s for this challenge but got %s",
							hex.EncodeToString(expKeyAuthHash[:]), hex.EncodeToString(incorrectTokenHash[:]))

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"fail/keyauth-mismatch-store-error": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))
			incorrectTokenHash := sha256.Sum256([]byte("mismatched"))

			cert, err := newTLSALPNValidationCert(incorrectTokenHash[:], false, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: "+
							"expected acmeValidationV1 extension value %s for this challenge but got %s",
							hex.EncodeToString(expKeyAuthHash[:]), hex.EncodeToString(incorrectTokenHash[:]))

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				srv: srv,
				jwk: jwk,
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/error-obsolete-oid": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], true, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: "+
							"obsolete id-pe-acmeIdentifier in acmeValidationV1 extension")

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"fail/obsolete-oid-store-error": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], true, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusInvalid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)

						err := NewError(ErrorRejectedIdentifierType, "incorrect certificate for tls-alpn-01 challenge: "+
							"obsolete id-pe-acmeIdentifier in acmeValidationV1 extension")

						assert.EqualError(t, updch.Error.Err, err.Err.Error())
						assert.Equal(t, err.Type, updch.Error.Type)
						assert.Equal(t, err.Detail, updch.Error.Detail)
						assert.Equal(t, err.Status, updch.Error.Status)
						assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

						return errors.New("force")
					},
				},
				srv: srv,
				jwk: jwk,
				err: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok": func(t *testing.T) test {
			ch := makeTLSCh()

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusValid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "zap.internal", updch.Value)
						assert.Nil(t, updch.Error)

						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
		"ok/ip": func(t *testing.T) test {
			ch := makeTLSCh()
			ch.Value = "127.0.0.1"

			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)

			expKeyAuth, err := KeyAuthorization(ch.Token, jwk)
			require.NoError(t, err)
			expKeyAuthHash := sha256.Sum256([]byte(expKeyAuth))

			cert, err := newTLSALPNValidationCert(expKeyAuthHash[:], false, true, ch.Value)
			require.NoError(t, err)

			srv, tlsDial := newTestTLSALPNServer(cert)
			srv.Start()

			return test{
				ch: ch,
				vc: &mockClient{
					tlsDial: tlsDial,
				},
				db: &MockDB{
					MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
						assert.Equal(t, "chID", updch.ID)
						assert.Equal(t, "token", updch.Token)
						assert.Equal(t, StatusValid, updch.Status)
						assert.Equal(t, ChallengeType("tls-alpn-01"), updch.Type)
						assert.Equal(t, "127.0.0.1", updch.Value)
						assert.Nil(t, updch.Error)

						return nil
					},
				},
				srv: srv,
				jwk: jwk,
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)

			if tc.srv != nil {
				defer tc.srv.Close()
			}

			ctx := NewClientContext(context.Background(), tc.vc)
			if err := tlsalpn01Validate(ctx, tc.ch, tc.db, tc.jwk); err != nil {
				if assert.Error(t, tc.err) {
					var k *Error
					if errors.As(err, &k) {
						assert.Equal(t, tc.err.Type, k.Type)
						assert.Equal(t, tc.err.Detail, k.Detail)
						assert.Equal(t, tc.err.Status, k.Status)
						assert.Equal(t, tc.err.Err.Error(), k.Err.Error())
						assert.Equal(t, tc.err.Subproblems, k.Subproblems)
					} else {
						assert.Fail(t, "unexpected error type")
					}
				}
			} else {
				assert.Nil(t, tc.err)
			}
		})
	}
}

func Test_reverseAddr(t *testing.T) {
	type args struct {
		ip net.IP
	}
	tests := []struct {
		name     string
		args     args
		wantArpa string
	}{
		{
			name: "ok/ipv4",
			args: args{
				ip: net.ParseIP("127.0.0.1"),
			},
			wantArpa: "1.0.0.127.in-addr.arpa.",
		},
		{
			name: "ok/ipv6",
			args: args{
				ip: net.ParseIP("2001:db8::567:89ab"),
			},
			wantArpa: "b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if gotArpa := reverseAddr(tt.args.ip); gotArpa != tt.wantArpa {
				t.Errorf("reverseAddr() = %v, want %v", gotArpa, tt.wantArpa)
			}
		})
	}
}

func Test_serverName(t *testing.T) {
	type args struct {
		ch *Challenge
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		{
			name: "ok/dns",
			args: args{
				ch: &Challenge{
					Value: "example.com",
				},
			},
			want: "example.com",
		},
		{
			name: "ok/ipv4",
			args: args{
				ch: &Challenge{
					Value: "127.0.0.1",
				},
			},
			want: "1.0.0.127.in-addr.arpa.",
		},
		{
			name: "ok/ipv6",
			args: args{
				ch: &Challenge{
					Value: "2001:db8::567:89ab",
				},
			},
			want: "b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := serverName(tt.args.ch); got != tt.want {
				t.Errorf("serverName() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_http01ChallengeHost(t *testing.T) {
	tests := []struct {
		name  string
		value string
		want  string
	}{
		{
			name:  "dns",
			value: "www.example.com",
			want:  "www.example.com",
		},
		{
			name:  "ipv4",
			value: "127.0.0.1",
			want:  "127.0.0.1",
		},
		{
			name:  "ipv6",
			value: "::1",
			want:  "[::1]",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := http01ChallengeHost(tt.value); got != tt.want {
				t.Errorf("http01ChallengeHost() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_doAppleAttestationFormat(t *testing.T) {
	ctx := context.Background()
	ca, err := minica.New()
	if err != nil {
		t.Fatal(err)
	}
	caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
	signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	leaf, err := ca.Sign(&x509.Certificate{
		Subject:   pkix.Name{CommonName: "attestation cert"},
		PublicKey: signer.Public(),
		ExtraExtensions: []pkix.Extension{
			{Id: oidAppleSerialNumber, Value: []byte("serial-number")},
			{Id: oidAppleUniqueDeviceIdentifier, Value: []byte("udid")},
			{Id: oidAppleSecureEnclaveProcessorOSVersion, Value: []byte("16.0")},
			{Id: oidAppleNonce, Value: []byte("nonce")},
		},
	})
	if err != nil {
		t.Fatal(err)
	}
	fingerprint, err := keyutil.Fingerprint(signer.Public())
	if err != nil {
		t.Fatal(err)
	}

	type args struct {
		ctx  context.Context
		prov Provisioner
		ch   *Challenge
		att  *attestationObject
	}
	tests := []struct {
		name    string
		args    args
		want    *appleAttestationData
		wantErr bool
	}{
		{"ok", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{
			Format: "apple",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
			},
		}}, &appleAttestationData{
			Nonce:        []byte("nonce"),
			SerialNumber: "serial-number",
			UDID:         "udid",
			SEPVersion:   "16.0",
			Certificate:  leaf,
			Fingerprint:  fingerprint,
		}, false},
		{"fail apple issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{}, &attestationObject{
			Format: "apple",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
			},
		}}, nil, true},
		{"fail missing x5c", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{
			Format: "apple",
			AttStatement: map[string]interface{}{
				"foo": "bar",
			},
		}}, nil, true},
		{"fail empty issuer", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{
			Format: "apple",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{},
			},
		}}, nil, true},
		{"fail leaf type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{
			Format: "apple",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{"leaf", ca.Intermediate.Raw},
			},
		}}, nil, true},
		{"fail leaf parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{
			Format: "apple",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw[:100], ca.Intermediate.Raw},
			},
		}}, nil, true},
		{"fail intermediate type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{
			Format: "apple",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, "intermediate"},
			},
		}}, nil, true},
		{"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{
			Format: "apple",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw[:100]},
			},
		}}, nil, true},
		{"fail verify", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{
			Format: "apple",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw},
			},
		}}, nil, true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := doAppleAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.att)
			if (err != nil) != tt.wantErr {
				t.Errorf("doAppleAttestationFormat() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("doAppleAttestationFormat() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_doStepAttestationFormat(t *testing.T) {
	ctx := context.Background()
	ca, err := minica.New()
	if err != nil {
		t.Fatal(err)
	}
	caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})

	makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
		leaf, err := ca.Sign(&x509.Certificate{
			Subject:   pkix.Name{CommonName: "attestation cert"},
			PublicKey: signer.Public(),
			ExtraExtensions: []pkix.Extension{
				{Id: oidYubicoSerialNumber, Value: serialNumber},
			},
		})
		if err != nil {
			t.Fatal(err)
		}
		return leaf
	}
	mustSigner := func(kty, crv string, size int) crypto.Signer {
		s, err := keyutil.GenerateSigner(kty, crv, size)
		if err != nil {
			t.Fatal(err)
		}
		return s
	}

	signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	serialNumber, err := asn1.Marshal(1234)
	if err != nil {
		t.Fatal(err)
	}
	leaf := makeLeaf(signer, serialNumber)
	fingerprint, err := keyutil.Fingerprint(signer.Public())
	if err != nil {
		t.Fatal(err)
	}

	jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
	if err != nil {
		t.Fatal(err)
	}
	keyAuth, err := KeyAuthorization("token", jwk)
	if err != nil {
		t.Fatal(err)
	}
	keyAuthSum := sha256.Sum256([]byte(keyAuth))
	sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
	if err != nil {
		t.Fatal(err)
	}
	cborSig, err := cbor.Marshal(sig)
	if err != nil {
		t.Fatal(err)
	}

	otherSigner, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	otherSig, err := otherSigner.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
	if err != nil {
		t.Fatal(err)
	}
	otherCBORSig, err := cbor.Marshal(otherSig)
	if err != nil {
		t.Fatal(err)
	}

	type args struct {
		ctx  context.Context
		prov Provisioner
		ch   *Challenge
		jwk  *jose.JSONWebKey
		att  *attestationObject
	}
	tests := []struct {
		name    string
		args    args
		want    *stepAttestationData
		wantErr bool
	}{
		{"ok", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, &stepAttestationData{
			SerialNumber: "1234",
			Certificate:  leaf,
			Fingerprint:  fingerprint,
		}, false},
		{"fail yubico issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail x5c type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": [][]byte{leaf.Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail x5c empty", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail leaf type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{"leaf", ca.Intermediate.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail leaf parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw[:100], ca.Intermediate.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail intermediate type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, "intermediate"},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw[:100]},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail verify", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail sig type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": string(cborSig),
			},
		}}, nil, true},
		{"fail sig unmarshal", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": []byte("bad-sig"),
			},
		}}, nil, true},
		{"fail keyAuthorization", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, &jose.JSONWebKey{Key: []byte("not an asymmetric key")}, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail sig verify P-256", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": otherCBORSig,
			},
		}}, nil, true},
		{"fail sig verify P-384", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{makeLeaf(mustSigner("EC", "P-384", 0), serialNumber).Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail sig verify RSA", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{makeLeaf(mustSigner("RSA", "", 2048), serialNumber).Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail sig verify Ed25519", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{makeLeaf(mustSigner("OKP", "Ed25519", 0), serialNumber).Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
		{"fail unmarshal serial number", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{makeLeaf(signer, []byte("bad-serial")).Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := doStepAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)
			if (err != nil) != tt.wantErr {
				t.Errorf("doStepAttestationFormat() error = %#v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("doStepAttestationFormat() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_doStepAttestationFormat_noCAIntermediate(t *testing.T) {
	ctx := context.Background()

	// This CA simulates a YubiKey v5.2.4, where the attestation intermediate in
	// the CA does not have the basic constraint extension. With the current
	// validation of the certificate the test case below returns an error. If
	// we change the validation to support this use case, the test case below
	// should change.
	//
	// See https://github.com/Yubico/yubikey-manager/issues/522
	ca, err := minica.New(minica.WithIntermediateTemplate(`{"subject": {{ toJson .Subject }}}`))
	if err != nil {
		t.Fatal(err)
	}
	caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})

	makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
		leaf, err := ca.Sign(&x509.Certificate{
			Subject:   pkix.Name{CommonName: "attestation cert"},
			PublicKey: signer.Public(),
			ExtraExtensions: []pkix.Extension{
				{Id: oidYubicoSerialNumber, Value: serialNumber},
			},
		})
		if err != nil {
			t.Fatal(err)
		}
		return leaf
	}

	signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	serialNumber, err := asn1.Marshal(1234)
	if err != nil {
		t.Fatal(err)
	}
	leaf := makeLeaf(signer, serialNumber)

	jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
	if err != nil {
		t.Fatal(err)
	}
	keyAuth, err := KeyAuthorization("token", jwk)
	if err != nil {
		t.Fatal(err)
	}
	keyAuthSum := sha256.Sum256([]byte(keyAuth))
	sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
	if err != nil {
		t.Fatal(err)
	}
	cborSig, err := cbor.Marshal(sig)
	if err != nil {
		t.Fatal(err)
	}

	type args struct {
		ctx  context.Context
		prov Provisioner
		ch   *Challenge
		jwk  *jose.JSONWebKey
		att  *attestationObject
	}
	tests := []struct {
		name    string
		args    args
		want    *stepAttestationData
		wantErr bool
	}{
		{"fail no intermediate", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
				"alg": -7,
				"sig": cborSig,
			},
		}}, nil, true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := doStepAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)
			if (err != nil) != tt.wantErr {
				t.Errorf("doStepAttestationFormat() error = %#v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("doStepAttestationFormat() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_deviceAttest01Validate(t *testing.T) {
	invalidPayload := "!?"
	errorPayload, err := json.Marshal(struct {
		Error string `json:"error"`
	}{
		Error: "an error",
	})
	require.NoError(t, err)
	errorBase64Payload, err := json.Marshal(struct {
		AttObj string `json:"attObj"`
	}{
		AttObj: "?!",
	})
	require.NoError(t, err)
	errorCBORPayload, err := json.Marshal(struct {
		AttObj string `json:"attObj"`
	}{
		AttObj: "AAAA",
	})
	require.NoError(t, err)
	type args struct {
		ctx     context.Context
		ch      *Challenge
		db      DB
		jwk     *jose.JSONWebKey
		payload []byte
	}
	type test struct {
		args    args
		wantErr *Error
	}
	tests := map[string]func(t *testing.T) test{
		"fail/getAuthorization": func(t *testing.T) test {
			return test{
				args: args{
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							return nil, errors.New("not found")
						},
					},
					payload: []byte(invalidPayload),
				},
				wantErr: NewErrorISE("error loading authorization: not found"),
			}
		},
		"fail/json.Unmarshal": func(t *testing.T) test {
			return test{
				args: args{
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
					},
					payload: []byte(invalidPayload),
				},
				wantErr: NewErrorISE("error unmarshalling JSON: invalid character '!' looking for beginning of value"),
			}

		},
		"fail/storeError": func(t *testing.T) test {
			return test{
				args: args{
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					payload: errorPayload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
							assert.Equal(t, "chID", updch.ID)
							assert.Equal(t, "token", updch.Token)
							assert.Equal(t, StatusInvalid, updch.Status)
							assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
							assert.Equal(t, "12345678", updch.Value)

							err := NewError(ErrorRejectedIdentifierType, "payload contained error: an error")

							assert.EqualError(t, updch.Error.Err, err.Err.Error())
							assert.Equal(t, err.Type, updch.Error.Type)
							assert.Equal(t, err.Detail, updch.Error.Detail)
							assert.Equal(t, err.Status, updch.Error.Status)
							assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

							return errors.New("force")
						},
					},
				},
				wantErr: NewErrorISE("failure saving error to acme challenge: force"),
			}
		},
		"ok/storeError-return-nil": func(t *testing.T) test {
			return test{
				args: args{
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					payload: errorPayload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
							assert.Equal(t, "chID", updch.ID)
							assert.Equal(t, "token", updch.Token)
							assert.Equal(t, StatusInvalid, updch.Status)
							assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
							assert.Equal(t, "12345678", updch.Value)

							err := NewError(ErrorRejectedIdentifierType, "payload contained error: an error")

							assert.EqualError(t, updch.Error.Err, err.Err.Error())
							assert.Equal(t, err.Type, updch.Error.Type)
							assert.Equal(t, err.Detail, updch.Error.Detail)
							assert.Equal(t, err.Status, updch.Error.Status)
							assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

							return nil
						},
					},
				},
				wantErr: nil,
			}
		},
		"fail/base64-decode": func(t *testing.T) test {
			return test{
				args: args{
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
					},
					payload: errorBase64Payload,
				},
				wantErr: NewErrorISE("error base64 decoding attObj: illegal base64 data at input byte 0"),
			}
		},
		"fail/cbor.Unmarshal": func(t *testing.T) test {
			return test{
				args: args{
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
					},
					payload: errorCBORPayload,
				},
				wantErr: NewErrorISE("error unmarshalling CBOR: cbor:"),
			}
		},
		"ok/prov.IsAttestationFormatEnabled": func(t *testing.T) test {
			jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
			payload, _, _ := mustAttestYubikey(t, "nonce", keyAuth, 12345678)
			ctx := NewProvisionerContext(context.Background(), mustNonAttestationProvisioner(t))

			return test{
				args: args{
					ctx: ctx,
					jwk: jwk,
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					payload: payload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
							assert.Equal(t, "chID", updch.ID)
							assert.Equal(t, "token", updch.Token)
							assert.Equal(t, StatusInvalid, updch.Status)
							assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
							assert.Equal(t, "12345678", updch.Value)

							err := NewError(ErrorBadAttestationStatementType, "attestation format %q is not enabled", "step")

							assert.EqualError(t, updch.Error.Err, err.Err.Error())
							assert.Equal(t, err.Type, updch.Error.Type)
							assert.Equal(t, err.Detail, updch.Error.Detail)
							assert.Equal(t, err.Status, updch.Error.Status)
							assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

							return nil
						},
					},
				},
				wantErr: nil,
			}
		},
		"ok/doAppleAttestationFormat-storeError": func(t *testing.T) test {
			ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, nil))
			attObj, err := cbor.Marshal(struct {
				Format       string                 `json:"fmt"`
				AttStatement map[string]interface{} `json:"attStmt,omitempty"`
			}{
				Format:       "apple",
				AttStatement: map[string]interface{}{},
			})
			require.NoError(t, err)
			payload, err := json.Marshal(struct {
				AttObj string `json:"attObj"`
			}{
				AttObj: base64.RawURLEncoding.EncodeToString(attObj),
			})
			require.NoError(t, err)
			return test{
				args: args{
					ctx: ctx,
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					payload: payload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
							assert.Equal(t, "chID", updch.ID)
							assert.Equal(t, "token", updch.Token)
							assert.Equal(t, StatusInvalid, updch.Status)
							assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
							assert.Equal(t, "12345678", updch.Value)

							err := NewDetailedError(ErrorBadAttestationStatementType, "x5c not present")

							assert.EqualError(t, updch.Error.Err, err.Err.Error())
							assert.Equal(t, err.Type, updch.Error.Type)
							assert.Equal(t, err.Detail, updch.Error.Detail)
							assert.Equal(t, err.Status, updch.Error.Status)
							assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

							return nil
						},
					},
				},
				wantErr: nil,
			}
		},
		"ok/doAppleAttestationFormat-non-matching-nonce": func(t *testing.T) test {
			jwk, _ := mustAccountAndKeyAuthorization(t, "token")
			payload, _, root := mustAttestApple(t, "bad-nonce")

			caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
			ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))

			return test{
				args: args{
					ctx: ctx,
					jwk: jwk,
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "serial-number",
					},
					payload: payload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
							assert.Equal(t, "chID", updch.ID)
							assert.Equal(t, "token", updch.Token)
							assert.Equal(t, StatusInvalid, updch.Status)
							assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
							assert.Equal(t, "serial-number", updch.Value)

							err := NewDetailedError(ErrorBadAttestationStatementType, "challenge token does not match")

							assert.EqualError(t, updch.Error.Err, err.Err.Error())
							assert.Equal(t, err.Type, updch.Error.Type)
							assert.Equal(t, err.Detail, updch.Error.Detail)
							assert.Equal(t, err.Status, updch.Error.Status)
							assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

							return nil
						},
					},
				},
				wantErr: nil,
			}
		},
		"ok/doAppleAttestationFormat-non-matching-challenge-value": func(t *testing.T) test {
			jwk, _ := mustAccountAndKeyAuthorization(t, "token")
			payload, _, root := mustAttestApple(t, "nonce")

			caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
			ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
			return test{
				args: args{
					ctx: ctx,
					jwk: jwk,
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "nonce",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "non-matching-value",
					},
					payload: payload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
							assert.Equal(t, "chID", updch.ID)
							assert.Equal(t, "nonce", updch.Token)
							assert.Equal(t, StatusInvalid, updch.Status)
							assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
							assert.Equal(t, "non-matching-value", updch.Value)

							subproblem := NewSubproblemWithIdentifier(
								ErrorRejectedIdentifierType,
								Identifier{Type: "permanent-identifier", Value: "non-matching-value"},
								`challenge identifier "non-matching-value" doesn't match any of the attested hardware identifiers ["udid" "serial-number"]`,
							)
							err := NewDetailedError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem)

							assert.EqualError(t, updch.Error.Err, err.Err.Error())
							assert.Equal(t, err.Type, updch.Error.Type)
							assert.Equal(t, err.Detail, updch.Error.Detail)
							assert.Equal(t, err.Status, updch.Error.Status)
							assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

							return nil
						},
					},
				},
				wantErr: nil,
			}
		},
		"ok/doStepAttestationFormat-storeError": func(t *testing.T) test {
			ca, err := minica.New()
			require.NoError(t, err)
			caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw})
			signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
			require.NoError(t, err)
			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)
			token := "token"
			keyAuth, err := KeyAuthorization(token, jwk)
			require.NoError(t, err)
			keyAuthSum := sha256.Sum256([]byte(keyAuth))
			sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
			require.NoError(t, err)
			cborSig, err := cbor.Marshal(sig)
			require.NoError(t, err)
			ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
			attObj, err := cbor.Marshal(struct {
				Format       string                 `json:"fmt"`
				AttStatement map[string]interface{} `json:"attStmt,omitempty"`
			}{
				Format: "step",
				AttStatement: map[string]interface{}{
					"alg": -7,
					"sig": cborSig,
				},
			})
			require.NoError(t, err)
			payload, err := json.Marshal(struct {
				AttObj string `json:"attObj"`
			}{
				AttObj: base64.RawURLEncoding.EncodeToString(attObj),
			})
			require.NoError(t, err)
			return test{
				args: args{
					ctx: ctx,
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					payload: payload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
							assert.Equal(t, "chID", updch.ID)
							assert.Equal(t, "token", updch.Token)
							assert.Equal(t, StatusInvalid, updch.Status)
							assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
							assert.Equal(t, "12345678", updch.Value)

							err := NewDetailedError(ErrorBadAttestationStatementType, "x5c not present")

							assert.EqualError(t, updch.Error.Err, err.Err.Error())
							assert.Equal(t, err.Type, updch.Error.Type)
							assert.Equal(t, err.Detail, updch.Error.Detail)
							assert.Equal(t, err.Status, updch.Error.Status)
							assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

							return nil
						},
					},
				},
				wantErr: nil,
			}
		},
		"ok/doStepAttestationFormat-non-matching-identifier": func(t *testing.T) test {
			jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
			payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 87654321)

			caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
			ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))

			return test{
				args: args{
					ctx: ctx,
					jwk: jwk,
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					payload: payload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
							fingerprint, err := keyutil.Fingerprint(leaf.PublicKey)
							assert.NoError(t, err)
							assert.Equal(t, "azID", az.ID)
							assert.Equal(t, fingerprint, az.Fingerprint)
							return nil
						},
						MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
							assert.Equal(t, "chID", updch.ID)
							assert.Equal(t, "token", updch.Token)
							assert.Equal(t, StatusInvalid, updch.Status)
							assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
							assert.Equal(t, "12345678", updch.Value)

							err := NewDetailedError(ErrorBadAttestationStatementType, "permanent identifier does not match").
								AddSubproblems(NewSubproblemWithIdentifier(
									ErrorRejectedIdentifierType,
									Identifier{Type: "permanent-identifier", Value: "12345678"},
									"challenge identifier \"12345678\" doesn't match the attested hardware identifier \"87654321\"",
								))

							assert.EqualError(t, updch.Error.Err, err.Err.Error())
							assert.Equal(t, err.Type, updch.Error.Type)
							assert.Equal(t, err.Detail, updch.Error.Detail)
							assert.Equal(t, err.Status, updch.Error.Status)
							assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

							return nil
						},
					},
				},
				wantErr: nil,
			}
		},
		"ok/unknown-attestation-format": func(t *testing.T) test {
			ca, err := minica.New()
			require.NoError(t, err)
			signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
			require.NoError(t, err)
			jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
			require.NoError(t, err)
			token := "token"
			keyAuth, err := KeyAuthorization(token, jwk)
			require.NoError(t, err)
			keyAuthSum := sha256.Sum256([]byte(keyAuth))
			sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256)
			require.NoError(t, err)
			cborSig, err := cbor.Marshal(sig)
			require.NoError(t, err)
			ctx := NewProvisionerContext(context.Background(), mustNonAttestationProvisioner(t))
			makeLeaf := func(signer crypto.Signer, serialNumber []byte) *x509.Certificate {
				leaf, err := ca.Sign(&x509.Certificate{
					Subject:   pkix.Name{CommonName: "attestation cert"},
					PublicKey: signer.Public(),
					ExtraExtensions: []pkix.Extension{
						{Id: oidYubicoSerialNumber, Value: serialNumber},
					},
				})
				if err != nil {
					t.Fatal(err)
				}
				return leaf
			}
			require.NoError(t, err)
			serialNumber, err := asn1.Marshal(87654321)
			require.NoError(t, err)
			leaf := makeLeaf(signer, serialNumber)
			attObj, err := cbor.Marshal(struct {
				Format       string                 `json:"fmt"`
				AttStatement map[string]interface{} `json:"attStmt,omitempty"`
			}{
				Format: "bogus-format",
				AttStatement: map[string]interface{}{
					"x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw},
					"alg": -7,
					"sig": cborSig,
				},
			})
			require.NoError(t, err)
			payload, err := json.Marshal(struct {
				AttObj string `json:"attObj"`
			}{
				AttObj: base64.RawURLEncoding.EncodeToString(attObj),
			})
			require.NoError(t, err)
			return test{
				args: args{
					ctx: ctx,
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					payload: payload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
							assert.Equal(t, "chID", updch.ID)
							assert.Equal(t, "token", updch.Token)
							assert.Equal(t, StatusInvalid, updch.Status)
							assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
							assert.Equal(t, "12345678", updch.Value)

							err := NewDetailedError(ErrorBadAttestationStatementType, `unsupported attestation object format "bogus-format"`)

							assert.EqualError(t, updch.Error.Err, err.Err.Error())
							assert.Equal(t, err.Type, updch.Error.Type)
							assert.Equal(t, err.Detail, updch.Error.Detail)
							assert.Equal(t, err.Status, updch.Error.Status)
							assert.Equal(t, err.Subproblems, updch.Error.Subproblems)

							return nil
						},
					},
					jwk: jwk,
				},
				wantErr: nil,
			}
		},
		"fail/db.UpdateAuthorization": func(t *testing.T) test {
			jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
			payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 12345678)

			caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
			ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))

			return test{
				args: args{
					ctx: ctx,
					jwk: jwk,
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					payload: payload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
							fingerprint, err := keyutil.Fingerprint(leaf.PublicKey)
							assert.NoError(t, err)
							assert.Equal(t, "azID", az.ID)
							assert.Equal(t, fingerprint, az.Fingerprint)
							return errors.New("force")
						},
					},
				},
				wantErr: NewError(ErrorServerInternalType, "error updating authorization: force"),
			}
		},
		"fail/db.UpdateChallenge": func(t *testing.T) test {
			jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
			payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 12345678)

			caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
			ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))

			return test{
				args: args{
					ctx: ctx,
					jwk: jwk,
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					payload: payload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
							fingerprint, err := keyutil.Fingerprint(leaf.PublicKey)
							assert.NoError(t, err)
							assert.Equal(t, "azID", az.ID)
							assert.Equal(t, fingerprint, az.Fingerprint)
							return nil
						},
						MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
							assert.Equal(t, "chID", updch.ID)
							assert.Equal(t, "token", updch.Token)
							assert.Equal(t, StatusValid, updch.Status)
							assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
							assert.Equal(t, "12345678", updch.Value)

							return errors.New("force")
						},
					},
				},
				wantErr: NewError(ErrorServerInternalType, "error updating challenge: force"),
			}
		},
		"ok": func(t *testing.T) test {
			jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
			payload, leaf, root := mustAttestYubikey(t, "nonce", keyAuth, 12345678)

			caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
			ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))

			return test{
				args: args{
					ctx: ctx,
					jwk: jwk,
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "12345678",
					},
					payload: payload,
					db: &MockDB{
						MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
							assert.Equal(t, "azID", id)
							return &Authorization{ID: "azID"}, nil
						},
						MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
							fingerprint, err := keyutil.Fingerprint(leaf.PublicKey)
							assert.NoError(t, err)
							assert.Equal(t, "azID", az.ID)
							assert.Equal(t, fingerprint, az.Fingerprint)
							return nil
						},
						MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
							assert.Equal(t, "chID", updch.ID)
							assert.Equal(t, "token", updch.Token)
							assert.Equal(t, StatusValid, updch.Status)
							assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
							assert.Equal(t, "12345678", updch.Value)

							return nil
						},
					},
				},
				wantErr: nil,
			}
		},
	}
	for name, run := range tests {
		t.Run(name, func(t *testing.T) {
			tc := run(t)

			if err := deviceAttest01Validate(tc.args.ctx, tc.args.ch, tc.args.db, tc.args.jwk, tc.args.payload); err != nil {
				if assert.Error(t, tc.wantErr) {
					assert.ErrorContains(t, err, tc.wantErr.Error())
				}
				return
			}

			assert.Nil(t, tc.wantErr)
		})
	}
}

var (
	oidTPMManufacturer = asn1.ObjectIdentifier{2, 23, 133, 2, 1}
	oidTPMModel        = asn1.ObjectIdentifier{2, 23, 133, 2, 2}
	oidTPMVersion      = asn1.ObjectIdentifier{2, 23, 133, 2, 3}
)

func generateValidAKCertificate(t *testing.T) *x509.Certificate {
	t.Helper()
	signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	require.NoError(t, err)
	template := &x509.Certificate{
		PublicKey:          signer.Public(),
		Version:            3,
		IsCA:               false,
		UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
	}
	asn1Value := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
	sans := []x509util.SubjectAlternativeName{
		{Type: x509util.DirectoryNameType,
			ASN1Value: asn1Value},
	}
	ext, err := createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)
	require.NoError(t, err)
	ext.Set(template)
	ca, err := minica.New()
	require.NoError(t, err)
	cert, err := ca.Sign(template)
	require.NoError(t, err)

	return cert
}

func Test_validateAKCertificate(t *testing.T) {
	cert := generateValidAKCertificate(t)
	tests := []struct {
		name   string
		c      *x509.Certificate
		expErr error
	}{
		{
			name:   "ok",
			c:      cert,
			expErr: nil,
		},
		{
			name: "fail/version",
			c: &x509.Certificate{
				Version: 1,
			},
			expErr: errors.New("AK certificate has invalid version 1; only version 3 is allowed"),
		},
		{
			name: "fail/subject",
			c: &x509.Certificate{
				Version: 3,
				Subject: pkix.Name{CommonName: "fail!"},
			},
			expErr: errors.New(`AK certificate subject must be empty; got "CN=fail!"`),
		},
		{
			name: "fail/isCA",
			c: &x509.Certificate{
				Version: 3,
				IsCA:    true,
			},
			expErr: errors.New("AK certificate must not be a CA"),
		},
		{
			name: "fail/extendedKeyUsage",
			c: &x509.Certificate{
				Version: 3,
			},
			expErr: errors.New("AK certificate is missing Extended Key Usage extension"),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateAKCertificate(tt.c)
			if tt.expErr != nil {
				if assert.Error(t, err) {
					assert.EqualError(t, err, tt.expErr.Error())
				}
				return
			}

			assert.NoError(t, err)
		})
	}
}

func Test_validateAKCertificateSubjectAlternativeNames(t *testing.T) {
	ok := generateValidAKCertificate(t)
	t.Helper()
	signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	require.NoError(t, err)

	getBase := func() *x509.Certificate {
		return &x509.Certificate{
			PublicKey:          signer.Public(),
			Version:            3,
			IsCA:               false,
			UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
		}
	}

	ca, err := minica.New()
	require.NoError(t, err)
	missingManufacturerASN1 := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
	sans := []x509util.SubjectAlternativeName{
		{Type: x509util.DirectoryNameType,
			ASN1Value: missingManufacturerASN1},
	}
	ext, err := createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)
	require.NoError(t, err)
	missingManufacturer := getBase()
	ext.Set(missingManufacturer)

	missingManufacturer, err = ca.Sign(missingManufacturer)
	require.NoError(t, err)

	missingModelASN1 := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMVersion, "7.55"))
	sans = []x509util.SubjectAlternativeName{
		{Type: x509util.DirectoryNameType,
			ASN1Value: missingModelASN1},
	}
	ext, err = createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)
	require.NoError(t, err)
	missingModel := getBase()
	ext.Set(missingModel)

	missingModel, err = ca.Sign(missingModel)
	require.NoError(t, err)

	missingFirmwareVersionASN1 := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0"))
	sans = []x509util.SubjectAlternativeName{
		{Type: x509util.DirectoryNameType,
			ASN1Value: missingFirmwareVersionASN1},
	}
	ext, err = createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)
	require.NoError(t, err)
	missingFirmwareVersion := getBase()
	ext.Set(missingFirmwareVersion)

	missingFirmwareVersion, err = ca.Sign(missingFirmwareVersion)
	require.NoError(t, err)

	tests := []struct {
		name   string
		c      *x509.Certificate
		expErr error
	}{
		{"ok", ok, nil},
		{"fail/missing-manufacturer", missingManufacturer, errors.New("missing TPM manufacturer")},
		{"fail/missing-model", missingModel, errors.New("missing TPM model")},
		{"fail/missing-firmware-version", missingFirmwareVersion, errors.New("missing TPM version")},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateAKCertificateSubjectAlternativeNames(tt.c)
			if tt.expErr != nil {
				if assert.Error(t, err) {
					assert.EqualError(t, err, tt.expErr.Error())
				}
				return
			}

			assert.NoError(t, err)
		})
	}
}

func Test_validateAKCertificateExtendedKeyUsage(t *testing.T) {
	ok := generateValidAKCertificate(t)
	missingEKU := &x509.Certificate{}
	t.Helper()
	signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	require.NoError(t, err)
	template := &x509.Certificate{
		PublicKey:   signer.Public(),
		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
	}
	ca, err := minica.New()
	require.NoError(t, err)
	wrongEKU, err := ca.Sign(template)
	require.NoError(t, err)
	tests := []struct {
		name   string
		c      *x509.Certificate
		expErr error
	}{
		{"ok", ok, nil},
		{"fail/wrong-eku", wrongEKU, errors.New("AK certificate is missing Extended Key Usage value tcg-kp-AIKCertificate (2.23.133.8.3)")},
		{"fail/missing-eku", missingEKU, errors.New("AK certificate is missing Extended Key Usage extension")},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateAKCertificateExtendedKeyUsage(tt.c)
			if tt.expErr != nil {
				if assert.Error(t, err) {
					assert.EqualError(t, err, tt.expErr.Error())
				}
				return
			}

			assert.NoError(t, err)
		})
	}
}

// createSubjectAltNameExtension will construct an Extension containing all
// SubjectAlternativeNames held in a Certificate. It implements more types than
// the golang x509 library, so it is used whenever OtherName or RegisteredID
// type SANs are present in the certificate.
//
// See also https://datatracker.ietf.org/doc/html/rfc5280.html#section-4.2.1.6
//
// TODO(hs): this was copied from go.step.sm/crypto/x509util to make it easier
// to create the SAN extension for testing purposes. Should it be exposed instead?
func createSubjectAltNameExtension(dnsNames, emailAddresses x509util.MultiString, ipAddresses x509util.MultiIP, uris x509util.MultiURL, sans []x509util.SubjectAlternativeName, subjectIsEmpty bool) (x509util.Extension, error) {
	var zero x509util.Extension

	var rawValues []asn1.RawValue
	for _, dnsName := range dnsNames {
		rawValue, err := x509util.SubjectAlternativeName{
			Type: x509util.DNSType, Value: dnsName,
		}.RawValue()
		if err != nil {
			return zero, err
		}

		rawValues = append(rawValues, rawValue)
	}

	for _, emailAddress := range emailAddresses {
		rawValue, err := x509util.SubjectAlternativeName{
			Type: x509util.EmailType, Value: emailAddress,
		}.RawValue()
		if err != nil {
			return zero, err
		}

		rawValues = append(rawValues, rawValue)
	}

	for _, ip := range ipAddresses {
		rawValue, err := x509util.SubjectAlternativeName{
			Type: x509util.IPType, Value: ip.String(),
		}.RawValue()
		if err != nil {
			return zero, err
		}

		rawValues = append(rawValues, rawValue)
	}

	for _, uri := range uris {
		rawValue, err := x509util.SubjectAlternativeName{
			Type: x509util.URIType, Value: uri.String(),
		}.RawValue()
		if err != nil {
			return zero, err
		}

		rawValues = append(rawValues, rawValue)
	}

	for _, san := range sans {
		rawValue, err := san.RawValue()
		if err != nil {
			return zero, err
		}

		rawValues = append(rawValues, rawValue)
	}

	// Now marshal the rawValues into the ASN1 sequence, and create an Extension object to hold the extension
	rawBytes, err := asn1.Marshal(rawValues)
	if err != nil {
		return zero, fmt.Errorf("error marshaling SubjectAlternativeName extension to ASN1: %w", err)
	}

	return x509util.Extension{
		ID:       x509util.ObjectIdentifier(oidSubjectAlternativeName),
		Critical: subjectIsEmpty,
		Value:    rawBytes,
	}, nil
}
