mirror of
https://github.com/sonr-io/common.git
synced 2026-01-11 20:08:57 +00:00
421 lines
14 KiB
Go
421 lines
14 KiB
Go
package webauthn
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/sonr-io/common/webauthn/webauthncbor"
|
|
)
|
|
|
|
func TestParseCredentialRequestResponse(t *testing.T) {
|
|
byteID, _ := base64.RawURLEncoding.DecodeString(
|
|
"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
|
|
)
|
|
byteAAGUID, _ := base64.RawURLEncoding.DecodeString("rc4AAjW8xgpkiwsl8fBVAw")
|
|
byteRPIDHash, _ := base64.RawURLEncoding.DecodeString(
|
|
"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA",
|
|
)
|
|
byteAuthData, _ := base64.RawURLEncoding.DecodeString(
|
|
"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiGa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ",
|
|
)
|
|
byteSignature, _ := base64.RawURLEncoding.DecodeString(
|
|
"MEUCIBtIVOQxzFYdyWQyxaLR0tik1TnuPhGVhXVSNgFwLmN5AiEAnxXdCq0UeAVGWxOaFcjBZ_mEZoXqNboY5IkQDdlWZYc",
|
|
)
|
|
byteUserHandle, _ := base64.RawURLEncoding.DecodeString("0ToAAAAAAAAAAA")
|
|
byteCredentialPubKey, _ := base64.RawURLEncoding.DecodeString(
|
|
"pQMmIAEhWCAoCF-x0dwEhzQo-ABxHIAgr_5WL6cJceREc81oIwFn7iJYIHEHx8ZhBIE42L26-rSC_3l0ZaWEmsHAKyP9rgslApUdAQI",
|
|
)
|
|
byteClientDataJSON, _ := base64.RawURLEncoding.DecodeString(
|
|
"eyJjaGFsbGVuZ2UiOiJFNFBUY0lIX0hmWDFwQzZTaWdrMVNDOU5BbGdlenROMDQzOXZpOHpfYzlrIiwibmV3X2tleXNfbWF5X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9",
|
|
)
|
|
|
|
type args struct {
|
|
responseName string
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
args args
|
|
expected *ParsedCredentialAssertionData
|
|
errString string
|
|
errType string
|
|
errDetails string
|
|
errInfo string
|
|
}{
|
|
{
|
|
name: "ShouldParseCredentialAssertion",
|
|
args: args{
|
|
"success",
|
|
},
|
|
expected: &ParsedCredentialAssertionData{
|
|
ParsedPublicKeyCredential: ParsedPublicKeyCredential{
|
|
ParsedCredential: ParsedCredential{
|
|
ID: "AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
|
|
Type: string(PublicKeyCredentialType),
|
|
},
|
|
RawID: byteID,
|
|
ClientExtensionResults: map[string]any{
|
|
"appID": "example.com",
|
|
},
|
|
},
|
|
Response: ParsedAssertionResponse{
|
|
CollectedClientData: CollectedClientData{
|
|
Type: CeremonyType("webauthn.get"),
|
|
Challenge: "E4PTcIH_HfX1pC6Sigk1SC9NAlgeztN0439vi8z_c9k",
|
|
Origin: "https://webauthn.io",
|
|
Hint: "do not compare clientDataJSON against a template. See https://goo.gl/yabPex",
|
|
},
|
|
AuthenticatorData: AuthenticatorData{
|
|
RPIDHash: byteRPIDHash,
|
|
Counter: 1553097241,
|
|
Flags: 0x045,
|
|
AttData: AttestedCredentialData{
|
|
AAGUID: byteAAGUID,
|
|
CredentialID: byteID,
|
|
CredentialPublicKey: byteCredentialPubKey,
|
|
},
|
|
},
|
|
Signature: byteSignature,
|
|
UserHandle: byteUserHandle,
|
|
},
|
|
Raw: CredentialAssertionResponse{
|
|
PublicKeyCredential: PublicKeyCredential{
|
|
Credential: Credential{
|
|
Type: string(PublicKeyCredentialType),
|
|
ID: "AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
|
|
},
|
|
RawID: byteID,
|
|
ClientExtensionResults: map[string]any{
|
|
"appID": "example.com",
|
|
},
|
|
},
|
|
AssertionResponse: AuthenticatorAssertionResponse{
|
|
AuthenticatorResponse: AuthenticatorResponse{
|
|
ClientDataJSON: byteClientDataJSON,
|
|
},
|
|
AuthenticatorData: byteAuthData,
|
|
Signature: byteSignature,
|
|
UserHandle: byteUserHandle,
|
|
},
|
|
},
|
|
},
|
|
errString: "",
|
|
},
|
|
{
|
|
name: "ShouldHandleTrailingData",
|
|
args: args{
|
|
"trailingData",
|
|
},
|
|
expected: nil,
|
|
errString: "Parse error for Assertion",
|
|
errType: "invalid_request",
|
|
errDetails: "Parse error for Assertion",
|
|
errInfo: "The body contains trailing data",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
body := io.NopCloser(
|
|
bytes.NewReader([]byte(testAssertionResponses[tc.args.responseName])),
|
|
)
|
|
|
|
actual, err := ParseCredentialRequestResponseBody(body)
|
|
|
|
if tc.errString != "" {
|
|
assert.EqualError(t, err, tc.errString)
|
|
|
|
AssertIsProtocolError(t, err, tc.errType, tc.errDetails, tc.errInfo)
|
|
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tc.expected.ClientExtensionResults, actual.ClientExtensionResults)
|
|
assert.Equal(t, tc.expected.ID, actual.ID)
|
|
assert.Equal(t, tc.expected.ParsedCredential, actual.ParsedCredential)
|
|
assert.Equal(t, tc.expected.ParsedPublicKeyCredential, actual.ParsedPublicKeyCredential)
|
|
assert.Equal(t, tc.expected.Raw, actual.Raw)
|
|
assert.Equal(t, tc.expected.RawID, actual.RawID)
|
|
|
|
assert.Equal(
|
|
t,
|
|
tc.expected.Response.CollectedClientData,
|
|
actual.Response.CollectedClientData,
|
|
)
|
|
|
|
var pkExpected, pkActual any
|
|
|
|
assert.NoError(
|
|
t,
|
|
webauthncbor.Unmarshal(
|
|
tc.expected.Response.AuthenticatorData.AttData.CredentialPublicKey,
|
|
&pkExpected,
|
|
),
|
|
)
|
|
assert.NoError(
|
|
t,
|
|
webauthncbor.Unmarshal(
|
|
actual.Response.AuthenticatorData.AttData.CredentialPublicKey,
|
|
&pkActual,
|
|
),
|
|
)
|
|
|
|
assert.Equal(t, pkExpected, pkActual)
|
|
assert.NotEqual(t, nil, pkExpected)
|
|
assert.NotEqual(t, nil, pkActual)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParsedCredentialAssertionData_Verify(t *testing.T) {
|
|
type fields struct {
|
|
ParsedPublicKeyCredential ParsedPublicKeyCredential
|
|
Response ParsedAssertionResponse
|
|
Raw CredentialAssertionResponse
|
|
}
|
|
|
|
type args struct {
|
|
storedChallenge URLEncodedBase64
|
|
relyingPartyID string
|
|
relyingPartyOrigin []string
|
|
verifyUser bool
|
|
credentialBytes []byte
|
|
}
|
|
|
|
// Helper function to create test credential
|
|
makeTestCredential := func() ParsedPublicKeyCredential {
|
|
return ParsedPublicKeyCredential{
|
|
ParsedCredential: ParsedCredential{
|
|
ID: "test-credential-id",
|
|
Type: "public-key",
|
|
},
|
|
}
|
|
}
|
|
|
|
// Helper function to create test response
|
|
makeTestResponse := func(challenge, origin string, flags byte) ParsedAssertionResponse {
|
|
// Generate valid RPID hash for "example.com"
|
|
rpidHash := sha256.Sum256([]byte("example.com"))
|
|
return ParsedAssertionResponse{
|
|
CollectedClientData: CollectedClientData{
|
|
Type: CeremonyType("webauthn.get"),
|
|
Challenge: challenge,
|
|
Origin: origin,
|
|
},
|
|
AuthenticatorData: AuthenticatorData{
|
|
RPIDHash: rpidHash[:],
|
|
Counter: 100,
|
|
Flags: AuthenticatorFlags(flags),
|
|
},
|
|
}
|
|
}
|
|
|
|
// Create a mock EC2 public key credential for testing
|
|
// This is a minimal CBOR-encoded EC2 public key
|
|
mockCredentialBytes := []byte{
|
|
0xa5, // map(5)
|
|
0x01, 0x02, // kty: 2 (EC2)
|
|
0x03, 0x26, // alg: -7 (ES256)
|
|
0x20, 0x01, // crv: 1 (P-256)
|
|
0x21, 0x58, 0x20, // x: bytes(32)
|
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
|
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
|
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
|
|
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
|
|
0x22, 0x58, 0x20, // y: bytes(32)
|
|
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
|
|
0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
|
|
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
|
|
0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
wantErr bool
|
|
}{
|
|
// Note: These "valid" test cases will fail at signature verification
|
|
// since we're using mock data. The purpose is to test the validation
|
|
// logic up to that point. Real signature verification is tested elsewhere.
|
|
{
|
|
name: "Valid assertion with user verification (fails at sig verify)",
|
|
fields: fields{
|
|
ParsedPublicKeyCredential: makeTestCredential(),
|
|
Response: makeTestResponse(
|
|
base64.RawURLEncoding.EncodeToString([]byte("test-challenge")),
|
|
"https://example.com",
|
|
0x05,
|
|
),
|
|
Raw: CredentialAssertionResponse{
|
|
PublicKeyCredential: PublicKeyCredential{
|
|
Credential: Credential{ID: "test-credential-id", Type: "public-key"},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
storedChallenge: URLEncodedBase64("test-challenge"),
|
|
relyingPartyID: "example.com",
|
|
relyingPartyOrigin: []string{"https://example.com"},
|
|
verifyUser: true,
|
|
credentialBytes: mockCredentialBytes,
|
|
},
|
|
wantErr: true, // Changed to true since sig verification will fail
|
|
},
|
|
{
|
|
name: "Invalid challenge",
|
|
fields: fields{
|
|
ParsedPublicKeyCredential: makeTestCredential(),
|
|
Response: makeTestResponse(
|
|
"wrong-challenge",
|
|
"https://example.com",
|
|
0x05,
|
|
),
|
|
Raw: CredentialAssertionResponse{
|
|
PublicKeyCredential: PublicKeyCredential{
|
|
Credential: Credential{ID: "test-credential-id", Type: "public-key"},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
storedChallenge: URLEncodedBase64("test-challenge"),
|
|
relyingPartyID: "example.com",
|
|
relyingPartyOrigin: []string{"https://example.com"},
|
|
verifyUser: true,
|
|
credentialBytes: mockCredentialBytes,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Invalid origin",
|
|
fields: fields{
|
|
ParsedPublicKeyCredential: makeTestCredential(),
|
|
Response: makeTestResponse(
|
|
"test-challenge",
|
|
"https://evil.com",
|
|
0x05,
|
|
),
|
|
Raw: CredentialAssertionResponse{
|
|
PublicKeyCredential: PublicKeyCredential{
|
|
Credential: Credential{ID: "test-credential-id", Type: "public-key"},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
storedChallenge: URLEncodedBase64("test-challenge"),
|
|
relyingPartyID: "example.com",
|
|
relyingPartyOrigin: []string{"https://example.com"},
|
|
verifyUser: true,
|
|
credentialBytes: mockCredentialBytes,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "User verification required but not performed",
|
|
fields: fields{
|
|
ParsedPublicKeyCredential: makeTestCredential(),
|
|
Response: makeTestResponse(
|
|
"test-challenge",
|
|
"https://example.com",
|
|
0x01,
|
|
),
|
|
Raw: CredentialAssertionResponse{
|
|
PublicKeyCredential: PublicKeyCredential{
|
|
Credential: Credential{ID: "test-credential-id", Type: "public-key"},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
storedChallenge: URLEncodedBase64("test-challenge"),
|
|
relyingPartyID: "example.com",
|
|
relyingPartyOrigin: []string{"https://example.com"},
|
|
verifyUser: true,
|
|
credentialBytes: mockCredentialBytes,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Valid assertion without user verification required (fails at sig verify)",
|
|
fields: fields{
|
|
ParsedPublicKeyCredential: makeTestCredential(),
|
|
Response: makeTestResponse(
|
|
base64.RawURLEncoding.EncodeToString([]byte("test-challenge")),
|
|
"https://example.com",
|
|
0x01,
|
|
),
|
|
Raw: CredentialAssertionResponse{
|
|
PublicKeyCredential: PublicKeyCredential{
|
|
Credential: Credential{ID: "test-credential-id", Type: "public-key"},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
storedChallenge: URLEncodedBase64("test-challenge"),
|
|
relyingPartyID: "example.com",
|
|
relyingPartyOrigin: []string{"https://example.com"},
|
|
verifyUser: false,
|
|
credentialBytes: mockCredentialBytes,
|
|
},
|
|
wantErr: true, // Changed to true since sig verification will fail
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
p := &ParsedCredentialAssertionData{
|
|
ParsedPublicKeyCredential: tt.fields.ParsedPublicKeyCredential,
|
|
Response: tt.fields.Response,
|
|
Raw: tt.fields.Raw,
|
|
}
|
|
|
|
if err := p.Verify(tt.args.storedChallenge.String(), tt.args.relyingPartyID, tt.args.relyingPartyOrigin, nil, TopOriginIgnoreVerificationMode, "", tt.args.verifyUser, false, tt.args.credentialBytes); (err != nil) != tt.wantErr {
|
|
t.Errorf(
|
|
"ParsedCredentialAssertionData.Verify() error = %v, wantErr %v",
|
|
err,
|
|
tt.wantErr,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var testAssertionResponses = map[string]string{
|
|
// None Attestation - MacOS TouchID.
|
|
`success`: `{
|
|
"id":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
|
|
"rawId":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
|
|
"clientExtensionResults":{"appID":"example.com"},
|
|
"type":"public-key",
|
|
"response":{
|
|
"authenticatorData":"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiGa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ",
|
|
"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJFNFBUY0lIX0hmWDFwQzZTaWdrMVNDOU5BbGdlenROMDQzOXZpOHpfYzlrIiwibmV3X2tleXNfbWF5X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9",
|
|
"signature":"MEUCIBtIVOQxzFYdyWQyxaLR0tik1TnuPhGVhXVSNgFwLmN5AiEAnxXdCq0UeAVGWxOaFcjBZ_mEZoXqNboY5IkQDdlWZYc",
|
|
"userHandle":"0ToAAAAAAAAAAA"}
|
|
}
|
|
`,
|
|
`trailingData`: `{
|
|
"id":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
|
|
"rawId":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
|
|
"clientExtensionResults":{"appID":"example.com"},
|
|
"type":"public-key",
|
|
"response":{
|
|
"authenticatorData":"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiGa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ",
|
|
"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJFNFBUY0lIX0hmWDFwQzZTaWdrMVNDOU5BbGdlenROMDQzOXZpOHpfYzlrIiwibmV3X2tleXNfbWF5X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9",
|
|
"signature":"MEUCIBtIVOQxzFYdyWQyxaLR0tik1TnuPhGVhXVSNgFwLmN5AiEAnxXdCq0UeAVGWxOaFcjBZ_mEZoXqNboY5IkQDdlWZYc",
|
|
"userHandle":"0ToAAAAAAAAAAA"}
|
|
}
|
|
|
|
trailing
|
|
`,
|
|
}
|