chromium/chromeos/ash/components/network/network_cert_loader.cc

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

#include "chromeos/ash/components/network/network_cert_loader.h"

#include <algorithm>
#include <initializer_list>
#include <map>
#include <memory>
#include <utility>

#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "chromeos/ash/components/network/certificate_helper.h"
#include "chromeos/ash/components/network/policy_certificate_provider.h"
#include "chromeos/ash/components/network/system_token_cert_db_storage.h"
#include "chromeos/components/onc/certificate_scope.h"
#include "crypto/chaps_support.h"
#include "crypto/nss_util.h"
#include "crypto/scoped_nss_types.h"
#include "net/cert/cert_database.h"
#include "net/cert/nss_cert_database.h"
#include "net/cert/nss_cert_database_chromeos.h"
#include "net/cert/x509_util_nss.h"

namespace ash {

namespace {

bool g_force_available_for_network_auth_for_test = false;
NetworkCertLoader* g_cert_loader = nullptr;

enum class NetworkCertType {
  kAuthorityCertificate,
  kClientCertificate,
  kOther
};

NetworkCertType GetNetworkCertType(CERTCertificate* cert) {
  net::CertType type = certificate::GetCertType(cert);
  if (type == net::USER_CERT)
    return NetworkCertType::kClientCertificate;
  if (type == net::CA_CERT)
    return NetworkCertType::kAuthorityCertificate;
  VLOG(2) << "Ignoring cert type: " << type;
  return NetworkCertType::kOther;
}

bool IsAvailableForNetworkAuth(CERTCertificate* cert) {
  if (g_force_available_for_network_auth_for_test)
    return true;
  return crypto::IsSlotProvidedByChaps(cert->slot);
}

// Returns all authority certificates with default (not restricted) scope
// provided by |policy_certificate_provider| as a list of NetworkCerts.
NetworkCertLoader::NetworkCertList GetPolicyProvidedAuthorities(
    const PolicyCertificateProvider* policy_certificate_provider,
    bool device_wide) {
  NetworkCertLoader::NetworkCertList result;
  if (!policy_certificate_provider)
    return result;
  for (const auto& certificate :
       policy_certificate_provider->GetAllAuthorityCertificates(
           chromeos::onc::CertificateScope::Default())) {
    net::ScopedCERTCertificate x509_cert =
        net::x509_util::CreateCERTCertificateFromX509Certificate(
            certificate.get());
    if (!x509_cert) {
      LOG(ERROR) << "Unable to create CERTCertificate";
      continue;
    }
    result.push_back(NetworkCertLoader::NetworkCert(
        std::move(x509_cert), /*available_for_network_auth=*/false,
        device_wide));
  }
  return result;
}

// Combines all NetworkCerts from all |network_cert_lists| to a resulting list,
// avoiding duplicates.
NetworkCertLoader::NetworkCertList CombineNetworkCertLists(
    std::initializer_list<const NetworkCertLoader::NetworkCertList*>
        network_cert_lists) {
  size_t total_size = 0;
  for (const NetworkCertLoader::NetworkCertList* list : network_cert_lists)
    total_size += list->size();
  NetworkCertLoader::NetworkCertList result;
  result.reserve(total_size);

  std::map<const CERTCertificate*, size_t> added_cert_to_position;
  for (const NetworkCertLoader::NetworkCertList* list : network_cert_lists) {
    for (const NetworkCertLoader::NetworkCert& network_cert : *list) {
      auto it = added_cert_to_position.find(network_cert.cert());
      if (it == added_cert_to_position.end()) {
        // This certificate wasn't added before.
        // Add it and save its position in the result list.
        added_cert_to_position.insert({network_cert.cert(), result.size()});
        result.push_back(network_cert.Clone());
      } else if (network_cert.is_device_wide()) {
        // Replace the already added certificate with the device-wide one so
        // that it can be used for shared configurations.
        size_t position = it->second;
        result[position] = network_cert.Clone();
      }
    }
  }
  return result;
}

}  // namespace

// Caches certificates from a single slot of a NSSCertDatabase. Handles
// reloading of certificates on update notifications and provides status flags
// (loading / loaded). NetworkCertLoader can use multiple CertCaches to combine
// certificates from multiple sources.
class NetworkCertLoader::CertCache : public net::CertDatabase::Observer {
 public:
  enum class State {
    // The CertCache is not initialized and not expected to be initialized soon.
    kNotInitialized,
    // The CertCache is expected to be initialized soon.
    kMarkedWillBeInitialized,
    // The CertCache initialization has started, the initial load of
    // certificates is in progress.
    kInitialLoadInProgress,
    // The CertCache is initialized and currently not re-loading certificates.
    kInitializedAndIdle,
    // The CertCache is initialized and currently re-loading certificates.
    kInitializedAndReloading
  };

  explicit CertCache(base::RepeatingClosure certificates_updated_callback)
      : certificates_updated_callback_(certificates_updated_callback) {}

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

  ~CertCache() override {
    net::CertDatabase::GetInstance()->RemoveObserver(this);
  }

  void MarkWillBeInitialized(bool will_be_initialized) {
    DCHECK(state_ == State::kNotInitialized ||
           state_ == State::kMarkedWillBeInitialized);
    state_ = will_be_initialized ? State::kMarkedWillBeInitialized
                                 : State::kNotInitialized;
  }

  void SetNSSDBAndSlot(net::NSSCertDatabase* nss_database,
                       crypto::ScopedPK11Slot slot,
                       bool is_slot_device_wide) {
    CHECK(!nss_database_);
    CHECK(slot);
    nss_database_ = nss_database;
    slot_ = std::move(slot);
    is_slot_device_wide_ = is_slot_device_wide;

    // Start observing cert database for changes.
    // Observing net::CertDatabase is preferred over observing |nss_database_|
    // directly, as |nss_database_| observers receive only events generated
    // directly by |nss_database_|, so they may miss a few relevant ones.
    // TODO(tbarzic): Once singleton NSSCertDatabase is removed, investigate if
    // it would be OK to observe |nss_database_| directly; or change
    // NSSCertDatabase to send notification on all relevant changes.
    net::CertDatabase::GetInstance()->AddObserver(this);

    LoadCertificates(/*initial_load=*/true);
  }

  net::NSSCertDatabase* nss_database() { return nss_database_; }

  // net::CertDatabase::Observer
  void OnTrustStoreChanged() override {
    VLOG(1) << "OnTrustStoreChanged";
    LoadCertificates(/*initial_load=*/false);
  }
  void OnClientCertStoreChanged() override {
    VLOG(1) << "OnClientCertStoreChanged";
    LoadCertificates(/*initial_load=*/false);
  }

  const NetworkCertList& authority_certs() const { return authority_certs_; }

  const NetworkCertList& client_certs() const { return client_certs_; }

  bool is_or_will_be_initialized() const {
    return state_ != State::kNotInitialized;
  }

  bool is_initialized() const { return nss_database_; }

  bool initial_load_running() const {
    return state_ == State::kInitialLoadInProgress;
  }

  bool certificates_update_running() const {
    return state_ == State::kInitialLoadInProgress ||
           state_ == State::kInitializedAndReloading;
  }

  bool initial_load_finished() const {
    return state_ == State::kInitializedAndIdle ||
           state_ == State::kInitializedAndReloading;
  }

 private:
  // Trigger a certificate load. If a certificate loading task is already in
  // progress, will start a reload once the current task is finished.
  void LoadCertificates(bool initial_load) {
    DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
    VLOG(1) << "LoadCertificates: " << certificates_update_running();

    if (!nss_database_)
      return;

    if (certificates_update_running()) {
      certificates_update_required_ = true;
      return;
    }

    state_ = initial_load ? State::kInitialLoadInProgress
                          : State::kInitializedAndReloading;
    certificates_update_required_ = false;

    nss_database_->ListCertsInSlot(
        base::BindOnce(&CertCache::UpdateCertificates,
                       weak_factory_.GetWeakPtr()),
        slot_.get());
  }

  // Called if a certificate load task is finished.
  void UpdateCertificates(net::ScopedCERTCertificateList cert_list) {
    DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
    DCHECK(certificates_update_running());
    VLOG(1) << "UpdateCertificates: " << cert_list.size();

    authority_certs_.clear();
    client_certs_.clear();
    for (auto& cert : cert_list) {
      NetworkCertType type = GetNetworkCertType(cert.get());
      if (type == NetworkCertType::kAuthorityCertificate) {
        authority_certs_.push_back(
            NetworkCert(std::move(cert), /*available_for_network_auth=*/false,
                        is_slot_device_wide_));
      } else if (type == NetworkCertType::kClientCertificate) {
        bool available_for_network_auth = IsAvailableForNetworkAuth(cert.get());
        client_certs_.push_back(NetworkCert(
            std::move(cert), available_for_network_auth, is_slot_device_wide_));
      }
    }

    state_ = State::kInitializedAndIdle;
    certificates_updated_callback_.Run();

    if (certificates_update_required_)
      LoadCertificates(/*initial_load=*/false);
  }

  // To be called when certificates have been updated.
  base::RepeatingClosure certificates_updated_callback_;

  // The state of this CertCache.
  State state_ = State::kNotInitialized;

  // This is true if a notification about certificate DB changes arrived while
  // loading certificates and means that we will have to trigger another
  // certificates load after that.
  bool certificates_update_required_ = false;

  // The NSS certificate database from which the certificates should be loaded.
  // Dangling during LoginIntegrationTest.TestLogin on
  // chromeos-amd64-generic-rel-gtest.
  raw_ptr<net::NSSCertDatabase, AcrossTasksDanglingUntriaged> nss_database_ =
      nullptr;

  // The slot from which certificates are listed.
  crypto::ScopedPK11Slot slot_;

  // true if |slot_| is available device-wide, so certificates listed from it
  // can be used for shared networks.
  bool is_slot_device_wide_ = false;

  // Authority Certificates loaded from the database.
  NetworkCertList authority_certs_;

  // Client Certificates loaded from the database.
  NetworkCertList client_certs_;

  THREAD_CHECKER(thread_checker_);

  base::WeakPtrFactory<CertCache> weak_factory_{this};
};

NetworkCertLoader::NetworkCert::NetworkCert(net::ScopedCERTCertificate cert,
                                            bool available_for_network_auth,
                                            bool device_wide)
    : cert_(std::move(cert)),
      available_for_network_auth_(available_for_network_auth),
      device_wide_(device_wide) {}

NetworkCertLoader::NetworkCert::~NetworkCert() = default;

NetworkCertLoader::NetworkCert::NetworkCert(NetworkCert&& other) = default;

NetworkCertLoader::NetworkCert& NetworkCertLoader::NetworkCert::operator=(
    NetworkCert&& other) = default;

bool NetworkCertLoader::NetworkCert::IsHardwareBacked() const {
  return net::NSSCertDatabase::IsHardwareBacked(cert_.get());
}

NetworkCertLoader::NetworkCert NetworkCertLoader::NetworkCert::Clone() const {
  return NetworkCert(net::x509_util::DupCERTCertificate(cert_.get()),
                     available_for_network_auth_, device_wide_);
}

// static
void NetworkCertLoader::Initialize() {
  CHECK(!g_cert_loader);
  g_cert_loader = new NetworkCertLoader();
}

// static
void NetworkCertLoader::Shutdown() {
  CHECK(g_cert_loader);
  delete g_cert_loader;
  g_cert_loader = nullptr;
}

// static
NetworkCertLoader* NetworkCertLoader::Get() {
  CHECK(g_cert_loader) << "NetworkCertLoader::Get() called before Initialize()";
  return g_cert_loader;
}

// static
bool NetworkCertLoader::IsInitialized() {
  return g_cert_loader;
}

NetworkCertLoader::NetworkCertLoader() {
  system_slot_cert_cache_ = std::make_unique<CertCache>(base::BindRepeating(
      &NetworkCertLoader::OnCertCacheUpdated, base::Unretained(this)));
  user_private_slot_cert_cache_ =
      std::make_unique<CertCache>(base::BindRepeating(
          &NetworkCertLoader::OnCertCacheUpdated, base::Unretained(this)));
  user_public_slot_cert_cache_ =
      std::make_unique<CertCache>(base::BindRepeating(
          &NetworkCertLoader::OnCertCacheUpdated, base::Unretained(this)));

  auto* system_token_cert_db_storage = SystemTokenCertDbStorage::Get();
  DCHECK(system_token_cert_db_storage);

  system_token_cert_db_storage->GetDatabase(base::BindOnce(
      &NetworkCertLoader::OnSystemNssDbReady, weak_factory_.GetWeakPtr()));
}

NetworkCertLoader::~NetworkCertLoader() {
  DCHECK(!device_policy_certificate_provider_);
  DCHECK(!user_policy_certificate_provider_);
}

void NetworkCertLoader::MarkSystemNSSDBWillBeInitialized() {
  system_slot_cert_cache_->MarkWillBeInitialized(true);
}

void NetworkCertLoader::SetSystemNssDbForTesting(
    net::NSSCertDatabase* system_slot_database) {
  system_slot_cert_cache_->SetNSSDBAndSlot(
      system_slot_database, system_slot_database->GetSystemSlot(),
      true /* is_slot_device_wide */);
}

void NetworkCertLoader::MarkUserNSSDBWillBeInitialized() {
  user_private_slot_cert_cache_->MarkWillBeInitialized(true);
  user_public_slot_cert_cache_->MarkWillBeInitialized(true);
}

void NetworkCertLoader::SetUserNSSDB(net::NSSCertDatabase* user_database) {
  // The private slot can be absent.
  crypto::ScopedPK11Slot private_slot = user_database->GetPrivateSlot();
  if (private_slot) {
    user_private_slot_cert_cache_->SetNSSDBAndSlot(
        user_database, std::move(private_slot),
        false /* is_slot_device_wide */);
  } else {
    user_private_slot_cert_cache_->MarkWillBeInitialized(false);
  }
  user_public_slot_cert_cache_->SetNSSDBAndSlot(
      user_database, user_database->GetPublicSlot(),
      false /* is_slot_device_wide */);
}

void NetworkCertLoader::SetDevicePolicyCertificateProvider(
    PolicyCertificateProvider* device_policy_certificate_provider) {
  if (device_policy_certificate_provider_) {
    device_policy_certificate_provider_->RemovePolicyProvidedCertsObserver(
        this);
  }
  device_policy_certificate_provider_ = device_policy_certificate_provider;
  if (device_policy_certificate_provider_)
    device_policy_certificate_provider_->AddPolicyProvidedCertsObserver(this);
  UpdateCertificates();
}

void NetworkCertLoader::SetUserPolicyCertificateProvider(
    PolicyCertificateProvider* user_policy_certificate_provider) {
  if (user_policy_certificate_provider_)
    user_policy_certificate_provider_->RemovePolicyProvidedCertsObserver(this);
  user_policy_certificate_provider_ = user_policy_certificate_provider;
  if (user_policy_certificate_provider_)
    user_policy_certificate_provider_->AddPolicyProvidedCertsObserver(this);
  UpdateCertificates();
}

void NetworkCertLoader::AddObserver(NetworkCertLoader::Observer* observer) {
  observers_.AddObserver(observer);
}

void NetworkCertLoader::RemoveObserver(NetworkCertLoader::Observer* observer) {
  observers_.RemoveObserver(observer);
}

bool NetworkCertLoader::initial_load_of_any_database_running() const {
  return system_slot_cert_cache_->initial_load_running() ||
         user_private_slot_cert_cache_->initial_load_running() ||
         user_public_slot_cert_cache_->initial_load_running();
}

bool NetworkCertLoader::initial_load_finished() const {
  return system_slot_cert_cache_->initial_load_finished() ||
         user_cert_database_load_finished();
}

bool NetworkCertLoader::user_cert_database_load_finished() const {
  if (!user_public_slot_cert_cache_->is_initialized())
    return false;

  // The private slot is optional, so it's possible that the private slot cert
  // cache is not initialized. In this case, only care about the public slot
  // cert cache's state.
  if (!user_private_slot_cert_cache_->is_initialized())
    return user_public_slot_cert_cache_->initial_load_finished();

  return user_private_slot_cert_cache_->initial_load_finished() &&
         user_public_slot_cert_cache_->initial_load_finished();
}

bool NetworkCertLoader::can_have_client_certificates() const {
  return system_slot_cert_cache_->is_or_will_be_initialized() ||
         user_private_slot_cert_cache_->is_or_will_be_initialized();
}

// static
net::ScopedCERTCertificateList
NetworkCertLoader::GetAllCertsFromNetworkCertList(
    const NetworkCertList& network_cert_list) {
  net::ScopedCERTCertificateList result;
  result.reserve(network_cert_list.size());
  for (const NetworkCert& network_cert : network_cert_list) {
    result.push_back(net::x509_util::DupCERTCertificate(network_cert.cert()));
  }
  return result;
}

// static
NetworkCertLoader::NetworkCertList NetworkCertLoader::CloneNetworkCertList(
    const NetworkCertList& network_cert_list) {
  NetworkCertList result;
  result.reserve(network_cert_list.size());
  for (const NetworkCert& network_cert : network_cert_list) {
    result.push_back(network_cert.Clone());
  }
  return result;
}

// static
void NetworkCertLoader::ForceAvailableForNetworkAuthForTesting() {
  g_force_available_for_network_auth_for_test = true;
}

// static
//
// For background see this discussion on dev-tech-crypto.lists.mozilla.org:
// http://web.archiveorange.com/archive/v/6JJW7E40sypfZGtbkzxX
//
// NOTE: This function relies on the convention that the same PKCS#11 ID
// is shared between a certificate and its associated private and public
// keys.  I tried to implement this with PK11_GetLowLevelKeyIDForCert(),
// but that always returns NULL on Chrome OS for me.
std::string NetworkCertLoader::GetPkcs11IdAndSlotForCert(CERTCertificate* cert,
                                                         int* slot_id) {
  DCHECK(slot_id);

  SECKEYPrivateKey* priv_key = PK11_FindKeyByAnyCert(cert, nullptr /* wincx */);
  if (!priv_key)
    return std::string();

  *slot_id = static_cast<int>(PK11_GetSlotID(priv_key->pkcs11Slot));

  // Get the CKA_ID attribute for a key.
  SECItem* sec_item = PK11_GetLowLevelKeyIDForPrivateKey(priv_key);
  std::string pkcs11_id;
  if (sec_item) {
    pkcs11_id = base::HexEncode(sec_item->data, sec_item->len);
    SECITEM_FreeItem(sec_item, PR_TRUE);
  }
  SECKEY_DestroyPrivateKey(priv_key);

  return pkcs11_id;
}

void NetworkCertLoader::OnSystemNssDbReady(
    net::NSSCertDatabase* system_slot_database) {
  // SystemTokenCertDbStorage informs callers that the system token certificate
  // database initialization failed by returning nullptr.
  if (!system_slot_database) {
    LOG(ERROR) << "Failed to retrieve system token certificate database";
    return;
  }

  system_slot_cert_cache_->SetNSSDBAndSlot(
      system_slot_database, system_slot_database->GetSystemSlot(),
      true /* is_slot_device_wide */);
}

void NetworkCertLoader::OnCertCacheUpdated() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  VLOG(1) << "OnCertCacheUpdated";

  if (is_shutting_down_)
    return;

  if (system_slot_cert_cache_->certificates_update_running() ||
      user_private_slot_cert_cache_->certificates_update_running() ||
      user_public_slot_cert_cache_->certificates_update_running()) {
    // Don't spam the observers - wait for the pending updates to be triggered.
    return;
  }

  certs_from_cache_loaded_ = true;
  UpdateCertificates();
}

void NetworkCertLoader::UpdateCertificates() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  if (is_shutting_down_)
    return;

  // Only trigger a notification to observers if one of the |CertCache|s has
  // already loaded certificates. Don't trigger notifications if policy-provided
  // certificates change before that.
  // TODO(crbug.com/40595094): Now that we handle client and authority
  // certificates separately in NetworkCertLoader, we could fire different
  // notifications for policy-provided cert changes instead of holding back
  // notifications.
  if (!certs_from_cache_loaded_)
    return;

  NetworkCertList user_policy_authorities = GetPolicyProvidedAuthorities(
      user_policy_certificate_provider_, false /* device_wide */);
  NetworkCertList device_policy_authorities = GetPolicyProvidedAuthorities(
      device_policy_certificate_provider_, true /* device_wide */);
  all_authority_certs_ = CombineNetworkCertLists(
      {&system_slot_cert_cache_->authority_certs(),
       &user_public_slot_cert_cache_->authority_certs(),
       &user_private_slot_cert_cache_->authority_certs(),
       &user_policy_authorities, &device_policy_authorities});

  all_client_certs_ =
      CombineNetworkCertLists({&system_slot_cert_cache_->client_certs(),
                               &user_public_slot_cert_cache_->client_certs(),
                               &user_private_slot_cert_cache_->client_certs()});

  VLOG(1) << "OnCertCacheUpdated (all_authority_certs="
          << all_authority_certs_.size()
          << ", all_client_certs=" << all_client_certs_.size() << ")";
  NotifyCertificatesLoaded();
}

void NetworkCertLoader::NotifyCertificatesLoaded() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  if (is_shutting_down_)
    return;
  for (auto& observer : observers_)
    observer.OnCertificatesLoaded();
}

void NetworkCertLoader::OnPolicyProvidedCertsChanged() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  UpdateCertificates();
}

}  // namespace ash