Files
gamertan bfe6cfd29e
verify / verify (push) Successful in 3m40s
auth: publish passkey foundations preview
2026-08-21 17:33:00 -04:00

315 lines
8.2 KiB
Go
Vendored

package protocol
import (
"bytes"
"encoding/json"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tinylib/msgp/msgp"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
)
func TestPublicKeyCredentialRequestOptions_GetAllowedCredentialIDs(t *testing.T) {
type fields struct {
Challenge URLEncodedBase64
Timeout int
RelyingPartyID string
AllowedCredentials []CredentialDescriptor
UserVerification UserVerificationRequirement
Extensions AuthenticationExtensions
}
testCases := []struct {
name string
fields fields
expected [][]byte
}{
{
"CorrectCredentialIDs",
fields{
Challenge: URLEncodedBase64([]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}),
Timeout: 60,
AllowedCredentials: []CredentialDescriptor{
{
Type: PublicKeyCredentialType, CredentialID: []byte("1234"), Transport: []AuthenticatorTransport{"usb"},
},
},
RelyingPartyID: "test.org",
UserVerification: VerificationPreferred,
Extensions: AuthenticationExtensions{},
},
[][]byte{
[]byte("1234"),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
options := &PublicKeyCredentialRequestOptions{
Challenge: tc.fields.Challenge,
Timeout: tc.fields.Timeout,
RelyingPartyID: tc.fields.RelyingPartyID,
AllowedCredentials: tc.fields.AllowedCredentials,
UserVerification: tc.fields.UserVerification,
Extensions: tc.fields.Extensions,
}
assert.Equal(t, tc.expected, options.GetAllowedCredentialIDs())
})
}
}
func TestCredentialDescriptor_SignalUnknownCredential(t *testing.T) {
testCases := []struct {
name string
rpid string
have *CredentialDescriptor
expected *SignalUnknownCredential
expectedJSON string
}{
{
"ShouldHandleStandard",
"example.com",
&CredentialDescriptor{
CredentialID: URLEncodedBase64("1234"),
},
&SignalUnknownCredential{
CredentialID: URLEncodedBase64("1234"),
RPID: "example.com",
},
`{"credentialId":"MTIzNA","rpId":"example.com"}`,
},
{
"ShouldHandleNoID",
"example.com",
&CredentialDescriptor{},
&SignalUnknownCredential{
RPID: "example.com",
},
`{"credentialId":null,"rpId":"example.com"}`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := tc.have.SignalUnknownCredential(tc.rpid)
assert.Equal(t, tc.expected, actual)
data, err := json.Marshal(actual)
require.NoError(t, err)
assert.Equal(t, tc.expectedJSON, string(data))
})
}
}
func TestCredentialParameter_MsgpRoundTrip(t *testing.T) {
testCases := []struct {
name string
original CredentialParameter
}{
{"BothFieldsSet", CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}},
{"RS256", CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgRS256}},
{"Ed25519", CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgEdDSA}},
{"TypeOnly", CredentialParameter{Type: PublicKeyCredentialType}},
{"AlgorithmOnly", CredentialParameter{Algorithm: webauthncose.AlgES256}},
{"BothOmitted", CredentialParameter{}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data, err := tc.original.MarshalMsg(nil)
require.NoError(t, err)
var decoded CredentialParameter
left, err := decoded.UnmarshalMsg(data)
require.NoError(t, err)
assert.Empty(t, left)
assert.Equal(t, tc.original, decoded)
assert.LessOrEqual(t, len(data), tc.original.Msgsize())
var buf bytes.Buffer
require.NoError(t, msgp.Encode(&buf, tc.original))
var streamDecoded CredentialParameter
require.NoError(t, msgp.Decode(&buf, &streamDecoded))
assert.Equal(t, tc.original, streamDecoded)
})
}
}
func TestCredentialParameter_MsgpOmitEmpty(t *testing.T) {
testCases := []struct {
name string
value CredentialParameter
wantLen int
}{
{"BothPresent", CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}, 2},
{"TypeOnly", CredentialParameter{Type: PublicKeyCredentialType}, 1},
{"AlgorithmOnly", CredentialParameter{Algorithm: webauthncose.AlgES256}, 1},
{"BothOmitted", CredentialParameter{}, 0},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data, err := tc.value.MarshalMsg(nil)
require.NoError(t, err)
size, _, err := msgp.ReadMapHeaderBytes(data)
require.NoError(t, err)
assert.Equal(t, uint32(tc.wantLen), size)
})
}
}
func TestCredentialParameter_MsgpUnmarshalSkipsUnknownKeys(t *testing.T) {
t.Run("AlongsideKnown", func(t *testing.T) {
original := CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}
data, err := original.MarshalMsg(nil)
require.NoError(t, err)
size, rest, err := msgp.ReadMapHeaderBytes(data)
require.NoError(t, err)
spliced := msgp.AppendMapHeader(nil, size+1)
spliced = msgp.AppendString(spliced, "xyz")
spliced = msgp.AppendBool(spliced, true)
spliced = append(spliced, rest...)
var decoded CredentialParameter
left, err := decoded.UnmarshalMsg(spliced)
require.NoError(t, err)
assert.Empty(t, left)
assert.Equal(t, original, decoded)
})
t.Run("OnlyUnknown", func(t *testing.T) {
tiny := []byte{0x81, 0xa3, 'x', 'y', 'z', 0xc3}
var decoded CredentialParameter
left, err := decoded.UnmarshalMsg(tiny)
require.NoError(t, err)
assert.Empty(t, left)
assert.Equal(t, CredentialParameter{}, decoded)
var streamDecoded CredentialParameter
require.NoError(t, msgp.Decode(bytes.NewReader(tiny), &streamDecoded))
assert.Equal(t, CredentialParameter{}, streamDecoded)
})
}
func TestCredentialParameter_DecodeMsgInvalidTypes(t *testing.T) {
t.Run("NotAMap", func(t *testing.T) {
var c CredentialParameter
_, err := c.UnmarshalMsg(msgpString("not a map"))
require.Error(t, err)
var c2 CredentialParameter
require.Error(t, msgp.Decode(bytes.NewReader(msgpString("not a map")), &c2))
})
testCases := []struct {
name string
data []byte
wantSub string
}{
{"TypeAsInt", msgpOneFieldMap("typ", msgpInt64(42)), "Type"},
{"TypeAsBool", msgpOneFieldMap("typ", msgpBool(true)), "Type"},
{"AlgorithmAsString", msgpOneFieldMap("alg", msgpString("not an int")), "Algorithm"},
{"AlgorithmAsBool", msgpOneFieldMap("alg", msgpBool(true)), "Algorithm"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var c CredentialParameter
_, err := c.UnmarshalMsg(tc.data)
require.Error(t, err)
assert.Contains(t, err.Error(), tc.wantSub)
var c2 CredentialParameter
streamErr := msgp.Decode(bytes.NewReader(tc.data), &c2)
require.Error(t, streamErr)
assert.Contains(t, streamErr.Error(), tc.wantSub)
})
}
}
func TestCredentialParameter_MsgpEncodeErrorPaths(t *testing.T) {
v := CredentialParameter{Type: PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}
data, err := v.MarshalMsg(nil)
require.NoError(t, err)
exerciseEncodeMsgErrorPaths(t, v, data)
}
type failingWriter struct {
limit int
count int
}
func (w *failingWriter) Write(p []byte) (int, error) {
remaining := w.limit - w.count
if remaining <= 0 {
return 0, errors.New("failingWriter: exhausted")
}
if len(p) > remaining {
w.count = w.limit
return remaining, errors.New("failingWriter: exhausted")
}
w.count += len(p)
return len(p), nil
}
func exerciseEncodeMsgErrorPaths(t *testing.T, enc msgp.Encodable, marshalled []byte) {
t.Helper()
for limit := 0; limit <= len(marshalled); limit++ {
fw := &failingWriter{limit: limit}
wr := msgp.NewWriterSize(fw, 18)
err := enc.EncodeMsg(wr)
if err == nil {
err = wr.Flush()
}
if limit < len(marshalled) {
require.Errorf(t, err, "EncodeMsg should fail when underlying writer errors after %d bytes", limit)
} else {
require.NoError(t, err)
}
}
}
func msgpOneFieldMap(key string, value []byte) []byte {
b := msgp.AppendMapHeader(nil, 1)
b = msgp.AppendString(b, key)
return append(b, value...)
}
func msgpBool(v bool) []byte { return msgp.AppendBool(nil, v) }
func msgpInt64(v int64) []byte { return msgp.AppendInt64(nil, v) }
func msgpString(v string) []byte { return msgp.AppendString(nil, v) }