Commit acef6fd7 authored by Martin Kreichgauer's avatar Martin Kreichgauer Committed by Commit Bot

fido: implement credential list batching

This adds code to parse the maxCredentialCountInList and
maxCredentialIdLength fields from authenticatorGetInfo responses.

GetAssertionTask is changed to filter and batch allow lists when
silently probing credentials according to these values, if the
authenticator supports it. In particular, probing is skipped if the
allow list fits into a single batch (and the request doesn't have an App
ID that the GetAssertionTask might have to fall back to).

MakeCredentialTask similarly filters and batches before silently probing
for credentials in the exclude list parameter. Probing is skipped
entirely, if the exclude list fits into a single batch and the request
does not carry an appIdExclude extension.

Fixed: 954613
Change-Id: I644e602054e4e2ebb4101b98b867f5dbc61e0901
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1867448
Commit-Queue: Martin Kreichgauer <martinkr@google.com>
Reviewed-by: default avatarAdam Langley <agl@chromium.org>
Cr-Commit-Position: refs/heads/master@{#708023}
parent 802830d0
...@@ -112,6 +112,9 @@ typedef struct { ...@@ -112,6 +112,9 @@ typedef struct {
const char* claimed_authority; const char* claimed_authority;
} OriginClaimedAuthorityPair; } OriginClaimedAuthorityPair;
// The size of credential IDs returned by GetTestCredentials().
constexpr size_t kTestCredentialIdLength = 32u;
constexpr char kTestOrigin1[] = "https://a.google.com"; constexpr char kTestOrigin1[] = "https://a.google.com";
constexpr char kTestOrigin2[] = "https://acme.org"; constexpr char kTestOrigin2[] = "https://acme.org";
constexpr char kTestRelyingPartyId[] = "google.com"; constexpr char kTestRelyingPartyId[] = "google.com";
...@@ -293,7 +296,7 @@ std::vector<device::PublicKeyCredentialDescriptor> GetTestCredentials( ...@@ -293,7 +296,7 @@ std::vector<device::PublicKeyCredentialDescriptor> GetTestCredentials(
std::vector<device::PublicKeyCredentialDescriptor> descriptors; std::vector<device::PublicKeyCredentialDescriptor> descriptors;
for (size_t i = 0; i < num_credentials; i++) { for (size_t i = 0; i < num_credentials; i++) {
DCHECK(i <= std::numeric_limits<uint8_t>::max()); DCHECK(i <= std::numeric_limits<uint8_t>::max());
std::vector<uint8_t> id(32u, static_cast<uint8_t>(i)); std::vector<uint8_t> id(kTestCredentialIdLength, static_cast<uint8_t>(i));
base::flat_set<device::FidoTransportProtocol> transports{ base::flat_set<device::FidoTransportProtocol> transports{
device::FidoTransportProtocol::kUsbHumanInterfaceDevice, device::FidoTransportProtocol::kUsbHumanInterfaceDevice,
device::FidoTransportProtocol::kBluetoothLowEnergy}; device::FidoTransportProtocol::kBluetoothLowEnergy};
...@@ -2834,6 +2837,9 @@ TEST_F(AuthenticatorImplTest, ExtensionHMACSecret) { ...@@ -2834,6 +2837,9 @@ TEST_F(AuthenticatorImplTest, ExtensionHMACSecret) {
} }
} }
// Tests that for an authenticator that does not support batching, credential
// lists get probed silently to work around authenticators rejecting exclude
// lists exceeding a certain size.
TEST_F(AuthenticatorImplTest, MakeCredentialWithLargeExcludeList) { TEST_F(AuthenticatorImplTest, MakeCredentialWithLargeExcludeList) {
TestServiceManagerContext smc; TestServiceManagerContext smc;
NavigateAndCommit(GURL(kTestOrigin1)); NavigateAndCommit(GURL(kTestOrigin1));
...@@ -2868,6 +2874,9 @@ TEST_F(AuthenticatorImplTest, MakeCredentialWithLargeExcludeList) { ...@@ -2868,6 +2874,9 @@ TEST_F(AuthenticatorImplTest, MakeCredentialWithLargeExcludeList) {
} }
} }
// Tests that for an authenticator that does not support batching, credential
// lists get probed silently to work around authenticators rejecting allow lists
// exceeding a certain size.
TEST_F(AuthenticatorImplTest, GetAssertionWithLargeAllowList) { TEST_F(AuthenticatorImplTest, GetAssertionWithLargeAllowList) {
TestServiceManagerContext smc; TestServiceManagerContext smc;
NavigateAndCommit(GURL(kTestOrigin1)); NavigateAndCommit(GURL(kTestOrigin1));
...@@ -2903,6 +2912,90 @@ TEST_F(AuthenticatorImplTest, GetAssertionWithLargeAllowList) { ...@@ -2903,6 +2912,90 @@ TEST_F(AuthenticatorImplTest, GetAssertionWithLargeAllowList) {
} }
} }
// Tests that, regardless of batching support, GetAssertion requests with a
// single allowed credential ID don't result in a silent probing request.
TEST_F(AuthenticatorImplTest, GetAssertionSingleElementAllowListDoesNotProbe) {
TestServiceManagerContext smc;
NavigateAndCommit(GURL(kTestOrigin1));
for (bool supports_batching : {false, true}) {
SCOPED_TRACE(::testing::Message()
<< "supports_batching=" << supports_batching);
ResetVirtualDevice();
device::VirtualCtap2Device::Config config;
if (supports_batching) {
config.max_credential_id_length = kTestCredentialIdLength;
config.max_credential_count_in_list = 10;
}
config.reject_silent_authentication_requests = true;
virtual_device_factory_->SetCtap2Config(config);
mojo::Remote<blink::mojom::Authenticator> authenticator =
ConnectToAuthenticator();
auto test_credentials = GetTestCredentials(/*num_credentials=*/1);
ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration(
test_credentials.front().id(), kTestRelyingPartyId));
PublicKeyCredentialRequestOptionsPtr options =
GetTestPublicKeyCredentialRequestOptions();
options->allow_credentials = std::move(test_credentials);
TestGetAssertionCallback callback_receiver;
authenticator->GetAssertion(std::move(options),
callback_receiver.callback());
base::RunLoop().RunUntilIdle();
callback_receiver.WaitForCallback();
EXPECT_EQ(callback_receiver.status(), AuthenticatorStatus::SUCCESS);
}
}
// Tests that an allow list that fits into a single batch does not result in a
// silent probing request.
TEST_F(AuthenticatorImplTest, GetAssertionSingleBatchListDoesNotProbe) {
TestServiceManagerContext smc;
NavigateAndCommit(GURL(kTestOrigin1));
for (bool allow_list_fits_single_batch : {false, true}) {
SCOPED_TRACE(::testing::Message() << "allow_list_fits_single_batch="
<< allow_list_fits_single_batch);
ResetVirtualDevice();
device::VirtualCtap2Device::Config config;
config.max_credential_id_length = kTestCredentialIdLength;
constexpr size_t kBatchSize = 10;
config.max_credential_count_in_list = kBatchSize;
config.reject_silent_authentication_requests = true;
virtual_device_factory_->SetCtap2Config(config);
mojo::Remote<blink::mojom::Authenticator> authenticator =
ConnectToAuthenticator();
auto test_credentials = GetTestCredentials(
/*num_credentials=*/kBatchSize +
(allow_list_fits_single_batch ? 0 : 1));
ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration(
test_credentials.back().id(), kTestRelyingPartyId));
PublicKeyCredentialRequestOptionsPtr options =
GetTestPublicKeyCredentialRequestOptions();
options->allow_credentials = std::move(test_credentials);
TestGetAssertionCallback callback_receiver;
authenticator->GetAssertion(std::move(options),
callback_receiver.callback());
base::RunLoop().RunUntilIdle();
callback_receiver.WaitForCallback();
EXPECT_EQ(callback_receiver.status(),
allow_list_fits_single_batch
? AuthenticatorStatus::SUCCESS
: AuthenticatorStatus::NOT_ALLOWED_ERROR);
}
}
TEST_F(AuthenticatorImplTest, NoUnexpectedAuthenticatorExtensions) { TEST_F(AuthenticatorImplTest, NoUnexpectedAuthenticatorExtensions) {
TestServiceManagerContext smc; TestServiceManagerContext smc;
NavigateAndCommit(GURL(kTestOrigin1)); NavigateAndCommit(GURL(kTestOrigin1));
...@@ -2937,6 +3030,54 @@ TEST_F(AuthenticatorImplTest, NoUnexpectedAuthenticatorExtensions) { ...@@ -2937,6 +3030,54 @@ TEST_F(AuthenticatorImplTest, NoUnexpectedAuthenticatorExtensions) {
AuthenticatorStatus::NOT_ALLOWED_ERROR); AuthenticatorStatus::NOT_ALLOWED_ERROR);
} }
// Tests that on an authenticator that supports batching, exclude lists that fit
// into a single batch are sent without probing.
TEST_F(AuthenticatorImplTest, ExcludeListBatching) {
TestServiceManagerContext smc;
NavigateAndCommit(GURL(kTestOrigin1));
for (bool authenticator_has_excluded_credential : {false, true}) {
SCOPED_TRACE(::testing::Message()
<< "authenticator_has_excluded_credential="
<< authenticator_has_excluded_credential);
ResetVirtualDevice();
device::VirtualCtap2Device::Config config;
config.max_credential_id_length = kTestCredentialIdLength;
constexpr size_t kBatchSize = 10;
config.max_credential_count_in_list = kBatchSize;
// Reject silent authentication requests to ensure we are not probing
// credentials silently, since the exclude list should fit into a single
// batch.
config.reject_silent_authentication_requests = true;
virtual_device_factory_->SetCtap2Config(config);
auto test_credentials = GetTestCredentials(kBatchSize);
test_credentials.insert(
test_credentials.end() - 1,
{device::CredentialType::kPublicKey,
std::vector<uint8_t>(kTestCredentialIdLength + 1, 1)});
if (authenticator_has_excluded_credential) {
ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectRegistration(
test_credentials.back().id(), kTestRelyingPartyId));
}
mojo::Remote<blink::mojom::Authenticator> authenticator =
ConnectToAuthenticator();
PublicKeyCredentialCreationOptionsPtr options =
GetTestPublicKeyCredentialCreationOptions();
options->exclude_credentials = std::move(test_credentials);
TestMakeCredentialCallback callback;
authenticator->MakeCredential(std::move(options), callback.callback());
base::RunLoop().RunUntilIdle();
callback.WaitForCallback();
EXPECT_EQ(callback.status(), authenticator_has_excluded_credential
? AuthenticatorStatus::CREDENTIAL_EXCLUDED
: AuthenticatorStatus::SUCCESS);
}
}
class UVAuthenticatorImplTest : public AuthenticatorImplTest { class UVAuthenticatorImplTest : public AuthenticatorImplTest {
public: public:
UVAuthenticatorImplTest() = default; UVAuthenticatorImplTest() = default;
...@@ -4145,6 +4286,46 @@ TEST_F(ResidentKeyAuthenticatorImplTest, WinCredProtectApiVersion) { ...@@ -4145,6 +4286,46 @@ TEST_F(ResidentKeyAuthenticatorImplTest, WinCredProtectApiVersion) {
} }
#endif // defined(OS_WIN) #endif // defined(OS_WIN)
// Tests that an allowList with only credential IDs of a length exceeding the
// maxCredentialIdLength parameter is not mistakenly interpreted as an empty
// allow list.
TEST_F(ResidentKeyAuthenticatorImplTest,
AllowListWithOnlyOversizedCredentialIds) {
device::VirtualCtap2Device::Config config;
config.u2f_support = true;
config.pin_support = true;
config.resident_key_support = true;
config.max_credential_id_length = kTestCredentialIdLength;
config.max_credential_count_in_list = 10;
virtual_device_factory_->SetCtap2Config(config);
ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey(
/*credential_id=*/std::vector<uint8_t>(kTestCredentialIdLength, 1),
kTestRelyingPartyId,
/*user_id=*/{{1}}, base::nullopt, base::nullopt));
ASSERT_TRUE(virtual_device_factory_->mutable_state()->InjectResidentKey(
/*credential_id=*/std::vector<uint8_t>(kTestCredentialIdLength, 2),
kTestRelyingPartyId,
/*user_id=*/{{2}}, base::nullopt, base::nullopt));
TestServiceManagerContext smc;
mojo::Remote<blink::mojom::Authenticator> authenticator =
ConnectToAuthenticator();
TestGetAssertionCallback callback_receiver;
// |SelectAccount| should not be called since this is not a resident key
// request.
test_client_.expected_accounts = "<invalid>";
PublicKeyCredentialRequestOptionsPtr options = get_credential_options();
options->appid = kTestOrigin1;
options->allow_credentials = {device::PublicKeyCredentialDescriptor(
device::CredentialType::kPublicKey,
std::vector<uint8_t>(kTestCredentialIdLength + 1, 0))};
authenticator->GetAssertion(std::move(options), callback_receiver.callback());
callback_receiver.WaitForCallback();
EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR, callback_receiver.status());
}
class InternalAuthenticatorImplTest : public AuthenticatorTestBase { class InternalAuthenticatorImplTest : public AuthenticatorTestBase {
protected: protected:
InternalAuthenticatorImplTest() = default; InternalAuthenticatorImplTest() = default;
......
...@@ -65,6 +65,16 @@ std::vector<uint8_t> AuthenticatorGetInfoResponse::EncodeToCBOR( ...@@ -65,6 +65,16 @@ std::vector<uint8_t> AuthenticatorGetInfoResponse::EncodeToCBOR(
device_info_map.emplace(6, ToArrayValue(*response.pin_protocols)); device_info_map.emplace(6, ToArrayValue(*response.pin_protocols));
} }
if (response.max_credential_count_in_list) {
device_info_map.emplace(
7, base::strict_cast<int64_t>(*response.max_credential_count_in_list));
}
if (response.max_credential_id_length) {
device_info_map.emplace(
8, base::strict_cast<int64_t>(*response.max_credential_id_length));
}
auto encoded_bytes = auto encoded_bytes =
cbor::Writer::Write(cbor::Value(std::move(device_info_map))); cbor::Writer::Write(cbor::Value(std::move(device_info_map)));
DCHECK(encoded_bytes); DCHECK(encoded_bytes);
......
...@@ -37,6 +37,8 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorGetInfoResponse { ...@@ -37,6 +37,8 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorGetInfoResponse {
base::flat_set<ProtocolVersion> versions; base::flat_set<ProtocolVersion> versions;
std::array<uint8_t, kAaguidLength> aaguid; std::array<uint8_t, kAaguidLength> aaguid;
base::Optional<uint32_t> max_msg_size; base::Optional<uint32_t> max_msg_size;
base::Optional<uint32_t> max_credential_count_in_list;
base::Optional<uint32_t> max_credential_id_length;
base::Optional<std::vector<uint8_t>> pin_protocols; base::Optional<std::vector<uint8_t>> pin_protocols;
base::Optional<std::vector<std::string>> extensions; base::Optional<std::vector<std::string>> extensions;
AuthenticatorSupportedOptions options; AuthenticatorSupportedOptions options;
......
...@@ -38,6 +38,15 @@ ProtocolVersion ConvertStringToProtocolVersion(base::StringPiece version) { ...@@ -38,6 +38,15 @@ ProtocolVersion ConvertStringToProtocolVersion(base::StringPiece version) {
return ProtocolVersion::kUnknown; return ProtocolVersion::kUnknown;
} }
// Converts a CBOR unsigned integer value to a uint32_t. The conversion is
// clamped at uint32_max.
uint32_t CBORUnsignedToUint32Safe(const cbor::Value& value) {
DCHECK(value.is_unsigned());
constexpr uint32_t uint32_max = std::numeric_limits<uint32_t>::max();
const int64_t n = value.GetUnsigned();
return n > uint32_max ? uint32_max : n;
}
} // namespace } // namespace
using CBOR = cbor::Value; using CBOR = cbor::Value;
...@@ -337,7 +346,7 @@ base::Optional<AuthenticatorGetInfoResponse> ReadCTAPGetInfoResponse( ...@@ -337,7 +346,7 @@ base::Optional<AuthenticatorGetInfoResponse> ReadCTAPGetInfoResponse(
if (!it->second.is_unsigned()) if (!it->second.is_unsigned())
return base::nullopt; return base::nullopt;
response.max_msg_size = it->second.GetUnsigned(); response.max_msg_size = CBORUnsignedToUint32Safe(it->second);
} }
it = response_map.find(CBOR(6)); it = response_map.find(CBOR(6));
...@@ -355,6 +364,23 @@ base::Optional<AuthenticatorGetInfoResponse> ReadCTAPGetInfoResponse( ...@@ -355,6 +364,23 @@ base::Optional<AuthenticatorGetInfoResponse> ReadCTAPGetInfoResponse(
response.pin_protocols = std::move(supported_pin_protocols); response.pin_protocols = std::move(supported_pin_protocols);
} }
it = response_map.find(CBOR(7));
if (it != response_map.end()) {
if (!it->second.is_unsigned())
return base::nullopt;
response.max_credential_count_in_list =
CBORUnsignedToUint32Safe(it->second);
}
it = response_map.find(CBOR(8));
if (it != response_map.end()) {
if (!it->second.is_unsigned())
return base::nullopt;
response.max_credential_id_length = CBORUnsignedToUint32Safe(it->second);
}
return base::Optional<AuthenticatorGetInfoResponse>(std::move(response)); return base::Optional<AuthenticatorGetInfoResponse>(std::move(response));
} }
......
...@@ -79,49 +79,74 @@ void GetAssertionTask::StartTask() { ...@@ -79,49 +79,74 @@ void GetAssertionTask::StartTask() {
} }
CtapGetAssertionRequest GetAssertionTask::NextSilentRequest() { CtapGetAssertionRequest GetAssertionTask::NextSilentRequest() {
DCHECK(current_credential_ < request_.allow_list.size()); DCHECK(current_allow_list_batch_ < allow_list_batches_.size());
CtapGetAssertionRequest request = request_; CtapGetAssertionRequest request = request_;
request.allow_list = {{request_.allow_list.at(current_credential_)}}; request.allow_list = allow_list_batches_.at(current_allow_list_batch_++);
request.user_presence_required = false; request.user_presence_required = false;
request.user_verification = UserVerificationRequirement::kDiscouraged; request.user_verification = UserVerificationRequirement::kDiscouraged;
return request; return request;
} }
void GetAssertionTask::GetAssertion() { void GetAssertionTask::GetAssertion() {
// Silently probe each credential in the allow list to work around if (request_.allow_list.empty()) {
// authenticators rejecting lists over a certain size. Also probe silently if
// the request may fall back to U2F and the authenticator doesn't recognize
// any of the provided credential IDs.
if (((request_.allow_list.size() > 1 &&
// If the device supports credProtect then it might have UV-required
// credentials which it'll pretend don't exist for silent requests.
// TODO(agl): should support batching of, and filtering over-long,
// credentials based on GetInfo data. Also should support
// PIN-authenticated silent requests.
!device()->device_info()->options.supports_cred_protect) ||
MayFallbackToU2fWithAppIdExtension(*device(), request_)) &&
// caBLE devices might not support silent probing so don't do it with
// them.
device()->DeviceTransport() !=
FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy) {
sign_operation_ = std::make_unique<Ctap2DeviceOperation< sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>( CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), NextSilentRequest(), device(), request_,
base::BindOnce(&GetAssertionTask::HandleResponseToSilentRequest, base::BindOnce(&GetAssertionTask::HandleResponse,
weak_factory_.GetWeakPtr()), weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPGetAssertionResponse), base::BindOnce(&ReadCTAPGetAssertionResponse), StringFixupPredicate);
sign_operation_->Start();
return;
}
// Most authenticators can only process allowList parameters up to a certain
// size. Batch the list into chunks according to what the device can handle
// and filter out IDs that are too large to originate from this device.
allow_list_batches_ =
FilterAndBatchCredentialDescriptors(request_.allow_list, *device());
// If filtering eliminated all entries from the allowList, just collect a
// dummy touch, then fail the request.
if (allow_list_batches_.empty()) {
dummy_register_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>(
device(), MakeCredentialTask::GetTouchRequest(device()),
base::BindOnce(&GetAssertionTask::HandleDummyMakeCredentialComplete,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPMakeCredentialResponse,
device()->DeviceTransport()),
/*string_fixup_predicate=*/nullptr); /*string_fixup_predicate=*/nullptr);
dummy_register_operation_->Start();
return;
}
// If the filtered allowList is small enough to be sent in a single request,
// do so.
if (allow_list_batches_.size() == 1 &&
!MayFallbackToU2fWithAppIdExtension(*device(), request_)) {
CtapGetAssertionRequest request = request_;
request.allow_list = allow_list_batches_.front();
sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), std::move(request),
base::BindOnce(&GetAssertionTask::HandleResponse,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPGetAssertionResponse), StringFixupPredicate);
sign_operation_->Start(); sign_operation_->Start();
return; return;
} }
// If the filtered list is too large to be sent at once, or if an App ID might
// need to be tested because the site used the appid extension, probe the
// credential IDs silently.
sign_operation_ = sign_operation_ =
std::make_unique<Ctap2DeviceOperation<CtapGetAssertionRequest, std::make_unique<Ctap2DeviceOperation<CtapGetAssertionRequest,
AuthenticatorGetAssertionResponse>>( AuthenticatorGetAssertionResponse>>(
device(), request_, device(), NextSilentRequest(),
base::BindOnce(&GetAssertionTask::HandleResponse, base::BindOnce(&GetAssertionTask::HandleResponseToSilentRequest,
weak_factory_.GetWeakPtr()), weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPGetAssertionResponse), StringFixupPredicate); base::BindOnce(&ReadCTAPGetAssertionResponse),
/*string_fixup_predicate=*/nullptr);
sign_operation_->Start(); sign_operation_->Start();
} }
...@@ -164,18 +189,23 @@ void GetAssertionTask::HandleResponseToSilentRequest( ...@@ -164,18 +189,23 @@ void GetAssertionTask::HandleResponseToSilentRequest(
CtapDeviceResponseCode response_code, CtapDeviceResponseCode response_code,
base::Optional<AuthenticatorGetAssertionResponse> response_data) { base::Optional<AuthenticatorGetAssertionResponse> response_data) {
DCHECK(request_.allow_list.size() > 0); DCHECK(request_.allow_list.size() > 0);
DCHECK(allow_list_batches_.size() > 0);
DCHECK(0 < current_allow_list_batch_ &&
current_allow_list_batch_ <= allow_list_batches_.size());
if (canceled_) { if (canceled_) {
return; return;
} }
// Credential was recognized by the device. As this authentication was a // One credential from the previous batch was recognized by the device. As
// silent authentication (i.e. user touch was not provided), try again with // this authentication was a silent authentication (i.e. user touch was not
// only the matching credential, user presence enforced and with the original // provided), try again with only that batch, user presence enforced and with
// user verification configuration. // the original user verification configuration.
// TODO(martinkr): We could get the exact credential ID that was recognized
// from |response_data| and send only that.
if (response_code == CtapDeviceResponseCode::kSuccess) { if (response_code == CtapDeviceResponseCode::kSuccess) {
CtapGetAssertionRequest request = request_; CtapGetAssertionRequest request = request_;
request.allow_list = {{request_.allow_list.at(current_credential_)}}; request.allow_list = allow_list_batches_.at(current_allow_list_batch_ - 1);
sign_operation_ = std::make_unique<Ctap2DeviceOperation< sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>( CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), std::move(request), device(), std::move(request),
...@@ -189,7 +219,7 @@ void GetAssertionTask::HandleResponseToSilentRequest( ...@@ -189,7 +219,7 @@ void GetAssertionTask::HandleResponseToSilentRequest(
// Credential was not recognized or an error occurred. Probe the next // Credential was not recognized or an error occurred. Probe the next
// credential. // credential.
if (++current_credential_ < request_.allow_list.size()) { if (current_allow_list_batch_ < allow_list_batches_.size()) {
sign_operation_ = std::make_unique<Ctap2DeviceOperation< sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>( CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), NextSilentRequest(), device(), NextSilentRequest(),
......
...@@ -84,10 +84,13 @@ class COMPONENT_EXPORT(DEVICE_FIDO) GetAssertionTask : public FidoTask { ...@@ -84,10 +84,13 @@ class COMPONENT_EXPORT(DEVICE_FIDO) GetAssertionTask : public FidoTask {
base::Optional<AuthenticatorMakeCredentialResponse> response_data); base::Optional<AuthenticatorMakeCredentialResponse> response_data);
CtapGetAssertionRequest request_; CtapGetAssertionRequest request_;
std::vector<std::vector<PublicKeyCredentialDescriptor>> allow_list_batches_;
size_t current_allow_list_batch_ = 0;
std::unique_ptr<SignOperation> sign_operation_; std::unique_ptr<SignOperation> sign_operation_;
std::unique_ptr<RegisterOperation> dummy_register_operation_; std::unique_ptr<RegisterOperation> dummy_register_operation_;
GetAssertionTaskCallback callback_; GetAssertionTaskCallback callback_;
size_t current_credential_ = 0;
bool canceled_ = false; bool canceled_ = false;
base::WeakPtrFactory<GetAssertionTask> weak_factory_{this}; base::WeakPtrFactory<GetAssertionTask> weak_factory_{this};
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#include "device/fido/make_credential_task.h" #include "device/fido/make_credential_task.h"
#include <algorithm>
#include <utility> #include <utility>
#include "base/bind.h" #include "base/bind.h"
...@@ -126,42 +127,68 @@ void MakeCredentialTask::StartTask() { ...@@ -126,42 +127,68 @@ void MakeCredentialTask::StartTask() {
} }
} }
CtapGetAssertionRequest MakeCredentialTask::NextSilentSignRequest() { CtapGetAssertionRequest MakeCredentialTask::NextSilentRequest() {
DCHECK(current_credential_ < request_.exclude_list.size()); DCHECK(current_exclude_list_batch_ < exclude_list_batches_.size());
CtapGetAssertionRequest request( CtapGetAssertionRequest request(
probing_alternative_rp_id_ ? *request_.app_id : request_.rp.id, probing_alternative_rp_id_ ? *request_.app_id : request_.rp.id,
/*client_data_json=*/""); /*client_data_json=*/"");
request.allow_list = {{request_.exclude_list.at(current_credential_)}};
request.allow_list = exclude_list_batches_.at(current_exclude_list_batch_);
request.user_presence_required = false; request.user_presence_required = false;
request.user_verification = UserVerificationRequirement::kDiscouraged; request.user_verification = UserVerificationRequirement::kDiscouraged;
return request; return request;
} }
void MakeCredentialTask::MakeCredential() { void MakeCredentialTask::MakeCredential() {
// Silently probe each credential in the allow list to work around DCHECK_EQ(device()->supported_protocol(), ProtocolVersion::kCtap2);
// authenticators rejecting lists over a certain size. Also do this if a
// second RP ID needs to be tested because the site used the appidExclude if (!request_.app_id && request_.exclude_list.empty()) {
// extension. register_operation_ = std::make_unique<Ctap2DeviceOperation<
if (request_.exclude_list.size() > 1 || CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>(
(!request_.exclude_list.empty() && request_.app_id)) { device(), request_, std::move(callback_),
silent_sign_operation_ = std::make_unique<Ctap2DeviceOperation< base::BindOnce(&ReadCTAPMakeCredentialResponse,
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>( device()->DeviceTransport()),
device(), NextSilentSignRequest(),
base::BindOnce(&MakeCredentialTask::HandleResponseToSilentSignRequest,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPGetAssertionResponse),
/*string_fixup_predicate=*/nullptr); /*string_fixup_predicate=*/nullptr);
silent_sign_operation_->Start(); register_operation_->Start();
return; return;
} }
register_operation_ = std::make_unique<Ctap2DeviceOperation< // Most authenticators can only process excludeList parameters up to a certain
CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>( // size. Batch the list into chunks according to what the device can handle
device(), std::move(request_), std::move(callback_), // and filter out IDs that are too large to originate from this device.
base::BindOnce(&ReadCTAPMakeCredentialResponse, exclude_list_batches_ =
device()->DeviceTransport()), FilterAndBatchCredentialDescriptors(request_.exclude_list, *device());
/*string_fixup_predicate=*/nullptr);
register_operation_->Start(); // If the filtered excludeList is small enough to be sent in a single request,
// do so. (Note that the list may be empty now, even if it wasn't previously,
// due to filtering.)
if (!request_.app_id && exclude_list_batches_.size() <= 1) {
auto request = request_;
request.exclude_list = exclude_list_batches_.empty()
? std::vector<PublicKeyCredentialDescriptor>{}
: exclude_list_batches_.front();
register_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>(
device(), std::move(request), std::move(callback_),
base::BindOnce(&ReadCTAPMakeCredentialResponse,
device()->DeviceTransport()),
/*string_fixup_predicate=*/nullptr);
register_operation_->Start();
return;
}
// If the filtered list is too large to be sent at once, or if an App ID might
// need to be tested because the site used the appidExclude extension, probe
// the credential IDs silently.
silent_sign_operation_ =
std::make_unique<Ctap2DeviceOperation<CtapGetAssertionRequest,
AuthenticatorGetAssertionResponse>>(
device(), NextSilentRequest(),
base::BindOnce(&MakeCredentialTask::HandleResponseToSilentSignRequest,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPGetAssertionResponse),
/*string_fixup_predicate=*/nullptr);
silent_sign_operation_->Start();
} }
void MakeCredentialTask::HandleResponseToSilentSignRequest( void MakeCredentialTask::HandleResponseToSilentSignRequest(
...@@ -173,12 +200,13 @@ void MakeCredentialTask::HandleResponseToSilentSignRequest( ...@@ -173,12 +200,13 @@ void MakeCredentialTask::HandleResponseToSilentSignRequest(
return; return;
} }
// The authenticator recognized a credential from the exclude list. Send the // The authenticator recognized a credential from previous exclude list batch.
// actual request with only that credential in the exclude list to collect a // Send the actual request with only that exclude list batch to collect a
// touch and and the CTAP2_ERR_CREDENTIAL_EXCLUDED error code. // touch and and the CTAP2_ERR_CREDENTIAL_EXCLUDED error code.
if (response_code == CtapDeviceResponseCode::kSuccess) { if (response_code == CtapDeviceResponseCode::kSuccess) {
CtapMakeCredentialRequest request = request_; CtapMakeCredentialRequest request = request_;
request.exclude_list = {{request_.exclude_list.at(current_credential_)}}; request.exclude_list =
exclude_list_batches_.at(current_exclude_list_batch_);
if (probing_alternative_rp_id_) { if (probing_alternative_rp_id_) {
request.rp.id = *request_.app_id; request.rp.id = *request_.app_id;
} }
...@@ -210,21 +238,22 @@ void MakeCredentialTask::HandleResponseToSilentSignRequest( ...@@ -210,21 +238,22 @@ void MakeCredentialTask::HandleResponseToSilentSignRequest(
return; return;
} }
// The authenticator doesn't recognize this particular credential from the // The authenticator didn't recognize any credential from the previous exclude
// exclude list. Try the next one. // list batch. Try the next batch, if there is one.
current_credential_++; current_exclude_list_batch_++;
if (current_credential_ == request_.exclude_list.size() &&
if (current_exclude_list_batch_ == exclude_list_batches_.size() &&
!probing_alternative_rp_id_ && request_.app_id) { !probing_alternative_rp_id_ && request_.app_id) {
// All elements of |request_.exclude_list| have been tested, but there's a // All elements of |request_.exclude_list| have been tested, but there's a
// second RP ID so they need to be tested again. // second RP ID so they need to be tested again.
current_credential_ = 0;
probing_alternative_rp_id_ = true; probing_alternative_rp_id_ = true;
current_exclude_list_batch_ = 0;
} }
if (current_credential_ < request_.exclude_list.size()) { if (current_exclude_list_batch_ < exclude_list_batches_.size()) {
silent_sign_operation_ = std::make_unique<Ctap2DeviceOperation< silent_sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>( CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), NextSilentSignRequest(), device(), NextSilentRequest(),
base::BindOnce(&MakeCredentialTask::HandleResponseToSilentSignRequest, base::BindOnce(&MakeCredentialTask::HandleResponseToSilentSignRequest,
weak_factory_.GetWeakPtr()), weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPGetAssertionResponse), base::BindOnce(&ReadCTAPGetAssertionResponse),
...@@ -283,4 +312,49 @@ void MakeCredentialTask::MaybeRevertU2fFallback( ...@@ -283,4 +312,49 @@ void MakeCredentialTask::MaybeRevertU2fFallback(
std::move(callback_).Run(status, std::move(response)); std::move(callback_).Run(status, std::move(response));
} }
std::vector<std::vector<PublicKeyCredentialDescriptor>>
FilterAndBatchCredentialDescriptors(
const std::vector<PublicKeyCredentialDescriptor>& in,
const FidoDevice& device) {
DCHECK(!in.empty());
DCHECK_EQ(device.supported_protocol(), ProtocolVersion::kCtap2);
DCHECK(device.device_info().has_value());
if (device.DeviceTransport() ==
FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy) {
// caBLE devices might not support silent probing, so just put everything
// into one batch that can will be sent in a non-probing request.
return {in};
}
const auto& device_info = *device.device_info();
// Note that |max_credential_id_length| of 0 is interpreted as unbounded.
size_t max_credential_id_length =
device_info.max_credential_id_length.value_or(0);
// Protect against devices that claim to have a maximum list length of 0, or
// to know the maximum list length but not know the maximum size of an
// individual credential ID.
size_t max_credential_count_in_list =
max_credential_id_length > 0
? std::max(device_info.max_credential_count_in_list.value_or(1), 1u)
: 1;
std::vector<std::vector<PublicKeyCredentialDescriptor>> result;
for (const PublicKeyCredentialDescriptor& credential : in) {
if (0 < max_credential_id_length &&
max_credential_id_length < credential.id().size()) {
continue;
}
if (result.empty() ||
result.back().size() == max_credential_count_in_list) {
result.emplace_back();
}
result.back().push_back(credential);
}
return result;
}
} // namespace device } // namespace device
...@@ -55,7 +55,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) MakeCredentialTask : public FidoTask { ...@@ -55,7 +55,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) MakeCredentialTask : public FidoTask {
void StartTask() final; void StartTask() final;
void MakeCredential(); void MakeCredential();
CtapGetAssertionRequest NextSilentSignRequest(); CtapGetAssertionRequest NextSilentRequest();
void HandleResponseToSilentSignRequest( void HandleResponseToSilentSignRequest(
CtapDeviceResponseCode response_code, CtapDeviceResponseCode response_code,
base::Optional<AuthenticatorGetAssertionResponse> response_data); base::Optional<AuthenticatorGetAssertionResponse> response_data);
...@@ -69,10 +69,13 @@ class COMPONENT_EXPORT(DEVICE_FIDO) MakeCredentialTask : public FidoTask { ...@@ -69,10 +69,13 @@ class COMPONENT_EXPORT(DEVICE_FIDO) MakeCredentialTask : public FidoTask {
base::Optional<AuthenticatorMakeCredentialResponse> response); base::Optional<AuthenticatorMakeCredentialResponse> response);
CtapMakeCredentialRequest request_; CtapMakeCredentialRequest request_;
std::vector<std::vector<PublicKeyCredentialDescriptor>> exclude_list_batches_;
size_t current_exclude_list_batch_ = 0;
std::unique_ptr<RegisterOperation> register_operation_; std::unique_ptr<RegisterOperation> register_operation_;
std::unique_ptr<SignOperation> silent_sign_operation_; std::unique_ptr<SignOperation> silent_sign_operation_;
MakeCredentialTaskCallback callback_; MakeCredentialTaskCallback callback_;
size_t current_credential_ = 0;
// probing_alternative_rp_id_ is true if |app_id| is set in |request_| and // probing_alternative_rp_id_ is true if |app_id| is set in |request_| and
// thus the exclude list is being probed a second time with the alternative RP // thus the exclude list is being probed a second time with the alternative RP
// ID. // ID.
...@@ -84,6 +87,21 @@ class COMPONENT_EXPORT(DEVICE_FIDO) MakeCredentialTask : public FidoTask { ...@@ -84,6 +87,21 @@ class COMPONENT_EXPORT(DEVICE_FIDO) MakeCredentialTask : public FidoTask {
DISALLOW_COPY_AND_ASSIGN(MakeCredentialTask); DISALLOW_COPY_AND_ASSIGN(MakeCredentialTask);
}; };
// FilterAndBatchCredentialDescriptors splits a list of
// PublicKeyCredentialDescriptors such that each chunk is guaranteed to fit into
// an allowList parameter of a GetAssertion request for the given |device|.
//
// |device| must be a fully initialized CTAP2 device, i.e. its device_info()
// method must return an AuthenticatorGetInfoResponse.
//
// If |in| contains only credential descriptors with IDs longer than the
// device's |max_credential_id_length|, the result will be empty (rather than
// containing a single empty vector).
std::vector<std::vector<PublicKeyCredentialDescriptor>>
FilterAndBatchCredentialDescriptors(
const std::vector<PublicKeyCredentialDescriptor>& in,
const FidoDevice& device);
} // namespace device } // namespace device
#endif // DEVICE_FIDO_MAKE_CREDENTIAL_TASK_H_ #endif // DEVICE_FIDO_MAKE_CREDENTIAL_TASK_H_
...@@ -603,6 +603,15 @@ VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state, ...@@ -603,6 +603,15 @@ VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state,
device_info_->extensions.emplace( device_info_->extensions.emplace(
{std::string(device::kExtensionCredProtect)}); {std::string(device::kExtensionCredProtect)});
} }
if (config.max_credential_count_in_list > 0) {
device_info_->max_credential_count_in_list =
config.max_credential_count_in_list;
}
if (config.max_credential_id_length > 0) {
device_info_->max_credential_id_length = config.max_credential_id_length;
}
} }
VirtualCtap2Device::~VirtualCtap2Device() = default; VirtualCtap2Device::~VirtualCtap2Device() = default;
...@@ -722,28 +731,32 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnMakeCredential( ...@@ -722,28 +731,32 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnMakeCredential(
// 6. Check for already registered credentials. // 6. Check for already registered credentials.
const auto rp_id_hash = fido_parsing_utils::CreateSHA256Hash(request.rp.id); const auto rp_id_hash = fido_parsing_utils::CreateSHA256Hash(request.rp.id);
if (!request.exclude_list.empty()) { if ((config_.reject_large_allow_and_exclude_lists &&
if (config_.reject_large_allow_and_exclude_lists && request.exclude_list.size() > 1) ||
request.exclude_list.size() > 1) { (config_.max_credential_count_in_list &&
request.exclude_list.size() > config_.max_credential_count_in_list)) {
return CtapDeviceResponseCode::kCtap2ErrLimitExceeded;
}
for (const auto& excluded_credential : request.exclude_list) {
if (0 < config_.max_credential_id_length &&
config_.max_credential_id_length < excluded_credential.id().size()) {
return CtapDeviceResponseCode::kCtap2ErrLimitExceeded; return CtapDeviceResponseCode::kCtap2ErrLimitExceeded;
} }
const RegistrationData* found =
for (const auto& excluded_credential : request.exclude_list) { FindRegistrationData(excluded_credential.id(), rp_id_hash);
const RegistrationData* found = if (found) {
FindRegistrationData(excluded_credential.id(), rp_id_hash); if (found->protection == device::CredProtect::kUVRequired &&
if (found) { !user_verified) {
if (found->protection == device::CredProtect::kUVRequired && // Cannot disclose the existence of this credential without UV. If
!user_verified) { // a credentials ends up being created it'll overwrite this one.
// Cannot disclose the existence of this credential without UV. If continue;
// a credentials ends up being created it'll overwrite this one.
continue;
}
if (mutable_state()->simulate_press_callback &&
!mutable_state()->simulate_press_callback.Run(this)) {
return base::nullopt;
}
return CtapDeviceResponseCode::kCtap2ErrCredentialExcluded;
} }
if (mutable_state()->simulate_press_callback &&
!mutable_state()->simulate_press_callback.Run(this)) {
return base::nullopt;
}
return CtapDeviceResponseCode::kCtap2ErrCredentialExcluded;
} }
} }
...@@ -903,7 +916,6 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnGetAssertion( ...@@ -903,7 +916,6 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnGetAssertion(
return uv_error; return uv_error;
} }
// Resident keys are not supported.
if (!config_.resident_key_support && request.allow_list.empty()) { if (!config_.resident_key_support && request.allow_list.empty()) {
return CtapDeviceResponseCode::kCtap2ErrNoCredentials; return CtapDeviceResponseCode::kCtap2ErrNoCredentials;
} }
...@@ -918,8 +930,10 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnGetAssertion( ...@@ -918,8 +930,10 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnGetAssertion(
return CtapDeviceResponseCode::kCtap2ErrUnsupportedOption; return CtapDeviceResponseCode::kCtap2ErrUnsupportedOption;
} }
if (config_.reject_large_allow_and_exclude_lists && if ((config_.reject_large_allow_and_exclude_lists &&
request.allow_list.size() > 1) { request.allow_list.size() > 1) ||
(config_.max_credential_count_in_list &&
request.allow_list.size() > config_.max_credential_count_in_list)) {
return CtapDeviceResponseCode::kCtap2ErrLimitExceeded; return CtapDeviceResponseCode::kCtap2ErrLimitExceeded;
} }
...@@ -928,6 +942,10 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnGetAssertion( ...@@ -928,6 +942,10 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnGetAssertion(
// mirrors that to better reflect reality. CTAP 2.0 leaves it as undefined // mirrors that to better reflect reality. CTAP 2.0 leaves it as undefined
// behaviour. // behaviour.
for (const auto& allowed_credential : request.allow_list) { for (const auto& allowed_credential : request.allow_list) {
if (0 < config_.max_credential_id_length &&
config_.max_credential_id_length < allowed_credential.id().size()) {
return CtapDeviceResponseCode::kCtap2ErrLimitExceeded;
}
RegistrationData* found = RegistrationData* found =
FindRegistrationData(allowed_credential.id(), rp_id_hash); FindRegistrationData(allowed_credential.id(), rp_id_hash);
if (found) { if (found) {
......
...@@ -51,6 +51,20 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualCtap2Device ...@@ -51,6 +51,20 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualCtap2Device
uint8_t bio_enrollment_samples_required = 4; uint8_t bio_enrollment_samples_required = 4;
bool cred_protect_support = false; bool cred_protect_support = false;
// max_credential_count_in_list, if non-zero, is the value to return for
// maxCredentialCountInList in the authenticatorGetInfo reponse.
// CTAP2_ERR_LIMIT_EXCEEDED will be returned for requests with an allow or
// exclude list exceeding this limit. Note that the request handler
// implementations require maxCredentialIdLength be set in order for
// maxCredentialCountInList to be respected.
uint32_t max_credential_count_in_list = 0;
// max_credential_id_length, if non-zero, is the value to return for
// maxCredentialIdLength in the authenticatorGetInfo reponse.
// CTAP2_ERR_LIMIT_EXCEEDED will be returned for requests with an allow or
// exclude list containing a credential ID exceeding this limit.
uint32_t max_credential_id_length = 0;
// resident_credential_storage is the number of resident credentials that // resident_credential_storage is the number of resident credentials that
// the device will store before returning KEY_STORE_FULL. // the device will store before returning KEY_STORE_FULL.
size_t resident_credential_storage = 3; size_t resident_credential_storage = 3;
...@@ -68,7 +82,9 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualCtap2Device ...@@ -68,7 +82,9 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualCtap2Device
// reject_large_allow_and_exclude_lists causes the authenticator to respond // reject_large_allow_and_exclude_lists causes the authenticator to respond
// with an error if an allowList or an excludeList contains more than one // with an error if an allowList or an excludeList contains more than one
// credential ID. // credential ID. This can be used to simulate errors with oversized
// credential lists in an authenticator that does not support batching (i.e.
// maxCredentialCountInList and maxCredentialIdSize).
bool reject_large_allow_and_exclude_lists = false; bool reject_large_allow_and_exclude_lists = false;
// reject_silent_authenticator_requests causes the authenticator to return // reject_silent_authenticator_requests causes the authenticator to return
......
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