blob: 43c557f7f1906c710cd929ca574aa879cbc7829d [file] [log] [blame]
// 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/logging.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/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_return_code.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 "services/service_manager/public/cpp/connector.h"
#include "url/url_util.h"
namespace content {
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()))
? false
: true;
}
// 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 HasValidAlgorithm(
const std::vector<webauth::mojom::PublicKeyCredentialParametersPtr>&
parameters) {
for (const auto& params : parameters) {
if (params->algorithm_identifier == kCoseEs256)
return true;
}
return false;
}
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> CreateAppId(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;
}
webauth::mojom::MakeCredentialAuthenticatorResponsePtr
CreateMakeCredentialResponse(CollectedClientData client_data,
device::RegisterResponseData response_data) {
auto response = webauth::mojom::MakeCredentialAuthenticatorResponse::New();
auto common_info = webauth::mojom::CommonCredentialInfo::New();
std::string client_data_json = client_data.SerializeToJson();
common_info->client_data_json.assign(client_data_json.begin(),
client_data_json.end());
common_info->raw_id = response_data.raw_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(
CollectedClientData client_data,
device::SignResponseData response_data) {
auto response = webauth::mojom::GetAssertionAuthenticatorResponse::New();
auto common_info = webauth::mojom::CommonCredentialInfo::New();
std::string client_data_json = client_data.SerializeToJson();
common_info->client_data_json.assign(client_data_json.begin(),
client_data_json.end());
common_info->raw_id = response_data.raw_id();
common_info->id = response_data.GetId();
response->info = std::move(common_info);
response->authenticator_data = response_data.GetAuthenticatorDataBytes();
response->signature = response_data.signature();
response->user_handle.emplace();
return response;
}
} // 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_);
}
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() {}
void AuthenticatorImpl::Bind(webauth::mojom::AuthenticatorRequest request) {
bindings_.AddBinding(this, std::move(request));
}
// 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();
if (!HasValidEffectiveDomain(caller_origin)) {
bad_message::ReceivedBadMessage(render_frame_host_->GetProcess(),
bad_message::AUTH_INVALID_EFFECTIVE_DOMAIN);
std::move(callback).Run(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);
std::move(callback).Run(webauth::mojom::AuthenticatorStatus::INVALID_DOMAIN,
nullptr);
return;
}
// Check that at least one of the cryptographic parameters is supported.
// Only ES256 is currently supported by U2F_V2.
if (!HasValidAlgorithm(options->public_key_parameters)) {
std::move(callback).Run(
webauth::mojom::AuthenticatorStatus::NOT_SUPPORTED_ERROR, nullptr);
return;
}
DCHECK(make_credential_response_callback_.is_null());
make_credential_response_callback_ = std::move(callback);
client_data_ = CollectedClientData::Create(client_data::kCreateType,
caller_origin.Serialize(),
std::move(options->challenge));
// SHA-256 hash of the JSON data structure.
std::vector<uint8_t> client_data_hash(crypto::kSHA256Length);
crypto::SHA256HashString(client_data_.SerializeToJson(),
client_data_hash.data(), client_data_hash.size());
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.
client_data_ = CollectedClientData::Create(client_data::kCreateType,
caller_origin.Serialize(),
std::move(options->challenge));
const bool individual_attestation =
GetContentClient()
->browser()
->ShouldPermitIndividualAttestationForWebauthnRPID(
render_frame_host_->GetProcess()->GetBrowserContext(),
options->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(
options->relying_party->id, connector_, protocols_, registered_keys,
ConstructClientDataHash(client_data_.SerializeToJson()),
CreateAppId(options->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);
std::move(callback).Run(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);
std::move(callback).Run(webauth::mojom::AuthenticatorStatus::INVALID_DOMAIN,
nullptr);
return;
}
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.
client_data_ = CollectedClientData::Create(client_data::kGetType,
caller_origin.Serialize(),
std::move(options->challenge));
u2f_request_ = device::U2fSign::TrySign(
options->relying_party_id, connector_, protocols_, handles,
ConstructClientDataHash(client_data_.SerializeToJson()),
CreateAppId(options->relying_party_id),
base::BindOnce(&AuthenticatorImpl::OnSignResponse,
weak_factory_.GetWeakPtr()));
}
// Callback to handle the async registration response from a U2fDevice.
void AuthenticatorImpl::OnRegisterResponse(
device::U2fReturnCode status_code,
base::Optional<device::RegisterResponseData> response_data) {
timer_->Stop();
switch (status_code) {
case device::U2fReturnCode::CONDITIONS_NOT_SATISFIED:
// Duplicate registration.
std::move(make_credential_response_callback_)
.Run(webauth::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr);
break;
case device::U2fReturnCode::FAILURE:
case device::U2fReturnCode::INVALID_PARAMS:
std::move(make_credential_response_callback_)
.Run(webauth::mojom::AuthenticatorStatus::UNKNOWN_ERROR, nullptr);
break;
case device::U2fReturnCode::SUCCESS:
DCHECK(response_data.has_value());
if (attestation_preference_ ==
webauth::mojom::AttestationConveyancePreference::NONE) {
response_data->EraseAttestationStatement();
}
std::move(make_credential_response_callback_)
.Run(webauth::mojom::AuthenticatorStatus::SUCCESS,
CreateMakeCredentialResponse(std::move(client_data_),
std::move(*response_data)));
break;
}
Cleanup();
}
void AuthenticatorImpl::OnSignResponse(
device::U2fReturnCode status_code,
base::Optional<device::SignResponseData> response_data) {
timer_->Stop();
switch (status_code) {
case device::U2fReturnCode::CONDITIONS_NOT_SATISFIED:
// No authenticators contained the credential.
std::move(get_assertion_response_callback_)
.Run(webauth::mojom::AuthenticatorStatus::NOT_ALLOWED_ERROR, nullptr);
break;
case device::U2fReturnCode::FAILURE:
case device::U2fReturnCode::INVALID_PARAMS:
std::move(get_assertion_response_callback_)
.Run(webauth::mojom::AuthenticatorStatus::UNKNOWN_ERROR, nullptr);
break;
case device::U2fReturnCode::SUCCESS:
DCHECK(response_data.has_value());
std::move(get_assertion_response_callback_)
.Run(webauth::mojom::AuthenticatorStatus::SUCCESS,
CreateGetAssertionResponse(std::move(client_data_),
std::move(*response_data)));
break;
}
Cleanup();
}
void AuthenticatorImpl::OnTimeout() {
DCHECK(make_credential_response_callback_ ||
get_assertion_response_callback_);
if (make_credential_response_callback_) {
std::move(make_credential_response_callback_)
.Run(webauth::mojom::AuthenticatorStatus::TIMED_OUT, nullptr);
} else if (get_assertion_response_callback_) {
std::move(get_assertion_response_callback_)
.Run(webauth::mojom::AuthenticatorStatus::TIMED_OUT, nullptr);
}
Cleanup();
}
void AuthenticatorImpl::Cleanup() {
u2f_request_.reset();
make_credential_response_callback_.Reset();
get_assertion_response_callback_.Reset();
client_data_ = CollectedClientData();
}
} // namespace content