//go:build tpmsimulator
// +build tpmsimulator

package acme

import (
	"context"
	"crypto"
	"crypto/sha256"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"net/url"
	"testing"

	"github.com/fxamacker/cbor/v2"
	"github.com/google/go-attestation/attest"
	"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/tpm"
	"go.step.sm/crypto/tpm/simulator"
	tpmstorage "go.step.sm/crypto/tpm/storage"
	"go.step.sm/crypto/x509util"
)

func newSimulatedTPM(t *testing.T) *tpm.TPM {
	t.Helper()
	tmpDir := t.TempDir()
	tpm, err := tpm.New(withSimulator(t), tpm.WithStore(tpmstorage.NewDirstore(tmpDir))) // TODO: provide in-memory storage implementation instead
	require.NoError(t, err)
	return tpm
}

func withSimulator(t *testing.T) tpm.NewTPMOption {
	t.Helper()
	var sim simulator.Simulator
	t.Cleanup(func() {
		if sim == nil {
			return
		}
		err := sim.Close()
		require.NoError(t, err)
	})
	sim = simulator.New()
	err := sim.Open()
	require.NoError(t, err)
	return tpm.WithSimulator(sim)
}

func generateKeyID(t *testing.T, pub crypto.PublicKey) []byte {
	t.Helper()
	b, err := x509.MarshalPKIXPublicKey(pub)
	require.NoError(t, err)
	hash := sha256.Sum256(b)
	return hash[:]
}

func mustAttestTPM(t *testing.T, keyAuthorization string, permanentIdentifiers []string) ([]byte, crypto.Signer, *x509.Certificate) {
	t.Helper()
	aca, err := minica.New(
		minica.WithName("TPM Testing"),
		minica.WithGetSignerFunc(
			func() (crypto.Signer, error) {
				return keyutil.GenerateSigner("RSA", "", 2048)
			},
		),
	)
	require.NoError(t, err)

	// prepare simulated TPM and create an AK
	stpm := newSimulatedTPM(t)
	eks, err := stpm.GetEKs(context.Background())
	require.NoError(t, err)
	ak, err := stpm.CreateAK(context.Background(), "first-ak")
	require.NoError(t, err)
	require.NotNil(t, ak)

	// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
	ap, err := ak.AttestationParameters(context.Background())
	require.NoError(t, err)
	akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
	require.NoError(t, err)

	// create template and sign certificate for the AK public key
	keyID := generateKeyID(t, eks[0].Public())
	template := &x509.Certificate{
		PublicKey: akp.Public,
	}
	if len(permanentIdentifiers) == 0 {
		template.URIs = []*url.URL{
			{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)},
		}
	} else {
		san := x509util.SubjectAlternativeName{
			Type:  x509util.PermanentIdentifierType,
			Value: permanentIdentifiers[0], // TODO(hs): multiple?
		}
		ext, err := createSubjectAltNameExtension(nil, nil, nil, nil, []x509util.SubjectAlternativeName{san}, true)
		require.NoError(t, err)
		template.ExtraExtensions = append(template.ExtraExtensions,
			pkix.Extension{
				Id:       asn1.ObjectIdentifier(ext.ID),
				Critical: ext.Critical,
				Value:    ext.Value,
			},
		)
	}
	akCert, err := aca.Sign(template)
	require.NoError(t, err)
	require.NotNil(t, akCert)

	// create a new key attested by the AK, while including
	// the key authorization bytes as qualifying data.
	keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
	config := tpm.AttestKeyConfig{
		Algorithm:      "RSA",
		Size:           2048,
		QualifyingData: keyAuthSum[:],
	}
	key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
	require.NoError(t, err)
	require.NotNil(t, key)
	require.Equal(t, "first-key", key.Name())
	require.NotEqual(t, 0, len(key.Data()))
	require.Equal(t, "first-ak", key.AttestedBy())
	require.True(t, key.WasAttested())
	require.True(t, key.WasAttestedBy(ak))

	signer, err := key.Signer(context.Background())
	require.NoError(t, err)

	// prepare the attestation object with the AK certificate chain,
	// the attested key, its metadata and the signature signed by the
	// AK.
	params, err := key.CertificationParameters(context.Background())
	require.NoError(t, err)
	attObj, err := cbor.Marshal(struct {
		Format       string                 `json:"fmt"`
		AttStatement map[string]interface{} `json:"attStmt,omitempty"`
	}{
		Format: "tpm",
		AttStatement: map[string]interface{}{
			"ver":      "2.0",
			"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
			"alg":      int64(-257), // RS256
			"sig":      params.CreateSignature,
			"certInfo": params.CreateAttestation,
			"pubArea":  params.Public,
		},
	})
	require.NoError(t, err)

	// marshal the ACME payload
	payload, err := json.Marshal(struct {
		AttObj string `json:"attObj"`
	}{
		AttObj: base64.RawURLEncoding.EncodeToString(attObj),
	})
	require.NoError(t, err)

	return payload, signer, aca.Root
}

func Test_deviceAttest01ValidateWithTPMSimulator(t *testing.T) {
	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{
		"ok/doTPMAttestationFormat-storeError": func(t *testing.T) test {
			jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
			payload, _, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
			caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
			ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))

			// parse payload, set invalid "ver", remarshal
			var p payloadType
			err := json.Unmarshal(payload, &p)
			require.NoError(t, err)
			attObj, err := base64.RawURLEncoding.DecodeString(p.AttObj)
			require.NoError(t, err)
			att := attestationObject{}
			err = cbor.Unmarshal(attObj, &att)
			require.NoError(t, err)
			att.AttStatement["ver"] = "bogus"
			attObj, err = cbor.Marshal(struct {
				Format       string                 `json:"fmt"`
				AttStatement map[string]interface{} `json:"attStmt,omitempty"`
			}{
				Format:       "tpm",
				AttStatement: att.AttStatement,
			})
			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,
					jwk: jwk,
					ch: &Challenge{
						ID:              "chID",
						AuthorizationID: "azID",
						Token:           "token",
						Type:            "device-attest-01",
						Status:          StatusPending,
						Value:           "device.id.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, "device.id.12345678", updch.Value)

							err := NewError(ErrorBadAttestationStatementType, `version "bogus" is not supported`)

							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 with invalid PermanentIdentifier SAN": func(t *testing.T) test {
			jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
			payload, _, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
			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:           "device.id.99999999",
					},
					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, "device.id.99999999", updch.Value)

							err := NewError(ErrorRejectedIdentifierType, `permanent identifier does not match`).
								AddSubproblems(NewSubproblemWithIdentifier(
									ErrorMalformedType,
									Identifier{Type: "permanent-identifier", Value: "device.id.99999999"},
									`challenge identifier "device.id.99999999" doesn't match any of the attested hardware identifiers ["device.id.12345678"]`,
								))

							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": func(t *testing.T) test {
			jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
			payload, signer, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
			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:           "device.id.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(signer.Public())
							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, "device.id.12345678", updch.Value)
							return nil
						},
					},
				},
				wantErr: nil,
			}
		},
		"ok with PermanentIdentifier SAN": func(t *testing.T) test {
			jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
			payload, signer, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
			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:           "device.id.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(signer.Public())
							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, "device.id.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 {
				assert.Error(t, tc.wantErr)
				assert.EqualError(t, err, tc.wantErr.Error())
				return
			}

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

func newBadAttestationStatementError(msg string) *Error {
	return &Error{
		Type:   "urn:ietf:params:acme:error:badAttestationStatement",
		Status: 400,
		Err:    errors.New(msg),
	}
}

func newInternalServerError(msg string) *Error {
	return &Error{
		Type:   "urn:ietf:params:acme:error:serverInternal",
		Status: 500,
		Err:    errors.New(msg),
	}
}

func Test_doTPMAttestationFormat(t *testing.T) {
	ctx := context.Background()
	aca, err := minica.New(
		minica.WithName("TPM Testing"),
		minica.WithGetSignerFunc(
			func() (crypto.Signer, error) {
				return keyutil.GenerateSigner("RSA", "", 2048)
			},
		),
	)
	require.NoError(t, err)
	acaRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: aca.Root.Raw})

	// prepare simulated TPM and create an AK
	stpm := newSimulatedTPM(t)
	eks, err := stpm.GetEKs(context.Background())
	require.NoError(t, err)
	ak, err := stpm.CreateAK(context.Background(), "first-ak")
	require.NoError(t, err)
	require.NotNil(t, ak)

	// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
	ap, err := ak.AttestationParameters(context.Background())
	require.NoError(t, err)
	akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
	require.NoError(t, err)

	// create template and sign certificate for the AK public key
	keyID := generateKeyID(t, eks[0].Public())
	template := &x509.Certificate{
		PublicKey: akp.Public,
		URIs: []*url.URL{
			{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)},
		},
	}
	akCert, err := aca.Sign(template)
	require.NoError(t, err)
	require.NotNil(t, akCert)

	templateMissingSAN := &x509.Certificate{
		Subject: pkix.Name{
			CommonName: "testakcertmissingsan",
		},
		PublicKey: akp.Public,
	}
	akCertMissingSAN, err := aca.Sign(templateMissingSAN)
	require.NoError(t, err)
	require.NotNil(t, akCert)

	// generate a JWK and the key authorization value
	jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
	require.NoError(t, err)
	keyAuthorization, err := KeyAuthorization("token", jwk)
	require.NoError(t, err)

	// create a new key attested by the AK, while including
	// the key authorization bytes as qualifying data.
	keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
	config := tpm.AttestKeyConfig{
		Algorithm:      "RSA",
		Size:           2048,
		QualifyingData: keyAuthSum[:],
	}
	key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
	require.NoError(t, err)
	require.NotNil(t, key)
	params, err := key.CertificationParameters(context.Background())
	require.NoError(t, err)

	signer, err := key.Signer(context.Background())
	require.NoError(t, err)
	fingerprint, err := keyutil.Fingerprint(signer.Public())
	require.NoError(t, err)

	// attest another key and get its certification parameters
	anotherKey, err := stpm.AttestKey(context.Background(), "first-ak", "another-key", config)
	require.NoError(t, err)
	require.NotNil(t, key)
	anotherKeyParams, err := anotherKey.CertificationParameters(context.Background())
	require.NoError(t, err)

	type args struct {
		ctx  context.Context
		prov Provisioner
		ch   *Challenge
		jwk  *jose.JSONWebKey
		att  *attestationObject
	}
	tests := []struct {
		name   string
		args   args
		want   *tpmAttestationData
		expErr *Error
	}{
		{"ok", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), //
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, nil},
		{"fail ver not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("ver not present")},
		{"fail ver type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      []interface{}{},
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("ver not present")},
		{"fail bogus ver", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "bogus",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError(`version "bogus" is not supported`)},
		{"fail x5c not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("x5c not present")},
		{"fail x5c type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      [][]byte{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("x5c not present")},
		{"fail x5c empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("x5c is empty")},
		{"fail leaf type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{"leaf", aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("x5c is malformed")},
		{"fail leaf parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw[:100], aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
		{"fail intermediate type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, "intermediate"},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("x5c is malformed")},
		{"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw[:100]},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
		{"fail roots", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newInternalServerError("no root CA bundle available to verify the attestation certificate")},
		{"fail verify", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "step",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("x5c is not valid: x509: certificate signed by unknown authority")},
		{"fail missing SAN extension", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCertMissingSAN.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("AK certificate is missing Subject Alternative Name extension")},
		{"fail pubArea not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
			},
		}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
		{"fail pubArea type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  []interface{}{},
			},
		}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
		{"fail pubArea empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  []byte{},
			},
		}}, nil, newBadAttestationStatementError("pubArea is empty")},
		{"fail sig not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
		{"fail sig type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      []interface{}{},
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
		{"fail sig empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      []byte{},
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("sig is empty")},
		{"fail certInfo not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":     "2.0",
				"x5c":     []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":     int64(-257), // RS256
				"sig":     params.CreateSignature,
				"pubArea": params.Public,
			},
		}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
		{"fail certInfo type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": []interface{}{},
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
		{"fail certInfo empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": []byte{},
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("certInfo is empty")},
		{"fail alg not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("invalid alg in attestation statement")},
		{"fail alg type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(0), // invalid alg
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("invalid alg 0 in attestation statement")},
		{"fail attestation verification", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  anotherKeyParams.Public,
			},
		}}, nil, newBadAttestationStatementError("invalid certification parameters: certification refers to a different key")},
		{"fail keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, &jose.JSONWebKey{Key: []byte("not an asymmetric key")}, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), // RS256
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newInternalServerError("failed creating key auth digest: error generating JWK thumbprint: square/go-jose: unknown key type '[]uint8'")},
		{"fail different keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "aDifferentToken"}, jwk, &attestationObject{
			Format: "tpm",
			AttStatement: map[string]interface{}{
				"ver":      "2.0",
				"x5c":      []interface{}{akCert.Raw, aca.Intermediate.Raw},
				"alg":      int64(-257), //
				"sig":      params.CreateSignature,
				"certInfo": params.CreateAttestation,
				"pubArea":  params.Public,
			},
		}}, nil, newBadAttestationStatementError("key authorization does not match")},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := doTPMAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)
			if tt.expErr != nil {
				var ae *Error
				if assert.True(t, errors.As(err, &ae)) {
					assert.EqualError(t, err, tt.expErr.Error())
					assert.Equal(t, ae.StatusCode(), tt.expErr.StatusCode())
					assert.Equal(t, ae.Type, tt.expErr.Type)
				}
				assert.Nil(t, got)
				return
			}

			assert.NoError(t, err)
			if assert.NotNil(t, got) {
				assert.Equal(t, akCert, got.Certificate)
				assert.Equal(t, [][]*x509.Certificate{
					{
						akCert, aca.Intermediate, aca.Root,
					},
				}, got.VerifiedChains)
				assert.Equal(t, fingerprint, got.Fingerprint)
				assert.Empty(t, got.PermanentIdentifiers) // currently expected to be always empty
			}
		})
	}
}

// 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. 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
}