diff --git a/acme/challenge.go b/acme/challenge.go index db1f989f..18921b70 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -80,10 +80,9 @@ func (ch *Challenge) ToLog() (interface{}, error) { return string(b), nil } -// Validate attempts to validate the challenge. Stores changes to the Challenge -// type using the DB interface. -// satisfactorily validated, the 'status' and 'validated' attributes are -// updated. +// Validate attempts to validate the Challenge. Stores changes to the Challenge +// type using the DB interface. If the Challenge is validated, the 'status' and +// 'validated' attributes are updated. func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, payload []byte) error { // If already valid or invalid then return without performing validation. if ch.Status != StatusPending { @@ -336,21 +335,21 @@ func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebK return nil } -type Payload struct { +type payloadType struct { AttObj string `json:"attObj"` Error string `json:"error"` } -type AttestationObject struct { +type attestationObject struct { Format string `json:"fmt"` AttStatement map[string]interface{} `json:"attStmt,omitempty"` } // TODO(bweeks): move attestation verification to a shared package. -// TODO(bweeks): define new error type for failed attestation validation. func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error { - var p Payload + var p payloadType if err := json.Unmarshal(payload, &p); err != nil { + return WrapErrorISE(err, "error unmarshalling JSON") } if p.Error != "" { @@ -363,7 +362,7 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose return WrapErrorISE(err, "error base64 decoding attObj") } - att := AttestationObject{} + att := attestationObject{} if err := cbor.Unmarshal(attObj, &att); err != nil { return WrapErrorISE(err, "error unmarshalling CBOR") } @@ -475,7 +474,7 @@ type appleAttestationData struct { Certificate *x509.Certificate } -func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, att *AttestationObject) (*appleAttestationData, error) { +func doAppleAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, att *attestationObject) (*appleAttestationData, error) { // Use configured or default attestation roots if none is configured. roots, ok := prov.GetAttestationRoots() if !ok { @@ -576,7 +575,7 @@ type stepAttestationData struct { SerialNumber string } -func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *AttestationObject) (*stepAttestationData, error) { +func doStepAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*stepAttestationData, error) { // Use configured or default attestation roots if none is configured. roots, ok := prov.GetAttestationRoots() if !ok { diff --git a/acme/challenge_test.go b/acme/challenge_test.go index 1aa9f6ab..23d28d4e 100644 --- a/acme/challenge_test.go +++ b/acme/challenge_test.go @@ -15,6 +15,7 @@ import ( "encoding/asn1" "encoding/base64" "encoding/hex" + "encoding/json" "encoding/pem" "errors" "fmt" @@ -33,6 +34,7 @@ import ( "github.com/smallstep/assert" "github.com/smallstep/certificates/authority/config" "github.com/smallstep/certificates/authority/provisioner" + sassert "github.com/stretchr/testify/assert" "go.step.sm/crypto/jose" "go.step.sm/crypto/keyutil" "go.step.sm/crypto/minica" @@ -50,6 +52,23 @@ func (m *mockClient) TLSDial(network, addr string, tlsConfig *tls.Config) (*tls. return m.tlsDial(network, addr, tlsConfig) } +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() @@ -266,12 +285,14 @@ func TestKeyAuthorization(t *testing.T) { func TestChallenge_Validate(t *testing.T) { type test struct { - ch *Challenge - vc Client - jwk *jose.JSONWebKey - db DB - srv *httptest.Server - err *Error + 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 { @@ -629,6 +650,125 @@ func TestChallenge_Validate(t *testing.T) { jwk: jwk, } }, + "fail/device-attest-01": func(t *testing.T) test { + ch := &Challenge{ + ID: "chID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + } + payload, err := json.Marshal(struct { + Error string `json:"error"` + }{ + Error: "an error", + }) + sassert.NoError(t, err) + return test{ + ch: ch, + payload: payload, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, "token", updch.Token) + sassert.Equal(t, StatusInvalid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "12345678", updch.Value) + + err := NewError(ErrorRejectedIdentifierType, "payload contained error: an error") + + sassert.EqualError(t, err.Err, updch.Error.Err.Error()) + sassert.Equal(t, err.Type, updch.Error.Type) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Status, updch.Error.Status) + sassert.Equal(t, err.Detail, updch.Error.Detail) + + return errors.New("force") + }, + }, + err: NewError(ErrorServerInternalType, "failure saving error to acme challenge: force"), + } + }, + "ok/device-attest-01": func(t *testing.T) test { + ctx := context.Background() + ca, err := minica.New() + sassert.NoError(t, err) + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) + ctx = NewProvisionerContext(ctx, mustAttestationProvisioner(t, caRoot)) + 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) + sassert.NoError(t, err) + serialNumber, err := asn1.Marshal(1234) + sassert.NoError(t, err) + leaf := makeLeaf(signer, serialNumber) + + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + sassert.NoError(t, err) + token := "token" + keyAuth, err := KeyAuthorization(token, jwk) + sassert.NoError(t, err) + keyAuthSum := sha256.Sum256([]byte(keyAuth)) + sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) + sassert.NoError(t, err) + cborSig, err := cbor.Marshal(sig) + sassert.NoError(t, err) + + ch := &Challenge{ + ID: "chID", + Token: token, + Type: "device-attest-01", + Status: StatusPending, + Value: "1234", + } + 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, + }, + }) + sassert.NoError(t, err) + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + sassert.NoError(t, err) + return test{ + ch: ch, + payload: payload, + ctx: ctx, + jwk: jwk, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, token, updch.Token) + sassert.Equal(t, StatusValid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "1234", updch.Value) + + return nil + }, + }, + } + }, } for name, run := range tests { t.Run(name, func(t *testing.T) { @@ -638,8 +778,12 @@ func TestChallenge_Validate(t *testing.T) { defer tc.srv.Close() } - ctx := NewClientContext(context.Background(), tc.vc) - if err := tc.ch.Validate(ctx, tc.db, tc.jwk, nil); err != nil { + 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.NotNil(t, tc.err) { var k *Error if errors.As(err, &k) { @@ -2568,7 +2712,7 @@ func Test_doAppleAttestationFormat(t *testing.T) { ctx context.Context prov Provisioner ch *Challenge - att *AttestationObject + att *attestationObject } tests := []struct { name string @@ -2576,7 +2720,7 @@ func Test_doAppleAttestationFormat(t *testing.T) { want *appleAttestationData wantErr bool }{ - {"ok", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &AttestationObject{ + {"ok", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{ Format: "apple", AttStatement: map[string]interface{}{ "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, @@ -2588,49 +2732,49 @@ func Test_doAppleAttestationFormat(t *testing.T) { SEPVersion: "16.0", Certificate: leaf, }, false}, - {"fail apple issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{}, &AttestationObject{ + {"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{ + {"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{ + {"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{ + {"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{ + {"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{ + {"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{ + {"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{ + {"fail verify", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{}, &attestationObject{ Format: "apple", AttStatement: map[string]interface{}{ "x5c": []interface{}{leaf.Raw}, @@ -2726,7 +2870,7 @@ func Test_doStepAttestationFormat(t *testing.T) { prov Provisioner ch *Challenge jwk *jose.JSONWebKey - att *AttestationObject + att *attestationObject } tests := []struct { name string @@ -2734,7 +2878,7 @@ func Test_doStepAttestationFormat(t *testing.T) { want *stepAttestationData wantErr bool }{ - {"ok", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"ok", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{ Format: "step", AttStatement: map[string]interface{}{ "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, @@ -2745,7 +2889,7 @@ func Test_doStepAttestationFormat(t *testing.T) { SerialNumber: "1234", Certificate: leaf, }, false}, - {"fail yubico issuer", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2753,7 +2897,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail x5c type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2761,7 +2905,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail x5c empty", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"fail x5c empty", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{ Format: "step", AttStatement: map[string]interface{}{ "x5c": []interface{}{}, @@ -2769,7 +2913,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail leaf type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2777,7 +2921,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail leaf parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2785,7 +2929,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail intermediate type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"fail intermediate type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{ Format: "step", AttStatement: map[string]interface{}{ "x5c": []interface{}{leaf.Raw, "intermediate"}, @@ -2793,7 +2937,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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]}, @@ -2801,7 +2945,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail verify", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"fail verify", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &attestationObject{ Format: "step", AttStatement: map[string]interface{}{ "x5c": []interface{}{leaf.Raw}, @@ -2809,7 +2953,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail sig type", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2817,7 +2961,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": string(cborSig), }, }}, nil, true}, - {"fail sig unmarshal", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2825,7 +2969,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "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{ + {"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}, @@ -2833,7 +2977,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail sig verify P-256", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2841,7 +2985,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": otherCBORSig, }, }}, nil, true}, - {"fail sig verify P-384", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2849,7 +2993,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail sig verify RSA", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2857,7 +3001,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail sig verify Ed25519", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2865,7 +3009,7 @@ func Test_doStepAttestationFormat(t *testing.T) { "sig": cborSig, }, }}, nil, true}, - {"fail unmarshal serial number", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2951,7 +3095,7 @@ func Test_doStepAttestationFormat_noCAIntermediate(t *testing.T) { prov Provisioner ch *Challenge jwk *jose.JSONWebKey - att *AttestationObject + att *attestationObject } tests := []struct { name string @@ -2959,7 +3103,7 @@ func Test_doStepAttestationFormat_noCAIntermediate(t *testing.T) { want *stepAttestationData wantErr bool }{ - {"fail no intermediate", args{ctx, mustAttestationProvisioner(t, caRoot), &Challenge{Token: "token"}, jwk, &AttestationObject{ + {"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}, @@ -2981,3 +3125,765 @@ func Test_doStepAttestationFormat_noCAIntermediate(t *testing.T) { }) } } + +func Test_deviceAttest01Validate(t *testing.T) { + invalidPayload := "!?" + errorPayload, err := json.Marshal(struct { + Error string `json:"error"` + }{ + Error: "an error", + }) + sassert.NoError(t, err) + errorBase64Payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: "?!", + }) + sassert.NoError(t, err) + errorCBORPayload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: "AAAA", + }) + sassert.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/json.Unmarshal": func(t *testing.T) test { + return test{ + args: args{ + ch: &Challenge{ + ID: "chID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + 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", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + payload: errorPayload, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, "token", updch.Token) + sassert.Equal(t, StatusInvalid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "12345678", updch.Value) + + err := NewError(ErrorRejectedIdentifierType, "payload contained error: an error") + + sassert.EqualError(t, err.Err, updch.Error.Err.Error()) + sassert.Equal(t, err.Type, updch.Error.Type) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Status, updch.Error.Status) + sassert.Equal(t, err.Detail, updch.Error.Detail) + + 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", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + payload: errorPayload, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, "token", updch.Token) + sassert.Equal(t, StatusInvalid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "12345678", updch.Value) + + err := NewError(ErrorRejectedIdentifierType, "payload contained error: an error") + + sassert.EqualError(t, err.Err, updch.Error.Err.Error()) + sassert.Equal(t, err.Type, updch.Error.Type) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Status, updch.Error.Status) + sassert.Equal(t, err.Detail, updch.Error.Detail) + + return nil + }, + }, + }, + wantErr: nil, + } + }, + "fail/base64-decode": func(t *testing.T) test { + return test{ + args: args{ + ch: &Challenge{ + ID: "chID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + 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", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + payload: errorCBORPayload, + }, + wantErr: NewErrorISE("error unmarshalling CBOR: cbor: cannot unmarshal positive integer into Go value of type acme.attestationObject"), + } + }, + "ok/prov.IsAttestationFormatEnabled": func(t *testing.T) test { + ca, err := minica.New() + sassert.NoError(t, err) + 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) + sassert.NoError(t, err) + serialNumber, err := asn1.Marshal(1234) + sassert.NoError(t, err) + leaf := makeLeaf(signer, serialNumber) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + sassert.NoError(t, err) + token := "token" + keyAuth, err := KeyAuthorization(token, jwk) + sassert.NoError(t, err) + keyAuthSum := sha256.Sum256([]byte(keyAuth)) + sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) + sassert.NoError(t, err) + cborSig, err := cbor.Marshal(sig) + sassert.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), mustNonAttestationProvisioner(t)) + 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, + }, + }) + sassert.NoError(t, err) + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + sassert.NoError(t, err) + return test{ + args: args{ + ctx: ctx, + ch: &Challenge{ + ID: "chID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + payload: payload, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, "token", updch.Token) + sassert.Equal(t, StatusInvalid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "12345678", updch.Value) + + err := NewError(ErrorBadAttestationStatementType, "attestation format %q is not enabled", "step") + + sassert.EqualError(t, err.Err, updch.Error.Err.Error()) + sassert.Equal(t, err.Type, updch.Error.Type) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Status, updch.Error.Status) + sassert.Equal(t, err.Detail, updch.Error.Detail) + + 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{}{}, + }) + sassert.NoError(t, err) + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + sassert.NoError(t, err) + return test{ + args: args{ + ctx: ctx, + ch: &Challenge{ + ID: "chID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + payload: payload, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, "token", updch.Token) + sassert.Equal(t, StatusInvalid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "12345678", updch.Value) + + err := NewError(ErrorBadAttestationStatementType, "x5c not present") + + sassert.EqualError(t, err.Err, updch.Error.Err.Error()) + sassert.Equal(t, err.Type, updch.Error.Type) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Status, updch.Error.Status) + sassert.Equal(t, err.Detail, updch.Error.Detail) + + return nil + }, + }, + }, + wantErr: nil, + } + }, + "ok/doAppleAttestationFormat-non-matching-nonce": func(t *testing.T) test { + ca, err := minica.New() + sassert.NoError(t, err) + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + sassert.NoError(t, err) + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) + 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")}, + }, + }) + sassert.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: "apple", + AttStatement: map[string]interface{}{ + "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, + }, + }) + sassert.NoError(t, err) + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + sassert.NoError(t, err) + return test{ + args: args{ + ctx: ctx, + ch: &Challenge{ + ID: "chID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + payload: payload, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, "token", updch.Token) + sassert.Equal(t, StatusInvalid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "12345678", updch.Value) + + err := NewError(ErrorBadAttestationStatementType, "challenge token does not match") + + sassert.EqualError(t, updch.Error.Err, err.Err.Error()) + sassert.Equal(t, err.Type, updch.Error.Type) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Status, updch.Error.Status) + sassert.Equal(t, err.Detail, updch.Error.Detail) + + return nil + }, + }, + }, + wantErr: nil, + } + }, + "ok/doAppleAttestationFormat-non-matching-challenge-value": func(t *testing.T) test { + ca, err := minica.New() + sassert.NoError(t, err) + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + sassert.NoError(t, err) + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) + nonce := 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: nonce[:]}, + }, + }) + sassert.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: "apple", + AttStatement: map[string]interface{}{ + "x5c": []interface{}{leaf.Raw, ca.Intermediate.Raw}, + }, + }) + sassert.NoError(t, err) + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + sassert.NoError(t, err) + return test{ + args: args{ + ctx: ctx, + ch: &Challenge{ + ID: "chID", + Token: "nonce", + Type: "device-attest-01", + Status: StatusPending, + Value: "non-matching-value", + }, + payload: payload, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, "nonce", updch.Token) + sassert.Equal(t, StatusInvalid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "non-matching-value", updch.Value) + + err := NewError(ErrorBadAttestationStatementType, "permanent identifier does not match") + + sassert.EqualError(t, updch.Error.Err, err.Err.Error()) + sassert.Equal(t, err.Type, updch.Error.Type) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Status, updch.Error.Status) + sassert.Equal(t, err.Detail, updch.Error.Detail) + + return nil + }, + }, + }, + wantErr: nil, + } + }, + "ok/doStepAttestationFormat-storeError": func(t *testing.T) test { + ca, err := minica.New() + sassert.NoError(t, err) + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + sassert.NoError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + sassert.NoError(t, err) + token := "token" + keyAuth, err := KeyAuthorization(token, jwk) + sassert.NoError(t, err) + keyAuthSum := sha256.Sum256([]byte(keyAuth)) + sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) + sassert.NoError(t, err) + cborSig, err := cbor.Marshal(sig) + sassert.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, + }, + }) + sassert.NoError(t, err) + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + sassert.NoError(t, err) + return test{ + args: args{ + ctx: ctx, + ch: &Challenge{ + ID: "chID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + payload: payload, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, "token", updch.Token) + sassert.Equal(t, StatusInvalid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "12345678", updch.Value) + + err := NewError(ErrorBadAttestationStatementType, "x5c not present") + + sassert.EqualError(t, updch.Error.Err, err.Err.Error()) + sassert.Equal(t, err.Type, updch.Error.Type) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Status, updch.Error.Status) + sassert.Equal(t, err.Detail, updch.Error.Detail) + + return nil + }, + }, + }, + wantErr: nil, + } + }, + "ok/doStepAttestationFormat-non-matching-identifier": func(t *testing.T) test { + ca, err := minica.New() + sassert.NoError(t, err) + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + sassert.NoError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + sassert.NoError(t, err) + token := "token" + keyAuth, err := KeyAuthorization(token, jwk) + sassert.NoError(t, err) + keyAuthSum := sha256.Sum256([]byte(keyAuth)) + sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) + sassert.NoError(t, err) + cborSig, err := cbor.Marshal(sig) + sassert.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) + 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 + } + sassert.NoError(t, err) + serialNumber, err := asn1.Marshal(87654321) + sassert.NoError(t, err) + leaf := makeLeaf(signer, serialNumber) + 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, + }, + }) + sassert.NoError(t, err) + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + sassert.NoError(t, err) + return test{ + args: args{ + ctx: ctx, + ch: &Challenge{ + ID: "chID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + payload: payload, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, "token", updch.Token) + sassert.Equal(t, StatusInvalid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "12345678", updch.Value) + + err := NewError(ErrorBadAttestationStatementType, "permanent identifier does not match"). + AddSubproblems(NewSubproblemWithIdentifier( + ErrorMalformedType, + Identifier{Type: "permanent-identifier", Value: "12345678"}, + "challenge identifier \"12345678\" doesn't match the attested hardware identifier \"87654321\"", + )) + + sassert.EqualError(t, err.Err, updch.Error.Err.Error()) + sassert.Equal(t, err.Type, updch.Error.Type) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Status, updch.Error.Status) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Subproblems, updch.Error.Subproblems) + + return nil + }, + }, + jwk: jwk, + }, + wantErr: nil, + } + }, + "ok/unknown-attestation-format": func(t *testing.T) test { + ca, err := minica.New() + sassert.NoError(t, err) + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + sassert.NoError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + sassert.NoError(t, err) + token := "token" + keyAuth, err := KeyAuthorization(token, jwk) + sassert.NoError(t, err) + keyAuthSum := sha256.Sum256([]byte(keyAuth)) + sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) + sassert.NoError(t, err) + cborSig, err := cbor.Marshal(sig) + sassert.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 + } + sassert.NoError(t, err) + serialNumber, err := asn1.Marshal(87654321) + sassert.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, + }, + }) + sassert.NoError(t, err) + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + sassert.NoError(t, err) + return test{ + args: args{ + ctx: ctx, + ch: &Challenge{ + ID: "chID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + payload: payload, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, "token", updch.Token) + sassert.Equal(t, StatusInvalid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "12345678", updch.Value) + + err := NewError(ErrorBadAttestationStatementType, "unexpected attestation object format") + + sassert.EqualError(t, err.Err, updch.Error.Err.Error()) + sassert.Equal(t, err.Type, updch.Error.Type) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Status, updch.Error.Status) + sassert.Equal(t, err.Detail, updch.Error.Detail) + sassert.Equal(t, err.Subproblems, updch.Error.Subproblems) + + return nil + }, + }, + jwk: jwk, + }, + wantErr: nil, + } + }, + "fail/db.UpdateChallenge": func(t *testing.T) test { + ca, err := minica.New() + sassert.NoError(t, err) + caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Root.Raw}) + signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + sassert.NoError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + sassert.NoError(t, err) + token := "token" + keyAuth, err := KeyAuthorization(token, jwk) + sassert.NoError(t, err) + keyAuthSum := sha256.Sum256([]byte(keyAuth)) + sig, err := signer.Sign(rand.Reader, keyAuthSum[:], crypto.SHA256) + sassert.NoError(t, err) + cborSig, err := cbor.Marshal(sig) + sassert.NoError(t, err) + ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot)) + 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 + } + sassert.NoError(t, err) + serialNumber, err := asn1.Marshal(12345678) + sassert.NoError(t, err) + leaf := makeLeaf(signer, serialNumber) + 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, + }, + }) + sassert.NoError(t, err) + payload, err := json.Marshal(struct { + AttObj string `json:"attObj"` + }{ + AttObj: base64.RawURLEncoding.EncodeToString(attObj), + }) + sassert.NoError(t, err) + return test{ + args: args{ + ctx: ctx, + ch: &Challenge{ + ID: "chID", + Token: "token", + Type: "device-attest-01", + Status: StatusPending, + Value: "12345678", + }, + payload: payload, + db: &MockDB{ + MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error { + sassert.Equal(t, "chID", updch.ID) + sassert.Equal(t, "token", updch.Token) + sassert.Equal(t, StatusValid, updch.Status) + sassert.Equal(t, ChallengeType("device-attest-01"), updch.Type) + sassert.Equal(t, "12345678", updch.Value) + + return errors.New("force") + }, + }, + jwk: jwk, + }, + wantErr: NewError(ErrorServerInternalType, "error updating challenge: force"), + } + }, + } + 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 { + sassert.NotNil(t, tc.wantErr) + sassert.EqualError(t, tc.wantErr, err.Error()) + return + } + + sassert.Nil(t, tc.wantErr) + + // TODO: more validations? + }) + } +}