blob: 392b7ed8ccf08439fd0418f178ec7b86ca232b17 [file] [log] [blame]
// Copyright 2013 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 "chrome/browser/extensions/api/cryptotoken_private/cryptotoken_private_api.h"
#include <algorithm>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/extension_api_unittest.h"
#include "chrome/browser/extensions/extension_function_test_utils.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h"
#include "components/permissions/permission_request_manager.h"
#include "components/permissions/test/mock_permission_prompt_factory.h"
#include "components/sessions/content/session_tab_helper.h"
#include "crypto/sha2.h"
#include "device/fido/features.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/browser/extension_function_dispatcher.h"
#include "extensions/browser/pref_names.h"
#include "extensions/common/extension_features.h"
#include "testing/gtest/include/gtest/gtest.h"
#if defined(OS_WIN)
#include "base/win/windows_version.h"
#endif
namespace extensions {
namespace {
bool GetSingleBooleanResult(ExtensionFunction* function, bool* result) {
const base::ListValue* result_list = function->GetResultList();
if (!result_list) {
ADD_FAILURE() << "Function has no result list.";
return false;
}
if (result_list->GetList().size() != 1u) {
ADD_FAILURE() << "Invalid number of results.";
return false;
}
if (!result_list->GetBoolean(0, result)) {
ADD_FAILURE() << "Result is not boolean.";
return false;
}
return true;
}
class CryptoTokenPrivateApiTest : public extensions::ExtensionApiUnittest {
public:
CryptoTokenPrivateApiTest() {}
~CryptoTokenPrivateApiTest() override {}
protected:
bool GetCanOriginAssertAppIdResult(const std::string& origin,
const std::string& app_id,
bool* out_result) {
auto function = base::MakeRefCounted<
api::CryptotokenPrivateCanOriginAssertAppIdFunction>();
function->set_has_callback(true);
auto args = std::make_unique<base::ListValue>();
args->Append(origin);
args->Append(app_id);
if (!extension_function_test_utils::RunFunction(
function.get(), std::move(args), browser(), api_test_utils::NONE)) {
return false;
}
return GetSingleBooleanResult(function.get(), out_result);
}
bool GetAppIdHashInEnterpriseContext(const std::string& app_id,
bool* out_result) {
auto function = base::MakeRefCounted<
api::CryptotokenPrivateIsAppIdHashInEnterpriseContextFunction>();
function->set_has_callback(true);
auto args = std::make_unique<base::Value>(base::Value::Type::LIST);
args->Append(
base::Value(base::Value::BlobStorage(app_id.begin(), app_id.end())));
if (!extension_function_test_utils::RunFunction(
function.get(), base::ListValue::From(std::move(args)), browser(),
api_test_utils::NONE)) {
return false;
}
return GetSingleBooleanResult(function.get(), out_result);
}
};
TEST_F(CryptoTokenPrivateApiTest, CanOriginAssertAppId) {
std::string origin1("https://www.example.com");
bool result;
ASSERT_TRUE(GetCanOriginAssertAppIdResult(origin1, origin1, &result));
EXPECT_TRUE(result);
std::string same_origin_appid("https://www.example.com/appId");
ASSERT_TRUE(
GetCanOriginAssertAppIdResult(origin1, same_origin_appid, &result));
EXPECT_TRUE(result);
std::string same_etld_plus_one_appid("https://appid.example.com/appId");
ASSERT_TRUE(GetCanOriginAssertAppIdResult(origin1, same_etld_plus_one_appid,
&result));
EXPECT_TRUE(result);
std::string different_etld_plus_one_appid("https://www.different.com/appId");
ASSERT_TRUE(GetCanOriginAssertAppIdResult(
origin1, different_etld_plus_one_appid, &result));
EXPECT_FALSE(result);
// For legacy purposes, google.com is allowed to use certain appIds hosted at
// gstatic.com.
// TODO(juanlang): remove once the legacy constraints are removed.
std::string google_origin("https://accounts.google.com");
std::string gstatic_appid("https://www.gstatic.com/securitykey/origins.json");
ASSERT_TRUE(
GetCanOriginAssertAppIdResult(google_origin, gstatic_appid, &result));
EXPECT_TRUE(result);
// Not all gstatic urls are allowed, just those specifically allowlisted.
std::string gstatic_otherurl("https://www.gstatic.com/foobar");
ASSERT_TRUE(
GetCanOriginAssertAppIdResult(google_origin, gstatic_otherurl, &result));
EXPECT_FALSE(result);
}
TEST_F(CryptoTokenPrivateApiTest, IsAppIdHashInEnterpriseContext) {
const std::string example_com("https://example.com/");
const std::string example_com_hash(crypto::SHA256HashString(example_com));
const std::string rp_id_hash(crypto::SHA256HashString("example.com"));
const std::string foo_com_hash(crypto::SHA256HashString("https://foo.com/"));
bool result;
ASSERT_TRUE(GetAppIdHashInEnterpriseContext(example_com_hash, &result));
EXPECT_FALSE(result);
ASSERT_TRUE(GetAppIdHashInEnterpriseContext(foo_com_hash, &result));
EXPECT_FALSE(result);
ASSERT_TRUE(GetAppIdHashInEnterpriseContext(rp_id_hash, &result));
EXPECT_FALSE(result);
base::Value::ListStorage permitted_list;
permitted_list.emplace_back(example_com);
profile()->GetPrefs()->Set(prefs::kSecurityKeyPermitAttestation,
base::Value(permitted_list));
ASSERT_TRUE(GetAppIdHashInEnterpriseContext(example_com_hash, &result));
EXPECT_TRUE(result);
ASSERT_TRUE(GetAppIdHashInEnterpriseContext(foo_com_hash, &result));
EXPECT_FALSE(result);
ASSERT_TRUE(GetAppIdHashInEnterpriseContext(rp_id_hash, &result));
EXPECT_FALSE(result);
}
TEST_F(CryptoTokenPrivateApiTest, RecordRegisterRequest) {
const GURL url("https://example.com/signin");
AddTab(browser(), url);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetWebContentsAt(0);
const int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
page_load_metrics::PageLoadMetricsTestWaiter web_feature_waiter(web_contents);
web_feature_waiter.AddWebFeatureExpectation(
blink::mojom::WebFeature::kU2FCryptotokenRegister);
// Force the metrics waiter to attach.
NavigateAndCommitActiveTab(url);
auto function = base::MakeRefCounted<
api::CryptotokenPrivateRecordRegisterRequestFunction>();
auto args = std::make_unique<base::ListValue>();
args->Append(tab_id);
args->Append(0 /* top-level frame */);
ASSERT_TRUE(extension_function_test_utils::RunFunction(
function.get(), base::ListValue::From(std::move(args)), browser(),
api_test_utils::NONE));
ASSERT_EQ(function->GetResultList()->GetList().size(), 0u);
web_feature_waiter.Wait();
}
TEST_F(CryptoTokenPrivateApiTest, RecordSignRequest) {
const GURL url("https://example.com/signin");
AddTab(browser(), url);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetWebContentsAt(0);
const int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id();
page_load_metrics::PageLoadMetricsTestWaiter web_feature_waiter(web_contents);
web_feature_waiter.AddWebFeatureExpectation(
blink::mojom::WebFeature::kU2FCryptotokenSign);
// Force the metrics waiter to attach.
NavigateAndCommitActiveTab(url);
auto function =
base::MakeRefCounted<api::CryptotokenPrivateRecordSignRequestFunction>();
auto args = std::make_unique<base::ListValue>();
args->Append(tab_id);
args->Append(0 /* top-level frame */);
ASSERT_TRUE(extension_function_test_utils::RunFunction(
function.get(), base::ListValue::From(std::move(args)), browser(),
api_test_utils::NONE));
ASSERT_EQ(function->GetResultList()->GetList().size(), 0u);
web_feature_waiter.Wait();
}
} // namespace
class CryptoTokenPermissionTest : public ExtensionApiUnittest {
public:
CryptoTokenPermissionTest() = default;
CryptoTokenPermissionTest(const CryptoTokenPermissionTest&) = delete;
CryptoTokenPermissionTest& operator=(const CryptoTokenPermissionTest&) =
delete;
~CryptoTokenPermissionTest() override = default;
void SetUp() override {
ExtensionApiUnittest::SetUp();
const GURL url("http://example.com");
AddTab(browser(), url);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetWebContentsAt(0);
tab_id_ = sessions::SessionTabHelper::IdForTab(web_contents).id();
permissions::PermissionRequestManager::CreateForWebContents(web_contents);
prompt_factory_ =
std::make_unique<permissions::MockPermissionPromptFactory>(
permissions::PermissionRequestManager::FromWebContents(
web_contents));
}
void TearDown() override {
prompt_factory_.reset();
ExtensionApiUnittest::TearDown();
}
protected:
// CanAppIdGetAttestation calls the cryptotoken private API of the same name
// for |app_id| and sets |*out_result| to the result. If |bubble_action| is
// not |NONE| then it waits for the permissions prompt to be shown and
// performs the given action. Otherwise, the call is expected to be
// synchronous.
bool CanAppIdGetAttestation(
const std::string& app_id,
permissions::PermissionRequestManager::AutoResponseType bubble_action,
bool* out_result) {
if (bubble_action != permissions::PermissionRequestManager::NONE) {
prompt_factory_->set_response_type(bubble_action);
auto* web_contents = browser()->tab_strip_model()->GetWebContentsAt(0);
prompt_factory_->DocumentOnLoadCompletedInMainFrame(
web_contents->GetMainFrame());
}
auto function = base::MakeRefCounted<
api::CryptotokenPrivateCanAppIdGetAttestationFunction>();
function->set_has_callback(true);
base::Value::DictStorage dict;
dict.emplace("appId", app_id);
dict.emplace("tabId", tab_id_);
dict.emplace("origin", app_id);
auto args = std::make_unique<base::Value>(base::Value::Type::LIST);
args->Append(base::Value(std::move(dict)));
auto args_list = base::ListValue::From(std::move(args));
extension_function_test_utils::RunFunction(
function.get(), std::move(args_list), browser(), api_test_utils::NONE);
return GetSingleBooleanResult(function.get(), out_result);
}
// CanMakeU2fApiRequest calls the cryptotoken private API of the same name
// for |origin| and sets |*out_result| to the result. If |bubble_action| is
// not |NONE| then it waits for the permissions prompt to be shown and
// performs the given action. Otherwise, the call is expected to be
// synchronous.
bool CanMakeU2fApiRequest(
const std::string& origin,
permissions::PermissionRequestManager::AutoResponseType bubble_action,
bool* out_result) {
if (bubble_action != permissions::PermissionRequestManager::NONE) {
prompt_factory_->set_response_type(bubble_action);
auto* web_contents = browser()->tab_strip_model()->GetWebContentsAt(0);
prompt_factory_->DocumentOnLoadCompletedInMainFrame(
web_contents->GetMainFrame());
}
auto function = base::MakeRefCounted<
api::CryptotokenPrivateCanMakeU2fApiRequestFunction>();
function->set_has_callback(true);
base::Value::DictStorage dict;
dict.emplace("appId", origin);
dict.emplace("tabId", tab_id_);
dict.emplace("origin", origin);
auto args = std::make_unique<base::Value>(base::Value::Type::LIST);
args->Append(base::Value(std::move(dict)));
auto args_list = base::ListValue::From(std::move(args));
extension_function_test_utils::RunFunction(
function.get(), std::move(args_list), browser(), api_test_utils::NONE);
return GetSingleBooleanResult(function.get(), out_result);
}
base::test::ScopedFeatureList feature_list_;
private:
int tab_id_ = -1;
std::unique_ptr<permissions::MockPermissionPromptFactory> prompt_factory_;
};
TEST_F(CryptoTokenPermissionTest, AttestationPrompt) {
#if defined(OS_WIN)
// TODO(crbug.com/1225335) This test is failing on WIN10_20H2.
if (base::win::OSInfo::GetInstance()->version() >=
base::win::Version::WIN10_20H2)
return;
#endif
const std::vector<permissions::PermissionRequestManager::AutoResponseType>
actions = {
permissions::PermissionRequestManager::ACCEPT_ALL,
permissions::PermissionRequestManager::DENY_ALL,
permissions::PermissionRequestManager::DISMISS,
};
for (const auto& action : actions) {
SCOPED_TRACE(action);
bool result = false;
ASSERT_TRUE(CanAppIdGetAttestation("https://test.com", action, &result));
// The result should only be positive if the user accepted the permissions
// prompt.
EXPECT_EQ(action == permissions::PermissionRequestManager::ACCEPT_ALL,
result);
}
}
TEST_F(CryptoTokenPermissionTest, PolicyOverridesAttestationPrompt) {
const std::string example_com("https://example.com");
base::Value::ListStorage permitted_list;
permitted_list.emplace_back(example_com);
profile()->GetPrefs()->Set(prefs::kSecurityKeyPermitAttestation,
base::Value(permitted_list));
// If an appId is configured by enterprise policy then attestation requests
// should be permitted without showing a prompt.
bool result = false;
ASSERT_TRUE(CanAppIdGetAttestation(
example_com, permissions::PermissionRequestManager::NONE, &result));
EXPECT_TRUE(result);
}
TEST_F(CryptoTokenPermissionTest, RequestPrompt) {
const std::vector<permissions::PermissionRequestManager::AutoResponseType>
actions = {
permissions::PermissionRequestManager::ACCEPT_ALL,
permissions::PermissionRequestManager::DENY_ALL,
permissions::PermissionRequestManager::DISMISS,
};
for (const auto& action : actions) {
SCOPED_TRACE(action);
bool result = false;
ASSERT_TRUE(CanMakeU2fApiRequest("https://test.com", action, &result));
// The result should only be positive if the user accepted the permissions
// prompt.
EXPECT_EQ(action == permissions::PermissionRequestManager::ACCEPT_ALL,
result);
}
}
TEST_F(CryptoTokenPermissionTest, FeatureFlagOverridesRequestPrompt) {
// Disabling the permission prompt feature flag should suppress it.
feature_list_.InitAndDisableFeature(device::kU2fPermissionPrompt);
bool result = false;
ASSERT_TRUE(CanMakeU2fApiRequest("https://test.com",
permissions::PermissionRequestManager::NONE,
&result));
EXPECT_TRUE(result);
}
TEST_F(CryptoTokenPermissionTest, EnterprisePolicyOverridesRequestPrompt) {
// Setting the deprecation override policy should cause the prompt to be
// suppressed. This should be true even when the API has been
// default-disabled, because the policy overrides that too.
for (bool api_enabled : {false, true}) {
SCOPED_TRACE(api_enabled);
feature_list_.InitWithFeatureState(extensions_features::kU2FSecurityKeyAPI,
api_enabled);
browser()->profile()->GetPrefs()->Set(
extensions::pref_names::kU2fSecurityKeyApiEnabled, base::Value(true));
bool result = false;
ASSERT_TRUE(CanMakeU2fApiRequest(
"https://test.com", permissions::PermissionRequestManager::NONE,
&result));
EXPECT_TRUE(result);
feature_list_.Reset();
}
}
} // namespace extensions