Commit 45c3d39c authored by Jun Choi's avatar Jun Choi Committed by Commit Bot

Make U2F devices blink on empty allow list

Currently when empty allow list is passed on as a parameter to
GetAssertion request, all requests to U2F devices are dropped. This is
in accordance with current version of the CTAP spec, as empty allow list
implies resident key credentials and U2F devices do not support resident
keys.

However, this behavior causes requests to U2F devices to be dropped
without U2F devices blinking, which causes user confusion. In order to
minimize user confusion, sent fake registration call to U2F devices for
GetAssertion request with empty allow list.

Bug: 860062
Change-Id: I8e62d01e46cd90f393035149254286b58a932e78
Reviewed-on: https://chromium-review.googlesource.com/1132456
Commit-Queue: Jun Choi <hongjunchoi@chromium.org>
Reviewed-by: default avatarKim Paulhamus <kpaulhamus@chromium.org>
Cr-Commit-Position: refs/heads/master@{#575363}
parent 38cfb4d6
...@@ -877,7 +877,8 @@ TEST_F(AuthenticatorImplTest, GetAssertionWithEmptyAllowCredentials) { ...@@ -877,7 +877,8 @@ TEST_F(AuthenticatorImplTest, GetAssertionWithEmptyAllowCredentials) {
base::RunLoop().RunUntilIdle(); base::RunLoop().RunUntilIdle();
task_runner->FastForwardBy(base::TimeDelta::FromMinutes(1)); task_runner->FastForwardBy(base::TimeDelta::FromMinutes(1));
callback_receiver.WaitForCallback(); callback_receiver.WaitForCallback();
EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, callback_receiver.status()); EXPECT_EQ(AuthenticatorStatus::CREDENTIAL_NOT_RECOGNIZED,
callback_receiver.status());
} }
TEST_F(AuthenticatorImplTest, MakeCredentialAlreadyRegistered) { TEST_F(AuthenticatorImplTest, MakeCredentialAlreadyRegistered) {
......
...@@ -67,6 +67,10 @@ constexpr char kTimeoutErrorMessage[] = ...@@ -67,6 +67,10 @@ constexpr char kTimeoutErrorMessage[] =
"webauth: NotAllowedError: The operation either timed out or was not " "webauth: NotAllowedError: The operation either timed out or was not "
"allowed. See: https://w3c.github.io/webauthn/#sec-assertion-privacy."; "allowed. See: https://w3c.github.io/webauthn/#sec-assertion-privacy.";
constexpr char kInvalidStateErrorMessage[] =
"webauth: InvalidStateError: The user attempted to use an authenticator "
"that recognized none of the provided credentials.";
constexpr char kRelyingPartySecurityErrorMessage[] = constexpr char kRelyingPartySecurityErrorMessage[] =
"webauth: SecurityError: The relying party ID 'localhost' is not a " "webauth: SecurityError: The relying party ID 'localhost' is not a "
"registrable domain suffix of, nor equal to 'https://www.acme.com"; "registrable domain suffix of, nor equal to 'https://www.acme.com";
...@@ -803,7 +807,7 @@ IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest, ...@@ -803,7 +807,7 @@ IN_PROC_BROWSER_TEST_F(WebAuthJavascriptClientBrowserTest,
ASSERT_TRUE(content::ExecuteScriptAndExtractString( ASSERT_TRUE(content::ExecuteScriptAndExtractString(
shell()->web_contents()->GetMainFrame(), shell()->web_contents()->GetMainFrame(),
BuildGetCallWithParameters(parameters), &result)); BuildGetCallWithParameters(parameters), &result));
ASSERT_EQ(kTimeoutErrorMessage, result); ASSERT_EQ(kInvalidStateErrorMessage, result);
} }
// WebAuthBrowserBleDisabledTest // WebAuthBrowserBleDisabledTest
......
...@@ -71,8 +71,10 @@ void GetAssertionTask::GetAssertion() { ...@@ -71,8 +71,10 @@ void GetAssertionTask::GetAssertion() {
} }
void GetAssertionTask::U2fSign() { void GetAssertionTask::U2fSign() {
DCHECK(!device()->device_info());
device()->set_supported_protocol(ProtocolVersion::kU2f); device()->set_supported_protocol(ProtocolVersion::kU2f);
if (!IsConvertibleToU2fSignCommand(request_)) {
if (!CheckUserVerificationCompatible()) {
std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrOther, std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrOther,
base::nullopt); base::nullopt);
return; return;
......
...@@ -341,6 +341,9 @@ TEST_F(FidoGetAssertionTaskTest, TestU2fSignRequestWithEmptyAllowedList) { ...@@ -341,6 +341,9 @@ TEST_F(FidoGetAssertionTaskTest, TestU2fSignRequestWithEmptyAllowedList) {
auto device = std::make_unique<MockFidoDevice>(); auto device = std::make_unique<MockFidoDevice>();
device->ExpectCtap2CommandAndRespondWith( device->ExpectCtap2CommandAndRespondWith(
CtapRequestCommand::kAuthenticatorGetInfo, base::nullopt); CtapRequestCommand::kAuthenticatorGetInfo, base::nullopt);
device->ExpectRequestAndRespondWith(
test_data::kU2fFakeRegisterCommand,
test_data::kApduEncodedNoErrorSignResponse);
auto task = std::make_unique<GetAssertionTask>( auto task = std::make_unique<GetAssertionTask>(
device.get(), std::move(request), device.get(), std::move(request),
...@@ -348,7 +351,7 @@ TEST_F(FidoGetAssertionTaskTest, TestU2fSignRequestWithEmptyAllowedList) { ...@@ -348,7 +351,7 @@ TEST_F(FidoGetAssertionTaskTest, TestU2fSignRequestWithEmptyAllowedList) {
get_assertion_callback_receiver().WaitForCallback(); get_assertion_callback_receiver().WaitForCallback();
EXPECT_EQ(device->supported_protocol(), ProtocolVersion::kU2f); EXPECT_EQ(device->supported_protocol(), ProtocolVersion::kU2f);
EXPECT_EQ(CtapDeviceResponseCode::kCtap2ErrOther, EXPECT_EQ(CtapDeviceResponseCode::kCtap2ErrCredentialNotValid,
get_assertion_callback_receiver().status()); get_assertion_callback_receiver().status());
EXPECT_FALSE(get_assertion_callback_receiver().value()); EXPECT_FALSE(get_assertion_callback_receiver().value());
} }
......
...@@ -27,15 +27,30 @@ U2fSignOperation::U2fSignOperation(FidoDevice* device, ...@@ -27,15 +27,30 @@ U2fSignOperation::U2fSignOperation(FidoDevice* device,
U2fSignOperation::~U2fSignOperation() = default; U2fSignOperation::~U2fSignOperation() = default;
void U2fSignOperation::Start() { void U2fSignOperation::Start() {
// Non-empty allow list in |request_| is checked from above const auto& allow_list = request().allow_list();
// IsConvertibleToU2fSignCommand(). if (allow_list && !allow_list->empty()) {
auto it = request().allow_list()->cbegin(); const auto it = allow_list->cbegin();
DispatchDeviceRequest(
ConvertToU2fSignCommand(request(), ApplicationParameterType::kPrimary,
it->id(), true /* is_check_only */),
base::BindOnce(&U2fSignOperation::OnCheckForKeyHandlePresence,
weak_factory_.GetWeakPtr(),
ApplicationParameterType::kPrimary, it));
} else {
// In order to make U2F authenticators blink on sign request with an empty
// allow list, we send fake enrollment to the device and error out if the
// user has provided user presence.
SendFakeEnrollment();
}
}
void U2fSignOperation::SendFakeEnrollment() {
DispatchDeviceRequest( DispatchDeviceRequest(
ConvertToU2fSignCommand(request(), ApplicationParameterType::kPrimary, ConstructBogusU2fRegistrationCommand(),
it->id(), true /* is_check_only */), base::BindOnce(&U2fSignOperation::OnSignResponseReceived,
base::BindOnce(&U2fSignOperation::OnCheckForKeyHandlePresence, weak_factory_.GetWeakPtr(), true /* is_fake_enrollment */,
weak_factory_.GetWeakPtr(), ApplicationParameterType::kPrimary,
ApplicationParameterType::kPrimary, it)); std::vector<uint8_t>()));
} }
void U2fSignOperation::RetrySign( void U2fSignOperation::RetrySign(
...@@ -160,13 +175,9 @@ void U2fSignOperation::OnCheckForKeyHandlePresence( ...@@ -160,13 +175,9 @@ void U2fSignOperation::OnCheckForKeyHandlePresence(
} else { } else {
// No provided key was accepted by this device. Send registration // No provided key was accepted by this device. Send registration
// (Fake enroll) request to device. // (Fake enroll) request to device.
DispatchDeviceRequest( SendFakeEnrollment();
ConstructBogusU2fRegistrationCommand(),
base::BindOnce(
&U2fSignOperation::OnSignResponseReceived,
weak_factory_.GetWeakPtr(), true /* is_fake_enrollment */,
ApplicationParameterType::kPrimary, std::vector<uint8_t>()));
} }
break; break;
} }
default: default:
......
...@@ -46,6 +46,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) U2fSignOperation ...@@ -46,6 +46,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) U2fSignOperation
using AllowedListIterator = using AllowedListIterator =
std::vector<PublicKeyCredentialDescriptor>::const_iterator; std::vector<PublicKeyCredentialDescriptor>::const_iterator;
void SendFakeEnrollment();
void RetrySign(bool is_fake_enrollment, void RetrySign(bool is_fake_enrollment,
ApplicationParameterType application_parameter_type, ApplicationParameterType application_parameter_type,
const std::vector<uint8_t>& key_handle); const std::vector<uint8_t>& key_handle);
......
...@@ -4396,8 +4396,6 @@ crbug.com/826936 external/wpt/webauthn/getcredential-extensions.https.html [ Pas ...@@ -4396,8 +4396,6 @@ crbug.com/826936 external/wpt/webauthn/getcredential-extensions.https.html [ Pas
crbug.com/826936 external/wpt/webauthn/getcredential-passing.https.html [ Pass Timeout ] crbug.com/826936 external/wpt/webauthn/getcredential-passing.https.html [ Pass Timeout ]
crbug.com/826936 external/wpt/webauthn/getcredential-timeout.https.html [ Pass Timeout ] crbug.com/826936 external/wpt/webauthn/getcredential-timeout.https.html [ Pass Timeout ]
crbug.com/826936 external/wpt/webauthn/createcredential-pubkeycredparams.https.html [ Pass Timeout ] crbug.com/826936 external/wpt/webauthn/createcredential-pubkeycredparams.https.html [ Pass Timeout ]
crbug.com/826936 http/tests/credentialmanager/credentialscontainer-get-from-nested-frame.html [ Pass Timeout ]
crbug.com/826936 http/tests/credentialmanager/credentialscontainer-get-with-virtual-authenticator.html [ Pass Timeout ]
# WebRTC with Unified Plan # WebRTC with Unified Plan
crbug.com/828866 virtual/webrtc-wpt-unified-plan/external/wpt/webrtc/RTCPeerConnection-setRemoteDescription-tracks.https.html [ Timeout ] crbug.com/828866 virtual/webrtc-wpt-unified-plan/external/wpt/webrtc/RTCPeerConnection-setRemoteDescription-tracks.https.html [ Timeout ]
......
...@@ -120,9 +120,9 @@ promise_test(async t => { ...@@ -120,9 +120,9 @@ promise_test(async t => {
someOtherCredential.id = new TextEncoder().encode("someOtherCredential"); someOtherCredential.id = new TextEncoder().encode("someOtherCredential");
delete customGetAssertionOptions.allowCredentials; delete customGetAssertionOptions.allowCredentials;
return promise_rejects(t, "NotSupportedError", return promise_rejects(t, "InvalidStateError",
navigator.credentials.get({ publicKey : customGetAssertionOptions})); navigator.credentials.get({ publicKey : customGetAssertionOptions}));
}, "navigator.credentials.get() with empty allowCredentials returns NotSupportedError"); }, "navigator.credentials.get() with empty allowCredentials returns InvalidStateError if the user has provided user presence");
promise_test(t => { promise_test(t => {
return navigator.credentials.test.clearAuthenticators(); return navigator.credentials.test.clearAuthenticators();
......
...@@ -43,9 +43,9 @@ promise_test(async t => { ...@@ -43,9 +43,9 @@ promise_test(async t => {
someOtherCredential.id = new TextEncoder().encode("someOtherCredential"); someOtherCredential.id = new TextEncoder().encode("someOtherCredential");
delete customGetAssertionOptions.allowCredentials; delete customGetAssertionOptions.allowCredentials;
return promise_rejects(t, "NotSupportedError", return promise_rejects(t, "InvalidStateError",
navigator.credentials.get({ publicKey : customGetAssertionOptions})); navigator.credentials.get({ publicKey : customGetAssertionOptions}));
}, "navigator.credentials.get() with empty allowCredentials returns NotSupportedError"); }, "navigator.credentials.get() with empty allowCredentials returns InvalidStateError if the user has provided user presence");
promise_test(t => { promise_test(t => {
return navigator.credentials.test.clearAuthenticators(); return navigator.credentials.test.clearAuthenticators();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment