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

1049 lines
33 KiB
Go
Vendored

package webauthn
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"testing"
"time"
"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/protocol"
"github.com/go-webauthn/webauthn/protocol/webauthncose"
"github.com/go-webauthn/webauthn/testing/mocks"
)
func TestWithRegistrationRelyingPartyID(t *testing.T) {
testCases := []struct {
name string
have *Config
opts []RegistrationOption
expectedID string
expectedName string
err string
}{
{
name: "OptionDefinedInConfig",
have: &Config{
RPID: "example.com",
RPDisplayName: "Test Display Name",
RPOrigins: []string{"https://example.com"},
},
opts: nil,
expectedID: "example.com",
expectedName: "Test Display Name",
},
{
name: "OptionDefinedInConfigAndOpts",
have: &Config{
RPID: "example.com",
RPDisplayName: "Test Display Name",
RPOrigins: []string{"https://example.com"},
},
opts: []RegistrationOption{WithRegistrationRelyingPartyID("a.example.com"), WithRegistrationRelyingPartyName("Test Display Name2")},
expectedID: "a.example.com",
expectedName: "Test Display Name2",
},
{
name: "OptionDefinedInConfigWithNoErrAndInOptsWithError",
have: &Config{
RPID: "example.com",
RPDisplayName: "Test Display Name",
RPOrigins: []string{"https://example.com"},
},
opts: []RegistrationOption{WithRegistrationRelyingPartyID("---::~!!~@#M!@OIK#N!@IOK@@@@@@@@@@"), WithRegistrationRelyingPartyName("Test Display Name2")},
err: "error generating credential creation: the relying party id failed to validate as it's not a valid domain string with error: parse \"---::~!!~@\": first path segment in URL cannot contain colon",
},
{
name: "OptionDefinedInOpts",
have: &Config{
RPOrigins: []string{"example.com"},
},
opts: []RegistrationOption{WithRegistrationRelyingPartyID("example.com"), WithRegistrationRelyingPartyName("Test Display Name")},
expectedID: "example.com",
expectedName: "Test Display Name",
},
{
name: "OptionDisplayNameNotDefined",
have: &Config{
RPOrigins: []string{"https://example.com"},
},
opts: []RegistrationOption{WithRegistrationRelyingPartyID("example.com")},
err: "error generating credential creation: the relying party display name must be provided via the configuration or a functional option for a creation",
},
{
name: "OptionIDNotDefined",
have: &Config{
RPOrigins: []string{"https://example.com"},
},
opts: []RegistrationOption{WithRegistrationRelyingPartyName("Test Display Name")},
err: "error generating credential creation: the relying party id must be provided via the configuration or a functional option for a creation",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
w, err := New(tc.have)
assert.NoError(t, err)
user := &defaultUser{}
creation, _, err := w.BeginRegistration(user, tc.opts...)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
} else {
assert.NoError(t, err)
require.NotNil(t, creation)
assert.Equal(t, tc.expectedID, creation.Response.RelyingParty.ID)
assert.Equal(t, tc.expectedName, creation.Response.RelyingParty.Name)
}
})
}
}
func TestRegistration_FinishRegistrationFailure(t *testing.T) {
user := &defaultUser{
id: []byte("123"),
}
session := SessionData{
UserID: []byte("ABC"),
}
webauthn := &WebAuthn{}
credential, err := webauthn.FinishRegistration(user, session, nil)
if err == nil {
t.Errorf("FinishRegistration() error = nil, want %v", protocol.ErrBadRequest.Type)
}
if credential != nil {
t.Errorf("FinishRegistration() credential = %v, want nil", credential)
}
}
func TestEntityEncoding(t *testing.T) {
testCases := []struct {
name string
b64 bool
have, expected string
}{
{"ShouldEncodeBase64", true, "abc", `{"name":"","displayName":"","id":"YWJj"}`},
{"ShouldEncodeString", false, "abc", `{"name":"","displayName":"","id":"abc"}`},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
entityUser := protocol.UserEntity{}
if tc.b64 {
entityUser.ID = protocol.URLEncodedBase64(tc.have)
} else {
entityUser.ID = tc.have
}
data, err := json.Marshal(entityUser)
assert.NoError(t, err)
assert.Equal(t, tc.expected, string(data))
})
}
}
func TestCreateCredential_Errors(t *testing.T) {
testCases := []struct {
name string
user User
session SessionData
err string
}{
{
name: "ShouldFailUserIDMismatch",
user: &defaultUser{
id: []byte("123"),
},
session: SessionData{
UserID: []byte("456"),
},
err: "ID mismatch for User and Session",
},
{
name: "ShouldFailSessionExpired",
user: &defaultUser{
id: []byte("123"),
},
session: SessionData{
UserID: []byte("123"),
Expires: time.Now().Add(-time.Hour),
},
err: "Session has Expired",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
w := &WebAuthn{Config: &Config{
RPID: "example.com",
RPOrigins: []string{"https://example.com"},
}}
credential, err := w.CreateCredential(tc.user, tc.session, nil)
assert.Nil(t, credential)
assert.EqualError(t, err, tc.err)
})
}
}
func TestBeginRegistration_Timeouts(t *testing.T) {
testCases := []struct {
name string
config *Config
opts []RegistrationOption
expectedTimeout int
}{
{
name: "ShouldUseDefaultTimeout",
config: &Config{
RPID: "example.com",
RPDisplayName: "Test Display Name",
RPOrigins: []string{"https://example.com"},
},
expectedTimeout: 300000,
},
{
name: "ShouldUseUVDTimeout",
config: &Config{
RPID: "example.com",
RPDisplayName: "Test Display Name",
RPOrigins: []string{"https://example.com"},
AuthenticatorSelection: protocol.AuthenticatorSelection{
UserVerification: protocol.VerificationDiscouraged,
},
},
expectedTimeout: 120000,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
w, err := New(tc.config)
require.NoError(t, err)
user := &defaultUser{id: []byte("123")}
creation, _, err := w.BeginRegistration(user, tc.opts...)
require.NoError(t, err)
assert.Equal(t, tc.expectedTimeout, creation.Response.Timeout)
})
}
}
func TestBeginRegistration_EncodeUserIDAsString(t *testing.T) {
testCases := []struct {
name string
encodeAsString bool
userID string
expectedIDType string
}{
{
name: "ShouldEncodeAsBase64",
encodeAsString: false,
userID: "testuser",
expectedIDType: "protocol.URLEncodedBase64",
},
{
name: "ShouldEncodeAsString",
encodeAsString: true,
userID: "testuser",
expectedIDType: "string",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := &Config{
RPID: "example.com",
RPDisplayName: "Test Display Name",
RPOrigins: []string{"https://example.com"},
EncodeUserIDAsString: tc.encodeAsString,
}
w, err := New(config)
require.NoError(t, err)
user := &defaultUser{id: []byte(tc.userID)}
creation, _, err := w.BeginRegistration(user)
require.NoError(t, err)
if tc.encodeAsString {
_, ok := creation.Response.User.ID.(string)
assert.True(t, ok)
} else {
_, ok := creation.Response.User.ID.(protocol.URLEncodedBase64)
assert.True(t, ok)
}
})
}
}
func TestBeginMediatedRegistration_ChallengeLength(t *testing.T) {
// withTestChallenge is a test-only RegistrationOption that overrides the generated challenge. It mirrors the
// WithChallenge login option; no equivalent is exported for registration, so we synthesise one here to exercise
// the minimum-length guard.
withTestChallenge := func(challenge []byte) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.Challenge = challenge
}
}
config := &Config{
RPID: "example.com",
RPDisplayName: "Test Display Name",
RPOrigins: []string{"https://example.com"},
}
testCases := []struct {
name string
opts []RegistrationOption
expLen int
err string
}{
{
name: "ShouldSucceedWithDefaultChallenge",
opts: nil,
expLen: 32,
},
{
name: "ShouldFailNilChallenge",
opts: []RegistrationOption{withTestChallenge(nil)},
err: "error generating credential creation: the challenge must be at least 16 bytes",
},
{
name: "ShouldFailEmptyChallenge",
opts: []RegistrationOption{withTestChallenge([]byte{})},
err: "error generating credential creation: the challenge must be at least 16 bytes",
},
{
name: "ShouldFailEightByteChallenge",
opts: []RegistrationOption{withTestChallenge(bytes.Repeat([]byte{0xab}, 8))},
err: "error generating credential creation: the challenge must be at least 16 bytes",
},
{
name: "ShouldFailFifteenByteChallenge",
opts: []RegistrationOption{withTestChallenge(bytes.Repeat([]byte{0xab}, 15))},
err: "error generating credential creation: the challenge must be at least 16 bytes",
},
{
name: "ShouldSucceedSixteenByteChallenge",
opts: []RegistrationOption{withTestChallenge(bytes.Repeat([]byte{0xab}, 16))},
expLen: 16,
},
{
name: "ShouldSucceedThirtyTwoByteChallenge",
opts: []RegistrationOption{withTestChallenge(bytes.Repeat([]byte{0xab}, 32))},
expLen: 32,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
w, err := New(config)
require.NoError(t, err)
user := &defaultUser{id: []byte("123")}
creation, session, err := w.BeginMediatedRegistration(user, protocol.MediationDefault, tc.opts...)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
assert.Nil(t, creation)
assert.Nil(t, session)
return
}
require.NoError(t, err)
require.NotNil(t, creation)
require.NotNil(t, session)
assert.Len(t, []byte(creation.Response.Challenge), tc.expLen)
assert.NotEmpty(t, session.Challenge)
assert.Equal(t, creation.Response.Challenge.String(), session.Challenge)
})
}
}
func TestBeginMediatedRegistration_EnforceTimeout(t *testing.T) {
config := &Config{
RPID: "example.com",
RPDisplayName: "Test Display Name",
RPOrigins: []string{"https://example.com"},
Timeouts: TimeoutsConfig{
Registration: TimeoutConfig{
Enforce: true,
Timeout: time.Second * 60,
},
},
}
w, err := New(config)
require.NoError(t, err)
user := &defaultUser{id: []byte("123")}
_, session, err := w.BeginMediatedRegistration(user, protocol.MediationConditional)
require.NoError(t, err)
assert.False(t, session.Expires.IsZero())
assert.Equal(t, protocol.MediationConditional, session.Mediation)
}
func TestRegistrationOptions(t *testing.T) {
tv := true
fv := false
testCases := []struct {
name string
opts []RegistrationOption
have protocol.PublicKeyCredentialCreationOptions
expected protocol.PublicKeyCredentialCreationOptions
}{
{
name: "Empty",
opts: nil,
},
{
name: "CredentialParametersDefault",
opts: []RegistrationOption{WithCredentialParameters(CredentialParametersDefault())},
expected: protocol.PublicKeyCredentialCreationOptions{
Parameters: CredentialParametersDefault(),
},
},
{
name: "CredentialParametersL3Extended",
opts: []RegistrationOption{WithCredentialParameters(CredentialParametersExtendedL3())},
expected: protocol.PublicKeyCredentialCreationOptions{
Parameters: CredentialParametersExtendedL3(),
},
},
{
name: "CredentialParametersL3Recommended",
opts: []RegistrationOption{WithCredentialParameters(CredentialParametersRecommendedL3())},
expected: protocol.PublicKeyCredentialCreationOptions{
Parameters: CredentialParametersRecommendedL3(),
},
},
{
name: "Exclusions",
opts: []RegistrationOption{WithExclusions([]protocol.CredentialDescriptor{{Type: protocol.PublicKeyCredentialType, CredentialID: []byte("123"), Transport: []protocol.AuthenticatorTransport{protocol.Hybrid}}})},
expected: protocol.PublicKeyCredentialCreationOptions{
CredentialExcludeList: []protocol.CredentialDescriptor{
{Type: protocol.PublicKeyCredentialType, CredentialID: []byte("123"), Transport: []protocol.AuthenticatorTransport{protocol.Hybrid}},
},
},
},
{
name: "Selections",
opts: []RegistrationOption{WithAuthenticatorSelection(protocol.AuthenticatorSelection{
AuthenticatorAttachment: protocol.CrossPlatform,
RequireResidentKey: &tv,
ResidentKey: protocol.ResidentKeyRequirementRequired,
UserVerification: protocol.VerificationRequired,
})},
expected: protocol.PublicKeyCredentialCreationOptions{
AuthenticatorSelection: protocol.AuthenticatorSelection{
AuthenticatorAttachment: protocol.CrossPlatform,
RequireResidentKey: &tv,
ResidentKey: protocol.ResidentKeyRequirementRequired,
UserVerification: protocol.VerificationRequired,
},
},
},
{
name: "ResidentKeyRequirementRequired",
opts: []RegistrationOption{WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired)},
expected: protocol.PublicKeyCredentialCreationOptions{
AuthenticatorSelection: protocol.AuthenticatorSelection{
RequireResidentKey: &tv,
ResidentKey: protocol.ResidentKeyRequirementRequired,
},
},
},
{
name: "ResidentKeyRequirementPreferred",
opts: []RegistrationOption{WithResidentKeyRequirement(protocol.ResidentKeyRequirementPreferred)},
expected: protocol.PublicKeyCredentialCreationOptions{
AuthenticatorSelection: protocol.AuthenticatorSelection{
RequireResidentKey: &fv,
ResidentKey: protocol.ResidentKeyRequirementPreferred,
},
},
},
{
name: "PublicKeyCredentialHints",
opts: []RegistrationOption{WithPublicKeyCredentialHints([]protocol.PublicKeyCredentialHints{
protocol.PublicKeyCredentialHintSecurityKey,
})},
expected: protocol.PublicKeyCredentialCreationOptions{
Hints: []protocol.PublicKeyCredentialHints{
protocol.PublicKeyCredentialHintSecurityKey,
},
},
},
{
name: "ConveyancePreference",
opts: []RegistrationOption{WithConveyancePreference(protocol.PreferEnterpriseAttestation)},
expected: protocol.PublicKeyCredentialCreationOptions{
Attestation: protocol.PreferEnterpriseAttestation,
},
},
{
name: "AttestationFormats",
opts: []RegistrationOption{WithAttestationFormats([]protocol.AttestationFormat{protocol.AttestationFormatPacked})},
expected: protocol.PublicKeyCredentialCreationOptions{
AttestationFormats: []protocol.AttestationFormat{protocol.AttestationFormatPacked},
},
},
{
name: "Extensions",
opts: []RegistrationOption{WithExtensions(map[string]any{"appID": "example"})},
expected: protocol.PublicKeyCredentialCreationOptions{
Extensions: map[string]any{"appID": "example"},
},
},
{
name: "AppIDExcludeExtensionWithNoExclusions",
opts: []RegistrationOption{WithAppIdExcludeExtension("apple")},
expected: protocol.PublicKeyCredentialCreationOptions{},
},
{
name: "AppIDExcludeExtensionWithExclusions",
opts: []RegistrationOption{WithExclusions([]protocol.CredentialDescriptor{
{Type: protocol.PublicKeyCredentialType, AttestationFormat: string(protocol.AttestationFormatFIDOUniversalSecondFactor), CredentialID: []byte("123"), Transport: []protocol.AuthenticatorTransport{protocol.Hybrid}},
}), WithAppIdExcludeExtension("apple")},
expected: protocol.PublicKeyCredentialCreationOptions{
CredentialExcludeList: []protocol.CredentialDescriptor{
{Type: protocol.PublicKeyCredentialType, AttestationFormat: string(protocol.AttestationFormatFIDOUniversalSecondFactor), CredentialID: []byte("123"), Transport: []protocol.AuthenticatorTransport{protocol.Hybrid}},
},
Extensions: map[string]any{"appidExclude": "apple"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := &tc.have
for _, opt := range tc.opts {
opt(opts)
}
assert.Equal(t, tc.expected, *opts)
})
}
}
func TestCreateCredential_Full(t *testing.T) {
credParams := []protocol.CredentialParameter{{Type: protocol.PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}}
testCases := []struct {
name string
have struct {
specVector func(t *testing.T) (body []byte, challenge string, credentialID []byte)
challenge string
mediation protocol.CredentialMediationRequirement
setup func(t *testing.T, provider *mocks.MockMetadataProvider)
}
expected struct {
attestationType string
attestationFormat string
err string
}
}{
{
name: "ShouldSucceedNoneFormat",
have: struct {
specVector func(t *testing.T) (body []byte, challenge string, credentialID []byte)
challenge string
mediation protocol.CredentialMediationRequirement
setup func(t *testing.T, provider *mocks.MockMetadataProvider)
}{
specVector: testRegistrationSpecVectorNoneES256,
},
expected: struct {
attestationType string
attestationFormat string
err string
}{
attestationType: "none",
attestationFormat: "none",
},
},
{
name: "ShouldSucceedPackedSelfFormat",
have: struct {
specVector func(t *testing.T) (body []byte, challenge string, credentialID []byte)
challenge string
mediation protocol.CredentialMediationRequirement
setup func(t *testing.T, provider *mocks.MockMetadataProvider)
}{
specVector: testRegistrationSpecVectorPackedSelfES256,
},
expected: struct {
attestationType string
attestationFormat string
err string
}{
attestationType: "basic_surrogate",
attestationFormat: "packed",
},
},
{
name: "ShouldSucceedWithMDS",
have: struct {
specVector func(t *testing.T) (body []byte, challenge string, credentialID []byte)
challenge string
mediation protocol.CredentialMediationRequirement
setup func(t *testing.T, provider *mocks.MockMetadataProvider)
}{
specVector: testRegistrationSpecVectorPackedSelfES256,
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)
},
},
expected: struct {
attestationType string
attestationFormat string
err string
}{
attestationType: "basic_surrogate",
attestationFormat: "packed",
},
},
{
name: "ShouldFailWithMDSGetEntryError",
have: struct {
specVector func(t *testing.T) (body []byte, challenge string, credentialID []byte)
challenge string
mediation protocol.CredentialMediationRequirement
setup func(t *testing.T, provider *mocks.MockMetadataProvider)
}{
specVector: testRegistrationSpecVectorPackedSelfES256,
setup: func(t *testing.T, provider *mocks.MockMetadataProvider) {
t.Helper()
provider.EXPECT().GetEntry(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("entry lookup failed"))
},
},
expected: struct {
attestationType string
attestationFormat string
err string
}{
err: "Failed to validate authenticator metadata for Authenticator Attestation GUID 'df850e09-db6a-fbdf-ab51-697791506cfc'. Error occurred retrieving the metadata entry: entry lookup failed",
},
},
{
name: "ShouldSucceedWithMDSValidateStatusReports",
have: struct {
specVector func(t *testing.T) (body []byte, challenge string, credentialID []byte)
challenge string
mediation protocol.CredentialMediationRequirement
setup func(t *testing.T, provider *mocks.MockMetadataProvider)
}{
specVector: testRegistrationSpecVectorPackedSelfES256,
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)
provider.EXPECT().GetValidateStatus(gomock.Any()).Return(true)
provider.EXPECT().ValidateStatusReports(gomock.Any(), gomock.Any()).Return(nil)
provider.EXPECT().GetValidateTrustAnchor(gomock.Any()).Return(false)
},
},
expected: struct {
attestationType string
attestationFormat string
err string
}{
attestationType: "basic_surrogate",
attestationFormat: "packed",
},
},
{
name: "ShouldFailVerifyError",
have: struct {
specVector func(t *testing.T) (body []byte, challenge string, credentialID []byte)
challenge string
mediation protocol.CredentialMediationRequirement
setup func(t *testing.T, provider *mocks.MockMetadataProvider)
}{
specVector: testRegistrationSpecVectorNoneES256,
challenge: "wrong-challenge",
},
expected: struct {
attestationType string
attestationFormat string
err string
}{
err: "Error validating challenge",
},
},
{
name: "ShouldSucceedMediationConditional",
have: struct {
specVector func(t *testing.T) (body []byte, challenge string, credentialID []byte)
challenge string
mediation protocol.CredentialMediationRequirement
setup func(t *testing.T, provider *mocks.MockMetadataProvider)
}{
specVector: testRegistrationSpecVectorNoneES256,
mediation: protocol.MediationConditional,
},
expected: struct {
attestationType string
attestationFormat string
err string
}{
attestationType: "none",
attestationFormat: "none",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
body, challenge, credentialID := tc.have.specVector(t)
parsedResponse, err := protocol.ParseCredentialCreationResponseBytes(body)
require.NoError(t, err)
if tc.have.challenge != "" {
challenge = tc.have.challenge
}
userID := []byte("test-user-id")
config := &Config{
RPID: "example.org",
RPOrigins: []string{"https://example.org"},
}
if tc.have.setup != nil {
ctrl := gomock.NewController(t)
provider := mocks.NewMockMetadataProvider(ctrl)
tc.have.setup(t, provider)
config.MDS = provider
}
w := &WebAuthn{Config: config}
session := SessionData{
Challenge: challenge,
UserID: userID,
CredParams: credParams,
Mediation: tc.have.mediation,
}
user := &defaultUser{id: userID}
credential, err := w.CreateCredential(user, session, parsedResponse)
if tc.expected.err != "" {
assert.Nil(t, credential)
assert.EqualError(t, err, tc.expected.err)
} else {
assert.NoError(t, err)
require.NotNil(t, credential)
assert.Equal(t, credentialID, credential.ID)
assert.Equal(t, tc.expected.attestationType, credential.AttestationType)
assert.Equal(t, tc.expected.attestationFormat, credential.AttestationFormat)
}
})
}
}
func TestFinishRegistration_Success(t *testing.T) {
body, challenge, credentialID := testRegistrationSpecVectorNoneES256(t)
credParams := []protocol.CredentialParameter{{Type: protocol.PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}}
userID := []byte("test-user-id")
w := &WebAuthn{
Config: &Config{
RPID: "example.org",
RPOrigins: []string{"https://example.org"},
},
}
session := SessionData{
Challenge: challenge,
UserID: userID,
CredParams: credParams,
}
user := &defaultUser{id: userID}
reqBody := io.NopCloser(bytes.NewReader(body))
httpReq := &http.Request{Body: reqBody}
credential, err := w.FinishRegistration(user, session, httpReq)
require.NoError(t, err)
require.NotNil(t, credential)
assert.Equal(t, credentialID, credential.ID)
assert.Equal(t, "none", credential.AttestationType)
assert.Equal(t, "none", credential.AttestationFormat)
}
func TestValidateFilteredCredential(t *testing.T) {
aaguidA := uuid.MustParse("00000000-0000-0000-0000-00000000000a")
aaguidB := uuid.MustParse("00000000-0000-0000-0000-00000000000b")
aaguidC := uuid.MustParse("00000000-0000-0000-0000-00000000000c")
credentialWith := func(aaguid uuid.UUID, backupEligible bool) *Credential {
b, _ := aaguid.MarshalBinary()
flags := protocol.FlagUserPresent
if backupEligible {
flags |= protocol.FlagBackupEligible
}
return &Credential{
Flags: NewCredentialFlags(flags),
Authenticator: Authenticator{AAGUID: b},
}
}
testCases := []struct {
name string
filtering *FilteringConfig
credential *Credential
err string
}{
{
name: "ShouldAllowWhenFilteringNil",
filtering: nil,
credential: credentialWith(aaguidA, true),
},
{
name: "ShouldAllowNilCredentialWhenFilteringNil",
filtering: nil,
credential: nil,
},
{
name: "ShouldRejectNilCredentialWhenFilteringSet",
filtering: &FilteringConfig{},
credential: nil,
err: "Error reading the request data",
},
{
name: "ShouldRejectNilCredentialWhenFilteringHasPermittedList",
filtering: &FilteringConfig{PermittedAAGUIDs: []uuid.UUID{aaguidA}},
credential: nil,
err: "Error reading the request data",
},
{
name: "ShouldAllowWhenFilteringAllZeroValues",
filtering: &FilteringConfig{},
credential: credentialWith(aaguidA, true),
},
{
name: "ShouldRejectBackupEligibleWhenProhibited",
filtering: &FilteringConfig{ProhibitBackupEligibility: true},
credential: credentialWith(aaguidA, true),
err: "Policy restriction prevented the operation from completing",
},
{
name: "ShouldAllowNonBackupEligibleWhenProhibited",
filtering: &FilteringConfig{ProhibitBackupEligibility: true},
credential: credentialWith(aaguidA, false),
},
{
name: "ShouldAllowBackupEligibleWhenNotProhibited",
filtering: &FilteringConfig{ProhibitBackupEligibility: false},
credential: credentialWith(aaguidA, true),
},
{
name: "ShouldRejectMalformedAAGUID",
filtering: &FilteringConfig{},
credential: &Credential{
Authenticator: Authenticator{AAGUID: []byte{0x01, 0x02, 0x03}},
},
err: "Error reading the request data",
},
{
name: "ShouldAllowPermittedAAGUID",
filtering: &FilteringConfig{PermittedAAGUIDs: []uuid.UUID{aaguidA, aaguidB}},
credential: credentialWith(aaguidA, false),
},
{
name: "ShouldRejectAAGUIDNotInPermitList",
filtering: &FilteringConfig{PermittedAAGUIDs: []uuid.UUID{aaguidA, aaguidB}},
credential: credentialWith(aaguidC, false),
err: "Policy restriction prevented the operation from completing",
},
{
name: "ShouldAllowZeroAAGUIDEvenWhenNotInPermitList",
filtering: &FilteringConfig{PermittedAAGUIDs: []uuid.UUID{aaguidA}},
credential: credentialWith(uuid.Nil, false),
},
{
name: "ShouldAllowZeroAAGUIDWhenExplicitlyInPermitList",
filtering: &FilteringConfig{PermittedAAGUIDs: []uuid.UUID{uuid.Nil, aaguidA}},
credential: credentialWith(uuid.Nil, false),
},
{
name: "ShouldRejectProhibitedAAGUID",
filtering: &FilteringConfig{ProhibitedAAGUIDs: []uuid.UUID{aaguidB, aaguidC}},
credential: credentialWith(aaguidB, false),
err: "Policy restriction prevented the operation from completing",
},
{
name: "ShouldAllowAAGUIDNotInProhibitList",
filtering: &FilteringConfig{ProhibitedAAGUIDs: []uuid.UUID{aaguidB, aaguidC}},
credential: credentialWith(aaguidA, false),
},
{
name: "ShouldApplyBothPermittedAndProhibitedListsWhenSet",
filtering: &FilteringConfig{
PermittedAAGUIDs: []uuid.UUID{aaguidA, aaguidB},
ProhibitedAAGUIDs: []uuid.UUID{aaguidB},
},
credential: credentialWith(aaguidB, false),
err: "Policy restriction prevented the operation from completing",
},
{
name: "ShouldAllowWhenPermittedAndNotProhibited",
filtering: &FilteringConfig{
PermittedAAGUIDs: []uuid.UUID{aaguidA, aaguidB},
ProhibitedAAGUIDs: []uuid.UUID{aaguidC},
},
credential: credentialWith(aaguidA, false),
},
{
name: "ShouldCheckBackupEligibilityBeforeAAGUID",
filtering: &FilteringConfig{
ProhibitBackupEligibility: true,
PermittedAAGUIDs: []uuid.UUID{aaguidA},
},
credential: credentialWith(aaguidC, true),
err: "Policy restriction prevented the operation from completing",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := ValidateFilteredCredential(tc.credential, tc.filtering)
if tc.err == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.err)
}
})
}
}
// testRegistrationSpecVectorNoneES256 returns the spec test vector data for NoneES256 registration.
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-none-es256
func testRegistrationSpecVectorNoneES256(t *testing.T) (body []byte, challenge string, credentialID []byte) {
t.Helper()
const (
attestationObjectHex = "a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220"
clientDataJSONHex = "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22414d4d507434557878475453746e63647134313759447742466938767049612d7077386f4f755657345441222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20426b5165446a646354427258426941774a544c453551227d"
credentialIDHex = "f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4" //nolint:gosec
challengeHex = "00c30fb78531c464d2b6771dab8d7b603c01162f2fa486bea70f283ae556e130"
)
credentialID, err := hex.DecodeString(credentialIDHex)
require.NoError(t, err)
challenge = base64.RawURLEncoding.EncodeToString(testRegDecodeHex(t, challengeHex))
id := base64.RawURLEncoding.EncodeToString(credentialID)
attObj := base64.RawURLEncoding.EncodeToString(testRegDecodeHex(t, attestationObjectHex))
cdj := base64.RawURLEncoding.EncodeToString(testRegDecodeHex(t, clientDataJSONHex))
response := map[string]any{
"id": id,
"rawId": id,
"type": "public-key",
"response": map[string]any{
"attestationObject": attObj,
"clientDataJSON": cdj,
},
}
body, err = json.Marshal(response)
require.NoError(t, err)
return body, challenge, credentialID
}
// testRegistrationSpecVectorPackedSelfES256 returns the spec test vector data for Packed Self ES256 registration.
// See: https://www.w3.org/TR/webauthn-3/#sctn-test-vectors-packed-self-es256
func testRegistrationSpecVectorPackedSelfES256(t *testing.T) (body []byte, challenge string, credentialID []byte) {
t.Helper()
const (
attestationObjectHex = "a363666d74667061636b65646761747453746d74a263616c672663736967584630440220067a20754ab925005dbf378097c92120031581c73228d1fb4f5b881bcd7da98302207fc7b147558c7c0eba3af18bd9d121fa3d3a26d17fe3f220272178f473b6006d68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000df850e09db6afbdfab51697791506cfc0020455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58ca5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2"
clientDataJSONHex = "7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2265476e4374334c55745936366b336a506a796e6962506b31716e666644616966715a774c33417032392d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205539685458764b453255526b4d6e625f307859485667227d"
credentialIDHex = "455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58c" //nolint:gosec
challengeHex = "7869c2b772d4b58eba9378cf8f29e26cf935aa77df0da89fa99c0bdc0a76f7e5"
)
credentialID, err := hex.DecodeString(credentialIDHex)
require.NoError(t, err)
challenge = base64.RawURLEncoding.EncodeToString(testRegDecodeHex(t, challengeHex))
id := base64.RawURLEncoding.EncodeToString(credentialID)
attObj := base64.RawURLEncoding.EncodeToString(testRegDecodeHex(t, attestationObjectHex))
cdj := base64.RawURLEncoding.EncodeToString(testRegDecodeHex(t, clientDataJSONHex))
response := map[string]any{
"id": id,
"rawId": id,
"type": "public-key",
"response": map[string]any{
"attestationObject": attObj,
"clientDataJSON": cdj,
},
}
body, err = json.Marshal(response)
require.NoError(t, err)
return body, challenge, credentialID
}
func testRegDecodeHex(t *testing.T, s string) []byte {
t.Helper()
data, err := hex.DecodeString(s)
require.NoError(t, err)
return data
}