| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/web/certificate_policy_app_agent.h" |
| |
| #import "base/bind.h" |
| #import "base/memory/scoped_refptr.h" |
| #import "base/run_loop.h" |
| #import "base/test/ios/wait_util.h" |
| #import "base/time/time.h" |
| #import "ios/chrome/app/application_delegate/app_state.h" |
| #import "ios/chrome/app/application_delegate/browser_launcher.h" |
| #import "ios/chrome/app/application_delegate/startup_information.h" |
| #import "ios/chrome/app/main_application_delegate.h" |
| #import "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #import "ios/chrome/browser/browser_state/test_chrome_browser_state.h" |
| #import "ios/chrome/browser/main/browser_list.h" |
| #import "ios/chrome/browser/main/browser_list_factory.h" |
| #import "ios/chrome/browser/main/test_browser.h" |
| #import "ios/chrome/browser/web_state_list/web_state_list.h" |
| #import "ios/chrome/browser/web_state_list/web_state_opener.h" |
| #import "ios/chrome/test/block_cleanup_test.h" |
| #import "ios/web/public/security/certificate_policy_cache.h" |
| #import "ios/web/public/session/session_certificate_policy_cache.h" |
| #import "ios/web/public/test/fakes/fake_web_state.h" |
| #import "ios/web/public/test/web_task_environment.h" |
| #import "ios/web/public/thread/web_task_traits.h" |
| #import "ios/web/public/thread/web_thread.h" |
| #import "net/cert/x509_certificate.h" |
| #import "net/test/cert_test_util.h" |
| #import "net/test/test_data_directory.h" |
| #import "third_party/ocmock/OCMock/OCMock.h" |
| #import "third_party/ocmock/gtest_support.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| using base::test::ios::kWaitForActionTimeout; |
| using base::test::ios::SpinRunLoopWithMaxDelay; |
| using base::test::ios::WaitUntilConditionOrTimeout; |
| |
| // Test fixture for the cert policy app agent. The APIs under test operate on |
| // the UI thread (hence the task environment setup). The app agent updates the |
| // cert cache based on the contents of multiple browsers, so most test cases |
| // involve setting up web states with various session caches in multiple |
| // browsers, and then inducing the app agent to update the global cache. |
| class CertificatePolicyAppStateAgentTest : public BlockCleanupTest { |
| protected: |
| CertificatePolicyAppStateAgentTest() |
| : task_environment_(web::WebTaskEnvironment::Options::REAL_IO_THREAD), |
| cert_(net::ImportCertFromFile(net::GetTestCertsDirectory(), |
| "ok_cert.pem")), |
| status_(net::CERT_STATUS_REVOKED) { |
| // Mocks for AppState dependencies. |
| browser_launcher_mock_ = |
| [OCMockObject mockForProtocol:@protocol(BrowserLauncher)]; |
| startup_information_mock_ = |
| [OCMockObject mockForProtocol:@protocol(StartupInformation)]; |
| main_application_delegate_ = |
| [OCMockObject mockForClass:[MainApplicationDelegate class]]; |
| |
| TestChromeBrowserState::Builder test_cbs_builder; |
| chrome_browser_state_ = test_cbs_builder.Build(); |
| |
| BrowserList* browser_list = |
| BrowserListFactory::GetForBrowserState(chrome_browser_state_.get()); |
| |
| app_state_ = |
| [[AppState alloc] initWithBrowserLauncher:browser_launcher_mock_ |
| startupInformation:startup_information_mock_ |
| applicationDelegate:main_application_delegate_]; |
| app_state_.mainBrowserState = chrome_browser_state_.get(); |
| |
| // Create two regular and one OTR browsers. |
| regular_browser_1_ = |
| std::make_unique<TestBrowser>(chrome_browser_state_.get()); |
| regular_browser_2_ = |
| std::make_unique<TestBrowser>(chrome_browser_state_.get()); |
| incognito_browser_ = std::make_unique<TestBrowser>( |
| chrome_browser_state_->GetOffTheRecordChromeBrowserState()); |
| browser_list->AddBrowser(regular_browser_1_.get()); |
| browser_list->AddBrowser(regular_browser_2_.get()); |
| browser_list->AddIncognitoBrowser(incognito_browser_.get()); |
| |
| // Finally, create the app agent being tested and attach it to the app |
| // state. |
| app_agent_ = [[CertificatePolicyAppAgent alloc] init]; |
| [app_state_ addAgent:app_agent_]; |
| } |
| |
| // Adds a web state with `host` as the active URL to `browser`. |
| void AddWebStateToBrowser(std::string host, Browser* browser) { |
| auto test_web_state = std::make_unique<web::FakeWebStateWithPolicyCache>( |
| browser->GetBrowserState()); |
| GURL url(host); |
| test_web_state->SetCurrentURL(url); |
| WebStateList* web_state_list = browser->GetWebStateList(); |
| web_state_list->InsertWebState( |
| WebStateList::kInvalidIndex, std::move(test_web_state), |
| WebStateList::INSERT_NO_FLAGS, WebStateOpener()); |
| } |
| |
| // Adds a web state with `host` as the active URL, and with `host` registered |
| // as having a valid certificate to `browser`. |
| void AddCertifiedWebStateToBrowser(std::string host, Browser* browser) { |
| auto test_web_state = std::make_unique<web::FakeWebStateWithPolicyCache>( |
| browser->GetBrowserState()); |
| GURL url(host); |
| test_web_state->SetCurrentURL(url); |
| test_web_state->GetSessionCertificatePolicyCache() |
| ->RegisterAllowedCertificate(cert_, host, status_); |
| WebStateList* web_state_list = browser->GetWebStateList(); |
| web_state_list->InsertWebState( |
| WebStateList::kInvalidIndex, std::move(test_web_state), |
| WebStateList::INSERT_NO_FLAGS, WebStateOpener()); |
| } |
| |
| // Adds one web state to each browser with no certs. |
| void PopulateWebStatesWithNoCerts() { |
| AddWebStateToBrowser("x.com", regular_browser_1_.get()); |
| AddWebStateToBrowser("y.com", regular_browser_2_.get()); |
| AddWebStateToBrowser("z.com", incognito_browser_.get()); |
| } |
| |
| // Creates the populated fixture for all tests -- one web state in each |
| // browser with no allowed certificates, then two more web states, each with |
| // one allowed certificate, in each browser. The allowed hosts are |
| // {a,b,c,d}.com for the regular browsers, and {a,b}.com for the incognito |
| // browser. |
| void PopulateWebStates() { |
| PopulateWebStatesWithNoCerts(); |
| |
| // Adds web states with certs |
| AddCertifiedWebStateToBrowser("a.com", regular_browser_1_.get()); |
| AddCertifiedWebStateToBrowser("b.com", regular_browser_1_.get()); |
| AddCertifiedWebStateToBrowser("c.com", regular_browser_2_.get()); |
| AddCertifiedWebStateToBrowser("d.com", regular_browser_2_.get()); |
| AddCertifiedWebStateToBrowser("a.com", incognito_browser_.get()); |
| AddCertifiedWebStateToBrowser("b.com", incognito_browser_.get()); |
| |
| // Clear the policy caches after this, since RegisterAllowedCertificate adds |
| // to the global cache. |
| ClearPolicyCache(RegularPolicyCache()); |
| ClearPolicyCache(IncognitoPolicyCache()); |
| } |
| |
| // Triggers certificate cache updates in the app agent under test, and wait |
| // for updates to complete. |
| void TriggerCertCacheUpdate() { |
| [app_agent_ appDidEnterBackground]; |
| // Cache clearing is on the IO thread, and cache reconstruction is posted |
| // to the main thread, so both a wait and a RunUntilIdle() are needed. |
| ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^{ |
| base::RunLoop().RunUntilIdle(); |
| return !app_agent_.working; |
| })); |
| } |
| |
| // Checks `cache` to see if the policy for `host` is "allowed". For the |
| // purposes of this test, that's effectively testing if `host` is "in" |
| // `cache`. Checking the cache is async, so this method handles synchronous |
| // waiting for the result. |
| bool IsHostCertAllowed( |
| const scoped_refptr<web::CertificatePolicyCache>& cache, |
| const std::string& host) { |
| __block web::CertPolicy::Judgment judgement = |
| web::CertPolicy::Judgment::UNKNOWN; |
| __block bool completed = false; |
| web::GetIOThreadTaskRunner({})->PostTask(FROM_HERE, base::BindOnce(^{ |
| completed = true; |
| judgement = cache->QueryPolicy( |
| cert_.get(), host, status_); |
| })); |
| EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^{ |
| return completed; |
| })); |
| return judgement == web::CertPolicy::Judgment::ALLOWED; |
| } |
| |
| // Clears all entries from `cache`. This is posted to the IO thread and this |
| // method sync-waits for this to complete. |
| void ClearPolicyCache( |
| const scoped_refptr<web::CertificatePolicyCache>& cache) { |
| __block bool policies_cleared = false; |
| web::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(^{ |
| cache->ClearCertificatePolicies(); |
| policies_cleared = true; |
| })); |
| EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^{ |
| return policies_cleared; |
| })); |
| } |
| |
| // The policy cache for the regular browser state used in this test. |
| scoped_refptr<web::CertificatePolicyCache> RegularPolicyCache() { |
| return web::BrowserState::GetCertificatePolicyCache( |
| chrome_browser_state_.get()); |
| } |
| |
| // The policy cache for the incognito browser state used in this test. |
| scoped_refptr<web::CertificatePolicyCache> IncognitoPolicyCache() { |
| return web::BrowserState::GetCertificatePolicyCache( |
| chrome_browser_state_->GetOffTheRecordChromeBrowserState()); |
| } |
| |
| bool RegularPolicyCacheContainsHost(const std::string& host) { |
| return IsHostCertAllowed(RegularPolicyCache(), host); |
| } |
| |
| bool IncognitoPolicyCacheContainsHost(const std::string& host) { |
| return IsHostCertAllowed(IncognitoPolicyCache(), host); |
| } |
| |
| // Populates `cache` with allowed certs for the hosts in `hosts`. This is done |
| // in a single async call, and this method sync-waits on it completing. |
| void PopulatePolicyCache(std::vector<std::string> hosts, |
| scoped_refptr<web::CertificatePolicyCache> cache) { |
| __block bool hosts_added = false; |
| auto populate_cache = ^{ |
| for (std::string host : hosts) { |
| cache->AllowCertForHost(cert_.get(), host, status_); |
| } |
| hosts_added = true; |
| }; |
| web::GetIOThreadTaskRunner({})->PostTask(FROM_HERE, |
| base::BindOnce(populate_cache)); |
| EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^{ |
| return hosts_added; |
| })); |
| } |
| |
| private: |
| web::WebTaskEnvironment task_environment_; |
| AppState* app_state_; |
| CertificatePolicyAppAgent* app_agent_; |
| std::unique_ptr<ChromeBrowserState> chrome_browser_state_; |
| std::unique_ptr<TestBrowser> regular_browser_1_; |
| std::unique_ptr<TestBrowser> regular_browser_2_; |
| std::unique_ptr<TestBrowser> incognito_browser_; |
| |
| scoped_refptr<net::X509Certificate> cert_; |
| net::CertStatus status_; |
| |
| // Mocks for AppState dependencies. |
| id browser_launcher_mock_; |
| id startup_information_mock_; |
| id main_application_delegate_; |
| }; |
| |
| // Test that updating an empty cache with no webstates results in an empty |
| // cache. |
| TEST_F(CertificatePolicyAppStateAgentTest, EmptyCacheNoWebstates) { |
| // Empty cache, no webstates. |
| TriggerCertCacheUpdate(); |
| // Expect nothing is in the cache. |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("a.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("b.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("c.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("d.com")); |
| EXPECT_FALSE(IncognitoPolicyCacheContainsHost("a.com")); |
| EXPECT_FALSE(IncognitoPolicyCacheContainsHost("b.com")); |
| } |
| |
| // Test that updating an populated cache with no webstates results in an empty |
| // cache. |
| TEST_F(CertificatePolicyAppStateAgentTest, PopulatedCacheNoWebstates) { |
| // Populated caches. |
| PopulatePolicyCache({"a.com", "b.com"}, RegularPolicyCache()); |
| PopulatePolicyCache({"a.com", "b.com"}, IncognitoPolicyCache()); |
| // No webstates. |
| TriggerCertCacheUpdate(); |
| |
| // Expect nothing in caches. |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("a.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("b.com")); |
| EXPECT_FALSE(IncognitoPolicyCacheContainsHost("a.com")); |
| EXPECT_FALSE(IncognitoPolicyCacheContainsHost("b.com")); |
| } |
| |
| // Test that updating an empty cache with webstates having no certs results in |
| // an empty cache. |
| TEST_F(CertificatePolicyAppStateAgentTest, EmptyCacheNoCertedWebstates) { |
| // Empty cache. |
| // Webstates without certs: |
| PopulateWebStatesWithNoCerts(); |
| |
| TriggerCertCacheUpdate(); |
| |
| // Expect nothing is in the cache. |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("a.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("b.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("c.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("d.com")); |
| EXPECT_FALSE(IncognitoPolicyCacheContainsHost("a.com")); |
| EXPECT_FALSE(IncognitoPolicyCacheContainsHost("b.com")); |
| } |
| |
| // Test that updating an empty cache with webstates having certs results in all |
| // webstate entries being in the cache. (Also incidentally tests that populating |
| // test fixture web states doesn't also populate the cache). |
| TEST_F(CertificatePolicyAppStateAgentTest, EmptyCacheCertedWebstates) { |
| // Fully populated webstates, empty cache. |
| PopulateWebStates(); |
| |
| // Expect the cache is actually empty. |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("a.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("b.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("c.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("d.com")); |
| EXPECT_FALSE(IncognitoPolicyCacheContainsHost("a.com")); |
| EXPECT_FALSE(IncognitoPolicyCacheContainsHost("b.com")); |
| |
| TriggerCertCacheUpdate(); |
| |
| // Expect that entries for all web states are now in the cache. |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("a.com")); |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("b.com")); |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("c.com")); |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("d.com")); |
| EXPECT_TRUE(IncognitoPolicyCacheContainsHost("a.com")); |
| EXPECT_TRUE(IncognitoPolicyCacheContainsHost("b.com")); |
| } |
| |
| // Tests that entries in a cache that aren't in the webstates are removed. |
| TEST_F(CertificatePolicyAppStateAgentTest, CacheHasExtraCerts) { |
| // Fully populated web states. |
| PopulateWebStates(); |
| |
| // Populate the caches with entries for all webstates, and some extras -- |
| // {e,f}.com in the regular cache, and c.com in the incognito cache. |
| PopulatePolicyCache({"a.com", "b.com", "c.com", "d.com", "e.com", "f.com"}, |
| RegularPolicyCache()); |
| PopulatePolicyCache({"a.com", "b.com", "c.com"}, IncognitoPolicyCache()); |
| |
| TriggerCertCacheUpdate(); |
| |
| // Expect that the entries corresponding to the webstates are in the cache. |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("a.com")); |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("b.com")); |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("c.com")); |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("d.com")); |
| EXPECT_TRUE(IncognitoPolicyCacheContainsHost("a.com")); |
| EXPECT_TRUE(IncognitoPolicyCacheContainsHost("b.com")); |
| |
| // Expect the extra entries are not in the cache. |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("e.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("f.com")); |
| EXPECT_FALSE(IncognitoPolicyCacheContainsHost("c.com")); |
| } |
| |
| // Tests that a cache containing some (but not all) of the entries in the web |
| // states, and some extra entries, is properly updates to contain all and only |
| // the entries in the web states. |
| TEST_F(CertificatePolicyAppStateAgentTest, CacheAndWebstatesDiffer) { |
| // Fully populated web states. |
| PopulateWebStates(); |
| |
| // Populate the caches with entries for some webstates ({a,b}.com), and some |
| // extras -- ({e,f}.com in the regular cache, and c.com in the incognito |
| // cache). |
| PopulatePolicyCache({"a.com", "b.com", "e.com", "f.com"}, |
| RegularPolicyCache()); |
| PopulatePolicyCache({"a.com", "c.com"}, IncognitoPolicyCache()); |
| |
| TriggerCertCacheUpdate(); |
| |
| // Expect that entries for all of the webstates are in the cache. |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("a.com")); |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("b.com")); |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("c.com")); |
| EXPECT_TRUE(RegularPolicyCacheContainsHost("d.com")); |
| EXPECT_TRUE(IncognitoPolicyCacheContainsHost("a.com")); |
| EXPECT_TRUE(IncognitoPolicyCacheContainsHost("b.com")); |
| |
| // Expect that none of the "extra" entries are in the cache. |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("e.com")); |
| EXPECT_FALSE(RegularPolicyCacheContainsHost("f.com")); |
| EXPECT_FALSE(IncognitoPolicyCacheContainsHost("c.com")); |
| } |