chromium/chrome/browser/lacros/keystore_service_lacros_browsertest.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include <memory>
#include <string>

#include "base/ranges/algorithm.h"
#include "base/test/test_future.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/test/base/in_process_browser_test.h"
#include "chromeos/crosapi/cpp/keystore_service_util.h"
#include "chromeos/crosapi/mojom/keystore_service.mojom-shared.h"
#include "chromeos/crosapi/mojom/keystore_service.mojom-test-utils.h"
#include "chromeos/crosapi/mojom/keystore_service.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_test.h"
#include "net/cert/nss_cert_database.h"
#include "net/cert/scoped_nss_types.h"
#include "net/cert/x509_util.h"
#include "net/test/cert_builder.h"

// NOTE: Some 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 {

namespace is_cert_in_nss {
namespace internal {

void IsCertInNSSDatabaseOnIOThreadWithCertList(
    const std::vector<uint8_t>& expected_cert_der,
    bool* out_cert_found,
    base::OnceClosure done_closure,
    net::ScopedCERTCertificateList certs) {
  for (const net::ScopedCERTCertificate& cert : certs) {
    auto cert_der = base::make_span(cert->derCert.data, cert->derCert.len);
    if (base::ranges::equal(cert_der, expected_cert_der)) {
      *out_cert_found = true;
      break;
    }
  }

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

void IsCertInNSSDatabaseOnIOThreadWithCertDb(
    const std::vector<uint8_t>& expected_cert_der,
    bool* out_cert_found,
    base::OnceClosure done_closure,
    net::NSSCertDatabase* cert_db) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  cert_db->ListCerts(base::BindOnce(&IsCertInNSSDatabaseOnIOThreadWithCertList,
                                    expected_cert_der, out_cert_found,
                                    std::move(done_closure)));
}

void IsCertInNSSDatabaseOnIOThread(
    NssCertDatabaseGetter database_getter,
    const std::vector<uint8_t>& expected_cert_der,
    bool* out_cert_found,
    base::OnceClosure done_closure) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  auto did_get_cert_db_split_callback = base::SplitOnceCallback(base::BindOnce(
      &IsCertInNSSDatabaseOnIOThreadWithCertDb, expected_cert_der,
      out_cert_found, std::move(done_closure)));

  net::NSSCertDatabase* cert_db =
      std::move(database_getter)
          .Run(std::move(did_get_cert_db_split_callback.first));
  if (cert_db) {
    std::move(did_get_cert_db_split_callback.second).Run(cert_db);
  }
}
}  // namespace internal

// Returns true if a certificate with subject CommonName `common_name` is
// present in the `NSSCertDatabase` for `profile`.
bool IsCertInNSSDatabase(Profile* profile,
                         const std::vector<uint8_t>& expected_cert_der) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  base::RunLoop run_loop;
  bool cert_found = false;
  content::GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(internal::IsCertInNSSDatabaseOnIOThread,
                     NssServiceFactory::GetForContext(profile)
                         ->CreateNSSCertDatabaseGetterForIOThread(),
                     expected_cert_der, &cert_found, run_loop.QuitClosure()));
  run_loop.Run();
  return cert_found;
}

}  // namespace is_cert_in_nss

// Makes a CertBuilder that would return a valid x509 client certificate for the
// `public_key_spki`.
scoped_refptr<net::X509Certificate> MakeCert(
    const std::vector<uint8_t>& public_key_spki) {
  auto issuer = std::make_unique<net::CertBuilder>(/*orig_cert=*/nullptr,
                                                   /*issuer=*/nullptr);
  issuer->GenerateRSAKey();
  auto cert_builder =
      net::CertBuilder::FromSubjectPublicKeyInfo(public_key_spki, issuer.get());
  cert_builder->SetSignatureAlgorithm(
      bssl::SignatureAlgorithm::kRsaPkcs1Sha256);
  cert_builder->SetValidity(base::Time::Now(),
                            base::Time::Now() + base::Days(30));
  return cert_builder->GetX509Certificate();
}

// Returns x509 client certificate from the `cert_builder` as a DER-encoded
// certificate.
std::vector<uint8_t> CertToDer(scoped_refptr<net::X509Certificate> cert) {
  auto cert_span = net::x509_util::CryptoBufferAsSpan(cert->cert_buffer());
  return std::vector<uint8_t>(cert_span.begin(), cert_span.end());
}

// This class provides integration testing for the keystore service crosapi.
// TODO(crbug.com/40723023): The logic being tested does not rely on
// //chrome or //content so it would be helpful if this lived in a lower-level
// test suite.
class KeystoreServiceLacrosBrowserTest : public InProcessBrowserTest {
 protected:
  KeystoreServiceLacrosBrowserTest() = default;

  KeystoreServiceLacrosBrowserTest(const KeystoreServiceLacrosBrowserTest&) =
      delete;
  KeystoreServiceLacrosBrowserTest& operator=(
      const KeystoreServiceLacrosBrowserTest&) = delete;

  ~KeystoreServiceLacrosBrowserTest() override = default;

  mojo::Remote<crosapi::mojom::KeystoreService>& keystore_service_remote() {
    return chromeos::LacrosService::Get()
        ->GetRemote<crosapi::mojom::KeystoreService>();
  }

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

// Tests that providing an incorrectly formatted challenge for user's keystore
// returns error message.
IN_PROC_BROWSER_TEST_F(KeystoreServiceLacrosBrowserTest, WrongFormattingUser) {
  crosapi::mojom::ChallengeAttestationOnlyKeystoreResultPtr result;
  std::vector<uint8_t> incorrect_challenge = {10, 11, 12, 13, 14, 15};
  crosapi::mojom::KeystoreServiceAsyncWaiter async_waiter(
      keystore_service_remote().get());
  async_waiter.ChallengeAttestationOnlyKeystore(
      crosapi::mojom::KeystoreType::kUser, incorrect_challenge,
      /*migrate=*/false,
      crosapi::mojom::KeystoreSigningAlgorithmName::kRsassaPkcs115, &result);

  ASSERT_TRUE(result->is_error_message());

  // TODO(crbug.com/40151413): Currently this errors out because the
  // device is not enterprise enrolled. We want this to error out because of a
  // poorly formatted attestation message.
  const char expected_error_message[] =
      "Failed to get Enterprise certificate. Error code = 2";
  EXPECT_EQ(expected_error_message, result->get_error_message());
}

// Tests that get certificates will return empty list
IN_PROC_BROWSER_TEST_F(KeystoreServiceLacrosBrowserTest, GetCertificatesEmpty) {
  crosapi::mojom::GetCertificatesResultPtr result;
  crosapi::mojom::KeystoreServiceAsyncWaiter async_waiter(
      keystore_service_remote().get());
  async_waiter.GetCertificates(crosapi::mojom::KeystoreType::kUser, &result);
  ASSERT_TRUE(result->is_certificates());
  EXPECT_EQ(0u, result->get_certificates().size());
}

// Tests that generate RSA key works
IN_PROC_BROWSER_TEST_F(KeystoreServiceLacrosBrowserTest,
                       GenerateKeyPKCSSuccess) {
  crosapi::mojom::KeystoreBinaryResultPtr result;
  crosapi::mojom::KeystoreServiceAsyncWaiter async_waiter(
      keystore_service_remote().get());
  crosapi::mojom::KeystorePKCS115ParamsPtr params =
      crosapi::mojom::KeystorePKCS115Params::New();
  params->modulus_length = 1024;
  crosapi::mojom::KeystoreSigningAlgorithmPtr algo =
      crosapi::mojom::KeystoreSigningAlgorithm::NewPkcs115(std::move(params));

  async_waiter.GenerateKey(crosapi::mojom::KeystoreType::kUser, std::move(algo),
                           &result);

  ASSERT_TRUE(result->is_blob());
  // Testing that key has some length (162 comes from the test run).
  EXPECT_EQ(result->get_blob().size(), 162U);
}

IN_PROC_BROWSER_TEST_F(KeystoreServiceLacrosBrowserTest,
                       GenerateKeyECDSASuccess) {
  crosapi::mojom::KeystoreBinaryResultPtr result;
  crosapi::mojom::KeystoreServiceAsyncWaiter async_waiter(
      keystore_service_remote().get());
  crosapi::mojom::KeystoreECDSAParamsPtr params =
      crosapi::mojom::KeystoreECDSAParams::New();
  params->named_curve = "P-256";
  crosapi::mojom::KeystoreSigningAlgorithmPtr algo =
      crosapi::mojom::KeystoreSigningAlgorithm::NewEcdsa(std::move(params));

  async_waiter.GenerateKey(crosapi::mojom::KeystoreType::kUser, std::move(algo),
                           &result);

  ASSERT_TRUE(result->is_blob());
  // Testing that key has some length (91 comes from the test run).
  EXPECT_EQ(result->get_blob().size(), 91U);
}

// Tests that sign returns error because no private key.
IN_PROC_BROWSER_TEST_F(KeystoreServiceLacrosBrowserTest, SignReturnError) {
  crosapi::mojom::KeystoreBinaryResultPtr result;
  crosapi::mojom::KeystoreServiceAsyncWaiter async_waiter(
      keystore_service_remote().get());
  bool is_keystore_provided = true;

  async_waiter.Sign(
      is_keystore_provided, crosapi::mojom::KeystoreType::kUser,
      /*public_key=*/{1, 2, 3, 4, 5},
      /*scheme=*/crosapi::mojom::KeystoreSigningScheme::kRsassaPkcs1V15Sha256,
      /*data=*/{10, 11, 12, 13, 14, 15}, &result);

  // Errors out because the public key is not valid. Currently there's no way to
  // create a valid key in Ash-Chrome during browser tests.
  ASSERT_TRUE(result->is_error());
  EXPECT_EQ(result->get_error(), crosapi::mojom::KeystoreError::kKeyNotFound);
}

// Tests that trying to add/remove an incorrectly formatted certificate should
// fail.
IN_PROC_BROWSER_TEST_F(KeystoreServiceLacrosBrowserTest, CertificateBadFormat) {
  std::vector<uint8_t> dummy_certificate;
  dummy_certificate.push_back(15);
  crosapi::mojom::KeystoreServiceAsyncWaiter async_waiter(
      keystore_service_remote().get());
  bool is_result_error = false;
  crosapi::mojom::KeystoreError result_error_code;
  async_waiter.AddCertificate(crosapi::mojom::KeystoreType::kUser,
                              std::move(dummy_certificate), &is_result_error,
                              &result_error_code);
  ASSERT_TRUE(is_result_error) << "Error: " << result_error_code;
  EXPECT_EQ(result_error_code,
            crosapi::mojom::KeystoreError::kCertificateInvalid);

  bool is_remove_result_error = false;
  crosapi::mojom::KeystoreError remove_result_error_code;
  async_waiter.RemoveCertificate(
      crosapi::mojom::KeystoreType::kUser, std::move(dummy_certificate),
      &is_remove_result_error, &remove_result_error_code);

  ASSERT_TRUE(is_remove_result_error) << "Error: " << remove_result_error_code;
  EXPECT_EQ(remove_result_error_code,
            crosapi::mojom::KeystoreError::kCertificateInvalid);
}

// Tests that importing a correct certificate works and that it becomes visible
// in both Ash and Lacros. Tests that removing a certificate works.
IN_PROC_BROWSER_TEST_F(KeystoreServiceLacrosBrowserTest, AddRemoveCertificate) {
  crosapi::mojom::KeystoreServiceAsyncWaiter async_waiter(
      keystore_service_remote().get());

  // Creating a valid cert using this API requires generating a key for it
  // first.
  crosapi::mojom::KeystoreBinaryResultPtr generate_key_result;
  async_waiter.GenerateKey(
      crosapi::mojom::KeystoreType::kUser,
      crosapi::keystore_service_util::MakeRsaKeystoreSigningAlgorithm(
          /*modulus_length=*/2048, /*sw_backed=*/false),
      &generate_key_result);
  ASSERT_FALSE(generate_key_result->is_error());

  // Generate a client certificate for the generated key.
  scoped_refptr<net::X509Certificate> cert =
      MakeCert(generate_key_result->get_blob());
  std::vector<uint8_t> cert_der = CertToDer(cert);

  // Make Ash import the certificate.
  bool result_is_error = false;
  crosapi::mojom::KeystoreError result_error;
  async_waiter.AddCertificate(crosapi::mojom::KeystoreType::kUser, cert_der,
                              &result_is_error, &result_error);
  ASSERT_FALSE(result_is_error) << "Error: " << result_error;

  // Test that Lacros can see the certificate that was imported by Ash.
  EXPECT_TRUE(
      is_cert_in_nss::IsCertInNSSDatabase(browser()->profile(), cert_der));

  // Test that Ash also returns the imported certificate.
  crosapi::mojom::GetCertificatesResultPtr get_certs_result;
  async_waiter.GetCertificates(crosapi::mojom::KeystoreType::kUser,
                               &get_certs_result);
  ASSERT_FALSE(get_certs_result->is_error());
  EXPECT_TRUE(base::Contains(get_certs_result->get_certificates(), cert_der));

  // Make Ash remove the certificate.
  async_waiter.RemoveCertificate(crosapi::mojom::KeystoreType::kUser, cert_der,
                                 &result_is_error, &result_error);
  ASSERT_FALSE(result_is_error) << "Error: " << result_error;

  // Test that Lacros cannot see the certificate anymore.
  EXPECT_FALSE(
      is_cert_in_nss::IsCertInNSSDatabase(browser()->profile(), cert_der));

  // Test that Ash doesn't return the imported certificate anymore.
  async_waiter.GetCertificates(crosapi::mojom::KeystoreType::kUser,
                               &get_certs_result);
  ASSERT_FALSE(get_certs_result->is_error());
  EXPECT_FALSE(base::Contains(get_certs_result->get_certificates(), cert_der));
}

}  // namespace