| // 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_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_, 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_, 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::CREDENTIAL_NOT_RECOGNIZED; |
| 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 |