This commit is contained in:
+1157
@@ -0,0 +1,1157 @@
|
||||
package webauthn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/go-webauthn/webauthn/metadata"
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/testing/mocks"
|
||||
)
|
||||
|
||||
func TestNewCredentialFlags(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
flags protocol.AuthenticatorFlags
|
||||
expectedUserPresent bool
|
||||
expectedUserVerified bool
|
||||
expectedBackupEligible bool
|
||||
expectedBackupState bool
|
||||
}{
|
||||
{
|
||||
name: "ShouldHandleNoFlags",
|
||||
flags: 0,
|
||||
expectedUserPresent: false,
|
||||
expectedUserVerified: false,
|
||||
expectedBackupEligible: false,
|
||||
expectedBackupState: false,
|
||||
},
|
||||
{
|
||||
name: "ShouldHandleAllFlags",
|
||||
flags: protocol.FlagUserPresent | protocol.FlagUserVerified | protocol.FlagBackupEligible | protocol.FlagBackupState,
|
||||
expectedUserPresent: true,
|
||||
expectedUserVerified: true,
|
||||
expectedBackupEligible: true,
|
||||
expectedBackupState: true,
|
||||
},
|
||||
{
|
||||
name: "ShouldHandleUserPresentOnly",
|
||||
flags: protocol.FlagUserPresent,
|
||||
expectedUserPresent: true,
|
||||
expectedUserVerified: false,
|
||||
expectedBackupEligible: false,
|
||||
expectedBackupState: false,
|
||||
},
|
||||
{
|
||||
name: "ShouldHandleBackupFlags",
|
||||
flags: protocol.FlagBackupEligible | protocol.FlagBackupState,
|
||||
expectedUserPresent: false,
|
||||
expectedUserVerified: false,
|
||||
expectedBackupEligible: true,
|
||||
expectedBackupState: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := NewCredentialFlags(tc.flags)
|
||||
|
||||
assert.Equal(t, tc.expectedUserPresent, result.UserPresent)
|
||||
assert.Equal(t, tc.expectedUserVerified, result.UserVerified)
|
||||
assert.Equal(t, tc.expectedBackupEligible, result.BackupEligible)
|
||||
assert.Equal(t, tc.expectedBackupState, result.BackupState)
|
||||
assert.Equal(t, tc.flags, result.ProtocolValue())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredential_Verify(t *testing.T) {
|
||||
assert.EqualError(t, (&Credential{}).Verify(nil), "error verifying credential: the metadata provider must be provided but it's nil")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
credential func(t *testing.T) Credential
|
||||
setup func(t *testing.T, provider *mocks.MockMetadataProvider)
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "ShouldFailParseError",
|
||||
credential: func(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
return Credential{
|
||||
Attestation: CredentialAttestation{
|
||||
ClientDataJSON: []byte(`{}`),
|
||||
Object: []byte("not-valid-cbor"),
|
||||
},
|
||||
}
|
||||
},
|
||||
err: "error verifying credential: error parsing attestation: Error parsing the authenticator response",
|
||||
},
|
||||
{
|
||||
name: "ShouldVerifyNoneFormatWithClientDataHash",
|
||||
credential: func(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
return testCredentialFromNoneAttestation(t)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ShouldVerifyNoneFormatWithEmptyClientDataHash",
|
||||
credential: func(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
credential := testCredentialFromNoneAttestation(t)
|
||||
credential.Attestation.ClientDataHash = nil
|
||||
|
||||
return credential
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ShouldVerifyNoneFormatWithTransports",
|
||||
credential: func(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
credential := testCredentialFromNoneAttestation(t)
|
||||
credential.Transport = []protocol.AuthenticatorTransport{protocol.USB, protocol.NFC}
|
||||
|
||||
return credential
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ShouldVerifyPackedFormatWithMetadataValidation",
|
||||
credential: func(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
return testCredentialFromPackedAttestation(t)
|
||||
},
|
||||
setup: func(t *testing.T, provider *mocks.MockMetadataProvider) {
|
||||
t.Helper()
|
||||
|
||||
provider.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(&metadata.Entry{
|
||||
MetadataStatement: metadata.Statement{
|
||||
AttestationTypes: metadata.AuthenticatorAttestationTypes{metadata.BasicFull},
|
||||
},
|
||||
}, nil)
|
||||
provider.EXPECT().GetValidateAttestationTypes(gomock.Any()).Return(false)
|
||||
provider.EXPECT().GetValidateStatus(gomock.Any()).Return(false)
|
||||
provider.EXPECT().GetValidateTrustAnchor(gomock.Any()).Return(false)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ShouldFailPackedFormatMetadataGetEntryError",
|
||||
credential: func(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
return testCredentialFromPackedAttestation(t)
|
||||
},
|
||||
setup: func(t *testing.T, provider *mocks.MockMetadataProvider) {
|
||||
t.Helper()
|
||||
|
||||
provider.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("entry lookup failed"))
|
||||
},
|
||||
err: "error verifying credential: error verifying attestation: Failed to validate authenticator metadata for Authenticator Attestation GUID '2369d4d0-13ce-48cb-9f26-f7ed8c9a6068'. Error occurred retrieving the metadata entry: entry lookup failed",
|
||||
},
|
||||
{
|
||||
name: "ShouldFailPackedFormatMetadataValidateStatus",
|
||||
credential: func(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
return testCredentialFromPackedAttestation(t)
|
||||
},
|
||||
setup: func(t *testing.T, provider *mocks.MockMetadataProvider) {
|
||||
t.Helper()
|
||||
|
||||
provider.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(&metadata.Entry{
|
||||
MetadataStatement: metadata.Statement{
|
||||
AttestationTypes: metadata.AuthenticatorAttestationTypes{metadata.BasicFull},
|
||||
},
|
||||
}, nil)
|
||||
provider.EXPECT().GetValidateAttestationTypes(gomock.Any()).Return(false)
|
||||
provider.EXPECT().GetValidateStatus(gomock.Any()).Return(true)
|
||||
provider.EXPECT().ValidateStatusReports(gomock.Any(), gomock.Any()).Return(fmt.Errorf("status report invalid"))
|
||||
},
|
||||
err: "error verifying credential: error verifying attestation: Failed to validate authenticator metadata for Authenticator Attestation GUID '2369d4d0-13ce-48cb-9f26-f7ed8c9a6068'. Error occurred validating the authenticator status: status report invalid",
|
||||
},
|
||||
{
|
||||
name: "ShouldVerifyPackedFormatMetadataEntryNilNoValidation",
|
||||
credential: func(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
return testCredentialFromPackedAttestation(t)
|
||||
},
|
||||
setup: func(t *testing.T, provider *mocks.MockMetadataProvider) {
|
||||
t.Helper()
|
||||
|
||||
provider.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(nil, nil)
|
||||
provider.EXPECT().GetValidateEntry(gomock.Any()).Return(false)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ShouldFailPackedFormatMetadataEntryNilValidateEntryRequired",
|
||||
credential: func(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
return testCredentialFromPackedAttestation(t)
|
||||
},
|
||||
setup: func(t *testing.T, provider *mocks.MockMetadataProvider) {
|
||||
t.Helper()
|
||||
|
||||
provider.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(nil, nil)
|
||||
provider.EXPECT().GetValidateEntry(gomock.Any()).Return(true)
|
||||
},
|
||||
err: "error verifying credential: error verifying attestation: Failed to validate authenticator metadata for Authenticator Attestation GUID '2369d4d0-13ce-48cb-9f26-f7ed8c9a6068'. The authenticator has no registered metadata.",
|
||||
},
|
||||
{
|
||||
name: "ShouldVerifyPackedFormatWithAttestationTypeValidation",
|
||||
credential: func(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
return testCredentialFromPackedAttestation(t)
|
||||
},
|
||||
setup: func(t *testing.T, provider *mocks.MockMetadataProvider) {
|
||||
t.Helper()
|
||||
|
||||
provider.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(&metadata.Entry{
|
||||
MetadataStatement: metadata.Statement{
|
||||
AttestationTypes: metadata.AuthenticatorAttestationTypes{metadata.BasicFull},
|
||||
},
|
||||
}, nil)
|
||||
provider.EXPECT().GetValidateAttestationTypes(gomock.Any()).Return(true)
|
||||
provider.EXPECT().GetValidateStatus(gomock.Any()).Return(false)
|
||||
provider.EXPECT().GetValidateTrustAnchor(gomock.Any()).Return(false)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ShouldFailPackedFormatAttestationTypeMismatch",
|
||||
credential: func(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
return testCredentialFromPackedAttestation(t)
|
||||
},
|
||||
setup: func(t *testing.T, provider *mocks.MockMetadataProvider) {
|
||||
t.Helper()
|
||||
|
||||
provider.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(&metadata.Entry{
|
||||
MetadataStatement: metadata.Statement{
|
||||
AttestationTypes: metadata.AuthenticatorAttestationTypes{metadata.BasicSurrogate},
|
||||
},
|
||||
}, nil)
|
||||
provider.EXPECT().GetValidateAttestationTypes(gomock.Any()).Return(true)
|
||||
},
|
||||
err: "error verifying credential: error verifying attestation: Failed to validate authenticator metadata for Authenticator Attestation GUID '2369d4d0-13ce-48cb-9f26-f7ed8c9a6068'. The attestation type 'basic_full' is not known to be used by this authenticator.",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
provider := mocks.NewMockMetadataProvider(ctrl)
|
||||
|
||||
if tc.setup != nil {
|
||||
tc.setup(t, provider)
|
||||
}
|
||||
|
||||
credential := tc.credential(t)
|
||||
|
||||
err := credential.Verify(provider)
|
||||
|
||||
if tc.err == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredential_Verify_RestoresAttestationType(t *testing.T) {
|
||||
credential := testCredentialFromNoneAttestation(t)
|
||||
credential.AttestationType = ""
|
||||
credential.AttestationFormat = "none"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
provider := mocks.NewMockMetadataProvider(ctrl)
|
||||
|
||||
require.NoError(t, credential.Verify(provider))
|
||||
assert.Equal(t, "none", credential.AttestationType)
|
||||
assert.Equal(t, "none", credential.AttestationFormat)
|
||||
}
|
||||
|
||||
func TestCredential_Verify_LeavesExistingAttestationTypeAlone(t *testing.T) {
|
||||
// The self-heal must not overwrite a caller-supplied AttestationType. If the field is already populated,
|
||||
// Verify leaves it alone.
|
||||
credential := testCredentialFromNoneAttestation(t)
|
||||
credential.AttestationType = "caller-set-value"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
provider := mocks.NewMockMetadataProvider(ctrl)
|
||||
|
||||
require.NoError(t, credential.Verify(provider))
|
||||
assert.Equal(t, "caller-set-value", credential.AttestationType)
|
||||
}
|
||||
|
||||
func TestCredential_Verify_RejectsTamperedPublicKey(t *testing.T) {
|
||||
t.Run("MismatchReturnsError", func(t *testing.T) {
|
||||
credential := testCredentialFromNoneAttestation(t)
|
||||
|
||||
tampered := make([]byte, len(credential.PublicKey))
|
||||
copy(tampered, credential.PublicKey)
|
||||
tampered[0] ^= 0xFF
|
||||
credential.PublicKey = tampered
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
provider := mocks.NewMockMetadataProvider(ctrl)
|
||||
|
||||
err := credential.Verify(provider)
|
||||
assert.EqualError(t, err, "error verifying credential: stored public key does not match the credential public key embedded in the attestation object")
|
||||
})
|
||||
|
||||
t.Run("EmptyPublicKeyReturnsError", func(t *testing.T) {
|
||||
credential := testCredentialFromNoneAttestation(t)
|
||||
credential.PublicKey = nil
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
provider := mocks.NewMockMetadataProvider(ctrl)
|
||||
|
||||
err := credential.Verify(provider)
|
||||
assert.EqualError(t, err, "error verifying credential: stored public key does not match the credential public key embedded in the attestation object")
|
||||
})
|
||||
}
|
||||
|
||||
// testCredentialFromNoneAttestation constructs a Credential with valid "none" format attestation data for testing.
|
||||
func testCredentialFromNoneAttestation(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
attObject, err := base64.RawURLEncoding.DecodeString("o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw")
|
||||
require.NoError(t, err)
|
||||
|
||||
clientDataJSON, err := base64.RawURLEncoding.DecodeString("eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ")
|
||||
require.NoError(t, err)
|
||||
|
||||
clientDataHash := sha256.Sum256(clientDataJSON)
|
||||
|
||||
parsed := (&protocol.AuthenticatorAttestationResponse{
|
||||
AuthenticatorResponse: protocol.AuthenticatorResponse{ClientDataJSON: clientDataJSON},
|
||||
AttestationObject: attObject,
|
||||
})
|
||||
|
||||
parsedResponse, err := parsed.Parse()
|
||||
require.NoError(t, err)
|
||||
|
||||
return Credential{
|
||||
ID: []byte("credential-id"),
|
||||
PublicKey: parsedResponse.AttestationObject.AuthData.AttData.CredentialPublicKey,
|
||||
AttestationType: "none",
|
||||
Attestation: CredentialAttestation{
|
||||
ClientDataJSON: clientDataJSON,
|
||||
ClientDataHash: clientDataHash[:],
|
||||
Object: attObject,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCredential(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
clientDataHash []byte
|
||||
parsed *protocol.ParsedCredentialCreationData
|
||||
expected *Credential
|
||||
}{
|
||||
{
|
||||
name: "ShouldCreateCredential",
|
||||
clientDataHash: []byte("client-data-hash"),
|
||||
parsed: &protocol.ParsedCredentialCreationData{
|
||||
ParsedPublicKeyCredential: protocol.ParsedPublicKeyCredential{
|
||||
AuthenticatorAttachment: protocol.Platform,
|
||||
},
|
||||
Response: protocol.ParsedAttestationResponse{
|
||||
AttestationObject: protocol.AttestationObject{
|
||||
Format: "packed",
|
||||
Type: string(metadata.BasicFull),
|
||||
AuthData: protocol.AuthenticatorData{
|
||||
Counter: 1,
|
||||
Flags: protocol.FlagUserPresent | protocol.FlagUserVerified,
|
||||
AttData: protocol.AttestedCredentialData{
|
||||
AAGUID: []byte("aaguid-value-here"),
|
||||
CredentialID: []byte("credential-id"),
|
||||
CredentialPublicKey: []byte("public-key"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Transports: []protocol.AuthenticatorTransport{protocol.USB},
|
||||
},
|
||||
Raw: protocol.CredentialCreationResponse{
|
||||
AttestationResponse: protocol.AuthenticatorAttestationResponse{
|
||||
AuthenticatorResponse: protocol.AuthenticatorResponse{
|
||||
ClientDataJSON: []byte("client-data-json"),
|
||||
},
|
||||
AuthenticatorData: []byte("auth-data"),
|
||||
PublicKeyAlgorithm: -7,
|
||||
AttestationObject: []byte("attestation-object"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &Credential{
|
||||
ID: []byte("credential-id"),
|
||||
PublicKey: []byte("public-key"),
|
||||
AttestationType: string(metadata.BasicFull),
|
||||
AttestationFormat: "packed",
|
||||
Transport: []protocol.AuthenticatorTransport{protocol.USB},
|
||||
Flags: NewCredentialFlags(protocol.FlagUserPresent | protocol.FlagUserVerified),
|
||||
Authenticator: Authenticator{
|
||||
AAGUID: []byte("aaguid-value-here"),
|
||||
SignCount: 1,
|
||||
Attachment: protocol.Platform,
|
||||
},
|
||||
Attestation: CredentialAttestation{
|
||||
ClientDataJSON: []byte("client-data-json"),
|
||||
ClientDataHash: []byte("client-data-hash"),
|
||||
AuthenticatorData: []byte("auth-data"),
|
||||
PublicKeyAlgorithm: -7,
|
||||
Object: []byte("attestation-object"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ShouldCreateCredentialWithNilHash",
|
||||
clientDataHash: nil,
|
||||
parsed: &protocol.ParsedCredentialCreationData{
|
||||
Response: protocol.ParsedAttestationResponse{
|
||||
AttestationObject: protocol.AttestationObject{
|
||||
Format: "none",
|
||||
Type: string(metadata.None),
|
||||
AuthData: protocol.AuthenticatorData{
|
||||
AttData: protocol.AttestedCredentialData{
|
||||
CredentialID: []byte("cred-2"),
|
||||
CredentialPublicKey: []byte("pub-2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Raw: protocol.CredentialCreationResponse{},
|
||||
},
|
||||
expected: &Credential{
|
||||
ID: []byte("cred-2"),
|
||||
PublicKey: []byte("pub-2"),
|
||||
AttestationType: string(metadata.None),
|
||||
AttestationFormat: "none",
|
||||
Flags: CredentialFlags{},
|
||||
Authenticator: Authenticator{},
|
||||
Attestation: CredentialAttestation{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual, err := NewCredential(tc.clientDataHash, tc.parsed)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredential_SignalUnknownCredential(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
rpid string
|
||||
have *Credential
|
||||
expected *protocol.SignalUnknownCredential
|
||||
expectedJSON string
|
||||
}{
|
||||
{
|
||||
"ShouldHandleStandard",
|
||||
"example.com",
|
||||
&Credential{
|
||||
ID: []byte("1234"),
|
||||
},
|
||||
&protocol.SignalUnknownCredential{
|
||||
CredentialID: protocol.URLEncodedBase64("1234"),
|
||||
RPID: "example.com",
|
||||
},
|
||||
`{"credentialId":"MTIzNA","rpId":"example.com"}`,
|
||||
},
|
||||
{
|
||||
"ShouldHandleNoID",
|
||||
"example.com",
|
||||
&Credential{},
|
||||
&protocol.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 TestCredentials_CredentialDescriptors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
have Credentials
|
||||
expected []protocol.CredentialDescriptor
|
||||
expectedJSON string
|
||||
}{
|
||||
{
|
||||
"ShouldHandleStandard",
|
||||
Credentials{
|
||||
Credential{
|
||||
ID: []byte("1234"),
|
||||
},
|
||||
},
|
||||
[]protocol.CredentialDescriptor{
|
||||
{
|
||||
Type: protocol.PublicKeyCredentialType,
|
||||
CredentialID: protocol.URLEncodedBase64("1234"),
|
||||
},
|
||||
},
|
||||
`[{"type":"public-key","id":"MTIzNA"}]`,
|
||||
},
|
||||
{
|
||||
"ShouldHandleEmpty",
|
||||
Credentials{},
|
||||
[]protocol.CredentialDescriptor{},
|
||||
`[]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := tc.have.CredentialDescriptors()
|
||||
|
||||
assert.Equal(t, tc.expected, actual)
|
||||
|
||||
data, err := json.Marshal(actual)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.expectedJSON, string(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredential_UnmarshalJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
attestationType string
|
||||
attestationFormat string
|
||||
}{
|
||||
{
|
||||
name: "ShouldMigrateLegacyRecordWithPackedFormat",
|
||||
input: `{"id":"MTIz","publicKey":"YWJj","attestationType":"packed"}`,
|
||||
attestationType: "",
|
||||
attestationFormat: "packed",
|
||||
},
|
||||
{
|
||||
name: "ShouldMigrateLegacyRecordWithNoneFormat",
|
||||
input: `{"id":"MTIz","publicKey":"YWJj","attestationType":"none"}`,
|
||||
attestationType: "",
|
||||
attestationFormat: "none",
|
||||
},
|
||||
{
|
||||
name: "ShouldMigrateLegacyRecordWithFIDOU2FFormat",
|
||||
input: `{"id":"MTIz","publicKey":"YWJj","attestationType":"fido-u2f"}`,
|
||||
attestationType: "",
|
||||
attestationFormat: "fido-u2f",
|
||||
},
|
||||
{
|
||||
name: "ShouldMigrateLegacyRecordWithAndroidKeyFormat",
|
||||
input: `{"id":"MTIz","publicKey":"YWJj","attestationType":"android-key"}`,
|
||||
attestationType: "",
|
||||
attestationFormat: "android-key",
|
||||
},
|
||||
{
|
||||
name: "ShouldPreserveNewRecordWithBothFields",
|
||||
input: `{"id":"MTIz","publicKey":"YWJj","attestationType":"basic_full","attestationFormat":"packed"}`,
|
||||
attestationType: "basic_full",
|
||||
attestationFormat: "packed",
|
||||
},
|
||||
{
|
||||
name: "ShouldPreserveNewRecordWithSurrogate",
|
||||
input: `{"id":"MTIz","publicKey":"YWJj","attestationType":"basic_surrogate","attestationFormat":"packed"}`,
|
||||
attestationType: "basic_surrogate",
|
||||
attestationFormat: "packed",
|
||||
},
|
||||
{
|
||||
name: "ShouldPreserveTypeValueThatIsNotAFormat",
|
||||
input: `{"id":"MTIz","publicKey":"YWJj","attestationType":"basic_full"}`,
|
||||
attestationType: "basic_full",
|
||||
attestationFormat: "",
|
||||
},
|
||||
{
|
||||
name: "ShouldHandleEmptyBothFields",
|
||||
input: `{"id":"MTIz","publicKey":"YWJj"}`,
|
||||
attestationType: "",
|
||||
attestationFormat: "",
|
||||
},
|
||||
{
|
||||
name: "ShouldHandleUnknownTypeString",
|
||||
input: `{"id":"MTIz","publicKey":"YWJj","attestationType":"something-unrecognised"}`,
|
||||
attestationType: "something-unrecognised",
|
||||
attestationFormat: "",
|
||||
},
|
||||
{
|
||||
name: "ShouldNotMigrateWhenFormatAlreadyPresent",
|
||||
input: `{"id":"MTIz","publicKey":"YWJj","attestationType":"packed","attestationFormat":"tpm"}`,
|
||||
attestationType: "packed",
|
||||
attestationFormat: "tpm",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var c Credential
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(tc.input), &c))
|
||||
assert.Equal(t, tc.attestationType, c.AttestationType)
|
||||
assert.Equal(t, tc.attestationFormat, c.AttestationFormat)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("ShouldRejectMalformedJSON", func(t *testing.T) {
|
||||
var c Credential
|
||||
|
||||
assert.Error(t, json.Unmarshal([]byte(`{not-json`), &c))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCredential_RoundTripJSON(t *testing.T) {
|
||||
// Marshal -> Unmarshal produces an equivalent record (no lossy migration applied to a record already carrying
|
||||
// both fields).
|
||||
original := Credential{
|
||||
ID: []byte("cred-id"),
|
||||
AttestationType: "basic_surrogate",
|
||||
AttestationFormat: "packed",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(original)
|
||||
require.NoError(t, err)
|
||||
|
||||
var round Credential
|
||||
|
||||
require.NoError(t, json.Unmarshal(data, &round))
|
||||
assert.Equal(t, original.AttestationType, round.AttestationType)
|
||||
assert.Equal(t, original.AttestationFormat, round.AttestationFormat)
|
||||
}
|
||||
|
||||
func TestCredential_Descriptor_PopulatesBothFields(t *testing.T) {
|
||||
// Descriptor() mirrors Credential's attestation type / format split into the descriptor. The format field is
|
||||
// what GetAppID and the option helpers key on (against the "fido-u2f" format string); the type field carries
|
||||
// the real attestation type for completeness.
|
||||
c := Credential{
|
||||
ID: []byte("cred-id"),
|
||||
AttestationType: "basic_full",
|
||||
AttestationFormat: "fido-u2f",
|
||||
}
|
||||
|
||||
descriptor := c.Descriptor()
|
||||
|
||||
assert.Equal(t, protocol.PublicKeyCredentialType, descriptor.Type)
|
||||
assert.Equal(t, "basic_full", descriptor.AttestationType)
|
||||
assert.Equal(t, "fido-u2f", descriptor.AttestationFormat)
|
||||
}
|
||||
|
||||
// testCredentialFromPackedAttestation constructs a Credential with valid "packed" format attestation data for testing.
|
||||
func testCredentialFromPackedAttestation(t *testing.T) Credential {
|
||||
t.Helper()
|
||||
|
||||
response := `{
|
||||
"id":"owBY6F5857tda9Pg5iFNCg6ksHpGOYhrNqIn46pkvhEMKIgNGcKS-vDGAUEroq0-VHnl1LhzQkPRQmYBTHjGcpLKZKSLa2m2ANI-91HjXzoJd_zFOiEnu7CDwQTff9KZ6uPlx7kUK-JJOHar-IyRKcNhc_kOJ2ezglmj1JYuIJLoDEyXlKkkviFdwk1vbWLnO3p_oWROUeIgH_S4CLVLPIJXkPe0YvMgp3ESs9CsrN6kvMTysVRIt_h5KUqpZo0TKCL96zwFk1X_2PwCLKWmOxVL35lJfUKOHG9rc3bmKlqZR6aOgZjerY6BpU8BTJkAqfOvdVlqFeEcywJQgveR7FOvnVtoqzd5oaEwjA",
|
||||
"rawId":"owBY6F5857tda9Pg5iFNCg6ksHpGOYhrNqIn46pkvhEMKIgNGcKS-vDGAUEroq0-VHnl1LhzQkPRQmYBTHjGcpLKZKSLa2m2ANI-91HjXzoJd_zFOiEnu7CDwQTff9KZ6uPlx7kUK-JJOHar-IyRKcNhc_kOJ2ezglmj1JYuIJLoDEyXlKkkviFdwk1vbWLnO3p_oWROUeIgH_S4CLVLPIJXkPe0YvMgp3ESs9CsrN6kvMTysVRIt_h5KUqpZo0TKCL96zwFk1X_2PwCLKWmOxVL35lJfUKOHG9rc3bmKlqZR6aOgZjerY6BpU8BTJkAqfOvdVlqFeEcywJQgveR7FOvnVtoqzd5oaEwjA",
|
||||
"response":{
|
||||
"attestationObject":"o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEgwRgIhAIXRMqmC2_bHTkKUwOvLvmAikuQPCk__9clILwjhOz3VAiEApJXTrN4WMiPwFXqTIh0oI8AZBm3vs-y_UotbQFSnX99jeDVjgVkCqzCCAqcwggJMoAMCAQICFGqj6W3EVhRWQJPun0qqCMyTlnqKMAoGCCqGSM49BAMCMC0xETAPBgNVBAoMCFNvbG9LZXlzMQswCQYDVQQGEwJDSDELMAkGA1UEAwwCRjEwIBcNMjEwNTIzMDA1MjA2WhgPMjA3MTA1MTEwMDUyMDZaMIGDMQswCQYDVQQGEwJVUzERMA8GA1UECgwIU29sb0tleXMxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xPTA7BgNVBAMMNFNvbG8gMiBORkMrVVNCLUMgMjM2OUQ0RDAxM0NFNDhDQjlGMjZGN0VEOEM5QTYwNjggQjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS6N5V2fT-agh34bRiW--Wl6CQPSsnLqqSEID0t5RRKjjl1NDI__mzuyYuOrWyb5yzGZRHgnHq65cm2ROpxo6AOo4HwMIHtMB0GA1UdDgQWBBQ6CEDC5W8_zAMOhVgV8wHJI8n3bzAfBgNVHSMEGDAWgBRBa7ZL76IZDeRiX_0pBJa5gim0-DAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly9pLnMycGtpLm5ldC9mMS8wJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL2MuczJwa2kubmV0L3IxLzAhBgsrBgEEAYLlHAEBBAQSBBAjadTQE85Iy58m9-2MmmBoMBMGCysGAQQBguUcAgEBBAQDAgQwMAoGCCqGSM49BAMCA0kAMEYCIQCP82Rolr0U2FvOJq53AZYcA6xfC4-cNDczvf0FtU1SQAIhAIvb21Z3D8RCvwk2-Ryn4wpsGnn2vma6Bw3E1f48hyVwaGF1dGhEYXRhWQFtarm78N-aFvkduzO7sTL6-dF8eCxIJsbscOzuWNl-9SpBAAAAJyNp1NATzkjLnyb37YyaYGgBDKMAWOhefOe7XWvT4OYhTQoOpLB6RjmIazaiJ-OqZL4RDCiIDRnCkvrwxgFBK6KtPlR55dS4c0JD0UJmAUx4xnKSymSki2tptgDSPvdR4186CXf8xTohJ7uwg8EE33_Smerj5ce5FCviSTh2q_iMkSnDYXP5Didns4JZo9SWLiCS6AxMl5SpJL4hXcJNb21i5zt6f6FkTlHiIB_0uAi1SzyCV5D3tGLzIKdxErPQrKzepLzE8rFUSLf4eSlKqWaNEygi_es8BZNV_9j8AiylpjsVS9-ZSX1Cjhxva3N25ipamUemjoGY3q2OgaVPAUyZAKnzr3VZahXhHMsCUIL3kexTr51baKs3eaGhMIykAQEDJyAGIVggjz9UkJ7cKooE3blSuzlqxkdLppMuFl3CIiST8odWS6k",
|
||||
"clientDataJSON":"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQ1dieENUMEc0TDJ5T1JwQkw2U1dWaWd3ZTJrUUVYQmhvNUw2d0U0Ny1FcyIsIm9yaWdpbiI6Imh0dHBzOi8vd2ViYXV0aG4uZmlyc3R5ZWFyLmlkLmF1IiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"
|
||||
},
|
||||
"type":"public-key"
|
||||
}`
|
||||
|
||||
var ccr protocol.CredentialCreationResponse
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(response), &ccr))
|
||||
|
||||
parsed, err := ccr.AttestationResponse.Parse()
|
||||
require.NoError(t, err)
|
||||
|
||||
clientDataHash := sha256.Sum256(ccr.AttestationResponse.ClientDataJSON)
|
||||
|
||||
return Credential{
|
||||
ID: ccr.RawID,
|
||||
PublicKey: parsed.AttestationObject.AuthData.AttData.CredentialPublicKey,
|
||||
AttestationType: "packed",
|
||||
Authenticator: Authenticator{
|
||||
AAGUID: parsed.AttestationObject.AuthData.AttData.AAGUID,
|
||||
},
|
||||
Attestation: CredentialAttestation{
|
||||
ClientDataJSON: ccr.AttestationResponse.ClientDataJSON,
|
||||
ClientDataHash: clientDataHash[:],
|
||||
Object: ccr.AttestationResponse.AttestationObject,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredential_MsgpRoundTrip(t *testing.T) {
|
||||
// Exercises the Marshal/Unmarshal and Encode/Decode paths of every top-level field branch on Credential,
|
||||
// including the Transport slice element, the CredentialFlags shim, and the nested Authenticator /
|
||||
// CredentialAttestation structs. The generated TestMarshalUnmarshalCredential only covers the zero value.
|
||||
original := newPopulatedCredential()
|
||||
|
||||
t.Run("MarshalUnmarshalPreservesFields", func(t *testing.T) {
|
||||
data, err := original.MarshalMsg(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded Credential
|
||||
|
||||
left, err := decoded.UnmarshalMsg(data)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, left, "UnmarshalMsg should consume all bytes")
|
||||
assert.Equal(t, original, decoded)
|
||||
|
||||
// Msgsize returns an upper-bound estimate; marshalled output must fit within it.
|
||||
assert.LessOrEqual(t, len(data), original.Msgsize())
|
||||
})
|
||||
|
||||
t.Run("EncodeDecodePreservesFields", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
err := msgp.Encode(&buf, &original)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded Credential
|
||||
|
||||
require.NoError(t, msgp.Decode(&buf, &decoded))
|
||||
assert.Equal(t, original, decoded)
|
||||
})
|
||||
|
||||
t.Run("UnmarshalSkipsUnknownKeysAlongsideKnown", func(t *testing.T) {
|
||||
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 Credential
|
||||
|
||||
left, err := decoded.UnmarshalMsg(spliced)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, left)
|
||||
assert.Equal(t, original, decoded)
|
||||
})
|
||||
|
||||
t.Run("UnmarshalSkipsUnknownKeysOnly", func(t *testing.T) {
|
||||
tiny := []byte{0x81, 0xa3, 'x', 'y', 'z', 0xc3}
|
||||
|
||||
var decoded Credential
|
||||
|
||||
left, err := decoded.UnmarshalMsg(tiny)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, left)
|
||||
assert.Equal(t, Credential{}, decoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCredentialAttestation_MsgpRoundTrip(t *testing.T) {
|
||||
original := CredentialAttestation{
|
||||
ClientDataJSON: []byte(`{"type":"webauthn.create"}`),
|
||||
ClientDataHash: bytes.Repeat([]byte{0x01}, 32),
|
||||
AuthenticatorData: []byte{0xff, 0xee, 0xdd},
|
||||
PublicKeyAlgorithm: -257,
|
||||
Object: []byte{0xca, 0xfe, 0xba, 0xbe},
|
||||
}
|
||||
|
||||
data, err := original.MarshalMsg(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded CredentialAttestation
|
||||
|
||||
left, err := decoded.UnmarshalMsg(data)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, left)
|
||||
assert.Equal(t, original, decoded)
|
||||
assert.LessOrEqual(t, len(data), original.Msgsize())
|
||||
|
||||
// EncodeMsg / DecodeMsg via msgp.Writer and msgp.Reader path.
|
||||
var buf bytes.Buffer
|
||||
|
||||
require.NoError(t, msgp.Encode(&buf, &original))
|
||||
|
||||
var streamDecoded CredentialAttestation
|
||||
|
||||
require.NoError(t, msgp.Decode(&buf, &streamDecoded))
|
||||
assert.Equal(t, original, streamDecoded)
|
||||
}
|
||||
|
||||
func TestCredentialFlags_MsgpRoundTrip(t *testing.T) {
|
||||
// The //msgp:shim directive makes CredentialFlags encode as a single msgpack byte via MsgpByte and decode via
|
||||
// CredentialFlagsFromMsgpByte. Generated code for CredentialFlags has the smallest surface of any msgp method
|
||||
// pair in this package and was 0%-covered before this test.
|
||||
testCases := []struct {
|
||||
name string
|
||||
flags protocol.AuthenticatorFlags
|
||||
}{
|
||||
{"Zero", 0},
|
||||
{"UserPresent", protocol.FlagUserPresent},
|
||||
{"UserVerified", protocol.FlagUserVerified},
|
||||
{"AllKnownFlags", protocol.FlagUserPresent | protocol.FlagUserVerified | protocol.FlagBackupEligible | protocol.FlagBackupState},
|
||||
{"AllBitsSet", protocol.AuthenticatorFlags(0xFF)},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
original := NewCredentialFlags(tc.flags)
|
||||
|
||||
data, err := original.MarshalMsg(nil)
|
||||
require.NoError(t, err)
|
||||
// msgpack encodes a byte as either a 1-byte positive fixint (values 0x00-0x7f) or a 2-byte uint 8
|
||||
// (0xcc + payload) for values ≥ 0x80; either is acceptable.
|
||||
assert.LessOrEqual(t, len(data), 2, "CredentialFlags should encode in at most 2 msgpack bytes")
|
||||
assert.GreaterOrEqual(t, len(data), 1)
|
||||
|
||||
var decoded CredentialFlags
|
||||
|
||||
left, err := decoded.UnmarshalMsg(data)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, left)
|
||||
assert.Equal(t, original, decoded)
|
||||
assert.Equal(t, original.ProtocolValue(), decoded.ProtocolValue())
|
||||
|
||||
// Stream path via msgp.Writer / msgp.Reader.
|
||||
var buf bytes.Buffer
|
||||
|
||||
require.NoError(t, msgp.Encode(&buf, original))
|
||||
|
||||
var streamDecoded CredentialFlags
|
||||
|
||||
require.NoError(t, msgp.Decode(&buf, &streamDecoded))
|
||||
assert.Equal(t, original, streamDecoded)
|
||||
})
|
||||
}
|
||||
|
||||
// Truncated-input error path.
|
||||
var decoded CredentialFlags
|
||||
|
||||
_, err := decoded.UnmarshalMsg(nil)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCredentials_MsgpRoundTrip(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
original Credentials
|
||||
}{
|
||||
{"Empty", Credentials{}},
|
||||
{"Single", Credentials{newPopulatedCredential()}},
|
||||
{"Multiple", Credentials{newPopulatedCredential(), newPopulatedCredential(), newPopulatedCredential()}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data, err := tc.original.MarshalMsg(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var decoded Credentials
|
||||
|
||||
left, err := decoded.UnmarshalMsg(data)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, left)
|
||||
|
||||
if len(tc.original) == 0 {
|
||||
assert.Empty(t, decoded)
|
||||
} else {
|
||||
assert.Equal(t, tc.original, decoded)
|
||||
}
|
||||
|
||||
assert.LessOrEqual(t, len(data), tc.original.Msgsize())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newPopulatedCredential() Credential {
|
||||
return Credential{
|
||||
ID: []byte{0x01, 0x02, 0x03, 0x04},
|
||||
PublicKey: []byte{0xa5, 0x01, 0x02, 0x03, 0x26},
|
||||
AttestationType: "basic_full",
|
||||
AttestationFormat: "packed",
|
||||
Transport: []protocol.AuthenticatorTransport{protocol.USB, protocol.NFC, protocol.Hybrid},
|
||||
Flags: NewCredentialFlags(protocol.FlagUserPresent | protocol.FlagUserVerified | protocol.FlagBackupEligible),
|
||||
Authenticator: Authenticator{
|
||||
AAGUID: bytes.Repeat([]byte{0x11}, 16),
|
||||
SignCount: 42,
|
||||
CloneWarning: true,
|
||||
Attachment: protocol.Platform,
|
||||
},
|
||||
Attestation: CredentialAttestation{
|
||||
ClientDataJSON: []byte(`{"type":"webauthn.create","challenge":"abc","origin":"https://example.com"}`),
|
||||
ClientDataHash: bytes.Repeat([]byte{0xde}, 32),
|
||||
AuthenticatorData: bytes.Repeat([]byte{0xaa}, 64),
|
||||
PublicKeyAlgorithm: -7,
|
||||
Object: bytes.Repeat([]byte{0xbb}, 128),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredential_MsgpEncodeErrorPaths(t *testing.T) {
|
||||
v := newPopulatedCredential()
|
||||
|
||||
data, err := v.MarshalMsg(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
exerciseEncodeMsgErrorPaths(t, &v, data)
|
||||
}
|
||||
|
||||
func TestCredentialAttestation_MsgpEncodeErrorPaths(t *testing.T) {
|
||||
v := CredentialAttestation{
|
||||
ClientDataJSON: []byte(`{"type":"webauthn.create"}`),
|
||||
ClientDataHash: bytes.Repeat([]byte{0x01}, 32),
|
||||
AuthenticatorData: []byte{0xff, 0xee, 0xdd},
|
||||
PublicKeyAlgorithm: -257,
|
||||
Object: []byte{0xca, 0xfe, 0xba, 0xbe},
|
||||
}
|
||||
|
||||
data, err := v.MarshalMsg(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
exerciseEncodeMsgErrorPaths(t, &v, data)
|
||||
}
|
||||
|
||||
func TestCredentialFlags_MsgpEncodeErrorPaths(t *testing.T) {
|
||||
v := NewCredentialFlags(protocol.FlagUserPresent | protocol.FlagUserVerified | protocol.FlagBackupEligible | protocol.FlagBackupState)
|
||||
|
||||
data, err := v.MarshalMsg(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
exerciseEncodeMsgErrorPaths(t, v, data)
|
||||
}
|
||||
|
||||
func TestCredentials_MsgpEncodeErrorPaths(t *testing.T) {
|
||||
v := Credentials{newPopulatedCredential(), newPopulatedCredential()}
|
||||
|
||||
data, err := v.MarshalMsg(nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
exerciseEncodeMsgErrorPaths(t, v, data)
|
||||
}
|
||||
|
||||
func TestCredential_DecodeMsgInvalidTypes(t *testing.T) {
|
||||
t.Run("NotAMap", func(t *testing.T) {
|
||||
var c Credential
|
||||
|
||||
_, err := c.UnmarshalMsg(msgpString("not a map"))
|
||||
require.Error(t, err)
|
||||
|
||||
var c2 Credential
|
||||
|
||||
require.Error(t, msgp.Decode(bytes.NewReader(msgpString("not a map")), &c2))
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
wantSub string
|
||||
}{
|
||||
{"IDAsBool", msgpOneFieldMap("id", msgpBool(true)), "ID"},
|
||||
{"PublicKeyAsInt", msgpOneFieldMap("pk", msgpInt64(42)), "PublicKey"},
|
||||
{"AttestationTypeAsInt", msgpOneFieldMap("atttype", msgpInt64(42)), "AttestationType"},
|
||||
{"AttestationFormatAsBool", msgpOneFieldMap("attfmt", msgpBool(true)), "AttestationFormat"},
|
||||
{"TransportNotArray", msgpOneFieldMap("t", msgpBool(true)), "Transport"},
|
||||
{"TransportElementNotString", msgpOneFieldMap("t", func() []byte {
|
||||
b := msgp.AppendArrayHeader(nil, 1)
|
||||
|
||||
return append(b, msgpBool(true)...)
|
||||
}()), "Transport"},
|
||||
{"FlagsAsString", msgpOneFieldMap("flg", msgpString("x")), "Flags"},
|
||||
{"AuthenticatorAsBool", msgpOneFieldMap("a", msgpBool(true)), "Authenticator"},
|
||||
{"AttestationAsBool", msgpOneFieldMap("att", msgpBool(true)), "Attestation"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var c Credential
|
||||
|
||||
_, err := c.UnmarshalMsg(tc.data)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.wantSub)
|
||||
|
||||
var c2 Credential
|
||||
|
||||
streamErr := msgp.Decode(bytes.NewReader(tc.data), &c2)
|
||||
require.Error(t, streamErr)
|
||||
assert.Contains(t, streamErr.Error(), tc.wantSub)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialAttestation_DecodeMsgInvalidTypes(t *testing.T) {
|
||||
t.Run("NotAMap", func(t *testing.T) {
|
||||
var c CredentialAttestation
|
||||
|
||||
_, err := c.UnmarshalMsg(msgpString("not a map"))
|
||||
require.Error(t, err)
|
||||
|
||||
var c2 CredentialAttestation
|
||||
|
||||
require.Error(t, msgp.Decode(bytes.NewReader(msgpString("not a map")), &c2))
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
wantSub string
|
||||
}{
|
||||
{"ClientDataJSONAsInt", msgpOneFieldMap("cdj", msgpInt64(42)), "ClientDataJSON"},
|
||||
{"ClientDataHashAsBool", msgpOneFieldMap("cdh", msgpBool(true)), "ClientDataHash"},
|
||||
{"AuthenticatorDataAsInt", msgpOneFieldMap("data", msgpInt64(42)), "AuthenticatorData"},
|
||||
{"PublicKeyAlgorithmAsString", msgpOneFieldMap("alg", msgpString("x")), "PublicKeyAlgorithm"},
|
||||
{"ObjectAsBool", msgpOneFieldMap("obj", msgpBool(true)), "Object"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var c CredentialAttestation
|
||||
|
||||
_, err := c.UnmarshalMsg(tc.data)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.wantSub)
|
||||
|
||||
var c2 CredentialAttestation
|
||||
|
||||
streamErr := msgp.Decode(bytes.NewReader(tc.data), &c2)
|
||||
require.Error(t, streamErr)
|
||||
assert.Contains(t, streamErr.Error(), tc.wantSub)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialFlags_DecodeMsgInvalidTypes(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"AsString", msgpString("x")},
|
||||
{"AsBool", msgpBool(true)},
|
||||
{"AsNil", msgp.AppendNil(nil)},
|
||||
{"Truncated", nil},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var f CredentialFlags
|
||||
|
||||
_, err := f.UnmarshalMsg(tc.data)
|
||||
require.Error(t, err)
|
||||
|
||||
if len(tc.data) > 0 {
|
||||
var f2 CredentialFlags
|
||||
|
||||
require.Error(t, msgp.Decode(bytes.NewReader(tc.data), &f2))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentials_DecodeMsgInvalidTypes(t *testing.T) {
|
||||
t.Run("NotAnArray", func(t *testing.T) {
|
||||
var c Credentials
|
||||
|
||||
_, err := c.UnmarshalMsg(msgpString("not an array"))
|
||||
require.Error(t, err)
|
||||
|
||||
var c2 Credentials
|
||||
|
||||
require.Error(t, msgp.Decode(bytes.NewReader(msgpString("not an array")), &c2))
|
||||
})
|
||||
|
||||
t.Run("ElementNotAMap", func(t *testing.T) {
|
||||
b := msgp.AppendArrayHeader(nil, 1)
|
||||
b = append(b, msgpBool(true)...)
|
||||
|
||||
var c Credentials
|
||||
|
||||
_, err := c.UnmarshalMsg(b)
|
||||
require.Error(t, err)
|
||||
|
||||
var c2 Credentials
|
||||
|
||||
require.Error(t, msgp.Decode(bytes.NewReader(b), &c2))
|
||||
})
|
||||
}
|
||||
|
||||
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) } //nolint:unparam
|
||||
func msgpInt64(v int64) []byte { return msgp.AppendInt64(nil, v) } //nolint:unparam
|
||||
func msgpString(v string) []byte { return msgp.AppendString(nil, v) }
|
||||
Reference in New Issue
Block a user