| // Copyright 2017 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_impl.h" |
| |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/base64url.h" |
| #include "base/json/json_writer.h" |
| #include "base/logging.h" |
| #include "base/rand_util.h" |
| #include "base/timer/timer.h" |
| #include "content/browser/bad_message.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.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/fido/u2f_register.h" |
| #include "device/fido/u2f_request.h" |
| #include "device/fido/u2f_sign.h" |
| #include "device/fido/u2f_transport_protocol.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.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" |
| |
| namespace content { |
| |
| namespace client_data { |
| const char kCreateType[] = "webauthn.create"; |
| const char kGetType[] = "webauthn.get"; |
| } // namespace client_data |
| |
| namespace { |
| constexpr int32_t kCoseEs256 = -7; |
| |
| // 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) { |
| return !caller_origin.unique() && |
| !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 (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; |
| } |
| |
| bool IsAppIdAllowedForOrigin(const GURL& appid, const url::Origin& origin) { |
| // See |
| // 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 |
| |
| // 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." |
| |
| // This step is handled before calling this function. |
| |
| // 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." |
| if (origin.scheme() != url::kHttpsScheme || |
| appid.scheme_piece() != origin.scheme()) { |
| return false; |
| } |
| |
| // This check is repeated inside |SameDomainOrHost|, just after this. However |
| // it's cheap and mirrors the structure of the spec. |
| if (appid.host_piece() == origin.host()) { |
| return true; |
| } |
| |
| // 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 |
| // neccessary in practice. See also |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=1244959#c8 |
| if (net::registry_controlled_domains::SameDomainOrHost( |
| appid, origin, |
| net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) { |
| return true; |
| } |
| |
| // 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.has_ref() && |
| (appid.EqualsIgnoringRef(kGstatic1) || |
| appid.EqualsIgnoringRef(kGstatic2))) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // Check that at least one of the cryptographic parameters is supported. |
| // Only ES256 is currently supported by U2F_V2 (CTAP 1.0). |
| bool IsAlgorithmSupportedByU2fAuthenticators( |
| const std::vector<webauth::mojom::PublicKeyCredentialParametersPtr>& |
| parameters) { |
| for (const auto& params : parameters) { |
| if (params->algorithm_identifier == kCoseEs256) |
| return true; |
| } |
| return false; |
| } |
| |
| // Verify that the request doesn't contain parameters that U2F authenticators |
| // cannot fulfill. |
| bool AreOptionsSupportedByU2fAuthenticators( |
| const webauth::mojom::PublicKeyCredentialCreationOptionsPtr& options) { |
| if (options->authenticator_selection) { |
| if (options->authenticator_selection->user_verification == |
| webauth::mojom::UserVerificationRequirement::REQUIRED || |
| options->authenticator_selection->require_resident_key) |
| return false; |
| } |
| if (!IsAlgorithmSupportedByU2fAuthenticators(options->public_key_parameters)) |
| return false; |
| return true; |
| } |
| |
| std::vector<std::vector<uint8_t>> FilterCredentialList( |
| const std::vector<webauth::mojom::PublicKeyCredentialDescriptorPtr>& |
| descriptors) { |
| std::vector<std::vector<uint8_t>> handles; |
| for (const auto& credential_descriptor : descriptors) { |
| if (credential_descriptor->type == |
| webauth::mojom::PublicKeyCredentialType::PUBLIC_KEY) { |
| handles.push_back(credential_descriptor->id); |
| } |
| } |
| return handles; |
| } |
| |
| std::vector<uint8_t> ConstructClientDataHash(const std::string& client_data) { |
| // SHA-256 hash of the JSON data structure. |
| std::vector<uint8_t> client_data_hash(crypto::kSHA256Length); |
| crypto::SHA256HashString(client_data, client_data_hash.data(), |
| client_data_hash.size()); |
| return client_data_hash; |
| } |
| |
| // 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::vector<uint8_t> CreateApplicationParameter( |
| const std::string& relying_party_id) { |
| std::vector<uint8_t> application_parameter(crypto::kSHA256Length); |
| crypto::SHA256HashString(relying_party_id, application_parameter.data(), |
| application_parameter.size()); |
| return application_parameter; |
| } |
| |
| base::Optional<std::vector<uint8_t>> ProcessAppIdExtension( |
| std::string appid, |
| const url::Origin& caller_origin) { |
| if (appid.empty()) { |
| // See step two in the comments in |IsAppIdAllowedForOrigin|. |
| appid = caller_origin.Serialize() + "/"; |
| } |
| |
| GURL appid_url = GURL(appid); |
| if (!(appid_url.is_valid() && |
| IsAppIdAllowedForOrigin(appid_url, caller_origin))) { |
| return base::nullopt; |
| } |
| |
| return CreateApplicationParameter(appid); |
| } |
| |
| webauth::mojom::MakeCredentialAuthenticatorResponsePtr |
| CreateMakeCredentialResponse( |
| const std::string& client_data_json, |
| device::AuthenticatorMakeCredentialResponse response_data) { |
| auto response = webauth::mojom::MakeCredentialAuthenticatorResponse::New(); |
| auto common_info = webauth::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->attestation_object = |
| response_data.GetCBOREncodedAttestationObject(); |
| return response; |
| } |
| |
| webauth::mojom::GetAssertionAuthenticatorResponsePtr CreateGetAssertionResponse( |
| const std::string& client_data_json, |
| device::AuthenticatorGetAssertionResponse response_data, |
| bool echo_appid_extension) { |
| auto response = webauth::mojom::GetAssertionAuthenticatorResponse::New(); |
| auto common_info = webauth::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(); |
| response->echo_appid_extension = echo_appid_extension; |
| response_data.user_entity() |
| ? response->user_handle.emplace(response_data.user_entity()->user_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; |
| } |
| |
| } // namespace |
| |
| AuthenticatorImpl::AuthenticatorImpl(RenderFrameHost* render_frame_host) |
| : timer_(std::make_unique<base::OneShotTimer>()), |
| render_frame_host_(render_frame_host), |
| weak_factory_(this) { |
| DCHECK(render_frame_host_); |
| DCHECK(timer_); |
| |
| protocols_.insert(device::U2fTransportProtocol::kUsbHumanInterfaceDevice); |
| if (base::FeatureList::IsEnabled(features::kWebAuthBle)) { |
| protocols_.insert(device::U2fTransportProtocol::kBluetoothLowEnergy); |
| } |
| } |
| |
| AuthenticatorImpl::AuthenticatorImpl(RenderFrameHost* render_frame_host, |
| service_manager::Connector* connector, |
| std::unique_ptr<base::OneShotTimer> timer) |
| : protocols_({/* no protocols in tests */}), |
| timer_(std::move(timer)), |
| render_frame_host_(render_frame_host), |
| connector_(connector), |
| weak_factory_(this) { |
| DCHECK(render_frame_host_); |
| DCHECK(timer_); |
| } |
| |
| AuthenticatorImpl::~AuthenticatorImpl() { |
| bindings_.CloseAllBindings(); |
| } |
| |
| void AuthenticatorImpl::Bind(webauth::mojom::AuthenticatorRequest request) { |
| bindings_.AddBinding(this, std::move(request)); |
| } |
| |
| // static |
| std::string AuthenticatorImpl::SerializeCollectedClientDataToJson( |
| const std::string& type, |
| const url::Origin& origin, |
| base::span<const uint8_t> challenge, |
| base::Optional<base::span<const uint8_t>> token_binding) { |
| static constexpr char kTypeKey[] = "type"; |
| static constexpr char kChallengeKey[] = "challenge"; |
| static constexpr char kOriginKey[] = "origin"; |
| static constexpr char kTokenBindingKey[] = "tokenBinding"; |
| |
| base::DictionaryValue client_data; |
| client_data.SetKey(kTypeKey, base::Value(type)); |
| client_data.SetKey(kChallengeKey, base::Value(Base64UrlEncode(challenge))); |
| client_data.SetKey(kOriginKey, base::Value(origin.Serialize())); |
| |
| base::DictionaryValue token_binding_dict; |
| static constexpr char kTokenBindingStatusKey[] = "status"; |
| static constexpr char kTokenBindingIdKey[] = "id"; |
| static constexpr char kTokenBindingSupportedStatus[] = "supported"; |
| static constexpr char kTokenBindingNotSupportedStatus[] = "not-supported"; |
| static constexpr char kTokenBindingPresentStatus[] = "present"; |
| |
| if (token_binding) { |
| if (token_binding->empty()) { |
| token_binding_dict.SetKey(kTokenBindingStatusKey, |
| base::Value(kTokenBindingSupportedStatus)); |
| } else { |
| token_binding_dict.SetKey(kTokenBindingStatusKey, |
| base::Value(kTokenBindingPresentStatus)); |
| token_binding_dict.SetKey(kTokenBindingIdKey, |
| base::Value(Base64UrlEncode(*token_binding))); |
| } |
| } else { |
| token_binding_dict.SetKey(kTokenBindingStatusKey, |
| base::Value(kTokenBindingNotSupportedStatus)); |
| } |
| |
| client_data.SetKey(kTokenBindingKey, std::move(token_binding_dict)); |
| |
| 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("new_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 AuthenticatorImpl::MakeCredential( |
| webauth::mojom::PublicKeyCredentialCreationOptionsPtr options, |
| MakeCredentialCallback callback) { |
| if (u2f_request_) { |
| std::move(callback).Run( |
| webauth::mojom::AuthenticatorStatus::PENDING_REQUEST, nullptr); |
| return; |
| } |
| |
| url::Origin caller_origin = render_frame_host_->GetLastCommittedOrigin(); |
| relying_party_id_ = options->relying_party->id; |
| |
| if (!HasValidEffectiveDomain(caller_origin)) { |
| bad_message::ReceivedBadMessage(render_frame_host_->GetProcess(), |
| bad_message::AUTH_INVALID_EFFECTIVE_DOMAIN); |
| InvokeCallbackAndCleanup( |
| std::move(callback), |
| webauth::mojom::AuthenticatorStatus::INVALID_DOMAIN, nullptr); |
| return; |
| } |
| |
| if (!IsRelyingPartyIdValid(relying_party_id_, caller_origin)) { |
| bad_message::ReceivedBadMessage(render_frame_host_->GetProcess(), |
| bad_message::AUTH_INVALID_RELYING_PARTY); |
| InvokeCallbackAndCleanup( |
| std::move(callback), |
| webauth::mojom::AuthenticatorStatus::INVALID_DOMAIN, nullptr); |
| return; |
| } |
| |
| // Verify that the request doesn't contain parameters that U2F authenticators |
| // cannot fulfill. |
| // TODO(crbug.com/819256): Improve messages for "Not Supported" errors. |
| if (!AreOptionsSupportedByU2fAuthenticators(options)) { |
| InvokeCallbackAndCleanup( |
| std::move(callback), |
| webauth::mojom::AuthenticatorStatus::NOT_SUPPORTED_ERROR, nullptr); |
| return; |
| } |
| |
| DCHECK(make_credential_response_callback_.is_null()); |
| make_credential_response_callback_ = std::move(callback); |
| |
| timer_->Start( |
| FROM_HERE, options->adjusted_timeout, |
| base::Bind(&AuthenticatorImpl::OnTimeout, base::Unretained(this))); |
| if (!connector_) |
| connector_ = ServiceManagerConnection::GetForProcess()->GetConnector(); |
| |
| // Extract list of credentials to exclude. |
| std::vector<std::vector<uint8_t>> registered_keys; |
| for (const auto& credential : options->exclude_credentials) { |
| registered_keys.push_back(credential->id); |
| } |
| |
| // Save client data to return with the authenticator response. |
| // TODO(kpaulhamus): Fetch and add the Token Binding ID public key used to |
| // communicate with the origin. |
| client_data_json_ = SerializeCollectedClientDataToJson( |
| client_data::kCreateType, caller_origin, std::move(options->challenge), |
| base::nullopt); |
| |
| const bool individual_attestation = |
| GetContentClient() |
| ->browser() |
| ->ShouldPermitIndividualAttestationForWebauthnRPID( |
| render_frame_host_->GetProcess()->GetBrowserContext(), |
| relying_party_id_); |
| |
| attestation_preference_ = options->attestation; |
| |
| // TODO(kpaulhamus): Mock U2fRegister for unit tests. |
| // http://crbug.com/785955. |
| // Per fido-u2f-raw-message-formats: |
| // The challenge parameter is the SHA-256 hash of the Client Data, |
| // Among other things, the Client Data contains the challenge from the |
| // relying party (hence the name of the parameter). |
| u2f_request_ = device::U2fRegister::TryRegistration( |
| connector_, protocols_, registered_keys, |
| ConstructClientDataHash(client_data_json_), |
| CreateApplicationParameter(relying_party_id_), individual_attestation, |
| base::BindOnce(&AuthenticatorImpl::OnRegisterResponse, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| // mojom:Authenticator |
| void AuthenticatorImpl::GetAssertion( |
| webauth::mojom::PublicKeyCredentialRequestOptionsPtr options, |
| GetAssertionCallback callback) { |
| if (u2f_request_) { |
| std::move(callback).Run( |
| webauth::mojom::AuthenticatorStatus::PENDING_REQUEST, nullptr); |
| return; |
| } |
| |
| url::Origin caller_origin = render_frame_host_->GetLastCommittedOrigin(); |
| |
| if (!HasValidEffectiveDomain(caller_origin)) { |
| bad_message::ReceivedBadMessage(render_frame_host_->GetProcess(), |
| bad_message::AUTH_INVALID_EFFECTIVE_DOMAIN); |
| InvokeCallbackAndCleanup( |
| std::move(callback), |
| webauth::mojom::AuthenticatorStatus::INVALID_DOMAIN, nullptr); |
| return; |
| } |
| |
| if (!IsRelyingPartyIdValid(options->relying_party_id, caller_origin)) { |
| bad_message::ReceivedBadMessage(render_frame_host_->GetProcess(), |
| bad_message::AUTH_INVALID_RELYING_PARTY); |
| InvokeCallbackAndCleanup( |
| std::move(callback), |
| webauth::mojom::AuthenticatorStatus::INVALID_DOMAIN, nullptr); |
| return; |
| } |
| |
| // To use U2F, the relying party must not require user verification. |
| if (options->user_verification == |
| webauth::mojom::UserVerificationRequirement::REQUIRED) { |
| InvokeCallbackAndCleanup( |
| std::move(callback), |
| webauth::mojom::AuthenticatorStatus::NOT_SUPPORTED_ERROR, nullptr); |
| return; |
| } |
| |
| std::vector<uint8_t> application_parameter = |
| CreateApplicationParameter(options->relying_party_id); |
| |
| base::Optional<std::vector<uint8_t>> alternative_application_parameter; |
| if (options->appid) { |
| auto appid_hash = ProcessAppIdExtension(*options->appid, caller_origin); |
| if (!appid_hash) { |
| std::move(callback).Run( |
| webauth::mojom::AuthenticatorStatus::INVALID_DOMAIN, nullptr); |
| return; |
| } |
| |
| alternative_application_parameter = std::move(appid_hash); |
| // TODO(agl): needs a test once a suitable, mock U2F device exists. |
| echo_appid_extension_ = true; |
| } |
| |
| DCHECK(get_assertion_response_callback_.is_null()); |
| get_assertion_response_callback_ = std::move(callback); |
| |
| // Pass along valid keys from allow_list, if any. |
| std::vector<std::vector<uint8_t>> handles = |
| FilterCredentialList(std::move(options->allow_credentials)); |
| |
| timer_->Start( |
| FROM_HERE, options->adjusted_timeout, |
| base::Bind(&AuthenticatorImpl::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 Token Binding ID public key used to |
| // communicate with the origin. |
| client_data_json_ = SerializeCollectedClientDataToJson( |
| client_data::kGetType, caller_origin, std::move(options->challenge), |
| base::nullopt); |
| |
| u2f_request_ = device::U2fSign::TrySign( |
| connector_, protocols_, handles, |
| ConstructClientDataHash(client_data_json_), application_parameter, |
| alternative_application_parameter, |
| base::BindOnce(&AuthenticatorImpl::OnSignResponse, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| // Callback to handle the async registration response from a U2fDevice. |
| void AuthenticatorImpl::OnRegisterResponse( |
| device::FidoReturnCode status_code, |
| base::Optional<device::AuthenticatorMakeCredentialResponse> response_data) { |
| // If callback is called immediately, this code will call |Cleanup| before |
| // |u2f_request_| has been assigned – violating invariants. |
| DCHECK(u2f_request_) << "unsupported callback hairpin"; |
| |
| switch (status_code) { |
| case device::FidoReturnCode::kConditionsNotSatisfied: |
| // Duplicate registration: the new credential would be created on an |
| // authenticator that already contains one of the credentials in |
| // |exclude_credentials|. |
| InvokeCallbackAndCleanup( |
| std::move(make_credential_response_callback_), |
| webauth::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr); |
| return; |
| case device::FidoReturnCode::kFailure: |
| // The response from the authenticator was corrupted. |
| InvokeCallbackAndCleanup( |
| std::move(make_credential_response_callback_), |
| webauth::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr); |
| return; |
| case device::FidoReturnCode::kInvalidParams: |
| NOTREACHED(); |
| return; |
| case device::FidoReturnCode::kSuccess: |
| DCHECK(response_data.has_value()); |
| |
| if (attestation_preference_ != |
| webauth::mojom::AttestationConveyancePreference::NONE) { |
| // Potentially show a permission prompt before returning the |
| // attestation data. |
| GetContentClient()->browser()->ShouldReturnAttestationForWebauthnRPID( |
| render_frame_host_, relying_party_id_, |
| render_frame_host_->GetLastCommittedOrigin(), |
| base::BindOnce( |
| &AuthenticatorImpl::OnRegisterResponseAttestationDecided, |
| weak_factory_.GetWeakPtr(), std::move(*response_data))); |
| return; |
| } |
| |
| response_data->EraseAttestationStatement(); |
| InvokeCallbackAndCleanup( |
| std::move(make_credential_response_callback_), |
| webauth::mojom::AuthenticatorStatus::SUCCESS, |
| CreateMakeCredentialResponse(std::move(client_data_json_), |
| std::move(*response_data))); |
| return; |
| } |
| NOTREACHED(); |
| } |
| |
| void AuthenticatorImpl::OnRegisterResponseAttestationDecided( |
| device::AuthenticatorMakeCredentialResponse response_data, |
| bool attestation_permitted) { |
| DCHECK(attestation_preference_ != |
| webauth::mojom::AttestationConveyancePreference::NONE); |
| |
| if (!attestation_permitted) { |
| InvokeCallbackAndCleanup( |
| std::move(make_credential_response_callback_), |
| webauth::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr); |
| return; |
| } |
| |
| // 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() && |
| !GetContentClient() |
| ->browser() |
| ->ShouldPermitIndividualAttestationForWebauthnRPID( |
| render_frame_host_->GetProcess()->GetBrowserContext(), |
| 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. |
| response_data.EraseAttestationStatement(); |
| } |
| |
| InvokeCallbackAndCleanup( |
| std::move(make_credential_response_callback_), |
| webauth::mojom::AuthenticatorStatus::SUCCESS, |
| CreateMakeCredentialResponse(std::move(client_data_json_), |
| std::move(response_data))); |
| } |
| |
| void AuthenticatorImpl::OnSignResponse( |
| device::FidoReturnCode status_code, |
| base::Optional<device::AuthenticatorGetAssertionResponse> response_data) { |
| // If callback is called immediately, this code will call |Cleanup| before |
| // |u2f_request_| has been assigned – violating invariants. |
| DCHECK(u2f_request_) << "unsupported callback hairpin"; |
| |
| switch (status_code) { |
| case device::FidoReturnCode::kConditionsNotSatisfied: |
| // No authenticators contained the credential. |
| InvokeCallbackAndCleanup( |
| std::move(get_assertion_response_callback_), |
| webauth::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr); |
| return; |
| case device::FidoReturnCode::kFailure: |
| // The response from the authenticator was corrupted. |
| InvokeCallbackAndCleanup( |
| std::move(make_credential_response_callback_), |
| webauth::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr); |
| return; |
| case device::FidoReturnCode::kInvalidParams: |
| NOTREACHED(); |
| return; |
| case device::FidoReturnCode::kSuccess: |
| DCHECK(response_data.has_value()); |
| InvokeCallbackAndCleanup( |
| std::move(get_assertion_response_callback_), |
| webauth::mojom::AuthenticatorStatus::SUCCESS, |
| CreateGetAssertionResponse(std::move(client_data_json_), |
| std::move(*response_data), |
| echo_appid_extension_)); |
| return; |
| } |
| NOTREACHED(); |
| } |
| |
| void AuthenticatorImpl::OnTimeout() { |
| // TODO(crbug.com/814418): Add layout tests to verify timeouts are |
| // indistinguishable from NOT_ALLOWED_ERROR cases. |
| DCHECK(make_credential_response_callback_ || |
| get_assertion_response_callback_); |
| if (make_credential_response_callback_) { |
| InvokeCallbackAndCleanup( |
| std::move(make_credential_response_callback_), |
| webauth::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr); |
| } else if (get_assertion_response_callback_) { |
| InvokeCallbackAndCleanup( |
| std::move(get_assertion_response_callback_), |
| webauth::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr); |
| } |
| } |
| |
| void AuthenticatorImpl::InvokeCallbackAndCleanup( |
| MakeCredentialCallback callback, |
| webauth::mojom::AuthenticatorStatus status, |
| webauth::mojom::MakeCredentialAuthenticatorResponsePtr response) { |
| std::move(callback).Run(status, std::move(response)); |
| Cleanup(); |
| } |
| |
| void AuthenticatorImpl::InvokeCallbackAndCleanup( |
| GetAssertionCallback callback, |
| webauth::mojom::AuthenticatorStatus status, |
| webauth::mojom::GetAssertionAuthenticatorResponsePtr response) { |
| std::move(callback).Run(status, std::move(response)); |
| Cleanup(); |
| } |
| |
| void AuthenticatorImpl::Cleanup() { |
| timer_->Stop(); |
| u2f_request_.reset(); |
| make_credential_response_callback_.Reset(); |
| get_assertion_response_callback_.Reset(); |
| client_data_json_.clear(); |
| echo_appid_extension_ = false; |
| } |
| |
| } // namespace content |