chromium/chrome/browser/lacros/cert/cert_db_initializer_browsertest.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "chrome/browser/lacros/cert/cert_db_initializer.h"
#include "chrome/browser/lacros/cert/cert_db_initializer_factory.h"
#include "chrome/browser/net/nss_service.h"
#include "chrome/browser/net/nss_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/crosapi/cpp/keystore_service_util.h"
#include "chromeos/crosapi/mojom/keystore_service.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "chromeos/lacros/lacros_test_helper.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "net/cert/cert_database.h"
#include "net/cert/nss_cert_database.h"
#include "net/cert/x509_certificate.h"
#include "net/cert/x509_util.h"
#include "net/cert/x509_util_nss.h"
#include "net/test/cert_builder.h"
#include "net/test/cert_test_util.h"
#include "services/network/public/mojom/network_context.mojom.h"

// NOTE: Tests in this file modify the certificate store. That is potentially a
// lasting side effect that can affect other tests.
// * To prevent interference with tests that are run in parallel, these tests
// are a part of lacros_chrome_browsertests test suite.
// * To prevent interference with following tests, they try to clean up all the
// side effects themself, e.g. if a test adds a cert, it is also responsible for
// deleting it.
// Subsequent runs of lacros browser tests share the same ash-chrome instance
// and thus also the same user certificate database. The certificate database is
// not cleaned automatically between tests because of performance concerns.

namespace {

constexpr char kRootCaCert[] = "root_ca_cert.pem";
// A PEM-encoded certificate which was signed by the Authority specified in
// |kRootCaCert|.
constexpr char kServerCert[] = "ok_cert.pem";

base::FilePath GetTestCertsPath() {
  base::FilePath test_data_dir;
  EXPECT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir));

  base::FilePath test_certs_path =
      test_data_dir.AppendASCII("policy").AppendASCII("test_certs");
  base::ScopedAllowBlockingForTesting allow_io;
  EXPECT_TRUE(base::DirectoryExists(test_certs_path));
  return test_certs_path;
}

void WaitUnitCertDbReady(Profile* profile) {
  base::RunLoop run_loop;
  CertDbInitializer* initializer =
      CertDbInitializerFactory::GetInstance()->GetForBrowserContext(profile);
  ASSERT_TRUE(initializer);
  auto subscription = initializer->WaitUntilReady(run_loop.QuitClosure());
  run_loop.Run();
}

net::ScopedCERTCertificateList GetCaCert() {
  static std::string cert_bytes;
  if (cert_bytes.empty()) {
    base::ScopedAllowBlockingForTesting allow_blocking;
    base::ReadFileToString(GetTestCertsPath().AppendASCII(kRootCaCert),
                           &cert_bytes);
  }

  return net::x509_util::CreateCERTCertificateListFromBytes(
      base::as_byte_span(cert_bytes), net::X509Certificate::FORMAT_AUTO);
}

void GetNssDatabaseOnIO(NssCertDatabaseGetter nss_getter,
                        base::OnceCallback<void(net::NSSCertDatabase*)> task) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  auto splitted_task = base::SplitOnceCallback(std::move(task));
  net::NSSCertDatabase* nss_db =
      std::move(nss_getter).Run(std::move(splitted_task.first));
  if (nss_db) {
    std::move(splitted_task.second).Run(nss_db);
  }
}

void GetNssDatabase(Profile* profile,
                    base::OnceCallback<void(net::NSSCertDatabase*)> task) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  NssService* nss_service =
      NssServiceFactory::GetInstance()->GetForContext(profile);

  content::GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&GetNssDatabaseOnIO,
                     nss_service->CreateNSSCertDatabaseGetterForIOThread(),
                     std::move(task)));
}

void ImportCaCertWithDb(base::OnceClosure done_callback,
                        net::NSSCertDatabase* nss_db) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  ASSERT_TRUE(nss_db);

  net::NSSCertDatabase::ImportCertFailureList failure_list;
  nss_db->ImportCACerts(GetCaCert(),
                        /*trust_bits=*/net::NSSCertDatabase::TRUSTED_SSL,
                        &failure_list);

  if (!failure_list.empty()) {
    for (const auto& failure : failure_list) {
      LOG(ERROR) << "Failed to import CA cert: " << failure.net_error;
    }
    ASSERT_TRUE(false);
  }

  std::move(done_callback).Run();
}

void ImportCaCert(Profile* profile) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  base::RunLoop run_loop;
  GetNssDatabase(profile,
                 base::BindOnce(&ImportCaCertWithDb, run_loop.QuitClosure()));
  run_loop.Run();
}

void DeleteCaCertWithDb(base::OnceClosure done_callback,
                        net::NSSCertDatabase* nss_db) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  ASSERT_TRUE(nss_db);
  auto cert = GetCaCert();
  ASSERT_EQ(cert.size(), 1u);
  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(nss_db->DeleteCertAndKey(cert[0].get()));
  std::move(done_callback).Run();
}

void DeleteCaCert(Profile* profile) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  base::RunLoop run_loop;
  GetNssDatabase(profile,
                 base::BindOnce(&DeleteCaCertWithDb, run_loop.QuitClosure()));
  run_loop.Run();
}

[[nodiscard]] int VerifyServerCert(Profile* profile) {
  base::FilePath server_cert_path = GetTestCertsPath().AppendASCII(kServerCert);
  scoped_refptr<net::X509Certificate> server_cert = net::ImportCertFromFile(
      server_cert_path.DirName(), server_cert_path.BaseName().value());
  EXPECT_TRUE(server_cert);

  base::test::TestFuture<int> future;
  profile->GetDefaultStoragePartition()
      ->GetNetworkContext()
      ->VerifyCertificateForTesting(
          server_cert, "127.0.0.1", /*ocsp_response=*/std::string(),
          /*sct_list=*/std::string(), future.GetCallback());
  return future.Get();
}

// Generates an x509 client certificate for the `public_key_spki` and returns
// it as a DER-encoded certificate.
[[nodiscard]] std::vector<uint8_t> GenerateClientCertForPublicKey(
    const std::vector<uint8_t>& public_key_spki) {
  net::CertBuilder issuer(/*orig_cert=*/nullptr, /*issuer=*/nullptr);
  issuer.GenerateRSAKey();
  auto cert_builder =
      net::CertBuilder::FromSubjectPublicKeyInfo(public_key_spki, &issuer);
  cert_builder->SetSignatureAlgorithm(
      bssl::SignatureAlgorithm::kRsaPkcs1Sha256);
  cert_builder->SetValidity(base::Time::Now(),
                            base::Time::Now() + base::Days(30));

  auto cert_span =
      net::x509_util::CryptoBufferAsSpan(cert_builder->GetCertBuffer());
  return std::vector<uint8_t>(cert_span.begin(), cert_span.end());
}

// Observes notifications about cert database changes during its lifetime.
class ScopedCertDatabaseObserver : public net::CertDatabase::Observer {
 public:
  static std::unique_ptr<ScopedCertDatabaseObserver> Create() {
    return std::make_unique<ScopedCertDatabaseObserver>();
  }
  ScopedCertDatabaseObserver() {
    net::CertDatabase::GetInstance()->AddObserver(this);
  }
  ~ScopedCertDatabaseObserver() override {
    net::CertDatabase::GetInstance()->RemoveObserver(this);
  }

  void OnClientCertStoreChanged() override {
    notifications_received_++;
    run_loop_.Quit();
  }

  // Waits for the next CertDBChanged notification if none were observed so
  // far. Returns the amount of notifications received since creation. The
  // counter is mostly used to detect unexpected notifications that could
  // cause flakiness / false positives.
  size_t Wait() {
    // Noop if Quit() was ever called.
    run_loop_.Run();
    return notifications_received_;
  }

 private:
  size_t notifications_received_ = 0;
  base::RunLoop run_loop_;
};

class CertDbInitializerTest : public InProcessBrowserTest {
 public:
  void SetUp() override {
    CertDbInitializerFactory::GetInstance()
        ->SetCreateWithBrowserContextForTesting(
            /*should_create=*/true);
    InProcessBrowserTest::SetUp();
  }
};

// Tests that CertDbInitializer eventually reports that cert database is ready
// for the main profile.
IN_PROC_BROWSER_TEST_F(CertDbInitializerTest, EventuallyReady) {
  EXPECT_TRUE(browser()->profile()->IsMainProfile());
  WaitUnitCertDbReady(browser()->profile());
}

// Tests that a CA certificate can be imported and used for cert verification.
IN_PROC_BROWSER_TEST_F(CertDbInitializerTest, CanImportAndDeleteCaCert) {
  WaitUnitCertDbReady(browser()->profile());
  ImportCaCert(browser()->profile());
  EXPECT_EQ(net::OK, VerifyServerCert(browser()->profile()));
  DeleteCaCert(browser()->profile());
  EXPECT_EQ(net::ERR_CERT_AUTHORITY_INVALID,
            VerifyServerCert(browser()->profile()));
}

// Tests that without importing a CA certificate, Chrome rejects unknown
// server certs.
IN_PROC_BROWSER_TEST_F(CertDbInitializerTest, CertRejectedByDefault) {
  WaitUnitCertDbReady(browser()->profile());
  EXPECT_EQ(net::ERR_CERT_AUTHORITY_INVALID,
            VerifyServerCert(browser()->profile()));
}

// Imports a CA certs that will be available in ImmediatelyAfterLaunch test.
IN_PROC_BROWSER_TEST_F(CertDbInitializerTest, PRE_ImmediatelyAfterLaunch) {
  WaitUnitCertDbReady(browser()->profile());
  ImportCaCert(browser()->profile());
}

// Tests that Chrome waits until certs are initialized and the imported CA
// cert from the PRE_ test is available before verifying server certs.
IN_PROC_BROWSER_TEST_F(CertDbInitializerTest, ImmediatelyAfterLaunch) {
  EXPECT_EQ(net::OK, VerifyServerCert(browser()->profile()));
  // Cleanup side effects of the test.
  DeleteCaCert(browser()->profile());
}

// Tests that when Ash imports a new certificate, Lacros receives a
// notification about it.
IN_PROC_BROWSER_TEST_F(CertDbInitializerTest, CertsChangedNotificationFromAsh) {
  auto& keystore_crosapi = chromeos::LacrosService::Get()
                               ->GetRemote<crosapi::mojom::KeystoreService>();

  // This test uses the Keystore mojo API to make Ash import a cert. Ash will
  // only successfully import a cert if it owns a key pair associated with it.
  // This call generates a new key pair.
  base::test::TestFuture<crosapi::mojom::KeystoreBinaryResultPtr>
      generate_key_result;
  keystore_crosapi->GenerateKey(
      crosapi::mojom::KeystoreType::kUser,
      crosapi::keystore_service_util::MakeRsaKeystoreSigningAlgorithm(
          /*modulus_length=*/2048, /*sw_backed=*/false),
      generate_key_result.GetCallback());
  ASSERT_FALSE(generate_key_result.Get()->is_error());

  std::vector<uint8_t> client_cert =
      GenerateClientCertForPublicKey(generate_key_result.Get()->get_blob());

  auto observer = ScopedCertDatabaseObserver::Create();

  // Generate and import a certificate.
  base::test::TestFuture<bool /*is_error*/, crosapi::mojom::KeystoreError>
      add_cert_result;
  keystore_crosapi->AddCertificate(crosapi::mojom::KeystoreType::kUser,
                                   client_cert, add_cert_result.GetCallback());
  ASSERT_FALSE(add_cert_result.Get<0>())
      << "Error: " << add_cert_result.Get<1>();

  // Wait for the notification from Ash about cert database changes.
  // If there are more than one, most likely there are other sources of
  // changes in the background and the test should be rewritten somehow.
  EXPECT_EQ(1u, observer->Wait());

  // Check that the cert was actually imported.
  base::test::TestFuture<crosapi::mojom::GetCertificatesResultPtr>
      get_certs_result;
  keystore_crosapi->GetCertificates(crosapi::mojom::KeystoreType::kUser,
                                    get_certs_result.GetCallback());
  ASSERT_FALSE(get_certs_result.Get()->is_error());
  EXPECT_TRUE(
      base::Contains(get_certs_result.Get()->get_certificates(), client_cert));

  observer = ScopedCertDatabaseObserver::Create();

  base::test::TestFuture<bool /*is_error*/, crosapi::mojom::KeystoreError>
      remove_cert_result;
  keystore_crosapi->RemoveCertificate(crosapi::mojom::KeystoreType::kUser,
                                      client_cert,
                                      remove_cert_result.GetCallback());
  ASSERT_FALSE(remove_cert_result.Get<0>())
      << "Error: " << remove_cert_result.Get<1>();

  EXPECT_EQ(1u, observer->Wait());
}

// TODO(b/191336682): Add a test similar to CertsChangedNotificationFromAsh,
// but about system keystore. Right now system slot is not available/emulated
// in lacros browser tests.

// For a test that covers notifications in Ash when Lacros changes the
// database, see network.CertSettingsPage tast test. Such a browser test could
// be written by adding new methods into crosapi.TestController, but their
// implementation would have a similar complexity to the notification
// mechanism itself.

}  // namespace