| Index: chrome/browser/extensions/api/certificate_provider/certificate_provider_apitest.cc
|
| diff --git a/chrome/browser/extensions/api/certificate_provider/certificate_provider_apitest.cc b/chrome/browser/extensions/api/certificate_provider/certificate_provider_apitest.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..d550b73cd707fa488cd3fefeff12db05ca77639f
|
| --- /dev/null
|
| +++ b/chrome/browser/extensions/api/certificate_provider/certificate_provider_apitest.cc
|
| @@ -0,0 +1,265 @@
|
| +// 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 <openssl/evp.h>
|
| +#include <openssl/rsa.h>
|
| +#include <stddef.h>
|
| +#include <stdint.h>
|
| +#include <stdlib.h>
|
| +
|
| +#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/memory/scoped_ptr.h"
|
| +#include "base/run_loop.h"
|
| +#include "base/stl_util.h"
|
| +#include "base/strings/string_number_conversions.h"
|
| +#include "base/strings/utf_string_conversions.h"
|
| +#include "base/values.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 "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 "crypto/scoped_openssl_types.h"
|
| +#include "extensions/common/extension.h"
|
| +#include "extensions/test/result_catcher.h"
|
| +#include "net/test/spawned_test_server/spawned_test_server.h"
|
| +#include "policy/policy_constants.h"
|
| +#include "testing/gmock/include/gmock/gmock.h"
|
| +
|
| +using testing::Return;
|
| +using testing::_;
|
| +
|
| +namespace {
|
| +
|
| +void IgnoreResult(const base::Closure& callback, const base::Value* value) {
|
| + callback.Run();
|
| +}
|
| +
|
| +void StoreBool(bool* result,
|
| + const base::Closure& callback,
|
| + const base::Value* value) {
|
| + value->GetAsBoolean(result);
|
| + callback.Run();
|
| +}
|
| +
|
| +void StoreString(std::string* result,
|
| + const base::Closure& callback,
|
| + const base::Value* value) {
|
| + value->GetAsString(result);
|
| + callback.Run();
|
| +}
|
| +
|
| +void StoreDigest(std::vector<uint8_t>* digest,
|
| + const base::Closure& callback,
|
| + const base::Value* value) {
|
| + const base::BinaryValue* binary = nullptr;
|
| + value->GetAsBinary(&binary);
|
| + const uint8_t* const binary_begin =
|
| + reinterpret_cast<const uint8_t*>(binary->GetBuffer());
|
| + digest->assign(binary_begin, binary_begin + binary->GetSize());
|
| +
|
| + callback.Run();
|
| +}
|
| +
|
| +// See net::SSLPrivateKey::SignDigest for the expected padding and DigestInfo
|
| +// prefixing.
|
| +bool RsaSign(const std::vector<uint8_t>& digest,
|
| + crypto::RSAPrivateKey* key,
|
| + std::vector<uint8_t>* signature) {
|
| + crypto::ScopedRSA rsa_key(EVP_PKEY_get1_RSA(key->key()));
|
| + if (!rsa_key)
|
| + return false;
|
| +
|
| + uint8_t* prefixed_digest = nullptr;
|
| + size_t prefixed_digest_len = 0;
|
| + int is_alloced = 0;
|
| + if (!RSA_add_pkcs1_prefix(&prefixed_digest, &prefixed_digest_len, &is_alloced,
|
| + NID_sha1, vector_as_array(&digest),
|
| + digest.size())) {
|
| + return false;
|
| + }
|
| + size_t len = 0;
|
| + signature->resize(RSA_size(rsa_key.get()));
|
| + const int rv = RSA_sign_raw(rsa_key.get(), &len, vector_as_array(signature),
|
| + signature->size(), prefixed_digest,
|
| + prefixed_digest_len, RSA_PKCS1_PADDING);
|
| + if (is_alloced)
|
| + free(prefixed_digest);
|
| +
|
| + if (rv) {
|
| + signature->resize(len);
|
| + return true;
|
| + } else {
|
| + signature->clear();
|
| + return false;
|
| + }
|
| +}
|
| +
|
| +// 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::UintToString(byte);
|
| + res += ", ";
|
| + }
|
| + res += "])";
|
| + return res;
|
| +}
|
| +
|
| +class CertificateProviderApiTest : public ExtensionApiTest {
|
| + public:
|
| + CertificateProviderApiTest() {}
|
| +
|
| + void SetUpInProcessBrowserTestFixture() override {
|
| + EXPECT_CALL(provider_, IsInitializationComplete(_))
|
| + .WillRepeatedly(Return(true));
|
| + policy::BrowserPolicyConnector::SetPolicyProviderForTesting(&provider_);
|
| +
|
| + ExtensionApiTest::SetUpInProcessBrowserTestFixture();
|
| + }
|
| +
|
| + void SetUpOnMainThread() override {
|
| + // Set up the AutoSelectCertificateForUrls policy to avoid the client
|
| + // certificate selection dialog.
|
| + const std::string autoselect_pattern =
|
| + "{\"pattern\": \"*\", \"filter\": {\"ISSUER\": {\"CN\": \"root\"}}}";
|
| +
|
| + scoped_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,
|
| + autoselect_policy.release(), nullptr);
|
| + provider_.UpdateChromePolicy(policy);
|
| +
|
| + content::RunAllPendingInMessageLoop();
|
| + }
|
| +
|
| + protected:
|
| + policy::MockConfigurationPolicyProvider provider_;
|
| +};
|
| +
|
| +} // namespace
|
| +
|
| +IN_PROC_BROWSER_TEST_F(CertificateProviderApiTest, 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(FILE_PATH_LITERAL("chrome/test/data")));
|
| + 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"), 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 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::Bind(&StoreDigest, &request_digest, run_loop.QuitClosure()));
|
| + run_loop.Run();
|
| + }
|
| +
|
| + VLOG(1) << "Sign the digest using the private key.";
|
| + std::string key_pk8;
|
| + 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());
|
| + scoped_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::Bind(&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::Bind(&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: "
|
| + "2ab3f55e06eb8b36a741fe285a769da45edb2695",
|
| + 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::Bind(&StoreBool, &result, run_loop.QuitClosure()));
|
| + run_loop.Run();
|
| + EXPECT_TRUE(result);
|
| + }
|
| +}
|
|
|