diff --git a/lib/auth/webauthncli/fido2.go b/lib/auth/webauthncli/fido2.go index 0eb930f0603c0..53bebb787f299 100644 --- a/lib/auth/webauthncli/fido2.go +++ b/lib/auth/webauthncli/fido2.go @@ -416,11 +416,19 @@ func fido2Register( }); { case errors.Is(err, libfido2.ErrNoCredentials): return true, nil - case err == nil: + case errors.Is(err, libfido2.ErrUserPresenceRequired): + // Yubikey4 does this when the credential exists. + return false, nil + case err != nil: + // Swallow unexpected errors: a double registration is better than + // aborting the ceremony. + log.Debugf( + "FIDO2: Device %v: excluded credential assertion failed, letting device through: err=%q", + info.path, err) + return true, nil + default: log.Debugf("FIDO2: Device %v: filtered due to presence of excluded credential", info.path) return false, nil - default: // unexpected error - return false, trace.Wrap(err) } } @@ -650,6 +658,7 @@ func findSuitableDevices(filter deviceFilterFunc, knownPaths map[string]struct{} } var info *libfido2.DeviceInfo + var u2f bool const infoAttempts = 3 for i := 0; i < infoAttempts; i++ { info, err = dev.Info() @@ -659,6 +668,7 @@ func findSuitableDevices(filter deviceFilterFunc, knownPaths map[string]struct{} // A FIDO/U2F device has no capabilities beyond MFA // registrations/assertions. info = &libfido2.DeviceInfo{} + u2f = true case errors.Is(err, libfido2.ErrTX): // Happens occasionally, give the device a short grace period and retry. time.Sleep(1 * time.Millisecond) @@ -673,7 +683,7 @@ func findSuitableDevices(filter deviceFilterFunc, knownPaths map[string]struct{} } log.Debugf("FIDO2: Info for device %v: %#v", path, info) - di := makeDevInfo(path, info) + di := makeDevInfo(path, info, u2f) switch ok, err := filter(dev, di); { case err != nil: return nil, trace.Wrap(err, "device %v: filter", path) @@ -838,6 +848,7 @@ func selectDevice( // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticatorGetInfo. type deviceInfo struct { path string + u2f bool plat bool rk bool clientPinCapable, clientPinSet bool @@ -850,8 +861,17 @@ func (di *deviceInfo) uvCapable() bool { return di.uv || di.clientPinSet } -func makeDevInfo(path string, info *libfido2.DeviceInfo) *deviceInfo { - di := &deviceInfo{path: path} +func makeDevInfo(path string, info *libfido2.DeviceInfo, u2f bool) *deviceInfo { + di := &deviceInfo{ + path: path, + u2f: u2f, + } + + // U2F devices don't respond to dev.Info(). + if u2f { + return di + } + for _, opt := range info.Options { // See // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticatorGetInfo. diff --git a/lib/auth/webauthncli/fido2_test.go b/lib/auth/webauthncli/fido2_test.go index 171968b5b448f..5fc3630762973 100644 --- a/lib/auth/webauthncli/fido2_test.go +++ b/lib/auth/webauthncli/fido2_test.go @@ -1659,6 +1659,55 @@ func TestFIDO2Register_errors(t *testing.T) { } } +func TestFIDO2Register_u2fExcludedCredentials(t *testing.T) { + resetFIDO2AfterTests(t) + + u2fDev := mustNewFIDO2Device("/u2f", "" /* pin */, nil /* info */) + u2fDev.u2fOnly = true + + // otherDev is FIDO2 in this test, but it could be any non-registered device. + otherDev := mustNewFIDO2Device("/fido2", "" /* pin */, &libfido2.DeviceInfo{ + Options: authOpts, + }) + + f2 := newFakeFIDO2(u2fDev, otherDev).withNonMeteredLocations() + f2.setCallbacks() + + const origin = "https://example.com" + cc := &wanlib.CredentialCreation{ + Response: protocol.PublicKeyCredentialCreationOptions{ + Challenge: make([]byte, 32), + RelyingParty: protocol.RelyingPartyEntity{ + ID: "example.com", + }, + Parameters: []protocol.CredentialParameter{ + {Type: protocol.PublicKeyCredentialType, Algorithm: webauthncose.AlgES256}, + }, + AuthenticatorSelection: protocol.AuthenticatorSelection{ + UserVerification: protocol.VerificationDiscouraged, + }, + Attestation: protocol.PreferNoAttestation, + }, + } + + ctx := context.Background() + + // Setup: register the U2F device. + resp, err := wancli.FIDO2Register(ctx, origin, cc, u2fDev) + require.NoError(t, err, "FIDO2Register errored") + + // Setup: mark the registered credential as excluded. + cc.Response.CredentialExcludeList = append(cc.Response.CredentialExcludeList, protocol.CredentialDescriptor{ + Type: protocol.PublicKeyCredentialType, + CredentialID: resp.GetWebauthn().GetRawId(), + }) + + // Register a new device, making sure a failed excluded credential assertion + // won't break the ceremony. + _, err = wancli.FIDO2Register(ctx, origin, cc, otherDev) + require.NoError(t, err, "FIDO2Register errored, expected a successful registration") +} + func resetFIDO2AfterTests(t *testing.T) { pollInterval := wancli.FIDO2PollInterval devLocations := wancli.FIDODeviceLocations