blob: f5777bd4f27f2e2a5cc64396b46bec90d5de024d [file] [log] [blame]
// Copyright 2019 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 "content/browser/webauth/authenticator_common.h"
#include <array>
#include <string>
#include <utility>
#include <vector>
#include "base/base64url.h"
#include "base/bind.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/strings/string_piece.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "content/browser/bad_message.h"
#include "content/browser/webauth/authenticator_environment_impl.h"
#include "content/browser/webauth/authenticator_type_converters.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/origin_util.h"
#include "content/public/common/service_manager_connection.h"
#include "crypto/sha2.h"
#include "device/base/features.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/fido/attestation_statement.h"
#include "device/fido/authenticator_selection_criteria.h"
#include "device/fido/ctap_get_assertion_request.h"
#include "device/fido/ctap_make_credential_request.h"
#include "device/fido/features.h"
#include "device/fido/fido_authenticator.h"
#include "device/fido/fido_constants.h"
#include "device/fido/fido_transport_protocol.h"
#include "device/fido/get_assertion_request_handler.h"
#include "device/fido/make_credential_request_handler.h"
#include "device/fido/public_key_credential_descriptor.h"
#include "device/fido/public_key_credential_params.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/cert/asn1_util.h"
#include "net/der/input.h"
#include "net/der/parse_values.h"
#include "net/der/parser.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_getter.h"
#include "services/service_manager/public/cpp/connector.h"
#include "url/url_constants.h"
#include "url/url_util.h"
#if defined(OS_MACOSX)
#include "device/fido/mac/authenticator.h"
#endif
#if defined(OS_WIN)
#include "device/fido/win/authenticator.h"
#endif
namespace content {
namespace client_data {
const char kCreateType[] = "webauthn.create";
const char kGetType[] = "webauthn.get";
const char kU2fSignType[] = "navigator.id.getAssertion";
const char kU2fRegisterType[] = "navigator.id.finishEnrollment";
} // namespace client_data
namespace {
constexpr char kCryptotokenOrigin[] =
"chrome-extension://kmendfapggjehodndflmmgagdbamhnfd";
// AttestationPromptResult enumerates events related to attestation prompts.
// These values are recorded in an UMA histogram and so should not be
// reassigned.
enum class AttestationPromptResult {
// kQueried indicates that the embedder was queried in order to determine
// whether attestation information should be returned to the origin.
kQueried = 0,
// kTimeout indicates that a timeout occurred while awaiting the result of an
// attestation query.
kTimeout = 1,
// kAllowed indicates that the query to the embedder was resolved positively.
// (E.g. the user clicked to allow, or the embedded allowed immediately by
// policy.)
kAllowed = 2,
// kBlocked indicates that the query to the embedder was resolved negatively.
// (E.g. the user clicked to block, or closed the dialog.)
kBlocked = 3,
// kAbandoned indications that the user closed the tab or navigated away while
// the attestation prompt was showing.
kAbandoned = 4,
kMaxValue = kAbandoned,
};
// The following enums correspond to UMA histograms and should not be
// reassigned.
enum class RelyingPartySecurityCheckFailure {
kOpaqueOrNonSecureOrigin = 0,
kRelyingPartyIdInvalid = 1,
kAppIdExtensionInvalid = 2,
kAppIdExtensionDomainMismatch = 3,
kMaxValue = kAppIdExtensionDomainMismatch,
};
void ReportSecurityCheckFailure(RelyingPartySecurityCheckFailure error) {
UMA_HISTOGRAM_ENUMERATION(
"WebAuthentication.RelyingPartySecurityCheckFailure", error);
}
bool OriginIsCryptoTokenExtension(const url::Origin& origin) {
auto cryptotoken_origin = url::Origin::Create(GURL(kCryptotokenOrigin));
return cryptotoken_origin == origin;
}
// Ensure that the origin's effective domain is a valid domain.
// Only the domain format of host is valid.
// Reference https://url.spec.whatwg.org/#valid-domain-string and
// https://html.spec.whatwg.org/multipage/origin.html#concept-origin-effective-domain.
bool HasValidEffectiveDomain(url::Origin caller_origin) {
// For calls originating in the CryptoToken U2F extension, allow CryptoToken
// to validate domain.
if (OriginIsCryptoTokenExtension(caller_origin)) {
return true;
}
return !caller_origin.opaque() &&
!url::HostIsIPAddress(caller_origin.host()) &&
content::IsOriginSecure(caller_origin.GetURL()) &&
// Additionally, the scheme is required to be HTTP(S). Other schemes
// may be supported in the future but the webauthn relying party is
// just the domain of the origin so we would have to define how the
// authority part of other schemes maps to a "domain" without
// collisions. Given the |IsOriginSecure| check, just above, HTTP is
// effectively restricted to just "localhost".
(caller_origin.scheme() == url::kHttpScheme ||
caller_origin.scheme() == url::kHttpsScheme);
}
// Ensure the relying party ID is a registrable domain suffix of or equal
// to the origin's effective domain. Reference:
// https://html.spec.whatwg.org/multipage/origin.html#is-a-registrable-domain-suffix-of-or-is-equal-to.
bool IsRelyingPartyIdValid(const std::string& relying_party_id,
url::Origin caller_origin) {
if (OriginIsCryptoTokenExtension(caller_origin)) {
return true;
}
if (relying_party_id.empty())
return false;
if (caller_origin.host() == relying_party_id)
return true;
if (!caller_origin.DomainIs(relying_party_id))
return false;
if (!net::registry_controlled_domains::HostHasRegistryControlledDomain(
caller_origin.host(),
net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES))
return false;
if (!net::registry_controlled_domains::HostHasRegistryControlledDomain(
relying_party_id,
net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES))
// TODO(crbug.com/803414): Accept corner-case situations like the following
// origin: "https://login.awesomecompany",
// relying_party_id: "awesomecompany".
return false;
return true;
}
// Validates whether the given origin is authorized to use the provided App
// ID value, mostly according to the rules in
// https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-appid-and-facets-v1.2-ps-20170411.html#determining-if-a-caller-s-facetid-is-authorized-for-an-appid.
//
// Returns the App ID to use for the request, or base::nullopt if the origin
// is not authorized to use the provided value.
base::Optional<std::string> ProcessAppIdExtension(std::string appid,
const url::Origin& origin) {
// The CryptoToken U2F extension checks the appid before calling the WebAuthn
// API so there is no need to validate it here.
if (OriginIsCryptoTokenExtension(origin)) {
if (!GURL(appid).is_valid()) {
DCHECK(false) << "cryptotoken request did not set a valid App ID";
return base::nullopt;
}
return appid;
}
// Step 1: "If the AppID is not an HTTPS URL, and matches the FacetID of the
// caller, no additional processing is necessary and the operation may
// proceed."
// Webauthn is only supported on secure origins and |HasValidEffectiveDomain|
// has already checked this property of |origin| before this call. Thus this
// step is moot.
DCHECK(content::IsOriginSecure(origin.GetURL()));
// Step 2: "If the AppID is null or empty, the client must set the AppID to be
// the FacetID of the caller, and the operation may proceed without additional
// processing."
if (appid.empty()) {
// While the U2F spec says to default the App ID to the Facet ID, which is
// the origin plus a trailing forward slash [1], cryptotoken and Firefox
// just use the site's Origin without trailing slash. We follow their
// implementations rather than the spec.
//
// [1]https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-appid-and-facets-v2.0-id-20180227.html#determining-the-facetid-of-a-calling-application
appid = origin.Serialize();
}
// Step 3: "If the caller's FacetID is an https:// Origin sharing the same
// host as the AppID, (e.g. if an application hosted at
// https://fido.example.com/myApp set an AppID of
// https://fido.example.com/myAppId), no additional processing is necessary
// and the operation may proceed."
GURL appid_url = GURL(appid);
if (!appid_url.is_valid() || appid_url.scheme() != url::kHttpsScheme ||
appid_url.scheme_piece() != origin.scheme()) {
ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure::kAppIdExtensionInvalid);
return base::nullopt;
}
// This check is repeated inside |SameDomainOrHost|, just after this. However
// it's cheap and mirrors the structure of the spec.
if (appid_url.host_piece() == origin.host()) {
return appid;
}
// At this point we diverge from the specification in order to avoid the
// complexity of making a network request which isn't believed to be
// necessary in practice. See also
// https://bugzilla.mozilla.org/show_bug.cgi?id=1244959#c8
if (net::registry_controlled_domains::SameDomainOrHost(
appid_url, origin,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) {
return appid;
}
// As a compatibility hack, sites within google.com are allowed to assert two
// special-case AppIDs. Firefox also does this:
// https://groups.google.com/forum/#!msg/mozilla.dev.platform/Uiu3fwnA2xw/201ynAiPAQAJ
const GURL kGstatic1 =
GURL("https://www.gstatic.com/securitykey/origins.json");
const GURL kGstatic2 =
GURL("https://www.gstatic.com/securitykey/a/google.com/origins.json");
DCHECK(kGstatic1.is_valid() && kGstatic2.is_valid());
if (origin.DomainIs("google.com") && !appid_url.has_ref() &&
(appid_url.EqualsIgnoringRef(kGstatic1) ||
appid_url.EqualsIgnoringRef(kGstatic2))) {
return appid;
}
ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure::kAppIdExtensionDomainMismatch);
return base::nullopt;
}
device::CtapMakeCredentialRequest CreateCtapMakeCredentialRequest(
const std::string& client_data_json,
const blink::mojom::PublicKeyCredentialCreationOptionsPtr& options,
bool is_incognito) {
auto credential_params = mojo::ConvertTo<
std::vector<device::PublicKeyCredentialParams::CredentialInfo>>(
options->public_key_parameters);
device::CtapMakeCredentialRequest make_credential_param(
client_data_json,
mojo::ConvertTo<device::PublicKeyCredentialRpEntity>(
options->relying_party),
mojo::ConvertTo<device::PublicKeyCredentialUserEntity>(options->user),
device::PublicKeyCredentialParams(std::move(credential_params)));
make_credential_param.exclude_list =
mojo::ConvertTo<std::vector<device::PublicKeyCredentialDescriptor>>(
options->exclude_credentials);
make_credential_param.hmac_secret = options->hmac_create_secret;
make_credential_param.is_incognito_mode = is_incognito;
return make_credential_param;
}
// The application parameter is the SHA-256 hash of the UTF-8 encoding of
// the application identity (i.e. relying_party_id) of the application
// requesting the registration.
std::array<uint8_t, crypto::kSHA256Length> CreateApplicationParameter(
const std::string& relying_party_id) {
std::array<uint8_t, crypto::kSHA256Length> application_parameter;
crypto::SHA256HashString(relying_party_id, application_parameter.data(),
application_parameter.size());
return application_parameter;
}
device::CtapGetAssertionRequest CreateCtapGetAssertionRequest(
const std::string& client_data_json,
const blink::mojom::PublicKeyCredentialRequestOptionsPtr& options,
base::Optional<std::string> app_id,
bool is_incognito) {
device::CtapGetAssertionRequest request_parameter(options->relying_party_id,
client_data_json);
request_parameter.allow_list =
mojo::ConvertTo<std::vector<device::PublicKeyCredentialDescriptor>>(
options->allow_credentials);
request_parameter.user_verification =
mojo::ConvertTo<device::UserVerificationRequirement>(
options->user_verification);
if (app_id) {
request_parameter.alternative_application_parameter =
CreateApplicationParameter(*app_id);
request_parameter.app_id = std::move(*app_id);
}
if (!options->cable_authentication_data.empty()) {
request_parameter.cable_extension =
mojo::ConvertTo<std::vector<device::CableDiscoveryData>>(
options->cable_authentication_data);
}
request_parameter.is_incognito_mode = is_incognito;
return request_parameter;
}
// Parses the FIDO transport types extension from the DER-encoded, X.509
// certificate in |der_cert| and appends any unique transport types found to
// |out_transports|.
void AppendUniqueTransportsFromCertificate(
base::span<const uint8_t> der_cert,
std::vector<device::FidoTransportProtocol>* out_transports) {
// See
// https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-authenticator-transports-extension-v1.2-ps-20170411.html#fido-u2f-certificate-transports-extension
static constexpr uint8_t kTransportTypesOID[] = {
0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xe5, 0x1c, 0x02, 0x01, 0x01};
bool present, critical;
base::StringPiece contents;
if (!net::asn1::ExtractExtensionFromDERCert(
base::StringPiece(reinterpret_cast<const char*>(der_cert.data()),
der_cert.size()),
base::StringPiece(reinterpret_cast<const char*>(kTransportTypesOID),
sizeof(kTransportTypesOID)),
&present, &critical, &contents) ||
!present) {
return;
}
const net::der::Input contents_der(contents);
net::der::Parser contents_parser(contents_der);
net::der::BitString transport_bits;
if (!contents_parser.ReadBitString(&transport_bits)) {
return;
}
// The certificate extension contains a BIT STRING where different bits
// indicate support for different transports. The following array maps
// between these bit indexes and the FidoTransportProtocol enum.
static constexpr struct {
uint8_t bit_index;
device::FidoTransportProtocol transport;
} kTransportMapping[] = {
// Bit 0 is "Bluetooth Classic", not BLE. Since webauthn doesn't define a
// transport type for this we ignore it.
{1, device::FidoTransportProtocol::kBluetoothLowEnergy},
{2, device::FidoTransportProtocol::kUsbHumanInterfaceDevice},
{3, device::FidoTransportProtocol::kNearFieldCommunication},
{4, device::FidoTransportProtocol::kInternal},
};
for (const auto& mapping : kTransportMapping) {
if (transport_bits.AssertsBit(mapping.bit_index) &&
!base::ContainsValue(*out_transports, mapping.transport)) {
out_transports->push_back(mapping.transport);
}
}
}
enum class AttestationErasureOption {
kIncludeAttestation,
kEraseAttestationButIncludeAaguid,
kEraseAttestationAndAaguid,
};
blink::mojom::MakeCredentialAuthenticatorResponsePtr
CreateMakeCredentialResponse(
const std::string& client_data_json,
device::AuthenticatorMakeCredentialResponse response_data,
AttestationErasureOption attestation_erasure) {
auto response = blink::mojom::MakeCredentialAuthenticatorResponse::New();
auto common_info = blink::mojom::CommonCredentialInfo::New();
common_info->client_data_json.assign(client_data_json.begin(),
client_data_json.end());
common_info->raw_id = response_data.raw_credential_id();
common_info->id = response_data.GetId();
response->info = std::move(common_info);
// The transport list must not contain duplicates but the order doesn't matter
// because Blink will sort the resulting strings before returning them.
std::vector<device::FidoTransportProtocol> transports;
if (response_data.transport_used()) {
transports.push_back(*response_data.transport_used());
}
// If the attestation certificate specifies that the token supports any other
// transports, include them in the list.
base::Optional<base::span<const uint8_t>> leaf_cert =
response_data.attestation_object()
.attestation_statement()
.GetLeafCertificate();
if (leaf_cert) {
AppendUniqueTransportsFromCertificate(*leaf_cert, &transports);
}
for (auto transport : transports) {
response->transports.push_back(
mojo::ConvertTo<blink::mojom::AuthenticatorTransport>(transport));
}
const base::Optional<cbor::Value>& maybe_extensions =
response_data.attestation_object().authenticator_data().extensions();
if (maybe_extensions) {
DCHECK(maybe_extensions->is_map());
const cbor::Value::MapValue& extensions = maybe_extensions->GetMap();
const auto hmac_secret_it =
extensions.find(cbor::Value(device::kExtensionHmacSecret));
if (hmac_secret_it != extensions.end() &&
hmac_secret_it->second.is_bool()) {
response->echo_hmac_create_secret = true;
response->hmac_create_secret = hmac_secret_it->second.GetBool();
}
}
switch (attestation_erasure) {
case AttestationErasureOption::kIncludeAttestation:
break;
case AttestationErasureOption::kEraseAttestationButIncludeAaguid:
response_data.EraseAttestationStatement(
device::AttestationObject::AAGUID::kInclude);
break;
case AttestationErasureOption::kEraseAttestationAndAaguid:
response_data.EraseAttestationStatement(
device::AttestationObject::AAGUID::kErase);
break;
}
response->attestation_object =
response_data.GetCBOREncodedAttestationObject();
return response;
}
blink::mojom::GetAssertionAuthenticatorResponsePtr CreateGetAssertionResponse(
const std::string& client_data_json,
device::AuthenticatorGetAssertionResponse response_data,
base::Optional<bool> echo_appid_extension) {
auto response = blink::mojom::GetAssertionAuthenticatorResponse::New();
auto common_info = blink::mojom::CommonCredentialInfo::New();
common_info->client_data_json.assign(client_data_json.begin(),
client_data_json.end());
common_info->raw_id = response_data.raw_credential_id();
common_info->id = response_data.GetId();
response->info = std::move(common_info);
response->authenticator_data =
response_data.auth_data().SerializeToByteArray();
response->signature = response_data.signature();
if (echo_appid_extension) {
response->echo_appid_extension = true;
response->appid_extension = *echo_appid_extension;
}
response_data.user_entity()
? response->user_handle.emplace(response_data.user_entity()->id)
: response->user_handle.emplace();
return response;
}
std::string Base64UrlEncode(const base::span<const uint8_t> input) {
std::string ret;
base::Base64UrlEncode(
base::StringPiece(reinterpret_cast<const char*>(input.data()),
input.size()),
base::Base64UrlEncodePolicy::OMIT_PADDING, &ret);
return ret;
}
base::flat_set<device::FidoTransportProtocol> GetTransportsEnabledByFlags() {
base::flat_set<device::FidoTransportProtocol> transports;
transports.insert(device::FidoTransportProtocol::kUsbHumanInterfaceDevice);
transports.insert(device::FidoTransportProtocol::kInternal);
// TODO(crbug.com/885165): We should not directly access the BLE stack here.
// It is used by //device/fido, so its availability should be checked there.
if (!device::BluetoothAdapterFactory::Get().IsLowEnergySupported())
return transports;
if (base::FeatureList::IsEnabled(features::kWebAuthBle)) {
transports.insert(device::FidoTransportProtocol::kBluetoothLowEnergy);
}
// caBLE is independent of the BLE transport.
if (base::FeatureList::IsEnabled(features::kWebAuthCable)) {
transports.insert(
device::FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy);
}
return transports;
}
} // namespace
AuthenticatorCommon::AuthenticatorCommon(
RenderFrameHost* render_frame_host,
service_manager::Connector* connector,
std::unique_ptr<base::OneShotTimer> timer)
: render_frame_host_(render_frame_host),
connector_(connector),
transports_(GetTransportsEnabledByFlags()),
timer_(std::move(timer)),
weak_factory_(this) {
DCHECK(render_frame_host_);
DCHECK(timer_);
}
AuthenticatorCommon::~AuthenticatorCommon() {
// This call exists to assert that |render_frame_host_| outlives this object.
// If this is violated, ASAN should notice.
render_frame_host_->GetRoutingID();
}
void AuthenticatorCommon::UpdateRequestDelegate() {
DCHECK(!request_delegate_);
DCHECK(!relying_party_id_.empty());
request_delegate_ =
GetContentClient()->browser()->GetWebAuthenticationRequestDelegate(
render_frame_host_, relying_party_id_);
}
bool AuthenticatorCommon::IsFocused() const {
return render_frame_host_->IsCurrent() && request_delegate_->IsFocused();
}
// static
std::string AuthenticatorCommon::SerializeCollectedClientDataToJson(
const std::string& type,
const std::string& origin,
base::span<const uint8_t> challenge,
bool use_legacy_u2f_type_key /* = false */) {
static constexpr char kChallengeKey[] = "challenge";
static constexpr char kOriginKey[] = "origin";
base::DictionaryValue client_data;
client_data.SetKey(use_legacy_u2f_type_key ? "typ" : "type",
base::Value(type));
client_data.SetKey(kChallengeKey, base::Value(Base64UrlEncode(challenge)));
client_data.SetKey(kOriginKey, base::Value(origin));
if (base::RandDouble() < 0.2) {
// An extra key is sometimes added to ensure that RPs do not make
// unreasonably specific assumptions about the clientData JSON. This is
// done in the fashion of
// https://tools.ietf.org/html/draft-davidben-tls-grease-01
client_data.SetKey("extra_keys_may_be_added_here",
base::Value("do not compare clientDataJSON against a "
"template. See https://goo.gl/yabPex"));
}
std::string json;
base::JSONWriter::Write(client_data, &json);
return json;
}
// mojom::Authenticator
void AuthenticatorCommon::MakeCredential(
url::Origin caller_origin,
blink::mojom::PublicKeyCredentialCreationOptionsPtr options,
blink::mojom::Authenticator::MakeCredentialCallback callback) {
if (request_) {
if (OriginIsCryptoTokenExtension(caller_origin)) {
// Requests originating from cryptotoken will generally outlive any
// navigation events on the tab of the request's sender. Evict pending
// requests if cryptotoken sends a new one such that requests from before
// a navigation event do not prevent new requests. See
// https://crbug.com/935480.
CancelWithStatus(blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR);
} else {
std::move(callback).Run(
blink::mojom::AuthenticatorStatus::PENDING_REQUEST, nullptr);
return;
}
}
DCHECK(!request_);
if (!HasValidEffectiveDomain(caller_origin)) {
ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure::kOpaqueOrNonSecureOrigin);
bad_message::ReceivedBadMessage(render_frame_host_->GetProcess(),
bad_message::AUTH_INVALID_EFFECTIVE_DOMAIN);
InvokeCallbackAndCleanup(std::move(callback),
blink::mojom::AuthenticatorStatus::INVALID_DOMAIN,
nullptr, Focus::kDontCheck);
return;
}
if (!IsRelyingPartyIdValid(options->relying_party->id, caller_origin)) {
ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure::kRelyingPartyIdInvalid);
bad_message::ReceivedBadMessage(render_frame_host_->GetProcess(),
bad_message::AUTH_INVALID_RELYING_PARTY);
InvokeCallbackAndCleanup(std::move(callback),
blink::mojom::AuthenticatorStatus::INVALID_DOMAIN,
nullptr, Focus::kDontCheck);
return;
}
caller_origin_ = caller_origin;
relying_party_id_ = options->relying_party->id;
UpdateRequestDelegate();
if (!request_delegate_) {
InvokeCallbackAndCleanup(std::move(callback),
blink::mojom::AuthenticatorStatus::PENDING_REQUEST,
nullptr, Focus::kDontCheck);
return;
}
if (!IsFocused()) {
InvokeCallbackAndCleanup(std::move(callback),
blink::mojom::AuthenticatorStatus::NOT_FOCUSED);
return;
}
const bool resident_key =
options->authenticator_selection &&
options->authenticator_selection->require_resident_key;
if (resident_key &&
(!base::FeatureList::IsEnabled(device::kWebAuthResidentKeys) ||
!request_delegate_->SupportsResidentKeys())) {
// Disallow the creation of resident credentials.
InvokeCallbackAndCleanup(
std::move(callback),
blink::mojom::AuthenticatorStatus::RESIDENT_CREDENTIALS_UNSUPPORTED);
return;
}
auto authenticator_selection_criteria =
options->authenticator_selection
? mojo::ConvertTo<device::AuthenticatorSelectionCriteria>(
options->authenticator_selection)
: device::AuthenticatorSelectionCriteria();
// Reject any non-sensical credProtect extension values.
if ( // Can't require the default policy (or no policy).
(options->enforce_protection_policy &&
(options->protection_policy ==
blink::mojom::ProtectionPolicy::UNSPECIFIED ||
options->protection_policy == blink::mojom::ProtectionPolicy::NONE)) ||
// For non-resident keys, NONE doesn't make sense. (UV_OR_CRED_ID_REQUIRED
// does because, with CTAP 2.0, just because a resident key isn't
// _required_ doesn't mean that one won't be created and an RP might want
// credProtect to take effect if that happens.)
(!resident_key &&
options->protection_policy == blink::mojom::ProtectionPolicy::NONE) ||
// UV_REQUIRED only makes sense if UV is required overall.
(options->protection_policy ==
blink::mojom::ProtectionPolicy::UV_REQUIRED &&
authenticator_selection_criteria.user_verification_requirement() !=
device::UserVerificationRequirement::kRequired)) {
InvokeCallbackAndCleanup(
std::move(callback),
blink::mojom::AuthenticatorStatus::PROTECTION_POLICY_INCONSISTENT);
return;
}
if (options->protection_policy ==
blink::mojom::ProtectionPolicy::UNSPECIFIED &&
resident_key) {
// If not specified, UV_OR_CRED_ID_REQUIRED is made the default.
options->protection_policy =
blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED;
}
DCHECK(make_credential_response_callback_.is_null());
make_credential_response_callback_ = std::move(callback);
timer_->Start(
FROM_HERE, options->adjusted_timeout,
base::BindOnce(&AuthenticatorCommon::OnTimeout, base::Unretained(this)));
if (!connector_)
connector_ = ServiceManagerConnection::GetForProcess()->GetConnector();
// Save client data to return with the authenticator response.
// TODO(kpaulhamus): Fetch and add the Channel ID/Token Binding ID public key
// used to communicate with the origin.
if (OriginIsCryptoTokenExtension(caller_origin_)) {
// Cryptotoken requests should be proxied without UI.
request_delegate_->DisableUI();
// As Cryptotoken validates the origin, accept the relying party id as the
// origin from requests originating from Cryptotoken. The origin is provided
// in Cryptotoken requests as the relying party name, which should be used
// as part of client data.
client_data_json_ = SerializeCollectedClientDataToJson(
client_data::kU2fRegisterType, options->relying_party->name,
std::move(options->challenge), true /* use_legacy_u2f_type_key */);
} else {
client_data_json_ = SerializeCollectedClientDataToJson(
client_data::kCreateType, caller_origin_.Serialize(),
std::move(options->challenge));
}
UMA_HISTOGRAM_COUNTS_100(
"WebAuthentication.MakeCredentialExcludeCredentialsCount",
options->exclude_credentials.size());
// U2F requests proxied from the cryptotoken extension are limited to USB
// devices.
const auto transports =
OriginIsCryptoTokenExtension(caller_origin_)
? base::flat_set<device::FidoTransportProtocol>(
{device::FidoTransportProtocol::kUsbHumanInterfaceDevice})
: transports_;
auto ctap_request = CreateCtapMakeCredentialRequest(
client_data_json_, options, browser_context()->IsOffTheRecord());
// On dual protocol CTAP2/U2F devices, force credential creation over U2F.
ctap_request.is_u2f_only = OriginIsCryptoTokenExtension(caller_origin_);
// Compute the effective attestation conveyance preference and set
// |attestation_requested_| for showing the attestation consent prompt later.
auto attestation = mojo::ConvertTo<::device::AttestationConveyancePreference>(
options->attestation);
if (attestation == ::device::AttestationConveyancePreference::ENTERPRISE &&
!request_delegate_->ShouldPermitIndividualAttestation(
relying_party_id_)) {
attestation = ::device::AttestationConveyancePreference::DIRECT;
}
ctap_request.attestation_preference = attestation;
attestation_requested_ =
attestation != ::device::AttestationConveyancePreference::NONE;
switch (options->protection_policy) {
case blink::mojom::ProtectionPolicy::UNSPECIFIED:
case blink::mojom::ProtectionPolicy::NONE:
break;
case blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED:
ctap_request.cred_protect =
std::make_pair(device::CredProtect::kUVOrCredIDRequired,
options->enforce_protection_policy);
break;
case blink::mojom::ProtectionPolicy::UV_REQUIRED:
ctap_request.cred_protect = std::make_pair(
device::CredProtect::kUVRequired, options->enforce_protection_policy);
break;
}
request_ = std::make_unique<device::MakeCredentialRequestHandler>(
connector_,
AuthenticatorEnvironmentImpl::GetInstance()->GetFactory(
render_frame_host_),
transports, std::move(ctap_request),
std::move(authenticator_selection_criteria),
base::BindOnce(&AuthenticatorCommon::OnRegisterResponse,
weak_factory_.GetWeakPtr()));
request_delegate_->RegisterActionCallbacks(
base::BindOnce(&AuthenticatorCommon::OnCancelFromUI,
weak_factory_.GetWeakPtr()) /* cancel_callback */,
base::BindRepeating(
&device::FidoRequestHandlerBase::StartAuthenticatorRequest,
request_->GetWeakPtr()) /* request_callback */,
base::BindRepeating(
&device::FidoRequestHandlerBase::PowerOnBluetoothAdapter,
request_->GetWeakPtr()) /* bluetooth_adapter_power_on_callback */,
base::BindRepeating(
&device::FidoRequestHandlerBase::InitiatePairingWithDevice,
request_->GetWeakPtr()) /* ble_pairing_callback */);
if (resident_key) {
request_delegate_->SetMightCreateResidentCredential(true);
}
request_->set_observer(request_delegate_.get());
request_->SetPlatformAuthenticatorOrMarkUnavailable(
CreatePlatformAuthenticatorIfAvailable());
}
// mojom:Authenticator
void AuthenticatorCommon::GetAssertion(
url::Origin caller_origin,
blink::mojom::PublicKeyCredentialRequestOptionsPtr options,
blink::mojom::Authenticator::GetAssertionCallback callback) {
if (request_) {
if (OriginIsCryptoTokenExtension(caller_origin)) {
// Requests originating from cryptotoken will generally outlive any
// navigation events on the tab of the request's sender. Evict pending
// requests if cryptotoken sends a new one such that requests from before
// a navigation event do not prevent new requests. See
// https://crbug.com/935480.
CancelWithStatus(blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR);
} else {
std::move(callback).Run(
blink::mojom::AuthenticatorStatus::PENDING_REQUEST, nullptr);
return;
}
}
DCHECK(!request_);
if (!HasValidEffectiveDomain(caller_origin)) {
ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure::kOpaqueOrNonSecureOrigin);
bad_message::ReceivedBadMessage(render_frame_host_->GetProcess(),
bad_message::AUTH_INVALID_EFFECTIVE_DOMAIN);
InvokeCallbackAndCleanup(std::move(callback),
blink::mojom::AuthenticatorStatus::INVALID_DOMAIN,
nullptr);
return;
}
if (!IsRelyingPartyIdValid(options->relying_party_id, caller_origin)) {
ReportSecurityCheckFailure(
RelyingPartySecurityCheckFailure::kRelyingPartyIdInvalid);
bad_message::ReceivedBadMessage(render_frame_host_->GetProcess(),
bad_message::AUTH_INVALID_RELYING_PARTY);
InvokeCallbackAndCleanup(std::move(callback),
blink::mojom::AuthenticatorStatus::INVALID_DOMAIN,
nullptr);
return;
}
caller_origin_ = caller_origin;
relying_party_id_ = options->relying_party_id;
UpdateRequestDelegate();
if (!request_delegate_) {
InvokeCallbackAndCleanup(std::move(callback),
blink::mojom::AuthenticatorStatus::PENDING_REQUEST,
nullptr);
return;
}
// Save client data to return with the authenticator response.
// TODO(kpaulhamus): Fetch and add the Channel ID/Token Binding ID public key
// used to communicate with the origin.
if (OriginIsCryptoTokenExtension(caller_origin)) {
request_delegate_->DisableUI();
// As Cryptotoken validates the origin, accept the relying party id as the
// origin from requests originating from Cryptotoken.
client_data_json_ = SerializeCollectedClientDataToJson(
client_data::kU2fSignType, options->relying_party_id,
std::move(options->challenge), true /* use_legacy_u2f_type_key */);
} else {
client_data_json_ = SerializeCollectedClientDataToJson(
client_data::kGetType, caller_origin_.Serialize(),
std::move(options->challenge));
}
if (options->allow_credentials.empty() &&
(!base::FeatureList::IsEnabled(device::kWebAuthResidentKeys) ||
!request_delegate_->SupportsResidentKeys())) {
InvokeCallbackAndCleanup(
std::move(callback),
blink::mojom::AuthenticatorStatus::RESIDENT_CREDENTIALS_UNSUPPORTED);
return;
}
if (options->appid) {
app_id_ = ProcessAppIdExtension(*options->appid, caller_origin_);
if (!app_id_) {
std::move(callback).Run(blink::mojom::AuthenticatorStatus::INVALID_DOMAIN,
nullptr);
return;
}
}
// U2F requests proxied from the cryptotoken extension are limited to USB
// devices.
const auto transports =
OriginIsCryptoTokenExtension(caller_origin_)
? base::flat_set<device::FidoTransportProtocol>(
{device::FidoTransportProtocol::kUsbHumanInterfaceDevice})
: transports_;
UMA_HISTOGRAM_COUNTS_100(
"WebAuthentication.CredentialRequestAllowCredentialsCount",
options->allow_credentials.size());
DCHECK(get_assertion_response_callback_.is_null());
get_assertion_response_callback_ = std::move(callback);
timer_->Start(
FROM_HERE, options->adjusted_timeout,
base::BindOnce(&AuthenticatorCommon::OnTimeout, base::Unretained(this)));
if (!connector_)
connector_ = ServiceManagerConnection::GetForProcess()->GetConnector();
auto ctap_request = CreateCtapGetAssertionRequest(
client_data_json_, std::move(options), app_id_,
browser_context()->IsOffTheRecord());
auto opt_platform_authenticator_info =
CreatePlatformAuthenticatorIfAvailableAndCheckIfCredentialExists(
ctap_request);
request_ = std::make_unique<device::GetAssertionRequestHandler>(
connector_,
AuthenticatorEnvironmentImpl::GetInstance()->GetFactory(
render_frame_host_),
transports, std::move(ctap_request),
base::BindOnce(&AuthenticatorCommon::OnSignResponse,
weak_factory_.GetWeakPtr()));
request_delegate_->RegisterActionCallbacks(
base::BindOnce(&AuthenticatorCommon::OnCancelFromUI,
weak_factory_.GetWeakPtr()) /* cancel_callback */,
base::BindRepeating(
&device::FidoRequestHandlerBase::StartAuthenticatorRequest,
request_->GetWeakPtr()) /* request_callback */,
base::BindRepeating(
&device::FidoRequestHandlerBase::PowerOnBluetoothAdapter,
request_->GetWeakPtr()) /* bluetooth_adapter_power_on_callback */,
base::BindRepeating(
&device::FidoRequestHandlerBase::InitiatePairingWithDevice,
request_->GetWeakPtr()) /* ble_pairing_callback*/);
request_->set_observer(request_delegate_.get());
request_->SetPlatformAuthenticatorOrMarkUnavailable(
std::move(opt_platform_authenticator_info));
}
void AuthenticatorCommon::IsUserVerifyingPlatformAuthenticatorAvailable(
blink::mojom::Authenticator::
IsUserVerifyingPlatformAuthenticatorAvailableCallback callback) {
const std::string relying_party_id =
render_frame_host_->GetLastCommittedOrigin().host();
// Use |request_delegate_| if a request is currently in progress; or create a
// temporary request delegate otherwise.
//
// Note that |GetWebAuthenticationRequestDelegate| may return nullptr if
// there is an active |request_delegate_| already.
std::unique_ptr<AuthenticatorRequestClientDelegate> maybe_request_delegate =
request_delegate_
? nullptr
: GetContentClient()->browser()->GetWebAuthenticationRequestDelegate(
render_frame_host_, relying_party_id);
AuthenticatorRequestClientDelegate* request_delegate_ptr =
request_delegate_ ? request_delegate_.get()
: maybe_request_delegate.get();
const bool result =
IsUserVerifyingPlatformAuthenticatorAvailableImpl(request_delegate_ptr);
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), result));
}
bool AuthenticatorCommon::IsUserVerifyingPlatformAuthenticatorAvailableImpl(
AuthenticatorRequestClientDelegate* request_delegate) {
if (request_delegate->ShouldDisablePlatformAuthenticators()) {
return false;
}
#if defined(OS_MACOSX)
// Touch ID is disabled, regardless of hardware support, if the embedder
// doesn't support it.
if (!GetContentClient()
->browser()
->IsWebAuthenticationTouchIdAuthenticatorSupported())
return false;
return device::fido::mac::TouchIdAuthenticator::IsAvailable();
#elif defined(OS_WIN)
return base::FeatureList::IsEnabled(device::kWebAuthUseNativeWinApi) &&
device::WinWebAuthnApiAuthenticator::
IsUserVerifyingPlatformAuthenticatorAvailable();
#else
return false;
#endif
}
void AuthenticatorCommon::Cancel() {
CancelWithStatus(blink::mojom::AuthenticatorStatus::ABORT_ERROR);
}
// Callback to handle the async registration response from a U2fDevice.
void AuthenticatorCommon::OnRegisterResponse(
device::FidoReturnCode status_code,
base::Optional<device::AuthenticatorMakeCredentialResponse> response_data,
base::Optional<device::FidoTransportProtocol> transport_used) {
if (!request_) {
// Either the callback was called immediately and |request_| has not yet
// been assigned (this is a bug), or a navigation caused the request to be
// canceled while a callback was enqueued.
return;
}
switch (status_code) {
case device::FidoReturnCode::kUserConsentButCredentialExcluded:
// Duplicate registration: the new credential would be created on an
// authenticator that already contains one of the credentials in
// |exclude_credentials|.
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kKeyAlreadyRegistered);
return;
case device::FidoReturnCode::kAuthenticatorResponseInvalid:
// The response from the authenticator was corrupted.
InvokeCallbackAndCleanup(
std::move(make_credential_response_callback_),
blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr,
Focus::kDoCheck);
return;
case device::FidoReturnCode::kUserConsentButCredentialNotRecognized:
// TODO(crbug/876109): This isn't strictly unreachable.
NOTREACHED();
return;
case device::FidoReturnCode::kUserConsentDenied:
InvokeCallbackAndCleanup(
std::move(make_credential_response_callback_),
blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr,
Focus::kDoCheck);
return;
case device::FidoReturnCode::kSoftPINBlock:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kSoftPINBlock);
return;
case device::FidoReturnCode::kHardPINBlock:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kHardPINBlock);
return;
case device::FidoReturnCode::kAuthenticatorRemovedDuringPINEntry:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kAuthenticatorRemovedDuringPINEntry);
return;
case device::FidoReturnCode::kAuthenticatorMissingResidentKeys:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kAuthenticatorMissingResidentKeys);
return;
case device::FidoReturnCode::kAuthenticatorMissingUserVerification:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kAuthenticatorMissingUserVerification);
return;
case device::FidoReturnCode::kAuthenticatorMissingCredentialManagement:
NOTREACHED()
<< "This should only be reachable from CredentialManagementHandler";
InvokeCallbackAndCleanup(
std::move(make_credential_response_callback_),
blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr,
Focus::kDoCheck);
return;
case device::FidoReturnCode::kStorageFull:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kStorageFull);
return;
case device::FidoReturnCode::kSuccess:
DCHECK(response_data.has_value());
if (transport_used) {
request_delegate_->UpdateLastTransportUsed(*transport_used);
}
bool is_transport_used_internal =
transport_used &&
*transport_used == device::FidoTransportProtocol::kInternal;
if (attestation_requested_) {
// Cryptotoken requests may bypass the attestation prompt because the
// extension implements its own. Invoking the attestation prompt code
// here would not work anyway, because the WebContents associated with
// the extension is not associated with any tab and therefore cannot
// draw modal dialogs for the UI.
//
// Note that for AttestationConveyancePreference::NONE, attestation
// erasure is still performed as usual.
if (OriginIsCryptoTokenExtension(caller_origin_)) {
InvokeCallbackAndCleanup(
std::move(make_credential_response_callback_),
blink::mojom::AuthenticatorStatus::SUCCESS,
CreateMakeCredentialResponse(
std::move(client_data_json_), std::move(*response_data),
AttestationErasureOption::kIncludeAttestation),
Focus::kDoCheck);
return;
}
UMA_HISTOGRAM_ENUMERATION("WebAuthentication.AttestationPromptResult",
AttestationPromptResult::kQueried);
awaiting_attestation_response_ = true;
request_delegate_->ShouldReturnAttestation(
relying_party_id_,
base::BindOnce(
&AuthenticatorCommon::OnRegisterResponseAttestationDecided,
weak_factory_.GetWeakPtr(), std::move(*response_data),
is_transport_used_internal));
return;
}
AttestationErasureOption attestation_erasure =
AttestationErasureOption::kEraseAttestationAndAaguid;
if (response_data->IsSelfAttestation()) {
attestation_erasure = AttestationErasureOption::kIncludeAttestation;
} else if (is_transport_used_internal) {
// Contrary to what the WebAuthn spec says, for internal (platform)
// authenticators we do not erase the AAGUID from authenticatorData,
// even if requested attestationConveyancePreference is "none".
attestation_erasure =
AttestationErasureOption::kEraseAttestationButIncludeAaguid;
}
InvokeCallbackAndCleanup(
std::move(make_credential_response_callback_),
blink::mojom::AuthenticatorStatus::SUCCESS,
CreateMakeCredentialResponse(std::move(client_data_json_),
std::move(*response_data),
attestation_erasure),
Focus::kDoCheck);
return;
}
NOTREACHED();
}
void AuthenticatorCommon::OnRegisterResponseAttestationDecided(
device::AuthenticatorMakeCredentialResponse response_data,
bool is_transport_used_internal,
bool attestation_permitted) {
awaiting_attestation_response_ = false;
if (!request_) {
// The request has already been cleaned up, probably because a navigation
// occurred while the permissions prompt was pending.
return;
}
DCHECK(attestation_requested_);
AttestationErasureOption attestation_erasure;
if (!attestation_permitted) {
UMA_HISTOGRAM_ENUMERATION("WebAuthentication.AttestationPromptResult",
AttestationPromptResult::kBlocked);
if (is_transport_used_internal) {
// For internal (platform) authenticators, we do not erase the
// AAGUID from authenticatorData even if the user declines to
// share attestation.
attestation_erasure =
AttestationErasureOption::kEraseAttestationButIncludeAaguid;
} else {
attestation_erasure =
AttestationErasureOption::kEraseAttestationAndAaguid;
}
} else {
UMA_HISTOGRAM_ENUMERATION("WebAuthentication.AttestationPromptResult",
AttestationPromptResult::kAllowed);
attestation_erasure = AttestationErasureOption::kIncludeAttestation;
}
// The check for IsAttestationCertificateInappropriatelyIdentifying is
// performed after the permissions prompt, even though we know the answer
// before, because this still effectively discloses the make & model of
// the authenticator: If an RP sees a "none" attestation from Chrome after
// requesting direct attestation then it knows that it was one of the
// tokens with inappropriate certs.
if (response_data.IsAttestationCertificateInappropriatelyIdentifying() &&
!request_delegate_->ShouldPermitIndividualAttestation(
relying_party_id_)) {
// The attestation response is incorrectly individually identifiable, but
// the consent is for make & model information about a token, not for
// individually-identifiable information. Erase the attestation to stop it
// begin a tracking signal.
// The only way to get the underlying attestation will be to list the RP ID
// in the enterprise policy, because that enables the individual attestation
// bit in the register request and permits individual attestation generally.
attestation_erasure = AttestationErasureOption::kEraseAttestationAndAaguid;
}
InvokeCallbackAndCleanup(std::move(make_credential_response_callback_),
blink::mojom::AuthenticatorStatus::SUCCESS,
CreateMakeCredentialResponse(
std::move(client_data_json_),
std::move(response_data), attestation_erasure),
Focus::kDoCheck);
}
void AuthenticatorCommon::OnSignResponse(
device::FidoReturnCode status_code,
base::Optional<std::vector<device::AuthenticatorGetAssertionResponse>>
response_data,
base::Optional<device::FidoTransportProtocol> transport_used) {
DCHECK(!response_data || !response_data->empty()); // empty vector is invalid
if (!request_) {
// Either the callback was called immediately and |request_| has not yet
// been assigned (this is a bug), or a navigation caused the request to be
// canceled while a callback was enqueued.
return;
}
switch (status_code) {
case device::FidoReturnCode::kUserConsentButCredentialNotRecognized:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kKeyNotRegistered);
return;
case device::FidoReturnCode::kAuthenticatorResponseInvalid:
// The response from the authenticator was corrupted.
InvokeCallbackAndCleanup(
std::move(get_assertion_response_callback_),
blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR);
return;
case device::FidoReturnCode::kUserConsentButCredentialExcluded:
// TODO(crbug/876109): This isn't strictly unreachable.
NOTREACHED();
return;
case device::FidoReturnCode::kUserConsentDenied:
InvokeCallbackAndCleanup(
std::move(get_assertion_response_callback_),
blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR);
return;
case device::FidoReturnCode::kSoftPINBlock:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kSoftPINBlock);
return;
case device::FidoReturnCode::kHardPINBlock:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kHardPINBlock);
return;
case device::FidoReturnCode::kAuthenticatorRemovedDuringPINEntry:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kAuthenticatorRemovedDuringPINEntry);
return;
case device::FidoReturnCode::kAuthenticatorMissingResidentKeys:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kAuthenticatorMissingResidentKeys);
return;
case device::FidoReturnCode::kAuthenticatorMissingUserVerification:
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kAuthenticatorMissingUserVerification);
return;
case device::FidoReturnCode::kAuthenticatorMissingCredentialManagement:
NOTREACHED()
<< "This should only be reachable from CredentialManagementHandler";
InvokeCallbackAndCleanup(
std::move(get_assertion_response_callback_),
blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR);
return;
case device::FidoReturnCode::kStorageFull:
NOTREACHED() << "Should not be possible for assertions.";
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::
kStorageFull);
return;
case device::FidoReturnCode::kSuccess:
DCHECK(response_data.has_value());
if (transport_used) {
request_delegate_->UpdateLastTransportUsed(*transport_used);
}
if (response_data->size() == 1) {
OnAccountSelected(std::move(response_data->at(0)));
} else {
request_delegate_->SelectAccount(
std::move(*response_data),
base::BindOnce(&AuthenticatorCommon::OnAccountSelected,
weak_factory_.GetWeakPtr()));
}
return;
}
NOTREACHED();
}
void AuthenticatorCommon::OnAccountSelected(
device::AuthenticatorGetAssertionResponse response) {
base::Optional<bool> echo_appid_extension;
if (app_id_) {
echo_appid_extension =
(response.GetRpIdHash() == CreateApplicationParameter(*app_id_));
}
InvokeCallbackAndCleanup(
std::move(get_assertion_response_callback_),
blink::mojom::AuthenticatorStatus::SUCCESS,
CreateGetAssertionResponse(std::move(client_data_json_),
std::move(response), echo_appid_extension));
return;
}
void AuthenticatorCommon::SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason reason) {
blink::mojom::AuthenticatorStatus status =
blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
switch (reason) {
case AuthenticatorRequestClientDelegate::InterestingFailureReason::
kKeyAlreadyRegistered:
status = blink::mojom::AuthenticatorStatus::CREDENTIAL_EXCLUDED;
break;
case AuthenticatorRequestClientDelegate::InterestingFailureReason::
kKeyNotRegistered:
status = blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
break;
case AuthenticatorRequestClientDelegate::InterestingFailureReason::kTimeout:
status = blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
break;
case AuthenticatorRequestClientDelegate::InterestingFailureReason::
kSoftPINBlock:
status = blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
break;
case AuthenticatorRequestClientDelegate::InterestingFailureReason::
kHardPINBlock:
status = blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
break;
case AuthenticatorRequestClientDelegate::InterestingFailureReason::
kAuthenticatorRemovedDuringPINEntry:
status = blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
break;
case AuthenticatorRequestClientDelegate::InterestingFailureReason::
kAuthenticatorMissingResidentKeys:
status = blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
break;
case AuthenticatorRequestClientDelegate::InterestingFailureReason::
kAuthenticatorMissingUserVerification:
status = blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
break;
case AuthenticatorRequestClientDelegate::InterestingFailureReason::
kStorageFull:
status = blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
break;
}
error_awaiting_user_acknowledgement_ = status;
// If WebAuthnUi is enabled, this error blocks until after receiving user
// acknowledgement. Otherwise, the error is returned right away.
if (request_delegate_->DoesBlockRequestOnFailure(reason)) {
// Cancel pending authenticator requests before the error dialog is shown.
request_->CancelActiveAuthenticators();
return;
}
CancelWithStatus(error_awaiting_user_acknowledgement_);
} // namespace content
// TODO(crbug.com/814418): Add web tests to verify timeouts are
// indistinguishable from NOT_ALLOWED_ERROR cases.
void AuthenticatorCommon::OnTimeout() {
DCHECK(request_delegate_);
if (awaiting_attestation_response_) {
UMA_HISTOGRAM_ENUMERATION("WebAuthentication.AttestationPromptResult",
AttestationPromptResult::kTimeout);
awaiting_attestation_response_ = false;
}
SignalFailureToRequestDelegate(
AuthenticatorRequestClientDelegate::InterestingFailureReason::kTimeout);
}
void AuthenticatorCommon::CancelWithStatus(
blink::mojom::AuthenticatorStatus status) {
// If response callback is invoked already, then ignore cancel request.
if (!make_credential_response_callback_ && !get_assertion_response_callback_)
return;
if (make_credential_response_callback_) {
InvokeCallbackAndCleanup(std::move(make_credential_response_callback_),
status);
} else if (get_assertion_response_callback_) {
InvokeCallbackAndCleanup(std::move(get_assertion_response_callback_),
status);
}
}
void AuthenticatorCommon::OnCancelFromUI() {
CancelWithStatus(error_awaiting_user_acknowledgement_);
}
void AuthenticatorCommon::InvokeCallbackAndCleanup(
blink::mojom::Authenticator::MakeCredentialCallback callback,
blink::mojom::AuthenticatorStatus status,
blink::mojom::MakeCredentialAuthenticatorResponsePtr response,
Focus check_focus) {
if (check_focus != Focus::kDontCheck && !(request_delegate_ && IsFocused())) {
std::move(callback).Run(blink::mojom::AuthenticatorStatus::NOT_FOCUSED,
nullptr);
} else {
std::move(callback).Run(status, std::move(response));
}
Cleanup();
}
void AuthenticatorCommon::InvokeCallbackAndCleanup(
blink::mojom::Authenticator::GetAssertionCallback callback,
blink::mojom::AuthenticatorStatus status,
blink::mojom::GetAssertionAuthenticatorResponsePtr response) {
std::move(callback).Run(status, std::move(response));
Cleanup();
}
void AuthenticatorCommon::Cleanup() {
if (awaiting_attestation_response_) {
UMA_HISTOGRAM_ENUMERATION("WebAuthentication.AttestationPromptResult",
AttestationPromptResult::kAbandoned);
awaiting_attestation_response_ = false;
}
timer_->Stop();
request_.reset();
request_delegate_.reset();
make_credential_response_callback_.Reset();
get_assertion_response_callback_.Reset();
client_data_json_.clear();
app_id_.reset();
caller_origin_ = url::Origin();
relying_party_id_.clear();
attestation_requested_ = false;
error_awaiting_user_acknowledgement_ =
blink::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR;
}
BrowserContext* AuthenticatorCommon::browser_context() const {
return content::WebContents::FromRenderFrameHost(render_frame_host_)
->GetBrowserContext();
}
#if defined(OS_MACOSX)
namespace {
std::unique_ptr<device::fido::mac::TouchIdAuthenticator>
CreateTouchIdAuthenticatorIfAvailable(
const AuthenticatorRequestClientDelegate* request_delegate) {
// Not all embedders may provide an authenticator config.
auto opt_authenticator_config =
request_delegate->GetTouchIdAuthenticatorConfig();
if (!opt_authenticator_config) {
return nullptr;
}
return device::fido::mac::TouchIdAuthenticator::CreateIfAvailable(
std::move(opt_authenticator_config->keychain_access_group),
std::move(opt_authenticator_config->metadata_secret));
}
} // namespace
#endif
base::Optional<device::PlatformAuthenticatorInfo>
AuthenticatorCommon::CreatePlatformAuthenticatorIfAvailable() {
// Incognito mode disables platform authenticators, so check for availability
// first.
if (!IsUserVerifyingPlatformAuthenticatorAvailableImpl(
request_delegate_.get())) {
return base::nullopt;
}
#if defined(OS_MACOSX)
return device::PlatformAuthenticatorInfo(
CreateTouchIdAuthenticatorIfAvailable(request_delegate_.get()), false);
#else
return base::nullopt;
#endif
}
base::Optional<device::PlatformAuthenticatorInfo> AuthenticatorCommon::
CreatePlatformAuthenticatorIfAvailableAndCheckIfCredentialExists(
const device::CtapGetAssertionRequest& request) {
// Incognito mode disables platform authenticators, so check for availability
// first.
if (!IsUserVerifyingPlatformAuthenticatorAvailableImpl(
request_delegate_.get())) {
return base::nullopt;
}
#if defined(OS_MACOSX)
std::unique_ptr<device::fido::mac::TouchIdAuthenticator> authenticator =
CreateTouchIdAuthenticatorIfAvailable(request_delegate_.get());
const bool has_credential =
authenticator->HasCredentialForGetAssertionRequest(request);
return device::PlatformAuthenticatorInfo(std::move(authenticator),
has_credential);
#else
return base::nullopt;
#endif
}
} // namespace content