Files
web/third_party/go-webauthn/protocol/attestation_compound_test.go
gamertan bfe6cfd29e
verify / verify (push) Successful in 3m40s
auth: publish passkey foundations preview
2026-08-21 17:33:00 -04:00

393 lines
11 KiB
Go
Vendored

package protocol
import (
"errors"
"reflect"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/go-webauthn/webauthn/metadata"
"github.com/go-webauthn/webauthn/testing/mocks"
)
func TestAttestationFormatValidationHandlerCompound(t *testing.T) {
t.Run("ShouldReturnValidationErrors", func(t *testing.T) {
withFreshAttestationRegistry(t)
attestationRegistry[AttestationFormatPacked] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
return "ok", nil, nil
}
base := AttestationObject{
Format: string(AttestationFormatCompound),
AttStatement: map[string]any{
stmtAttStmt: []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
},
},
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: make([]byte, 0),
},
},
}
testCases := []struct {
name string
mutate func(a AttestationObject) AttestationObject
expected string
err string
}{
{
name: "ShouldRejectInvalidAaguidBytes",
mutate: func(a AttestationObject) AttestationObject {
a.AuthData.AttData.AAGUID = []byte{0x01}
return a
},
expected: ErrInvalidAttestation.Type,
err: "Error occurred parsing AAGUID",
},
{
name: "ShouldRejectMissingAttStmt",
mutate: func(a AttestationObject) AttestationObject {
delete(a.AttStatement, stmtAttStmt)
return a
},
expected: ErrInvalidAttestation.Type,
err: "Compound statement missing attStmt",
},
{
name: "ShouldRejectAttStmtNotArray",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = "nope"
return a
},
expected: ErrInvalidAttestation.Type,
err: "Compound statement attStmt isn't an array",
},
{
name: "ShouldRejectAttStmtWithLessThanTwoItems",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "at least two",
},
{
name: "ShouldRejectAttStmtContainingNonObject",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
123,
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "isn't an object",
},
{
name: "ShouldRejectSubStatementMissingFmt",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "does not have a format",
},
{
name: "ShouldRejectSubStatementMissingAttStmt",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: string(AttestationFormatPacked)},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "does not have an attestation statement",
},
{
name: "ShouldRejectSubStatementWithCompoundFmt",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: string(AttestationFormatCompound), stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "format of compound",
},
{
name: "ShouldRejectSubStatementWithEmptyFmt",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: "", stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrInvalidAttestation.Type,
err: "empty format",
},
{
name: "ShouldRejectUnsupportedSubStatementFmt",
mutate: func(a AttestationObject) AttestationObject {
a.AttStatement[stmtAttStmt] = []any{
map[string]any{stmtFmt: "definitely-not-registered", stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
}
return a
},
expected: ErrAttestationFormat.Type,
err: "unsupported",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
att := tc.mutate(base)
attestationType, x5cs, err := attestationFormatValidationHandlerCompound(att, []byte("clientDataHash"), nil)
require.Error(t, err)
assert.Empty(t, attestationType)
assert.Nil(t, x5cs)
protoErr, ok := err.(*Error)
require.True(t, ok, "expected *Error, got %T: %v", err, err)
if tc.expected != "" {
assert.Equal(t, tc.expected, protoErr.Type)
}
combined := protoErr.Details + " " + protoErr.DevInfo
assert.Contains(t, combined, tc.err)
})
}
})
t.Run("ShouldCallSubHandlersAndReturnCompound", func(t *testing.T) {
withFreshAttestationRegistry(t)
type call struct {
format string
attStmt map[string]any
auth AuthenticatorData
rawAuth []byte
}
var calls []call
attestationRegistry[AttestationFormatPacked] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
calls = append(calls, call{
format: att.Format,
attStmt: att.AttStatement,
auth: att.AuthData,
rawAuth: att.RawAuthData,
})
return "packed-type", []any{[]byte("cert1")}, nil
}
attestationRegistry[AttestationFormatApple] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
calls = append(calls, call{
format: att.Format,
attStmt: att.AttStatement,
auth: att.AuthData,
rawAuth: att.RawAuthData,
})
return "apple-type", []any{[]byte("cert2")}, nil
}
auth := AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: make([]byte, 0),
},
}
att := AttestationObject{
Format: string(AttestationFormatCompound),
RawAuthData: []byte{0xAA, 0xBB},
AuthData: auth,
AttStatement: map[string]any{
stmtAttStmt: []any{
map[string]any{
stmtFmt: string(AttestationFormatPacked),
stmtAttStmt: map[string]any{"k1": "v1"},
},
map[string]any{
stmtFmt: string(AttestationFormatApple),
stmtAttStmt: map[string]any{"k2": "v2"},
},
},
},
}
gotType, gotX5Cs, err := attestationFormatValidationHandlerCompound(att, []byte("hash"), nil)
require.NoError(t, err)
assert.Equal(t, stmtTypNone, gotType)
assert.Nil(t, gotX5Cs)
require.Len(t, calls, 2)
assert.Equal(t, string(AttestationFormatPacked), calls[0].format)
assert.Equal(t, string(AttestationFormatApple), calls[1].format)
assert.True(t, reflect.DeepEqual(calls[0].auth, auth) && reflect.DeepEqual(calls[1].auth, auth),
"expected auth data to be passed through unchanged, got: %#v", calls)
assert.True(t, reflect.DeepEqual(calls[0].rawAuth, att.RawAuthData) && reflect.DeepEqual(calls[1].rawAuth, att.RawAuthData),
"expected raw auth data to be passed through unchanged, got: %#v", calls)
})
t.Run("ShouldPropagateSubHandlerError", func(t *testing.T) {
withFreshAttestationRegistry(t)
subErr := ErrInvalidAttestation.WithDetails("sub-handler failed")
attestationRegistry[AttestationFormatPacked] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
return "", nil, subErr
}
att := AttestationObject{
Format: string(AttestationFormatCompound),
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{AAGUID: make([]byte, 0)},
},
AttStatement: map[string]any{
stmtAttStmt: []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
},
},
}
_, _, err := attestationFormatValidationHandlerCompound(att, []byte("hash"), nil)
require.Error(t, err)
assert.True(t, errors.Is(err, subErr))
})
t.Run("ShouldWrapMetadataValidationFailure", func(t *testing.T) {
withFreshAttestationRegistry(t)
var handlerCalls int
attestationRegistry[AttestationFormatPacked] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
handlerCalls++
return testAttTypeSome, []any{[]byte("cert")}, nil
}
ctrl := gomock.NewController(t)
mds := mocks.NewMockMetadataProvider(ctrl)
u := uuid.New()
mds.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(nil, nil)
mds.EXPECT().GetValidateEntry(gomock.Any()).Return(true)
att := AttestationObject{
Format: string(AttestationFormatCompound),
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: u[:],
},
},
AttStatement: map[string]any{
stmtAttStmt: []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
},
},
}
_, _, err := attestationFormatValidationHandlerCompound(att, []byte("hash"), mds)
require.Error(t, err)
protoErr, ok := err.(*Error)
require.True(t, ok)
assert.Equal(t, ErrInvalidAttestation.Type, protoErr.Type)
assert.Contains(t, protoErr.DevInfo, "Error occurred validating metadata")
assert.Equal(t, 1, handlerCalls)
})
t.Run("ShouldNotValidateMetadataWhenMdsIsNil", func(t *testing.T) {
withFreshAttestationRegistry(t)
var handlerCalls int
attestationRegistry[AttestationFormatPacked] = func(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) {
handlerCalls++
return testAttTypeSome, []any{[]byte("cert")}, nil
}
att := AttestationObject{
Format: string(AttestationFormatCompound),
AuthData: AuthenticatorData{
AttData: AttestedCredentialData{
AAGUID: make([]byte, 0),
},
},
AttStatement: map[string]any{
stmtAttStmt: []any{
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
map[string]any{stmtFmt: string(AttestationFormatPacked), stmtAttStmt: map[string]any{}},
},
},
}
gotType, gotX5Cs, err := attestationFormatValidationHandlerCompound(att, []byte("hash"), nil)
require.NoError(t, err)
assert.Equal(t, stmtTypNone, gotType)
assert.Nil(t, gotX5Cs)
assert.Equal(t, 2, handlerCalls)
})
}
// Supporting functions.
func withFreshAttestationRegistry(t *testing.T) {
t.Helper()
orig := make(map[AttestationFormat]attestationFormatValidationHandler, len(attestationRegistry))
for k, v := range attestationRegistry {
orig[k] = v
}
t.Cleanup(func() {
for k := range attestationRegistry {
delete(attestationRegistry, k)
}
for k, v := range orig {
attestationRegistry[k] = v
}
})
}