Chromium Code Reviews| 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..22e8a949f2aa2577b4b3ccaf57d93975d4a8e262 |
| --- /dev/null |
| +++ b/chrome/browser/extensions/api/certificate_provider/certificate_provider_apitest.cc |
| @@ -0,0 +1,259 @@ |
| +// 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 "base/files/file_path.h" |
| +#include "base/macros.h" |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: Not used.
pneubeck (no reviews)
2015/09/07 17:21:33
Done.
|
| +#include "base/memory/scoped_ptr.h" |
| +#include "base/strings/string_number_conversions.h" |
| +#include "base/strings/utf_string_conversions.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 "chromeos/chromeos_switches.h" |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: Not used.
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
pneubeck (no reviews)
2015/09/07 17:21:33
Done.
|
| +#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/test/result_catcher.h" |
| +#include "net/base/escape.h" |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: Not used.
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| +#include "net/base/test_data_directory.h" |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: Not used.
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| +#include "net/cert/test_root_certs.h" |
|
bartfab (slow)
2015/09/03 17:30:52
Nit: Not used.
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| +#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(base::Closure callback, const base::Value* value) { |
|
bartfab (slow)
2015/09/03 17:30:53
Nit 1: Pass |callback| by const reference.
Nit 2:
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + callback.Run(); |
| +} |
| + |
| +void StoreBool(bool* result, base::Closure callback, const base::Value* value) { |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: Pass |callback| by const reference.
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + value->GetAsBoolean(result); |
| + callback.Run(); |
| +} |
| + |
| +void StoreString(std::string* result, |
|
bartfab (slow)
2015/09/03 17:30:52
Nit: #include <string>
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + base::Closure callback, |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: Pass |callback| by const reference.
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + const base::Value* value) { |
| + value->GetAsString(result); |
| + callback.Run(); |
| +} |
| + |
| +void StoreDigest(base::Closure callback, |
|
bartfab (slow)
2015/09/03 17:30:53
Nit 1: Pass |callback| by const reference.
Nit 2:
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + std::vector<uint8>* digest, |
|
bartfab (slow)
2015/09/03 17:30:53
Nit 1: Here and throughout the file: s/uint8/uint8
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + const base::Value* value) { |
| + const base::BinaryValue* binary = nullptr; |
| + value->GetAsBinary(&binary); |
| + const uint8* const binary_begin = |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: const pointer to const.
pneubeck (no reviews)
2015/09/07 17:21:32
It is already. Isn't it?
bartfab (slow)
2015/09/08 14:55:16
Sorry :).
|
| + reinterpret_cast<const uint8*>(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>& digest, |
| + crypto::RSAPrivateKey* key, |
| + std::vector<uint8>* 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; |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: #include <stddef.h>
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + int is_alloced = 0; |
| + if (!RSA_add_pkcs1_prefix(&prefixed_digest, &prefixed_digest_len, &is_alloced, |
| + NID_sha1, vector_as_array(&digest), |
|
bartfab (slow)
2015/09/03 17:30:52
Nit: #include "base/stl_util.h"
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + 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); |
|
bartfab (slow)
2015/09/03 17:30:52
Nit: #include <stdlib.h>
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + |
| + if (rv) { |
| + signature->resize(len); |
| + return true; |
| + } else { |
| + signature->clear(); |
| + return false; |
| + } |
| +} |
| + |
| +// Create a string that if evaluated in Javascript returns a Uint8Array with |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: s/Javascript/JavaScript/
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| +// |bytes| as content. |
| +std::string JsUint8Array(const std::vector<uint8>& bytes) { |
| + std::string res = "new Uint8Array(["; |
| + for (size_t i = 0; i < bytes.size(); i++) { |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: Why not use an iterator?
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + res += base::UintToString(bytes[i]); |
| + if (i < bytes.size() - 1) { |
|
bartfab (slow)
2015/09/03 17:30:53
JavaScript actually allows trailing commas, so you
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + 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 { |
| + // Setup the AutoSelectCertificateForUrls policy to avoid the client |
|
bartfab (slow)
2015/09/03 17:30:52
Nit: s/Setup/Set up/
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + // 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 certificates. |
|
bartfab (slow)
2015/09/03 17:30:52
Nit: s/certificates/certificate/
pneubeck (no reviews)
2015/09/07 17:21:33
Done.
|
| + 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"))); |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: Use |PathService::Get(chrome::DIR_TEST_DATA,
|
| + ASSERT_TRUE(https_server.Start()); |
| + |
| + extensions::ResultCatcher catcher; |
| + |
| + const base::FilePath extension_path = |
| + test_data_dir_.AppendASCII("certificate_provider"); |
| + const extensions::Extension* extension = LoadExtension(extension_path); |
|
bartfab (slow)
2015/09/03 17:30:52
Nit 1: const pointer to const.
Nit 2: #include "ex
pneubeck (no reviews)
2015/09/07 17:21:33
Done.
|
| + 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> request_digest; |
| + { |
| + base::RunLoop run_loop; |
|
bartfab (slow)
2015/09/03 17:30:52
Nit: #include "base/run_loop.h"
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + extension_contents->GetMainFrame()->ExecuteJavaScriptForTests( |
| + base::ASCIIToUTF16( |
| + "(function() {return signDigestRequest.digest;})();"), |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: Does this really need to be wrapped into a fu
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + base::Bind(&StoreDigest, run_loop.QuitClosure(), &request_digest)); |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: #include "base/bind.h"
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + 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); |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: #include "base/files/file_util.h"
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + |
| + const uint8* const key_pk8_begin = |
| + reinterpret_cast<const uint8*>(key_pk8.data()); |
| + scoped_ptr<crypto::RSAPrivateKey> key( |
| + crypto::RSAPrivateKey::CreateFromPrivateKeyInfo( |
| + std::vector<uint8>(key_pk8_begin, key_pk8_begin + key_pk8.size()))); |
| + ASSERT_TRUE(key); |
| + |
| + std::vector<uint8> signature; |
| + ASSERT_TRUE(RsaSign(request_digest, key.get(), &signature)); |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: EXPECT_TRUE() should be sufficient here.
pneubeck (no reviews)
2015/09/07 17:21:33
Done.
|
| + |
| + VLOG(1) << "Inject the signature back to the extension and let it reply."; |
| + { |
| + base::RunLoop run_loop; |
| + const std::string code = "(function() {replyWithSignature(" + |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: As above, does this really need to be wrapped
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + 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( |
| + "(function() {return document.body.textContent;})();"), |
|
bartfab (slow)
2015/09/03 17:30:53
Nit: As above, does this really need to be wrapped
pneubeck (no reviews)
2015/09/07 17:21:32
Done.
|
| + 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); |
| + } |
| +} |