123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- // Copyright 2018 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
- #include "device/fido/make_credential_task.h"
- #include <algorithm>
- #include <utility>
- #include "base/bind.h"
- #include "base/containers/contains.h"
- #include "device/base/features.h"
- #include "device/fido/ctap2_device_operation.h"
- #include "device/fido/ctap_make_credential_request.h"
- #include "device/fido/pin.h"
- #include "device/fido/u2f_command_constructor.h"
- #include "device/fido/u2f_register_operation.h"
- namespace device {
- namespace {
- // CTAP 2.0 specifies[1] that once a PIN has been set on an authenticator, the
- // PIN is required in order to make a credential. In some cases we don't want to
- // prompt for a PIN and so use U2F to make the credential instead.
- //
- // [1]
- // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential,
- // step 6
- bool CtapDeviceShouldUseU2fBecauseClientPinIsSet(
- const FidoDevice* device,
- const CtapMakeCredentialRequest& request) {
- if (!IsConvertibleToU2fRegisterCommand(request) ||
- ShouldPreferCTAP2EvenIfItNeedsAPIN(request)) {
- return false;
- }
- DCHECK_EQ(device->supported_protocol(), ProtocolVersion::kCtap2);
- // No need to fall back to U2F if CTAP2 registrations don't require UV.
- if (device->device_info()->options.make_cred_uv_not_required) {
- return false;
- }
- // Don't use U2F for requests that require UV or PIN which U2F doesn't
- // support. Note that |pin_auth| may also be set by GetTouchRequest(), but we
- // don't want those requests to use U2F either if CTAP is supported.
- if (request.user_verification == UserVerificationRequirement::kRequired ||
- request.pin_auth) {
- return false;
- }
- DCHECK(device && device->device_info());
- bool client_pin_set =
- device->device_info()->options.client_pin_availability ==
- AuthenticatorSupportedOptions::ClientPinAvailability::kSupportedAndPinSet;
- bool supports_u2f =
- base::Contains(device->device_info()->versions, ProtocolVersion::kU2f);
- return client_pin_set && supports_u2f;
- }
- // ConvertCTAPResponse returns the AuthenticatorMakeCredentialResponse for a
- // given CTAP response message in |cbor|. It wraps
- // ReadCTAPMakeCredentialResponse() and in addition fills in |is_resident_key|,
- // which requires looking at the request and device.
- absl::optional<AuthenticatorMakeCredentialResponse> ConvertCTAPResponse(
- FidoDevice* device,
- bool resident_key_required,
- const absl::optional<cbor::Value>& cbor) {
- DCHECK_EQ(device->supported_protocol(), ProtocolVersion::kCtap2);
- DCHECK(device->device_info());
- absl::optional<AuthenticatorMakeCredentialResponse> response =
- ReadCTAPMakeCredentialResponse(device->DeviceTransport(), cbor);
- if (!response) {
- return absl::nullopt;
- }
- // Fill in whether the created credential is client-side discoverable
- // (resident). CTAP 2.0 authenticators may decide to treat all credentials as
- // discoverable, so we need to omit the value unless a resident key was
- // required.
- DCHECK(!response->is_resident_key.has_value());
- if (resident_key_required) {
- response->is_resident_key = true;
- } else {
- const bool resident_key_supported =
- device->device_info()->options.supports_resident_key;
- const base::flat_set<Ctap2Version>& ctap2_versions =
- device->device_info()->ctap2_versions;
- DCHECK(!ctap2_versions.empty());
- const bool is_at_least_ctap2_1 =
- std::any_of(ctap2_versions.begin(), ctap2_versions.end(),
- [](Ctap2Version v) { return v > Ctap2Version::kCtap2_0; });
- if (!resident_key_supported || is_at_least_ctap2_1) {
- response->is_resident_key = false;
- }
- }
- if (device->device_info() && device->device_info()->transports) {
- response->transports = *device->device_info()->transports;
- }
- return response;
- }
- } // namespace
- MakeCredentialTask::MakeCredentialTask(FidoDevice* device,
- CtapMakeCredentialRequest request,
- MakeCredentialOptions options,
- MakeCredentialTaskCallback callback)
- : FidoTask(device),
- request_(std::move(request)),
- options_(std::move(options)),
- callback_(std::move(callback)) {
- // The UV parameter should have been made binary by this point because CTAP2
- // only takes a binary value.
- DCHECK_NE(request_.user_verification,
- UserVerificationRequirement::kPreferred);
- }
- MakeCredentialTask::~MakeCredentialTask() = default;
- // static
- CtapMakeCredentialRequest MakeCredentialTask::GetTouchRequest(
- const FidoDevice* device) {
- // We want to flash and wait for a touch. Newer versions of the CTAP2 spec
- // include a provision for blocking for a touch when an empty pinAuth is
- // specified, but devices exist that predate this part of the spec and also
- // the spec says that devices need only do that if they implement PIN support.
- // Therefore, in order to portably wait for a touch, a dummy credential is
- // created. This does assume that the device supports ECDSA P-256, however.
- PublicKeyCredentialUserEntity user({1} /* user ID */);
- // The user name is incorrectly marked as optional in the CTAP2 spec.
- user.name = "dummy";
- CtapMakeCredentialRequest req(
- "" /* client_data_json */, PublicKeyCredentialRpEntity(kDummyRpID),
- std::move(user),
- PublicKeyCredentialParams(
- {{CredentialType::kPublicKey,
- base::strict_cast<int>(CoseAlgorithmIdentifier::kEs256)}}));
- // If a device supports CTAP2 and has PIN support then setting an empty
- // pinAuth should trigger just a touch[1]. Our U2F code also understands
- // this convention.
- // [1]
- // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#using-pinToken-in-authenticatorGetAssertion
- if (device->supported_protocol() == ProtocolVersion::kU2f ||
- (device->device_info() &&
- device->device_info()->options.client_pin_availability !=
- AuthenticatorSupportedOptions::ClientPinAvailability::
- kNotSupported)) {
- req.pin_auth.emplace();
- req.pin_protocol = PINUVAuthProtocol::kV1;
- }
- DCHECK(IsConvertibleToU2fRegisterCommand(req));
- return req;
- }
- // static
- bool MakeCredentialTask::WillUseCTAP2(const FidoDevice* device,
- const CtapMakeCredentialRequest& request,
- const MakeCredentialOptions& options) {
- return device->supported_protocol() == ProtocolVersion::kCtap2 &&
- !options.make_u2f_api_credential &&
- !CtapDeviceShouldUseU2fBecauseClientPinIsSet(device, request);
- }
- void MakeCredentialTask::Cancel() {
- canceled_ = true;
- if (register_operation_) {
- register_operation_->Cancel();
- }
- if (silent_sign_operation_) {
- silent_sign_operation_->Cancel();
- }
- }
- void MakeCredentialTask::StartTask() {
- if (WillUseCTAP2(device(), request_, options_)) {
- MakeCredential();
- } else {
- // |device_info| should be present iff the device is CTAP2. This will be
- // used in |MaybeRevertU2fFallback| to restore the protocol of CTAP2 devices
- // once this task is complete.
- DCHECK_EQ(device()->supported_protocol() == ProtocolVersion::kCtap2,
- device()->device_info().has_value());
- device()->set_supported_protocol(ProtocolVersion::kU2f);
- U2fRegister();
- }
- }
- CtapGetAssertionRequest MakeCredentialTask::NextSilentRequest() {
- DCHECK(current_exclude_list_batch_ < exclude_list_batches_.size());
- CtapGetAssertionRequest request(request_.rp.id,
- /*client_data_json=*/"");
- request.allow_list = exclude_list_batches_.at(current_exclude_list_batch_);
- request.user_presence_required = false;
- request.user_verification = UserVerificationRequirement::kDiscouraged;
- // If a pinUvAuthToken was obtained for the original request, the silent
- // requests should carry one as well. This is to ensure that excluded
- // credentials with credProtect-level uvRequired can be matched.
- DCHECK_EQ(request_.pin_auth.has_value(),
- request_.pin_token_for_exclude_list_probing.has_value());
- if (request_.pin_token_for_exclude_list_probing) {
- std::tie(request.pin_protocol, request.pin_auth) =
- request_.pin_token_for_exclude_list_probing->PinAuth(
- request.client_data_hash);
- }
- return request;
- }
- void MakeCredentialTask::MakeCredential() {
- DCHECK_EQ(device()->supported_protocol(), ProtocolVersion::kCtap2);
- // Most authenticators can only process excludeList 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.
- exclude_list_batches_ =
- FilterAndBatchCredentialDescriptors(request_.exclude_list, *device());
- DCHECK(!exclude_list_batches_.empty());
- // If the filtered excludeList is small enough to be sent in a single request,
- // do so. (Note that the exclude list may be empty now, even if it wasn't
- // previously, due to filtering.)
- if (exclude_list_batches_.size() == 1 || device()->NoSilentRequests()) {
- auto request = request_;
- request.exclude_list = exclude_list_batches_.front();
- register_operation_ = std::make_unique<Ctap2DeviceOperation<
- CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>(
- device(), std::move(request), std::move(callback_),
- base::BindOnce(&ConvertCTAPResponse, device(),
- request_.resident_key_required),
- /*string_fixup_predicate=*/nullptr);
- register_operation_->Start();
- return;
- }
- // If the filtered list is too large to be sent at once then 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,
- device()->DeviceTransport()),
- /*string_fixup_predicate=*/nullptr);
- silent_sign_operation_->Start();
- }
- void MakeCredentialTask::HandleResponseToSilentSignRequest(
- CtapDeviceResponseCode response_code,
- absl::optional<AuthenticatorGetAssertionResponse> response_data) {
- if (canceled_) {
- return;
- }
- // The authenticator recognized a credential from previous exclude list batch.
- // Send the actual request with only that exclude list batch to collect a
- // touch and and the CTAP2_ERR_CREDENTIAL_EXCLUDED error code.
- if (response_code == CtapDeviceResponseCode::kSuccess) {
- CtapMakeCredentialRequest request = request_;
- request.exclude_list =
- exclude_list_batches_.at(current_exclude_list_batch_);
- register_operation_ = std::make_unique<Ctap2DeviceOperation<
- CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>(
- device(), std::move(request), std::move(callback_),
- base::BindOnce(&ConvertCTAPResponse, device(),
- request_.resident_key_required),
- /*string_fixup_predicate=*/nullptr);
- register_operation_->Start();
- return;
- }
- // The authenticator returned an unexpected error. Collect a touch to take the
- // authenticator out of the set of active devices.
- if (!FidoDevice::IsStatusForUnrecognisedCredentialID(response_code)) {
- register_operation_ = std::make_unique<Ctap2DeviceOperation<
- CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>(
- device(), GetTouchRequest(device()),
- base::BindOnce(&MakeCredentialTask::HandleResponseToDummyTouch,
- weak_factory_.GetWeakPtr()),
- base::BindOnce(&ConvertCTAPResponse, device(),
- /*resident_key_required=*/false),
- /*string_fixup_predicate=*/nullptr);
- register_operation_->Start();
- return;
- }
- // The authenticator didn't recognize any credential from the previous exclude
- // list batch. Try the next batch, if there is one.
- current_exclude_list_batch_++;
- if (current_exclude_list_batch_ < exclude_list_batches_.size()) {
- silent_sign_operation_ = std::make_unique<Ctap2DeviceOperation<
- CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
- device(), NextSilentRequest(),
- base::BindOnce(&MakeCredentialTask::HandleResponseToSilentSignRequest,
- weak_factory_.GetWeakPtr()),
- base::BindOnce(&ReadCTAPGetAssertionResponse,
- device()->DeviceTransport()),
- /*string_fixup_predicate=*/nullptr);
- silent_sign_operation_->Start();
- return;
- }
- // None of the credentials from the exclude list were recognized. The actual
- // register request may proceed but without the exclude list present in case
- // it exceeds the device's size limit.
- CtapMakeCredentialRequest request = request_;
- request.exclude_list = {};
- register_operation_ = std::make_unique<Ctap2DeviceOperation<
- CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>(
- device(), std::move(request), std::move(callback_),
- base::BindOnce(&ConvertCTAPResponse, device(),
- request_.resident_key_required),
- /*string_fixup_predicate=*/nullptr);
- register_operation_->Start();
- }
- void MakeCredentialTask::HandleResponseToDummyTouch(
- CtapDeviceResponseCode response_code,
- absl::optional<AuthenticatorMakeCredentialResponse> response_data) {
- std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrOther,
- absl::nullopt);
- }
- void MakeCredentialTask::U2fRegister() {
- if (!IsConvertibleToU2fRegisterCommand(request_)) {
- std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrOther,
- absl::nullopt);
- return;
- }
- DCHECK_EQ(ProtocolVersion::kU2f, device()->supported_protocol());
- register_operation_ = std::make_unique<U2fRegisterOperation>(
- device(), std::move(request_),
- base::BindOnce(&MakeCredentialTask::MaybeRevertU2fFallback,
- weak_factory_.GetWeakPtr()));
- register_operation_->Start();
- }
- void MakeCredentialTask::MaybeRevertU2fFallback(
- CtapDeviceResponseCode status,
- absl::optional<AuthenticatorMakeCredentialResponse> response) {
- DCHECK_EQ(ProtocolVersion::kU2f, device()->supported_protocol());
- if (device()->device_info()) {
- // This was actually a CTAP2 device, but the protocol version was set to U2F
- // because it had a PIN set and so, in order to make a credential, the U2F
- // interface was used.
- device()->set_supported_protocol(ProtocolVersion::kCtap2);
- }
- DCHECK(!response || *response->is_resident_key == false);
- std::move(callback_).Run(status, std::move(response));
- }
- std::vector<std::vector<PublicKeyCredentialDescriptor>>
- FilterAndBatchCredentialDescriptors(
- const std::vector<PublicKeyCredentialDescriptor>& in,
- const FidoDevice& device) {
- DCHECK_EQ(device.supported_protocol(), ProtocolVersion::kCtap2);
- DCHECK(device.device_info().has_value());
- if (device.NoSilentRequests()) {
- // 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;
- result.emplace_back();
- for (const PublicKeyCredentialDescriptor& credential : in) {
- if (0 < max_credential_id_length &&
- max_credential_id_length < credential.id.size()) {
- continue;
- }
- if (result.back().size() == max_credential_count_in_list) {
- result.emplace_back();
- }
- result.back().push_back(credential);
- }
- return result;
- }
- } // namespace device
|