blob: a8e4622543d2e1dbfccea94cdd34b01fb77a18a0 [file] [log] [blame]
// 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/get_assertion_task.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "device/base/features.h"
#include "device/fido/authenticator_get_assertion_response.h"
#include "device/fido/ctap2_device_operation.h"
#include "device/fido/make_credential_task.h"
#include "device/fido/pin.h"
#include "device/fido/u2f_sign_operation.h"
namespace device {
namespace {
bool MayFallbackToU2fWithAppIdExtension(
const FidoDevice& device,
const CtapGetAssertionRequest& request) {
bool ctap2_device_supports_u2f =
device.device_info() &&
base::Contains(device.device_info()->versions, ProtocolVersion::kU2f);
return request.alternative_application_parameter &&
ctap2_device_supports_u2f && !request.allow_list.empty();
}
// SetResponseCredential sets the credential information in |response|. If the
// allow list sent to the authenticator contained only a single entry then the
// authenticator may omit the chosen credential in the response and this
// function will fill it in. Otherwise, the credential chosen by the
// authenticator must be one of the ones requested in the allow list, unless the
// allow list was empty.
bool SetResponseCredential(
AuthenticatorGetAssertionResponse* response,
const std::vector<PublicKeyCredentialDescriptor>& allow_list) {
if (response->credential()) {
if (!allow_list.empty() &&
std::none_of(allow_list.cbegin(), allow_list.cend(),
[&response](const auto& credential) {
return credential.id() == response->raw_credential_id();
})) {
return false;
}
return true;
}
if (allow_list.size() != 1) {
return false;
}
response->SetCredential(allow_list[0]);
return true;
}
// HasCredentialSpecificPRFInputs returns true if |options| specifies any PRF
// inputs that are specific to a credential ID.
bool HasCredentialSpecificPRFInputs(const CtapGetAssertionOptions& options) {
const size_t num = options.prf_inputs.size();
return num > 1 ||
(num == 1 && options.prf_inputs[0].credential_id.has_value());
}
// GetDefaultPRFInput returns the default PRF input from |options|, if any.
const CtapGetAssertionOptions::PRFInput* GetDefaultPRFInput(
const CtapGetAssertionOptions& options) {
if (options.prf_inputs.empty() ||
options.prf_inputs[0].credential_id.has_value()) {
return nullptr;
}
return &options.prf_inputs[0];
}
// GetPRFInputForCredential returns the PRF input specific to the given
// credential ID from |options|, or the default PRF input if there's nothing
// specific for |id|, or |nullptr| if there's not a default value.
const CtapGetAssertionOptions::PRFInput* GetPRFInputForCredential(
const CtapGetAssertionOptions& options,
const std::vector<uint8_t>& id) {
for (const auto& prf_input : options.prf_inputs) {
if (prf_input.credential_id == id) {
return &prf_input;
}
}
return GetDefaultPRFInput(options);
}
} // namespace
GetAssertionTask::GetAssertionTask(FidoDevice* device,
CtapGetAssertionRequest request,
CtapGetAssertionOptions options,
GetAssertionTaskCallback callback)
: FidoTask(device),
request_(std::move(request)),
options_(std::move(options)),
callback_(std::move(callback)) {
// This code assumes that user-presence is requested in order to implement
// possible U2F-fallback.
DCHECK(request_.user_presence_required);
// 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);
}
GetAssertionTask::~GetAssertionTask() = default;
void GetAssertionTask::Cancel() {
canceled_ = true;
if (sign_operation_) {
sign_operation_->Cancel();
}
if (dummy_register_operation_) {
dummy_register_operation_->Cancel();
}
}
// static
bool GetAssertionTask::StringFixupPredicate(
const std::vector<const cbor::Value*>& path) {
// This filters out all elements that are not string-keyed, direct children
// of key 0x04, which is the `user` element of a getAssertion response.
if (path.size() != 2 || !path[0]->is_unsigned() ||
path[0]->GetUnsigned() != 4 || !path[1]->is_string()) {
return false;
}
// Of those string-keyed children, only `name` and `displayName` may have
// truncated UTF-8 in their values.
const std::string& user_key = path[1]->GetString();
return user_key == "name" || user_key == "displayName";
}
void GetAssertionTask::StartTask() {
if (device()->supported_protocol() == ProtocolVersion::kCtap2 &&
!request_.is_u2f_only) {
GetAssertion();
} else {
// |device_info| should be present iff the device is CTAP2.
// |MaybeRevertU2fFallbackAndInvokeCallback| uses this 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);
U2fSign();
}
}
CtapGetAssertionRequest GetAssertionTask::NextSilentRequest() {
DCHECK(current_allow_list_batch_ < allow_list_batches_.size());
CtapGetAssertionRequest request = request_;
request.allow_list = allow_list_batches_.at(current_allow_list_batch_++);
request.user_presence_required = false;
request.user_verification = UserVerificationRequirement::kDiscouraged;
return request;
}
void GetAssertionTask::GetAssertion() {
if (request_.allow_list.empty()) {
MaybeSetPRFParameters(&request_, GetDefaultPRFInput(options_));
sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), request_,
base::BindOnce(&GetAssertionTask::HandleResponse,
weak_factory_.GetWeakPtr(), request_.allow_list),
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());
DCHECK(!allow_list_batches_.empty());
// If filtering eliminated all entries from the allowList, just collect a
// dummy touch, then fail the request.
if (allow_list_batches_.size() == 1 && allow_list_batches_[0].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);
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_) &&
!HasCredentialSpecificPRFInputs(options_)) {
CtapGetAssertionRequest request = request_;
request.allow_list = allow_list_batches_.front();
MaybeSetPRFParameters(&request, GetDefaultPRFInput(options_));
sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), std::move(request),
base::BindOnce(&GetAssertionTask::HandleResponse,
weak_factory_.GetWeakPtr(), request.allow_list),
base::BindOnce(&ReadCTAPGetAssertionResponse), StringFixupPredicate);
sign_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 appid extension, or if we might
// need to send specific PRF inputs, probe the credential IDs silently.
sign_operation_ =
std::make_unique<Ctap2DeviceOperation<CtapGetAssertionRequest,
AuthenticatorGetAssertionResponse>>(
device(), NextSilentRequest(),
base::BindOnce(&GetAssertionTask::HandleResponseToSilentRequest,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPGetAssertionResponse),
/*string_fixup_predicate=*/nullptr);
sign_operation_->Start();
}
void GetAssertionTask::U2fSign() {
DCHECK_EQ(ProtocolVersion::kU2f, device()->supported_protocol());
sign_operation_ = std::make_unique<U2fSignOperation>(
device(), request_,
base::BindOnce(&GetAssertionTask::MaybeRevertU2fFallbackAndInvokeCallback,
weak_factory_.GetWeakPtr()));
sign_operation_->Start();
}
void GetAssertionTask::HandleResponse(
std::vector<PublicKeyCredentialDescriptor> allow_list,
CtapDeviceResponseCode response_code,
base::Optional<AuthenticatorGetAssertionResponse> response_data) {
if (canceled_) {
return;
}
if (response_code == CtapDeviceResponseCode::kCtap2ErrInvalidCredential) {
// Some authenticators will return this error before waiting for a touch if
// they don't recognise a credential. In other cases the result can be
// returned immediately.
// The request failed in a way that didn't request a touch. Simulate it.
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);
dummy_register_operation_->Start();
return;
}
if (response_code == CtapDeviceResponseCode::kSuccess) {
if (!SetResponseCredential(&response_data.value(), allow_list)) {
FIDO_LOG(DEBUG)
<< "Assertion response has invalid credential information";
std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrOther,
base::nullopt);
return;
}
// Decrypt any hmac-secret response.
const base::Optional<cbor::Value>& extensions_cbor =
response_data->auth_data().extensions();
if (extensions_cbor) {
// Parsing has already checked that |extensions_cbor| is a map.
const cbor::Value::MapValue& extensions = extensions_cbor->GetMap();
auto it = extensions.find(cbor::Value(kExtensionHmacSecret));
if (it != extensions.end()) {
if (!hmac_secret_request_ || !it->second.is_bytestring()) {
FIDO_LOG(DEBUG) << "Unexpected or invalid hmac_secret extension";
std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrOther,
base::nullopt);
return;
}
base::Optional<std::vector<uint8_t>> plaintext =
hmac_secret_request_->Decrypt(it->second.GetBytestring());
if (!plaintext) {
FIDO_LOG(DEBUG) << "Failed to decrypt hmac_secret extension";
std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrOther,
base::nullopt);
return;
}
response_data->set_hmac_secret(std::move(plaintext.value()));
}
}
}
std::move(callback_).Run(response_code, std::move(response_data));
}
void GetAssertionTask::HandleResponseToSilentRequest(
CtapDeviceResponseCode response_code,
base::Optional<AuthenticatorGetAssertionResponse> response_data) {
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_) {
return;
}
// One credential from the previous batch was recognized by the device. As
// this authentication was a silent authentication (i.e. user touch was not
// provided), try again with only that credential, user presence enforced and
// with the original user verification configuration.
if (response_code == CtapDeviceResponseCode::kSuccess &&
SetResponseCredential(
&response_data.value(),
allow_list_batches_.at(current_allow_list_batch_ - 1))) {
CtapGetAssertionRequest request = request_;
const PublicKeyCredentialDescriptor& matching_credential =
*response_data->credential();
request.allow_list = {matching_credential};
MaybeSetPRFParameters(
&request, GetPRFInputForCredential(options_, matching_credential.id()));
sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), std::move(request),
base::BindOnce(&GetAssertionTask::HandleResponse,
weak_factory_.GetWeakPtr(), request.allow_list),
base::BindOnce(&ReadCTAPGetAssertionResponse),
/*string_fixup_predicate=*/nullptr);
sign_operation_->Start();
return;
}
// Credential was not recognized or an error occurred. Probe the next
// credential.
if (current_allow_list_batch_ < allow_list_batches_.size()) {
sign_operation_ = std::make_unique<Ctap2DeviceOperation<
CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
device(), NextSilentRequest(),
base::BindOnce(&GetAssertionTask::HandleResponseToSilentRequest,
weak_factory_.GetWeakPtr()),
base::BindOnce(&ReadCTAPGetAssertionResponse),
/*string_fixup_predicate=*/nullptr);
sign_operation_->Start();
return;
}
// None of the credentials were recognized. Fall back to U2F or collect a
// dummy touch.
if (MayFallbackToU2fWithAppIdExtension(*device(), request_)) {
device()->set_supported_protocol(ProtocolVersion::kU2f);
U2fSign();
return;
}
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);
dummy_register_operation_->Start();
}
void GetAssertionTask::HandleDummyMakeCredentialComplete(
CtapDeviceResponseCode response_code,
base::Optional<AuthenticatorMakeCredentialResponse> response_data) {
std::move(callback_).Run(CtapDeviceResponseCode::kCtap2ErrNoCredentials,
base::nullopt);
}
void GetAssertionTask::MaybeSetPRFParameters(
CtapGetAssertionRequest* request,
const CtapGetAssertionOptions::PRFInput* maybe_inputs) {
if (maybe_inputs == nullptr) {
return;
}
hmac_secret_request_ = std::make_unique<pin::HMACSecretRequest>(
*request->pin_protocol, *options_.pin_key_agreement, maybe_inputs->salt1,
maybe_inputs->salt2);
request->hmac_secret.emplace(hmac_secret_request_->public_key_x962,
hmac_secret_request_->encrypted_salts,
hmac_secret_request_->salts_auth);
}
void GetAssertionTask::MaybeRevertU2fFallbackAndInvokeCallback(
CtapDeviceResponseCode status,
base::Optional<AuthenticatorGetAssertionResponse> 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
// in order to execute a sign command.
device()->set_supported_protocol(ProtocolVersion::kCtap2);
}
std::move(callback_).Run(status, std::move(response));
}
} // namespace device