315 lines
8.2 KiB
Go
Vendored
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) }
|