chromium/chrome/browser/ash/kcer/nssdb_migration/pkcs12_migrator_unittest.cc

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

#include "chrome/browser/ash/kcer/nssdb_migration/pkcs12_migrator.h"

#include <memory>

#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/test_future.h"
#include "chrome/browser/ash/kcer/kcer_factory_ash.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/net/fake_nss_service.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/components/kcer/chaps/mock_high_level_chaps_client.h"
#include "chromeos/components/kcer/kcer.h"
#include "chromeos/components/kcer/kcer_impl.h"
#include "chromeos/components/kcer/kcer_nss/test_utils.h"
#include "content/public/test/browser_task_environment.h"
#include "net/cert/scoped_nss_types.h"
#include "testing/gtest/include/gtest/gtest.h"

using ObjectHandle = kcer::SessionChapsClient::ObjectHandle;
using base::Bucket;
using base::test::RunOnceCallback;
using base::test::RunOnceCallbackRepeatedly;

namespace kcer {
namespace {

enum class NssSlot {
  kPublic,
  kPrivate,
};

std::u16string GetPassword(const std::string& file_name) {
  if (file_name == "client.p12") {
    return u"12345";
  }
  if (file_name == "client_with_ec_key.p12") {
    return u"123456";
  }
  if (file_name == "client-empty-password.p12") {
    return u"";
  }
  ADD_FAILURE() << "GetPassword() is called with an unexpected file name";
  return u"";
}

std::unique_ptr<KeyedService> CreateKcer(
    base::WeakPtr<internal::KcerToken> user_token,
    content::BrowserContext* context) {
  auto kcer = std::make_unique<internal::KcerImpl>();
  kcer->Initialize(content::GetUIThreadTaskRunner(), user_token, nullptr);
  return std::make_unique<KcerFactoryAsh::KcerService>(std::move(kcer));
}

class KcerPkcs12MigratorTest : public testing::Test {
 public:
  KcerPkcs12MigratorTest()
      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME,
                          base::test::TaskEnvironment::MainThreadType::UI),
        fake_user_manager_(std::make_unique<ash::FakeChromeUserManager>()) {}

  void SetUp() override {
    profile_ = TestingProfile::Builder().Build();
    auto account = AccountId::FromUserEmail("[email protected]");
    fake_user_manager_->AddUserWithAffiliationAndTypeAndProfile(
        account, false, user_manager::UserType::kRegular, profile_.get());
    fake_user_manager_->OnUserProfileCreated(account, profile_->GetPrefs());
    fake_user_manager_->LoginUser(account);

    migrator_ = std::make_unique<Pkcs12Migrator>(profile_.get());

    nss_service_ = FakeNssService::InitializeForBrowserContext(
        profile_.get(),
        /*enable_system_slot=*/false);

    InitKcer(profile_.get());

    // Sanity check that by default the flag is false for all tests.
    ASSERT_FALSE(GetDualWrittenFlag());
  }

  void TearDown() override { nss_service_ = nullptr; }

  void InitKcer(Profile* profile) {
    kcer_token_ =
        internal::KcerToken::CreateForNss(Token::kUser, &chaps_client_);
    kcer_token_->InitializeForNss(crypto::ScopedPK11Slot(
        PK11_ReferenceSlot(nss_service_->GetPrivateSlot())));
    KcerFactoryAsh::GetInstance()->SetTestingFactoryAndUse(
        profile, base::BindRepeating(&CreateKcer, kcer_token_->GetWeakPtr()));
  }

  void ImportPkcs12(const std::string& file_name, NssSlot slot) {
    base::test::TestFuture<net::NSSCertDatabase*> nss_waiter;
    nss_service_->UnsafelyGetNSSCertDatabaseForTesting(
        nss_waiter.GetCallback());
    net::NSSCertDatabase* nss_db = nss_waiter.Get();

    std::vector<uint8_t> pkcs12_bytes = ReadTestFile(file_name);
    std::string pkcs12_str(pkcs12_bytes.begin(), pkcs12_bytes.end());

    PK11SlotInfo* slot_info = nullptr;
    switch (slot) {
      case NssSlot::kPublic:
        slot_info = nss_db->GetPublicSlot().get();
        break;
      case NssSlot::kPrivate:
        slot_info = nss_db->GetPrivateSlot().get();
        break;
    }

    nss_db->ImportFromPKCS12(slot_info, std::move(pkcs12_str),
                             GetPassword(file_name), true, nullptr);
  }

  void ImportPkcs12PrivSlot(const std::string& file_name) {
    base::test::TestFuture<net::NSSCertDatabase*> nss_waiter;
    nss_service_->UnsafelyGetNSSCertDatabaseForTesting(
        nss_waiter.GetCallback());
    net::NSSCertDatabase* nss_db = nss_waiter.Get();

    std::vector<uint8_t> pkcs12_bytes = ReadTestFile(file_name);
    std::string pkcs12_str(pkcs12_bytes.begin(), pkcs12_bytes.end());

    nss_db->ImportFromPKCS12(nss_db->GetPrivateSlot().get(),
                             std::move(pkcs12_str), GetPassword(file_name),
                             true, nullptr);
  }

  bool GetDualWrittenFlag() {
    return user_manager::UserManager::Get()
        ->GetActiveUser()
        ->GetProfilePrefs()
        ->GetBoolean(prefs::kNssChapsDualWrittenCertsExist);
  }

 protected:
  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<TestingProfile> profile_;
  user_manager::TypedScopedUserManager<ash::FakeChromeUserManager>
      fake_user_manager_;
  std::unique_ptr<internal::KcerToken> kcer_token_;
  std::unique_ptr<Pkcs12Migrator> migrator_;
  MockHighLevelChapsClient chaps_client_;
  raw_ptr<FakeNssService> nss_service_;
  base::HistogramTester histogram_tester_;
};

// Test that Pkcs12Migrator doesn't migrate anything when there's nothing to
// migrate.
TEST_F(KcerPkcs12MigratorTest, NothingToMigrateSuccess) {
  migrator_->Start();
  task_environment_.FastForwardBy(base::Seconds(31));

  EXPECT_THAT(
      histogram_tester_.GetAllSamples(kKcerPkcs12MigrationUma),
      BucketsAre(Bucket(KcerPkcs12MigrationEvent::kMigrationStarted, 1),
                 Bucket(KcerPkcs12MigrationEvent::kkNothingToMigrate, 1)));

  EXPECT_FALSE(GetDualWrittenFlag());
}

// Test that Pkcs12Migrator can successfully migrate a single cert.
TEST_F(KcerPkcs12MigratorTest, OneCertMigratedSuccess) {
  ImportPkcs12("client.p12", NssSlot::kPublic);

  // The internal call to ImportPkcs12 tries to find the existing key before
  // importing it. The certs are checked by listing them from NSS (for
  // Kcer-over-NSS).
  EXPECT_CALL(chaps_client_, FindObjects)
      .WillOnce(RunOnceCallback<2>(std::vector<ObjectHandle>{},
                                   chromeos::PKCS11_CKR_OK));
  // Should create 3 objects - public key, private key, cert.
  EXPECT_CALL(chaps_client_, CreateObject)
      .Times(3)
      .WillRepeatedly(RunOnceCallbackRepeatedly<2>(ObjectHandle(0),
                                                   chromeos::PKCS11_CKR_OK));

  migrator_->Start();
  task_environment_.FastForwardBy(base::Seconds(31));

  EXPECT_THAT(
      histogram_tester_.GetAllSamples(kKcerPkcs12MigrationUma),
      BucketsAre(Bucket(KcerPkcs12MigrationEvent::kMigrationStarted, 1),
                 Bucket(KcerPkcs12MigrationEvent::kMigrationFinishedSuccess, 1),
                 Bucket(KcerPkcs12MigrationEvent::kCertMigratedSuccess, 1)));

  EXPECT_TRUE(GetDualWrittenFlag());
}

// Test that Pkcs12Migrator can successfully migrate multiple certs.
TEST_F(KcerPkcs12MigratorTest, MultipleCertsMigratedSuccess) {
  ImportPkcs12("client.p12", NssSlot::kPublic);
#if defined(MEMORY_SANITIZER)
  // For whatever reason NSS behaves differently under memory sanitizer and
  // fails to import the client_with_ec_key.p12 file. It's working on a normal
  // build, and it's useful to test files with EC keys, so only replace the file
  // for the MSAN builds.
  ImportPkcs12("client-empty-password.p12", NssSlot::kPublic);
#else
  ImportPkcs12("client_with_ec_key.p12", NssSlot::kPublic);
#endif

  // The internal call to ImportPkcs12 tries to find the existing key for each
  // PKCS#12 file before importing it. The certs are checked by listing them
  // from NSS (for Kcer-over-NSS).
  EXPECT_CALL(chaps_client_, FindObjects)
      .Times(2)
      .WillRepeatedly(RunOnceCallbackRepeatedly<2>(std::vector<ObjectHandle>{},
                                                   chromeos::PKCS11_CKR_OK));
  // Should create 6 objects - {public key, private key, cert} x2.
  EXPECT_CALL(chaps_client_, CreateObject)
      .Times(6)
      .WillRepeatedly(RunOnceCallbackRepeatedly<2>(ObjectHandle(0),
                                                   chromeos::PKCS11_CKR_OK));

  migrator_->Start();
  task_environment_.FastForwardBy(base::Seconds(31));

  EXPECT_THAT(
      histogram_tester_.GetAllSamples(kKcerPkcs12MigrationUma),
      BucketsAre(Bucket(KcerPkcs12MigrationEvent::kMigrationStarted, 1),
                 Bucket(KcerPkcs12MigrationEvent::kMigrationFinishedSuccess, 1),
                 Bucket(KcerPkcs12MigrationEvent::kCertMigratedSuccess, 2)));

  EXPECT_TRUE(GetDualWrittenFlag());
}

// Test that Pkcs12Migrator doesn't migrate certs that are already present in
// the private slot (i.e. in Chaps).
TEST_F(KcerPkcs12MigratorTest, CertAlreadyExists) {
  ImportPkcs12("client.p12", NssSlot::kPublic);
  ImportPkcs12("client.p12", NssSlot::kPrivate);

  migrator_->Start();
  task_environment_.FastForwardBy(base::Seconds(31));

  EXPECT_THAT(
      histogram_tester_.GetAllSamples(kKcerPkcs12MigrationUma),
      BucketsAre(Bucket(KcerPkcs12MigrationEvent::kMigrationStarted, 1),
                 Bucket(KcerPkcs12MigrationEvent::kkNothingToMigrate, 1)));

  EXPECT_FALSE(GetDualWrittenFlag());
}

// Test that Pkcs12Migrator doesn't migrate certs that are already present in
// the private slot (i.e. in Chaps), but migrates the other ones.
TEST_F(KcerPkcs12MigratorTest, SomeCertsAlreadyExist) {
  ImportPkcs12("client.p12", NssSlot::kPublic);
  ImportPkcs12("client.p12", NssSlot::kPrivate);

#if defined(MEMORY_SANITIZER)
  // For whatever reason NSS behaves differently under memory sanitizer and
  // fails to import the client_with_ec_key.p12 file. It's working on a normal
  // build, and it's useful to test files with EC keys, so only replace the file
  // for the MSAN builds.
  ImportPkcs12("client-empty-password.p12", NssSlot::kPublic);
#else
  ImportPkcs12("client_with_ec_key.p12", NssSlot::kPublic);
#endif

  // For files that should be imported, the internal call to ImportPkcs12 will
  // try to find the existing key before importing it for each file. The certs
  // are checked by listing them from NSS (for Kcer-over-NSS).
  EXPECT_CALL(chaps_client_, FindObjects)
      .WillOnce(RunOnceCallback<2>(std::vector<ObjectHandle>{},
                                   chromeos::PKCS11_CKR_OK));
  // Should create 3 objects - public key, private key, cert.
  EXPECT_CALL(chaps_client_, CreateObject)
      .Times(3)
      .WillRepeatedly(RunOnceCallbackRepeatedly<2>(ObjectHandle(0),
                                                   chromeos::PKCS11_CKR_OK));

  migrator_->Start();
  task_environment_.FastForwardBy(base::Seconds(31));

  EXPECT_THAT(
      histogram_tester_.GetAllSamples(kKcerPkcs12MigrationUma),
      BucketsAre(Bucket(KcerPkcs12MigrationEvent::kMigrationStarted, 1),
                 Bucket(KcerPkcs12MigrationEvent::kMigrationFinishedSuccess, 1),
                 Bucket(KcerPkcs12MigrationEvent::kCertMigratedSuccess, 1)));

  EXPECT_TRUE(GetDualWrittenFlag());
}

// Test that Pkcs12Migrator correctly handles errors from re-importing a cert.
TEST_F(KcerPkcs12MigratorTest, CertMigrationFailed) {
  ImportPkcs12("client.p12", NssSlot::kPublic);

  // The internal call to ImportPkcs12 tries to find the existing key before
  // importing it. The certs are checked by listing them from NSS (for
  // Kcer-over-NSS).
  EXPECT_CALL(chaps_client_, FindObjects)
      .WillOnce(RunOnceCallback<2>(std::vector<ObjectHandle>{},
                                   chromeos::PKCS11_CKR_OK));
  EXPECT_CALL(chaps_client_, CreateObject)
      .Times(1)
      .WillRepeatedly(RunOnceCallbackRepeatedly<2>(
          ObjectHandle(0), chromeos::PKCS11_CKR_GENERAL_ERROR));

  migrator_->Start();
  task_environment_.FastForwardBy(base::Seconds(31));

  EXPECT_THAT(
      histogram_tester_.GetAllSamples(kKcerPkcs12MigrationUma),
      BucketsAre(Bucket(KcerPkcs12MigrationEvent::kMigrationStarted, 1),
                 Bucket(KcerPkcs12MigrationEvent::kMigrationFinishedFailure, 1),
                 Bucket(KcerPkcs12MigrationEvent::kFailedToReimportCert, 1)));

  // True because even when it fails, some Chaps objects in theory might have
  // been created and not deleted.
  EXPECT_TRUE(GetDualWrittenFlag());
}

}  // namespace
}  // namespace kcer