chromium/chrome/browser/net/nss_service_chromeos.cc

// 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.

#include "chrome/browser/net/nss_service.h"

#include <memory>
#include <utility>

#include "base/callback_list.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/supports_user_data.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/ash/crosapi/cert_database_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/login/startup_utils.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chromeos/ash/components/dbus/dbus_thread_manager.h"
#include "chromeos/ash/components/dbus/userdataauth/cryptohome_pkcs11_client.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "chromeos/ash/components/tpm/tpm_token_info_getter.h"
#include "chromeos/components/kiosk/kiosk_utils.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "crypto/nss_util.h"
#include "crypto/nss_util_internal.h"
#include "net/cert/nss_cert_database_chromeos.h"

using content::BrowserThread;

namespace {

// The following four functions are responsible for initializing NSS for each
// profile on ChromeOS Ash, which has a separate NSS database and TPM slot
// per-profile.
//
// Initialization basically follows these steps:
// 1) Get some info from user_manager::UserManager about the User for this
// profile.
// 2) Tell nss_util to initialize the software slot for this profile.
// 3) Wait for the TPM module to be loaded by nss_util if it isn't already.
// 4) Ask CryptohomePkcs11Client which TPM slot id corresponds to this profile.
// 5) Tell nss_util to use that slot id on the TPM module.
//
// Some of these steps must happen on the UI thread, others must happen on the
// IO thread:
//               UI thread                              IO Thread
//
//  NssService::NssService
//                   |
//  ProfileHelper::Get()->GetUserByProfile()
//                   \---------------------------------------v
//                                                 StartNSSInitOnIOThread
//                                                           |
//                                          crypto::InitializeNSSForChromeOSUser
//                                                           |
//                                                crypto::IsTPMTokenEnabled
//                                                           |
//                                          StartTPMSlotInitializationOnIOThread
//                   v---------------------------------------/
//     GetTPMInfoForUserOnUIThread
//                   |
//   ash::TPMTokenInfoGetter::Start
//                   |
//     DidGetTPMInfoForUserOnUIThread
//                   \---------------------------------------v
//                                          crypto::InitializeTPMForChromeOSUser

void DidGetTPMInfoForUserOnUIThread(
    std::unique_ptr<ash::TPMTokenInfoGetter> getter,
    const std::string& username_hash,
    std::optional<user_data_auth::TpmTokenInfo> token_info) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (token_info.has_value() && token_info->slot() != -1) {
    DVLOG(1) << "Got TPM slot for " << username_hash << ": "
             << token_info->slot();
    content::GetIOThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&crypto::InitializeTPMForChromeOSUser,
                                  username_hash, token_info->slot()));
  } else {
    NOTREACHED_IN_MIGRATION() << "TPMTokenInfoGetter reported invalid token.";
  }
}

void GetTPMInfoForUserOnUIThread(const AccountId& account_id,
                                 const std::string& username_hash) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DVLOG(1) << "Getting TPM info from cryptohome for "
           << " " << account_id.Serialize() << " " << username_hash;
  std::unique_ptr<ash::TPMTokenInfoGetter> scoped_token_info_getter =
      ash::TPMTokenInfoGetter::CreateForUserToken(
          account_id, ash::CryptohomePkcs11Client::Get(),
          base::SingleThreadTaskRunner::GetCurrentDefault());
  ash::TPMTokenInfoGetter* token_info_getter = scoped_token_info_getter.get();

  // Bind |token_info_getter| to the callback to ensure it does not go away
  // before TPM token info is fetched.
  token_info_getter->Start(base::BindOnce(&DidGetTPMInfoForUserOnUIThread,
                                          std::move(scoped_token_info_getter),
                                          username_hash));
}

void StartTPMSlotInitializationOnIOThread(const AccountId& account_id,
                                          const std::string& username_hash,
                                          bool is_tpm_token_enabled) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  if (!is_tpm_token_enabled) {
    crypto::InitializePrivateSoftwareSlotForChromeOSUser(username_hash);
    return;
  }

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&GetTPMInfoForUserOnUIThread, account_id, username_hash));
}

void StartNSSInitOnIOThread(const AccountId& account_id,
                            const std::string& username_hash,
                            const base::FilePath& path,
                            bool is_kiosk) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  DVLOG(1) << "Starting NSS init for " << account_id.Serialize()
           << "  hash:" << username_hash;

  // Make sure NSS is initialized for the user.
  if (is_kiosk) {
    // Kiosk sessions don't have the UI that could result in interactions with
    // the public slot. Kiosk users are also not owner users and can't have
    // the owner key in the public slot. So the public slot is not used in
    // Kiosk sessions and can be replaced by the internal slot. This is done
    // mainly because Chrome sometimes fails to load the public slot and has
    // to crash because of that.
    crypto::InitializeNSSForChromeOSUserWithSlot(
        username_hash, crypto::ScopedPK11Slot(PK11_GetInternalKeySlot()));
  } else {
    crypto::InitializeNSSForChromeOSUser(username_hash, path);
  }

  // Check if it's OK to initialize TPM for the user before continuing. This
  // may not be the case if the TPM slot initialization was previously
  // requested for the same user.
  if (!crypto::ShouldInitializeTPMForChromeOSUser(username_hash))
    return;

  crypto::WillInitializeTPMForChromeOSUser(username_hash);
  crypto::IsTPMTokenEnabled(base::BindOnce(
      &StartTPMSlotInitializationOnIOThread, account_id, username_hash));
}

void NotifyCertsChangedInAshOnUIThread(
    crosapi::mojom::CertDatabaseChangeType change_type) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->cert_database_ash()
      ->NotifyCertsChangedInAsh(change_type);
}

}  // namespace

// Creates and manages a NSSCertDatabaseChromeOS. Created on the UI thread, but
// all other calls are made on the IO thread.
class NssService::NSSCertDatabaseChromeOSManager
    : public net::NSSCertDatabase::Observer {
 public:
  using GetNSSCertDatabaseCallback =
      base::OnceCallback<void(net::NSSCertDatabase*)>;

  NSSCertDatabaseChromeOSManager(std::string username_hash,
                                 bool enable_system_slot)
      : username_hash_(std::move(username_hash)),
        enable_system_slot_(enable_system_slot) {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
  }

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

  ~NSSCertDatabaseChromeOSManager() override {
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    if (nss_cert_database_) {
      nss_cert_database_->RemoveObserver(this);
    }
  }

  net::NSSCertDatabase* GetNSSCertDatabase(
      GetNSSCertDatabaseCallback callback) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

    if (nss_cert_database_) {
      return nss_cert_database_.get();
    }

    ready_callback_list_.AddUnsafe(std::move(callback));

    if (!database_creation_started_) {
      database_creation_started_ = true;
      content::GetIOThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(&NSSCertDatabaseChromeOSManager::StartDatabaseCreation,
                         weak_ptr_factory_.GetWeakPtr()));
    }

    return nullptr;
  }

  // net::NSSCertDatabase::Observer
  void OnTrustStoreChanged() override {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&NotifyCertsChangedInAshOnUIThread,
                       crosapi::mojom::CertDatabaseChangeType::kTrustStore));
  }
  void OnClientCertStoreChanged() override {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(
            &NotifyCertsChangedInAshOnUIThread,
            crosapi::mojom::CertDatabaseChangeType::kClientCertStore));
  }

 private:
  void StartDatabaseCreation() {
    DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

    crypto::ScopedPK11Slot private_slot(crypto::GetPrivateSlotForChromeOSUser(
        username_hash_,
        base::BindOnce(&NSSCertDatabaseChromeOSManager::DidGetPrivateSlot,
                       weak_ptr_factory_.GetWeakPtr())));
    if (private_slot)
      DidGetPrivateSlot(std::move(private_slot));
  }

  void DidGetPrivateSlot(crypto::ScopedPK11Slot private_slot) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

    if (!enable_system_slot_) {
      CreateDatabase(std::move(private_slot),
                     /*system_slot=*/crypto::ScopedPK11Slot());
      return;
    }

    crypto::GetSystemNSSKeySlot(base::BindOnce(
        &NSSCertDatabaseChromeOSManager::CreateDatabase,
        weak_ptr_factory_.GetWeakPtr(), std::move(private_slot)));
  }

  void CreateDatabase(crypto::ScopedPK11Slot private_slot,
                      crypto::ScopedPK11Slot system_slot) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

    auto public_slot = crypto::GetPublicSlotForChromeOSUser(username_hash_);

#if BUILDFLAG(IS_CHROMEOS) && !BUILDFLAG(IS_CHROMEOS_DEVICE)
    if (!public_slot) {
      // This is a "for testing" branch. The code below will intentionally crash
      // when the public slot fails to load. By default prevent this from
      // happening in tests that simply don't properly fake NSS. Consider using
      // FakeNssService if a specific NSS behavior is required in tests.
      public_slot = crypto::ScopedPK11Slot(PK11_GetInternalKeySlot());
    }
#endif

    // TODO(crbug.com/1163303): Remove when the bug is fixed.
    if (!public_slot) {
      Profile* profile = ProfileManager::GetActiveUserProfile();
      CHECK(profile);
      crypto::DiagnosePublicSlotAndCrash(
          crypto::GetSoftwareNSSDBPath(profile->GetPath()));
    }

    nss_cert_database_ = std::make_unique<net::NSSCertDatabaseChromeOS>(
        std::move(public_slot), std::move(private_slot));

    if (system_slot)
      nss_cert_database_->SetSystemSlot(std::move(system_slot));
    nss_cert_database_->AddObserver(this);

    ready_callback_list_.Notify(nss_cert_database_.get());
  }

  const std::string username_hash_;
  bool enable_system_slot_ = false;
  bool database_creation_started_ = false;

  std::unique_ptr<net::NSSCertDatabaseChromeOS> nss_cert_database_;
  base::OnceCallbackList<GetNSSCertDatabaseCallback::RunType>
      ready_callback_list_;

  base::WeakPtrFactory<NSSCertDatabaseChromeOSManager> weak_ptr_factory_{this};
};

NssService::NssService(content::BrowserContext* context) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(context);

  Profile* profile = Profile::FromBrowserContext(context);
  const user_manager::User* user =
      ash::ProfileHelper::Get()->GetUserByProfile(profile);
  // No need to initialize NSS for users with empty username hash:
  // Getters for a user's NSS slots always return a null slot if the user's
  // username hash is empty, even when the NSS is not initialized for the
  // user.
  std::string username_hash;
  bool enable_system_slot = false;
  if (user && !user->username_hash().empty()) {
    username_hash = user->username_hash();
    DCHECK(!username_hash.empty());
    content::GetIOThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&StartNSSInitOnIOThread, user->GetAccountId(),
                                  username_hash, profile->GetPath(),
                                  chromeos::IsKioskSession()));

    enable_system_slot = user->IsAffiliated();
  }

  DCHECK(!(username_hash.empty() && enable_system_slot));

  nss_cert_database_manager_ = std::make_unique<NSSCertDatabaseChromeOSManager>(
      std::move(username_hash), enable_system_slot);
}

NssService::~NssService() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  content::GetIOThreadTaskRunner({})->DeleteSoon(
      FROM_HERE, std::move(nss_cert_database_manager_));
}

NssCertDatabaseGetter NssService::CreateNSSCertDatabaseGetterForIOThread() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return base::BindOnce(&NSSCertDatabaseChromeOSManager::GetNSSCertDatabase,
                        base::Unretained(nss_cert_database_manager_.get()));
}