blob: cf69be606e4b40b889fa7e2a190d5c01dd2f738d [file] [log] [blame]
// Copyright 2015 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 <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <memory>
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/chromeos/certificate_provider/certificate_provider_service.h"
#include "chrome/browser/chromeos/certificate_provider/certificate_provider_service_factory.h"
#include "chrome/browser/chromeos/ui/request_pin_view.h"
#include "chrome/browser/extensions/api/certificate_provider/certificate_provider_api.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/policy/core/browser/browser_policy_connector.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/policy_types.h"
#include "components/policy/policy_constants.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "crypto/rsa_private_key.h"
#include "extensions/common/extension.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "net/test/spawned_test_server/spawned_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/boringssl/src/include/openssl/evp.h"
#include "third_party/boringssl/src/include/openssl/mem.h"
#include "third_party/boringssl/src/include/openssl/rsa.h"
#include "ui/gfx/color_palette.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
using testing::Return;
using testing::_;
namespace {
void IgnoreResult(const base::Closure& callback, base::Value value) {
callback.Run();
}
void StoreBool(bool* result, const base::Closure& callback, base::Value value) {
value.GetAsBoolean(result);
callback.Run();
}
void StoreString(std::string* result,
const base::Closure& callback,
base::Value value) {
value.GetAsString(result);
callback.Run();
}
void StoreDigest(std::vector<uint8_t>* digest,
const base::Closure& callback,
base::Value value) {
ASSERT_TRUE(value.is_blob()) << "Unexpected value in StoreDigest";
digest->assign(value.GetBlob().begin(), value.GetBlob().end());
callback.Run();
}
bool RsaSign(const std::vector<uint8_t>& digest,
crypto::RSAPrivateKey* key,
std::vector<uint8_t>* signature) {
RSA* rsa_key = EVP_PKEY_get0_RSA(key->key());
if (!rsa_key)
return false;
unsigned len = 0;
signature->resize(RSA_size(rsa_key));
if (!RSA_sign(NID_sha1, digest.data(), digest.size(), signature->data(), &len,
rsa_key)) {
signature->clear();
return false;
}
signature->resize(len);
return true;
}
// Create a string that if evaluated in JavaScript returns a Uint8Array with
// |bytes| as content.
std::string JsUint8Array(const std::vector<uint8_t>& bytes) {
std::string res = "new Uint8Array([";
for (const uint8_t byte : bytes) {
res += base::NumberToString(byte);
res += ", ";
}
res += "])";
return res;
}
class CertificateProviderApiTest : public extensions::ExtensionApiTest {
public:
CertificateProviderApiTest() {}
void SetUpInProcessBrowserTestFixture() override {
EXPECT_CALL(provider_, IsInitializationComplete(_))
.WillRepeatedly(Return(true));
policy::BrowserPolicyConnector::SetPolicyProviderForTesting(&provider_);
extensions::ExtensionApiTest::SetUpInProcessBrowserTestFixture();
}
void SetUpOnMainThread() override {
extensions::ExtensionApiTest::SetUpOnMainThread();
// Set up the AutoSelectCertificateForUrls policy to avoid the client
// certificate selection dialog.
const std::string autoselect_pattern =
"{\"pattern\": \"*\", \"filter\": {\"ISSUER\": {\"CN\": \"root\"}}}";
std::unique_ptr<base::ListValue> autoselect_policy(new base::ListValue);
autoselect_policy->AppendString(autoselect_pattern);
policy::PolicyMap policy;
policy.Set(policy::key::kAutoSelectCertificateForUrls,
policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
policy::POLICY_SOURCE_CLOUD, std::move(autoselect_policy),
nullptr);
provider_.UpdateChromePolicy(policy);
content::RunAllPendingInMessageLoop();
}
protected:
policy::MockConfigurationPolicyProvider provider_;
};
class CertificateProviderRequestPinTest : public CertificateProviderApiTest {
protected:
static constexpr int kFakeSignRequestId = 123;
static constexpr int kWrongPinAttemptsLimit = 3;
static constexpr const char* kCorrectPin = "1234";
static constexpr const char* kWrongPin = "567";
void SetUpOnMainThread() override {
CertificateProviderApiTest::SetUpOnMainThread();
cert_provider_service_ =
chromeos::CertificateProviderServiceFactory::GetForBrowserContext(
profile());
command_request_listener_ = std::make_unique<ExtensionTestMessageListener>(
"GetCommand", /*will_reply=*/true);
LoadRequestPinExtension();
}
void TearDownOnMainThread() override {
if (command_request_listener_->was_satisfied()) {
// Avoid destroying a non-replied extension function without.
command_request_listener_->Reply(/*message=*/std::string());
}
command_request_listener_.reset();
CertificateProviderApiTest::TearDownOnMainThread();
}
void AddFakeSignRequest() {
cert_provider_service_->pin_dialog_manager()->AddSignRequestId(
extension_->id(), kFakeSignRequestId, {});
}
void NavigateTo(const std::string& test_page_file_name) {
ui_test_utils::NavigateToURL(
browser(), extension_->GetResourceURL(test_page_file_name));
}
chromeos::RequestPinView* GetActivePinDialogView() {
return cert_provider_service_->pin_dialog_manager()
->default_dialog_host_for_testing()
->active_view_for_testing();
}
views::Widget* GetActivePinDialogWindow() {
return cert_provider_service_->pin_dialog_manager()
->default_dialog_host_for_testing()
->active_window_for_testing();
}
// Enters the code in the ShowPinDialog window and pushes the OK event.
void EnterCode(const std::string& code) {
GetActivePinDialogView()->textfield_for_testing()->SetText(
base::ASCIIToUTF16(code));
GetActivePinDialogView()->Accept();
base::RunLoop().RunUntilIdle();
}
// Enters the valid code for extensions from local example folders, in the
// ShowPinDialog window and waits for the window to close. The extension code
// is expected to send "Success" message after the validation and request to
// stopPinRequest is done.
void EnterCorrectPinAndWaitForMessage() {
ExtensionTestMessageListener listener("Success", false);
EnterCode(kCorrectPin);
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
// Enters an invalid code for extensions from local example folders, in the
// ShowPinDialog window and waits for the window to update with the error. The
// extension code is expected to send "Invalid PIN" message after the
// validation and the new requestPin (with the error) is done.
void EnterWrongPinAndWaitForMessage() {
ExtensionTestMessageListener listener("Invalid PIN", false);
EnterCode(kWrongPin);
ASSERT_TRUE(listener.WaitUntilSatisfied());
// Check that we have an error message displayed.
EXPECT_EQ(
gfx::kGoogleRed600,
GetActivePinDialogView()->error_label_for_testing()->GetEnabledColor());
}
bool SendCommand(const std::string& command) {
if (!command_request_listener_->WaitUntilSatisfied())
return false;
command_request_listener_->Reply(command);
command_request_listener_->Reset();
return true;
}
bool SendCommandAndWaitForMessage(const std::string& command,
const std::string& expected_message) {
ExtensionTestMessageListener listener(expected_message,
/*will_reply=*/false);
if (!SendCommand(command))
return false;
return listener.WaitUntilSatisfied();
}
private:
void LoadRequestPinExtension() {
const base::FilePath extension_path =
test_data_dir_.AppendASCII("certificate_provider/request_pin");
extension_ = LoadExtension(extension_path);
}
chromeos::CertificateProviderService* cert_provider_service_ = nullptr;
const extensions::Extension* extension_ = nullptr;
std::unique_ptr<ExtensionTestMessageListener> command_request_listener_;
};
} // namespace
// Flaky: crbug.com/1011606.
IN_PROC_BROWSER_TEST_F(CertificateProviderApiTest, DISABLED_Basic) {
// Start an HTTPS test server that requests a client certificate.
net::SpawnedTestServer::SSLOptions ssl_options;
ssl_options.request_client_certificate = true;
net::SpawnedTestServer https_server(net::SpawnedTestServer::TYPE_HTTPS,
ssl_options, base::FilePath());
ASSERT_TRUE(https_server.Start());
extensions::ResultCatcher catcher;
const base::FilePath extension_path =
test_data_dir_.AppendASCII("certificate_provider");
const extensions::Extension* const extension = LoadExtension(extension_path);
ui_test_utils::NavigateToURL(browser(),
extension->GetResourceURL("basic.html"));
ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
VLOG(1) << "Extension registered. Navigate to the test https page.";
content::WebContents* const extension_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::TestNavigationObserver navigation_observer(
nullptr /* no WebContents */);
navigation_observer.StartWatchingNewWebContents();
ui_test_utils::NavigateToURLWithDisposition(
browser(), https_server.GetURL("client-cert"),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_NONE);
content::WebContents* const https_contents =
browser()->tab_strip_model()->GetActiveWebContents();
VLOG(1) << "Wait for the extension to respond to the certificates request.";
ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
VLOG(1) << "Wait for the extension to receive the sign request.";
ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
VLOG(1) << "Fetch the digest from the sign request.";
std::vector<uint8_t> request_digest;
{
base::RunLoop run_loop;
extension_contents->GetMainFrame()->ExecuteJavaScriptForTests(
base::ASCIIToUTF16("signDigestRequest.digest;"),
base::BindOnce(&StoreDigest, &request_digest, run_loop.QuitClosure()));
run_loop.Run();
}
VLOG(1) << "Sign the digest using the private key.";
std::string key_pk8;
{
base::ScopedAllowBlockingForTesting allow_io;
base::ReadFileToString(extension_path.AppendASCII("l1_leaf.pk8"), &key_pk8);
}
const uint8_t* const key_pk8_begin =
reinterpret_cast<const uint8_t*>(key_pk8.data());
std::unique_ptr<crypto::RSAPrivateKey> key(
crypto::RSAPrivateKey::CreateFromPrivateKeyInfo(
std::vector<uint8_t>(key_pk8_begin, key_pk8_begin + key_pk8.size())));
ASSERT_TRUE(key);
std::vector<uint8_t> signature;
EXPECT_TRUE(RsaSign(request_digest, key.get(), &signature));
VLOG(1) << "Inject the signature back to the extension and let it reply.";
{
base::RunLoop run_loop;
const std::string code =
"replyWithSignature(" + JsUint8Array(signature) + ");";
extension_contents->GetMainFrame()->ExecuteJavaScriptForTests(
base::ASCIIToUTF16(code),
base::BindOnce(&IgnoreResult, run_loop.QuitClosure()));
run_loop.Run();
}
VLOG(1) << "Wait for the https navigation to finish.";
navigation_observer.Wait();
VLOG(1) << "Check whether the server acknowledged that a client certificate "
"was presented.";
{
base::RunLoop run_loop;
std::string https_reply;
https_contents->GetMainFrame()->ExecuteJavaScriptForTests(
base::ASCIIToUTF16("document.body.textContent;"),
base::BindOnce(&StoreString, &https_reply, run_loop.QuitClosure()));
run_loop.Run();
// Expect the server to return the fingerprint of the client cert that we
// presented, which should be the fingerprint of 'l1_leaf.der'.
// The fingerprint can be calculated independently using:
// openssl x509 -inform DER -noout -fingerprint -in
// chrome/test/data/extensions/api_test/certificate_provider/l1_leaf.der
ASSERT_EQ(
"got client cert with fingerprint: "
"edeb84ab3b5a36dd09a3203c74794b25efa8f126",
https_reply);
}
// Replying to the same signature request a second time must fail.
{
base::RunLoop run_loop;
const std::string code = "replyWithSignatureSecondTime();";
bool result = false;
extension_contents->GetMainFrame()->ExecuteJavaScriptForTests(
base::ASCIIToUTF16(code),
base::BindOnce(&StoreBool, &result, run_loop.QuitClosure()));
run_loop.Run();
EXPECT_TRUE(result);
}
}
// User enters the correct PIN.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest, ShowPinDialogAccept) {
AddFakeSignRequest();
NavigateTo("basic.html");
// Enter the valid PIN.
EnterCorrectPinAndWaitForMessage();
// The view should be set to nullptr when the window is closed.
EXPECT_FALSE(GetActivePinDialogView());
}
// User closes the dialog kMaxClosedDialogsPerMinute times, and the extension
// should be blocked from showing it again.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest, ShowPinDialogClose) {
AddFakeSignRequest();
NavigateTo("basic.html");
for (int i = 0;
i < extensions::api::certificate_provider::kMaxClosedDialogsPerMinute;
i++) {
ExtensionTestMessageListener listener("User closed the dialog", false);
GetActivePinDialogWindow()->Close();
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
ExtensionTestMessageListener close_listener("User closed the dialog", true);
GetActivePinDialogWindow()->Close();
ASSERT_TRUE(close_listener.WaitUntilSatisfied());
close_listener.Reply("GetLastError");
ExtensionTestMessageListener last_error_listener(
"This request exceeds the MAX_PIN_DIALOGS_CLOSED_PER_MINUTE quota.",
false);
ASSERT_TRUE(last_error_listener.WaitUntilSatisfied());
EXPECT_FALSE(GetActivePinDialogView());
}
// User enters a wrong PIN first and a correct PIN on the second try.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest,
ShowPinDialogWrongPin) {
AddFakeSignRequest();
NavigateTo("basic.html");
EnterWrongPinAndWaitForMessage();
// The window should be active.
EXPECT_TRUE(GetActivePinDialogWindow()->IsVisible());
EXPECT_TRUE(GetActivePinDialogView());
// Enter the valid PIN.
EnterCorrectPinAndWaitForMessage();
// The view should be set to nullptr when the window is closed.
EXPECT_FALSE(GetActivePinDialogView());
}
// User enters wrong PIN three times.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest,
ShowPinDialogWrongPinThreeTimes) {
AddFakeSignRequest();
NavigateTo("basic.html");
for (int i = 0; i < kWrongPinAttemptsLimit; i++)
EnterWrongPinAndWaitForMessage();
// The textfield has to be disabled, as extension does not allow input now.
EXPECT_FALSE(GetActivePinDialogView()->textfield_for_testing()->GetEnabled());
// Close the dialog.
ExtensionTestMessageListener listener("No attempt left", false);
GetActivePinDialogWindow()->Close();
ASSERT_TRUE(listener.WaitUntilSatisfied());
EXPECT_FALSE(GetActivePinDialogView());
}
// User closes the dialog while the extension is processing the request.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest,
ShowPinDialogCloseWhileProcessing) {
AddFakeSignRequest();
NavigateTo("operated.html");
EXPECT_TRUE(SendCommandAndWaitForMessage("Request", "request1:begun"));
ExtensionTestMessageListener listener(
base::StringPrintf("request1:success:%s", kWrongPin), false);
EnterCode(kWrongPin);
EXPECT_TRUE(listener.WaitUntilSatisfied());
GetActivePinDialogWindow()->Close();
base::RunLoop().RunUntilIdle();
// The view should be set to nullptr when the window is closed.
EXPECT_FALSE(GetActivePinDialogView());
}
// Extension closes the dialog kMaxClosedDialogsPerMinute times after the user
// inputs some value, and it should be blocked from showing it again.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest,
RepeatedProgrammaticCloseAfterInput) {
NavigateTo("operated.html");
for (int i = 0;
i <
extensions::api::certificate_provider::kMaxClosedDialogsPerMinute + 1;
i++) {
AddFakeSignRequest();
EXPECT_TRUE(SendCommandAndWaitForMessage(
"Request", base::StringPrintf("request%d:begun", i + 1)));
EnterCode(kCorrectPin);
EXPECT_TRUE(SendCommandAndWaitForMessage(
"Stop", base::StringPrintf("stop%d:success", i + 1)));
EXPECT_FALSE(GetActivePinDialogView());
}
AddFakeSignRequest();
EXPECT_TRUE(SendCommandAndWaitForMessage(
"Request",
base::StringPrintf(
"request%d:error:This request exceeds the "
"MAX_PIN_DIALOGS_CLOSED_PER_MINUTE quota.",
extensions::api::certificate_provider::kMaxClosedDialogsPerMinute +
2)));
EXPECT_FALSE(GetActivePinDialogView());
}
// Extension erroneously attempts to close the PIN dialog twice.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest, DoubleClose) {
AddFakeSignRequest();
NavigateTo("operated.html");
EXPECT_TRUE(SendCommand("Request"));
EXPECT_TRUE(SendCommandAndWaitForMessage("Stop", "stop1:success"));
EXPECT_TRUE(SendCommandAndWaitForMessage(
"Stop", "stop2:error:No active dialog from extension."));
EXPECT_FALSE(GetActivePinDialogView());
}
// Extension closes the dialog kMaxClosedDialogsPerMinute times before the user
// inputs anything, and it should be blocked from showing it again.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest,
RepeatedProgrammaticCloseBeforeInput) {
NavigateTo("operated.html");
for (int i = 0;
i <
extensions::api::certificate_provider::kMaxClosedDialogsPerMinute + 1;
i++) {
AddFakeSignRequest();
EXPECT_TRUE(SendCommand("Request"));
EXPECT_TRUE(SendCommandAndWaitForMessage(
"Stop", base::StringPrintf("stop%d:success", i + 1)));
EXPECT_FALSE(GetActivePinDialogView());
}
AddFakeSignRequest();
EXPECT_TRUE(SendCommandAndWaitForMessage(
"Request",
base::StringPrintf(
"request%d:error:This request exceeds the "
"MAX_PIN_DIALOGS_CLOSED_PER_MINUTE quota.",
extensions::api::certificate_provider::kMaxClosedDialogsPerMinute +
2)));
EXPECT_FALSE(GetActivePinDialogView());
}
// Extension erroneously attempts to stop the PIN request with an error before
// the user provided any input.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest,
StopWithErrorBeforeInput) {
AddFakeSignRequest();
NavigateTo("operated.html");
EXPECT_TRUE(SendCommand("Request"));
EXPECT_TRUE(SendCommandAndWaitForMessage(
"StopWithUnknownError", "stop1:error:No user input received"));
EXPECT_TRUE(GetActivePinDialogView()->textfield_for_testing()->GetEnabled());
}
// Extension erroneously uses an invalid sign request ID.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest, InvalidRequestId) {
NavigateTo("operated.html");
EXPECT_TRUE(SendCommandAndWaitForMessage(
"Request", "request1:error:Invalid signRequestId"));
EXPECT_FALSE(GetActivePinDialogView());
}
// Extension specifies zero left attempts in the very first PIN request.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest, ZeroAttemptsAtStart) {
AddFakeSignRequest();
NavigateTo("operated.html");
EXPECT_TRUE(SendCommandAndWaitForMessage("RequestWithZeroAttempts",
"request1:begun"));
// The textfield has to be disabled, as there are no attempts left.
EXPECT_FALSE(GetActivePinDialogView()->textfield_for_testing()->GetEnabled());
ExtensionTestMessageListener listener("request1:empty", false);
GetActivePinDialogWindow()->Close();
EXPECT_TRUE(listener.WaitUntilSatisfied());
}
// Extension erroneously passes a negative attempts left count.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest, NegativeAttempts) {
AddFakeSignRequest();
NavigateTo("operated.html");
EXPECT_TRUE(SendCommandAndWaitForMessage(
"RequestWithNegativeAttempts", "request1:error:Invalid attemptsLeft"));
EXPECT_FALSE(GetActivePinDialogView());
}
// Extension erroneously attempts to close a non-existing dialog.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest, CloseNonExisting) {
AddFakeSignRequest();
NavigateTo("operated.html");
EXPECT_TRUE(SendCommandAndWaitForMessage(
"Stop", "stop1:error:No active dialog from extension."));
EXPECT_FALSE(GetActivePinDialogView());
}
// Extension erroneously attempts to stop a non-existing dialog with an error.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest, StopNonExisting) {
AddFakeSignRequest();
NavigateTo("operated.html");
EXPECT_TRUE(SendCommandAndWaitForMessage(
"StopWithUnknownError", "stop1:error:No active dialog from extension."));
EXPECT_FALSE(GetActivePinDialogView());
}
// Extension erroneously attempts to start or stop the PIN request before the
// user closed the previously stopped with an error PIN request.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest,
UpdateAlreadyStopped) {
AddFakeSignRequest();
NavigateTo("operated.html");
EXPECT_TRUE(SendCommandAndWaitForMessage("Request", "request1:begun"));
EnterCode(kWrongPin);
EXPECT_TRUE(SendCommand("StopWithUnknownError"));
EXPECT_TRUE(SendCommandAndWaitForMessage(
"StopWithUnknownError", "stop2:error:No user input received"));
EXPECT_TRUE(SendCommandAndWaitForMessage(
"Request", "request2:error:Previous request not finished"));
EXPECT_FALSE(GetActivePinDialogView()->textfield_for_testing()->GetEnabled());
}
// Extension starts a new PIN request after it stopped the previous one with an
// error.
IN_PROC_BROWSER_TEST_F(CertificateProviderRequestPinTest, StartAfterStop) {
AddFakeSignRequest();
NavigateTo("operated.html");
EXPECT_TRUE(SendCommandAndWaitForMessage("Request", "request1:begun"));
EnterCode(kWrongPin);
EXPECT_TRUE(SendCommandAndWaitForMessage("Stop", "stop1:success"));
EXPECT_FALSE(GetActivePinDialogView());
EXPECT_TRUE(SendCommandAndWaitForMessage("Request", "request2:begun"));
ExtensionTestMessageListener listener(
base::StringPrintf("request2:success:%s", kCorrectPin), false);
EnterCode(kCorrectPin);
EXPECT_TRUE(listener.WaitUntilSatisfied());
EXPECT_FALSE(GetActivePinDialogView()->textfield_for_testing()->GetEnabled());
}