| // 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 "device/base/features.h" |
| #include "device/fido/ctap2_device_operation.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); |
| // 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. |
| base::Optional<AuthenticatorMakeCredentialResponse> ConvertCTAPResponse( |
| FidoDevice* device, |
| bool resident_key_required, |
| const base::Optional<cbor::Value>& cbor) { |
| DCHECK_EQ(device->supported_protocol(), ProtocolVersion::kCtap2); |
| DCHECK(device->device_info()); |
| |
| base::Optional<AuthenticatorMakeCredentialResponse> response = |
| ReadCTAPMakeCredentialResponse(device->DeviceTransport(), cbor); |
| if (!response) { |
| return base::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; |
| } |
| } |
| |
| return response; |
| } |
| |
| } // namespace |
| |
| MakeCredentialTask::MakeCredentialTask(FidoDevice* device, |
| CtapMakeCredentialRequest request, |
| MakeCredentialTaskCallback callback) |
| : FidoTask(device), |
| request_(std::move(request)), |
| 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; |
| } |
| |
| void MakeCredentialTask::Cancel() { |
| canceled_ = true; |
| |
| if (register_operation_) { |
| register_operation_->Cancel(); |
| } |
| if (silent_sign_operation_) { |
| silent_sign_operation_->Cancel(); |
| } |
| } |
| |
| void MakeCredentialTask::StartTask() { |
| if (device()->supported_protocol() == ProtocolVersion::kCtap2 && |
| !request_.is_u2f_only && |
| !CtapDeviceShouldUseU2fBecauseClientPinIsSet(device(), request_)) { |
| 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( |
| probing_alternative_rp_id_ ? *request_.app_id : 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; |
| 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.) |
| // |
| // Handling appidExclude requires that the |HandleResponseToSilentSignRequest| |
| // path be used below, so this is only valid if either there's no |
| // appidExclude, or the single batch is empty and thus there are no excluded |
| // credentials. |
| if (exclude_list_batches_.size() == 1 && |
| (!request_.app_id || exclude_list_batches_.front().empty())) { |
| 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, 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( |
| CtapDeviceResponseCode response_code, |
| base::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_); |
| if (probing_alternative_rp_id_) { |
| request.rp.id = *request_.app_id; |
| } |
| 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 (response_code != CtapDeviceResponseCode::kCtap2ErrInvalidCredential && |
| response_code != CtapDeviceResponseCode::kCtap2ErrNoCredentials && |
| response_code != CtapDeviceResponseCode::kCtap2ErrLimitExceeded && |
| response_code != CtapDeviceResponseCode::kCtap2ErrRequestTooLarge) { |
| 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() && |
| !probing_alternative_rp_id_ && request_.app_id) { |
| // All elements of |request_.exclude_list| have been tested, but there's a |
| // second RP ID so they need to be tested again. |
| probing_alternative_rp_id_ = true; |
| current_exclude_list_batch_ = 0; |
| } |
| |
| 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), |
| /*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, |
| base::Optional<AuthenticatorMakeCredentialResponse> response_data) { |
| std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrOther, |
| base::nullopt); |
| } |
| |
| void MakeCredentialTask::U2fRegister() { |
| if (!IsConvertibleToU2fRegisterCommand(request_)) { |
| std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrOther, |
| base::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, |
| base::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()); |
| |
| const auto transport = device.DeviceTransport(); |
| if (transport == FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy || |
| transport == FidoTransportProtocol::kAndroidAccessory) { |
| // 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 |