chromium/chrome/browser/ash/net/client_cert_store_kcer_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/net/client_cert_store_kcer.h"

#include <memory>
#include <optional>
#include <string>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "chrome/browser/certificate_provider/certificate_provider.h"
#include "chromeos/components/kcer/kcer_nss/test_utils.h"
#include "content/public/test/browser_task_environment.h"
#include "crypto/nss_util.h"
#include "crypto/nss_util_internal.h"
#include "crypto/scoped_nss_types.h"
#include "crypto/scoped_test_nss_chromeos_user.h"
#include "crypto/scoped_test_nss_db.h"
#include "crypto/scoped_test_system_nss_key_slot.h"
#include "net/cert/cert_database.h"
#include "net/cert/x509_certificate.h"
#include "net/cert/x509_util.h"
#include "net/cert/x509_util_nss.h"
#include "net/ssl/client_cert_identity_test_util.h"
#include "net/ssl/ssl_cert_request_info.h"
#include "net/ssl/ssl_private_key.h"
#include "net/ssl/ssl_private_key_test_util.h"
#include "net/test/cert_test_util.h"
#include "net/test/test_data_directory.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

namespace {

// "CN=B CA" - DER encoded DN of the issuer of client_1.pem
const unsigned char kAuthority1DN[] = {0x30, 0x0f, 0x31, 0x0d, 0x30, 0x0b,
                                       0x06, 0x03, 0x55, 0x04, 0x03, 0x0c,
                                       0x04, 0x42, 0x20, 0x43, 0x41};

// "CN=C Root CA" - DER encoded DN of the issuer of client_1_ca.pem,
// client_2_ca.pem, and client_3_ca.pem.
const unsigned char kAuthorityRootDN[] = {
    0x30, 0x14, 0x31, 0x12, 0x30, 0x10, 0x06, 0x03, 0x55, 0x04, 0x03,
    0x0c, 0x09, 0x43, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41,
};

void SaveIdentitiesAndQuitCallback(net::ClientCertIdentityList* out_identities,
                                   base::OnceClosure quit_closure,
                                   net::ClientCertIdentityList in_identities) {
  *out_identities = std::move(in_identities);
  std::move(quit_closure).Run();
}

void SavePrivateKeyAndQuitCallback(scoped_refptr<net::SSLPrivateKey>* out_key,
                                   base::OnceClosure quit_closure,
                                   scoped_refptr<net::SSLPrivateKey> in_key) {
  *out_key = std::move(in_key);
  std::move(quit_closure).Run();
}

}  // namespace

class ClientCertStoreKcerTest : public ::testing::Test {
 public:
  ClientCertStoreKcerTest() {}

  void SetUp() override {
    ASSERT_TRUE(user1_.constructed_successfully());
    ASSERT_TRUE(user2_.constructed_successfully());
    ASSERT_TRUE(system_slot_.ConstructedSuccessfully());
    kcer_holder_.emplace(
        crypto::GetPublicSlotForChromeOSUser(user1_.username_hash()).get(),
        system_slot_.slot());
  }

  void TearDown() override { kcer_holder_.reset(); }

  scoped_refptr<net::X509Certificate> ImportCertToSlot(
      const std::string& cert_filename,
      const std::string& key_filename,
      PK11SlotInfo* slot) {
    scoped_refptr<net::X509Certificate> result =
        net::ImportClientCertAndKeyFromFile(net::GetTestCertsDirectory(),
                                            cert_filename, key_filename, slot);
    // Kcer relies on notifications to invalidate caches when the client
    // certificate store changes.
    net::CertDatabase::GetInstance()->NotifyObserversClientCertStoreChanged();
    return result;
  }

 protected:
  crypto::ScopedTestNSSChromeOSUser user1_{"user1"};
  crypto::ScopedTestNSSChromeOSUser user2_{"user2"};
  crypto::ScopedTestSystemNSSKeySlot system_slot_{
      /*simulate_token_loader=*/true};
  std::optional<kcer::TestKcerHolder> kcer_holder_;
  content::BrowserTaskEnvironment task_environment_;
};

// Ensure that cert requests, that are started before NSS is initialized,
// will wait for the initialization and succeed afterwards.
TEST_F(ClientCertStoreKcerTest, RequestWaitsForNSSInitAndSucceeds) {
  ClientCertStoreKcer store(nullptr /* no additional provider */,
                            kcer_holder_->GetKcer());

  scoped_refptr<net::X509Certificate> cert_1(ImportCertToSlot(
      "client_1.pem", "client_1.pk8",
      crypto::GetPublicSlotForChromeOSUser(user1_.username_hash()).get()));
  ASSERT_TRUE(cert_1);

  // Request any client certificate, which is expected to match client_1.
  auto request_all = base::MakeRefCounted<net::SSLCertRequestInfo>();

  net::ClientCertIdentityList selected_identities;
  base::RunLoop run_loop;
  store.GetClientCerts(
      request_all,
      base::BindOnce(SaveIdentitiesAndQuitCallback, &selected_identities,
                     run_loop.QuitClosure()));

  {
    base::RunLoop run_loop_inner;
    run_loop_inner.RunUntilIdle();
    // GetClientCerts should wait for the initialization of NSS to finish.
    ASSERT_EQ(0u, selected_identities.size());
  }

  user1_.FinishInit();
  run_loop.Run();

  ASSERT_EQ(1u, selected_identities.size());
}

// Ensure that cert requests, that are started after NSS was initialized,
// will succeed.
TEST_F(ClientCertStoreKcerTest, RequestsAfterNSSInitSucceed) {
  user1_.FinishInit();

  ClientCertStoreKcer store(nullptr /* no additional provider */,
                            kcer_holder_->GetKcer());

  scoped_refptr<net::X509Certificate> cert_1(ImportCertToSlot(
      "client_1.pem", "client_1.pk8",
      crypto::GetPublicSlotForChromeOSUser(user1_.username_hash()).get()));
  ASSERT_TRUE(cert_1);

  auto request_all = base::MakeRefCounted<net::SSLCertRequestInfo>();

  base::RunLoop run_loop;
  net::ClientCertIdentityList selected_identities;
  store.GetClientCerts(
      request_all,
      base::BindOnce(SaveIdentitiesAndQuitCallback, &selected_identities,
                     run_loop.QuitClosure()));
  run_loop.Run();

  ASSERT_EQ(1u, selected_identities.size());
}

TEST_F(ClientCertStoreKcerTest, Filter) {
  user1_.FinishInit();
  user2_.FinishInit();

  crypto::ScopedPK11Slot slot_private1 = crypto::GetPrivateSlotForChromeOSUser(
      user1_.username_hash(), base::DoNothing());
  crypto::ScopedPK11Slot slot_private2 = crypto::GetPrivateSlotForChromeOSUser(
      user2_.username_hash(), base::DoNothing());
  // crypto::GetPrivateSlotForChromeOSUser() may return nullptr if the slot is
  // not initialized yet, but this should not happen.
  ASSERT_TRUE(slot_private1);
  ASSERT_TRUE(slot_private2);

  kcer::TestKcerHolder user1_kcer_without_system_slot(slot_private1.get(),
                                                      nullptr);
  kcer::TestKcerHolder user1_kcer_with_system_slot(slot_private1.get(),
                                                   system_slot_.slot());
  kcer::TestKcerHolder user2_kcer_without_system_slot(slot_private2.get(),
                                                      nullptr);
  kcer::TestKcerHolder user2_kcer_with_system_slot(slot_private2.get(),
                                                   system_slot_.slot());

  // Import a certificate into each slot.
  scoped_refptr<net::X509Certificate> cert_private1(
      ImportCertToSlot("client_1.pem", "client_1.pk8", slot_private1.get()));
  ASSERT_TRUE(cert_private1);
  scoped_refptr<net::X509Certificate> cert_private2(
      ImportCertToSlot("client_2.pem", "client_2.pk8", slot_private2.get()));
  ASSERT_TRUE(cert_private2);
  scoped_refptr<net::X509Certificate> cert_system(
      ImportCertToSlot("client_3.pem", "client_3.pk8", system_slot_.slot()));
  ASSERT_TRUE(cert_system);

  const struct FilterTest {
    std::string description;
    base::WeakPtr<kcer::Kcer> kcer;
    std::vector<raw_ptr<net::X509Certificate, VectorExperimental>> results;
  } kTests[] = {
      {"user1 without system slot",
       user1_kcer_without_system_slot.GetKcer(),
       {cert_private1.get()}},
      {"user1 with system slot",
       user1_kcer_with_system_slot.GetKcer(),
       {cert_private1.get(), cert_system.get()}},
      {"user2 without system slot",
       user2_kcer_without_system_slot.GetKcer(),
       {cert_private2.get()}},
      {"user2 with system slot",
       user2_kcer_with_system_slot.GetKcer(),
       {cert_private2.get(), cert_system.get()}},
  };

  for (const auto& test : kTests) {
    SCOPED_TRACE(test.description);

    ClientCertStoreKcer store(nullptr /* no additional provider */, test.kcer);

    auto request_all = base::MakeRefCounted<net::SSLCertRequestInfo>();

    base::RunLoop run_loop;
    net::ClientCertIdentityList selected_identities;
    store.GetClientCerts(
        request_all,
        base::BindOnce(SaveIdentitiesAndQuitCallback, &selected_identities,
                       run_loop.QuitClosure()));
    run_loop.Run();

    ASSERT_EQ(test.results.size(), selected_identities.size());
    for (size_t i = 0; i < test.results.size(); i++) {
      bool found_cert = false;
      for (const auto& identity : selected_identities) {
        if (test.results[i]->EqualsExcludingChain(identity->certificate())) {
          found_cert = true;
          break;
        }
      }
      EXPECT_TRUE(found_cert) << "Could not find certificate " << i;
    }
  }
}

// Ensure that the delegation of the request matching to the base class is
// functional.
TEST_F(ClientCertStoreKcerTest, CertRequestMatching) {
  user1_.FinishInit();

  ClientCertStoreKcer store(nullptr,  // no additional provider
                            kcer_holder_->GetKcer());

  crypto::ScopedPK11Slot slot =
      crypto::GetPublicSlotForChromeOSUser(user1_.username_hash());
  scoped_refptr<net::X509Certificate> cert_1(
      ImportCertToSlot("client_1.pem", "client_1.pk8", slot.get()));
  ASSERT_TRUE(cert_1);
  scoped_refptr<net::X509Certificate> cert_2(
      ImportCertToSlot("client_2.pem", "client_2.pk8", slot.get()));
  ASSERT_TRUE(cert_2.get());

  std::vector<std::string> authority_1 = {
      std::string(std::begin(kAuthority1DN), std::end(kAuthority1DN))};
  auto request = base::MakeRefCounted<net::SSLCertRequestInfo>();
  request->cert_authorities = authority_1;

  base::RunLoop run_loop;
  net::ClientCertIdentityList selected_identities;
  store.GetClientCerts(
      request, base::BindOnce(SaveIdentitiesAndQuitCallback,
                              &selected_identities, run_loop.QuitClosure()));
  run_loop.Run();

  ASSERT_EQ(1u, selected_identities.size());
  EXPECT_TRUE(
      cert_1->EqualsExcludingChain(selected_identities[0]->certificate()));
}

// Tests that ClientCertStoreKcer attempts to build a certificate chain by
// querying NSS before return a certificate.
TEST_F(ClientCertStoreKcerTest, BuildsCertificateChain) {
  user1_.FinishInit();

  crypto::ScopedPK11Slot private_slot = crypto::GetPrivateSlotForChromeOSUser(
      user1_.username_hash(), base::DoNothing());
  // crypto::GetPrivateSlotForChromeOSUser() may return nullptr if the slot is
  // not initialized yet, but this should not happen.
  ASSERT_TRUE(private_slot);

  // Import client_1.pem into the slot of user1.
  scoped_refptr<net::X509Certificate> client_1(
      net::ImportClientCertAndKeyFromFile(net::GetTestCertsDirectory(),
                                          "client_1.pem", "client_1.pk8",
                                          private_slot.get()));
  ASSERT_TRUE(client_1.get());
  scoped_refptr<net::X509Certificate> client_1_ca(
      net::ImportCertFromFile(net::GetTestCertsDirectory(), "client_1_ca.pem"));
  std::string pkcs8_key;
  ASSERT_TRUE(base::ReadFileToString(
      net::GetTestCertsDirectory().AppendASCII("client_1.pk8"), &pkcs8_key));

  // Import client_1_ca.pem into a separate slot, that is visible to NSS in
  // general, but not to Kcer for user1.
  crypto::ScopedTestNSSDB public_slot;
  ASSERT_TRUE(client_1_ca.get());
  ASSERT_TRUE(net::ImportClientCertToSlot(client_1_ca, public_slot.slot()));

  ClientCertStoreKcer store(nullptr,  // no additional provider
                            kcer_holder_->GetKcer());

  // These test keys are RSA keys.
  std::vector<uint16_t> expected =
      net::SSLPrivateKey::DefaultAlgorithmPreferences(EVP_PKEY_RSA,
                                                      true /* supports PSS */);

  {
    // Request certificates matching B CA, |client_1|'s issuer.
    auto request = base::MakeRefCounted<net::SSLCertRequestInfo>();
    request->cert_authorities.emplace_back(
        reinterpret_cast<const char*>(kAuthority1DN), sizeof(kAuthority1DN));

    net::ClientCertIdentityList selected_identities;
    base::RunLoop loop;
    store.GetClientCerts(
        request, base::BindOnce(SaveIdentitiesAndQuitCallback,
                                &selected_identities, loop.QuitClosure()));
    loop.Run();

    // The result be |client_1| with no intermediates.
    ASSERT_EQ(1u, selected_identities.size());
    scoped_refptr<net::X509Certificate> selected_cert =
        selected_identities[0]->certificate();
    EXPECT_TRUE(net::x509_util::CryptoBufferEqual(
        client_1->cert_buffer(), selected_cert->cert_buffer()));
    ASSERT_EQ(0u, selected_cert->intermediate_buffers().size());

    scoped_refptr<net::SSLPrivateKey> ssl_private_key;
    base::RunLoop key_loop;
    selected_identities[0]->AcquirePrivateKey(
        base::BindOnce(SavePrivateKeyAndQuitCallback, &ssl_private_key,
                       key_loop.QuitClosure()));
    key_loop.Run();

    ASSERT_TRUE(ssl_private_key);
    EXPECT_EQ(expected, ssl_private_key->GetAlgorithmPreferences());
    TestSSLPrivateKeyMatches(ssl_private_key.get(), pkcs8_key);
  }

  {
    // Request certificates matching C Root CA, |client_1_ca|'s issuer.
    auto request = base::MakeRefCounted<net::SSLCertRequestInfo>();
    request->cert_authorities.emplace_back(
        reinterpret_cast<const char*>(kAuthorityRootDN),
        sizeof(kAuthorityRootDN));

    net::ClientCertIdentityList selected_identities;
    base::RunLoop loop;
    store.GetClientCerts(
        request, base::BindOnce(SaveIdentitiesAndQuitCallback,
                                &selected_identities, loop.QuitClosure()));
    loop.Run();

    // The result be |client_1| with |client_1_ca| as an intermediate.
    ASSERT_EQ(1u, selected_identities.size());
    scoped_refptr<net::X509Certificate> selected_cert =
        selected_identities[0]->certificate();
    EXPECT_TRUE(net::x509_util::CryptoBufferEqual(
        client_1->cert_buffer(), selected_cert->cert_buffer()));
    ASSERT_EQ(1u, selected_cert->intermediate_buffers().size());
    EXPECT_TRUE(net::x509_util::CryptoBufferEqual(
        client_1_ca->cert_buffer(),
        selected_cert->intermediate_buffers()[0].get()));

    scoped_refptr<net::SSLPrivateKey> ssl_private_key;
    base::RunLoop key_loop;
    selected_identities[0]->AcquirePrivateKey(
        base::BindOnce(SavePrivateKeyAndQuitCallback, &ssl_private_key,
                       key_loop.QuitClosure()));
    key_loop.Run();
    ASSERT_TRUE(ssl_private_key);
    EXPECT_EQ(expected, ssl_private_key->GetAlgorithmPreferences());
    TestSSLPrivateKeyMatches(ssl_private_key.get(), pkcs8_key);
  }
}

}  // namespace ash