chromium/chromeos/ash/components/chaps_util/chaps_util_impl.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 "chromeos/ash/components/chaps_util/chaps_util_impl.h"

#include <dlfcn.h>
#include <keyhi.h>
#include <pk11pub.h>
#include <pkcs11.h>
#include <pkcs11t.h>
#include <stdint.h>

#include <optional>
#include <ostream>
#include <string>
#include <string_view>
#include <vector>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "chromeos/ash/components/chaps_util/chaps_slot_session.h"
#include "crypto/chaps_support.h"
#include "crypto/scoped_nss_types.h"
#include "third_party/boringssl/src/include/openssl/base.h"
#include "third_party/boringssl/src/include/openssl/mem.h"
#include "third_party/boringssl/src/include/openssl/pkcs8.h"
#include "third_party/boringssl/src/include/openssl/stack.h"

namespace chromeos {

namespace {

// TODO(b/202374261): Move these into a shared header.
// Signals to chaps that a generated key should be software-backed.
constexpr CK_ATTRIBUTE_TYPE kForceSoftwareAttribute = CKA_VENDOR_DEFINED + 4;
// Chaps sets this for keys that are software-backed.
constexpr CK_ATTRIBUTE_TYPE kKeyInSoftware = CKA_VENDOR_DEFINED + 5;
struct KeyPairHandles {
  CK_OBJECT_HANDLE public_key;
  CK_OBJECT_HANDLE private_key;
};

using Pkcs11Operation = base::RepeatingCallback<CK_RV()>;

// Performs |operation| and handles return values indicating that the PKCS11
// session has been closed by attempting to re-open the |chaps_session|.
// This is useful because the session could be closed e.g. because NSS could
// have called C_CloseAllSessions.
bool PerformWithRetries(ChapsSlotSession* chaps_session,
                        std::string_view operation_name,
                        const Pkcs11Operation& operation) {
  const int kMaxAttempts = 5;

  for (int attempt = 0; attempt < kMaxAttempts; ++attempt) {
    CK_RV result = operation.Run();
    if (result == CKR_OK) {
      return true;
    }
    if (result != CKR_SESSION_HANDLE_INVALID && result != CKR_SESSION_CLOSED) {
      LOG(ERROR) << operation_name << " failed with " << result;
      return false;
    }
    if (!chaps_session->ReopenSession()) {
      return false;
    }
  }
  LOG(ERROR) << operation_name << " failed";
  return false;
}

// Uses |chaps_session| to generate a software-backed RSA key pair with modulus
// length |num_bits|.
std::optional<KeyPairHandles> GenerateSoftwareBackedRSAKeyPair(
    ChapsSlotSession* chaps_session,
    uint16_t num_bits) {
  CK_ULONG modulus_bits = num_bits;
  CK_BBOOL true_value = CK_TRUE;
  CK_BBOOL false_value = CK_FALSE;
  CK_BYTE public_exponent[3] = {0x01, 0x00, 0x01};  // 65537

  // Public key attributes
  // Note: CKA_ID is set later (computed from the public key modulus) and
  // CKA_LABEL is not set to match NSS behavior
  // (https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/PKCS11_Implement).
  CK_ATTRIBUTE pub_attributes[] = {
      {CKA_TOKEN, &true_value, sizeof(true_value)},
      {CKA_PRIVATE, &false_value, sizeof(false_value)},
      {CKA_VERIFY, &true_value, sizeof(true_value)},
      {CKA_MODULUS_BITS, &modulus_bits, sizeof(modulus_bits)},
      {CKA_PUBLIC_EXPONENT, public_exponent, sizeof(public_exponent)}};

  // Private key attributes
  // Note: CKA_ID is set later (computed from the public key modulus) and
  // CKA_LABEL is not set to match NSS behavior
  // (https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/PKCS11_Implement).
  CK_ATTRIBUTE priv_attributes[] = {
      {CKA_TOKEN, &true_value, sizeof(true_value)},
      {CKA_PRIVATE, &true_value, sizeof(true_value)},
      {CKA_SENSITIVE, &true_value, sizeof(true_value)},
      {CKA_EXTRACTABLE, &false_value, sizeof(false_value)},
      {kForceSoftwareAttribute, &true_value, sizeof(true_value)},
      {CKA_SIGN, &true_value, sizeof(true_value)}};
  CK_MECHANISM mechanism = {CKM_RSA_PKCS_KEY_PAIR_GEN, /* pParameter */ nullptr,
                            /* ulParameterLen*/ 0};

  KeyPairHandles key_pair;

  if (!PerformWithRetries(
          chaps_session, "GenerateKeyPair",
          base::BindRepeating(&ChapsSlotSession::GenerateKeyPair,
                              base::Unretained(chaps_session), &mechanism,
                              pub_attributes, std::size(pub_attributes),
                              priv_attributes, std::size(priv_attributes),
                              &(key_pair.public_key),
                              &(key_pair.private_key)))) {
    return {};
  }
  return key_pair;
}

// Read the modulus of the public key identified by |pub_key_handle| and return
// it.
std::optional<std::vector<CK_BYTE>> ExtractModulus(
    ChapsSlotSession* chaps_session,
    CK_OBJECT_HANDLE pub_key_handle) {
  std::vector<CK_BYTE> modulus(256);
  CK_ATTRIBUTE attrs_get_modulus[] = {
      {CKA_MODULUS, modulus.data(), modulus.size()}};
  if (!PerformWithRetries(
          chaps_session, "GetAttributeValue",
          base::BindRepeating(&ChapsSlotSession::GetAttributeValue,
                              base::Unretained(chaps_session), pub_key_handle,
                              attrs_get_modulus,
                              std::size(attrs_get_modulus)))) {
    return {};
  }
  return modulus;
}

crypto::ScopedSECItem MakeIdFromPubKeyNss(
    const std::vector<uint8_t>& public_key_bytes) {
  SECItem secitem_modulus;
  secitem_modulus.data = const_cast<uint8_t*>(public_key_bytes.data());
  secitem_modulus.len = public_key_bytes.size();
  return crypto::ScopedSECItem(PK11_MakeIDFromPubKey(&secitem_modulus));
}

// Read the modulus of the public key identified by |pub_key_handle| and return
// it.
std::optional<bool> IsKeySoftwareBacked(ChapsSlotSession* chaps_session,
                                        CK_OBJECT_HANDLE private_key_handle) {
  CK_BBOOL key_in_software = CK_FALSE;
  CK_ATTRIBUTE attrs_get_key_in_software[] = {
      {kKeyInSoftware, &key_in_software, sizeof(key_in_software)}};
  if (!PerformWithRetries(
          chaps_session, "GetAttributeValue",
          base::BindRepeating(&ChapsSlotSession::GetAttributeValue,
                              base::Unretained(chaps_session),
                              private_key_handle, attrs_get_key_in_software,
                              std::size(attrs_get_key_in_software)))) {
    return {};
  }
  return key_in_software;
}

// Create the CKA_ID value that NSS would use for |key_pair| and return it.
crypto::ScopedSECItem CreateNssCkaId(ChapsSlotSession* chaps_session,
                                     const KeyPairHandles& key_pair) {
  auto modulus = ExtractModulus(chaps_session, key_pair.public_key);
  if (!modulus) {
    return nullptr;
  }
  return MakeIdFromPubKeyNss(modulus.value());
}

// Set the CKA_ID attribute of the public and private key objects in |key_pair|
// to |cka_id|.
bool SetCkaId(ChapsSlotSession* chaps_session,
              KeyPairHandles& key_pair,
              SECItem* cka_id) {
  CK_ATTRIBUTE attrs_set_id[] = {{CKA_ID, cka_id->data, cka_id->len}};
  if (!PerformWithRetries(
          chaps_session, "SetAttributeValue",
          base::BindRepeating(&ChapsSlotSession::SetAttributeValue,
                              base::Unretained(chaps_session),
                              key_pair.private_key, attrs_set_id,
                              std::size(attrs_set_id)))) {
    return false;
  }
  if (!PerformWithRetries(
          chaps_session, "SetAttributeValue",
          base::BindRepeating(&ChapsSlotSession::SetAttributeValue,
                              base::Unretained(chaps_session),
                              key_pair.public_key, attrs_set_id,
                              std::size(attrs_set_id)))) {
    return false;
  }
  return true;
}

}  // namespace

ChapsUtilImpl::ChapsUtilImpl(
    std::unique_ptr<ChapsSlotSessionFactory> chaps_slot_session_factory)
    : chaps_slot_session_factory_(std::move(chaps_slot_session_factory)) {}
ChapsUtilImpl::~ChapsUtilImpl() = default;

bool ChapsUtilImpl::GenerateSoftwareBackedRSAKey(
    PK11SlotInfo* slot,
    uint16_t num_bits,
    crypto::ScopedSECKEYPublicKey* out_public_key,
    crypto::ScopedSECKEYPrivateKey* out_private_key) {
  DCHECK(out_public_key);
  DCHECK(out_private_key);

  std::unique_ptr<ChapsSlotSession> chaps_session =
      GetChapsSlotSessionForSlot(slot);
  if (!chaps_session) {
    return false;
  }

  std::optional<KeyPairHandles> key_pair =
      GenerateSoftwareBackedRSAKeyPair(chaps_session.get(), num_bits);
  if (!key_pair) {
    return false;
  }

  // Safety check that software-backed key generation was triggered.
  std::optional<bool> is_software_backed =
      IsKeySoftwareBacked(chaps_session.get(), key_pair->private_key);
  if (!is_software_backed || !is_software_backed.value()) {
    return false;
  }

  crypto::ScopedSECItem cka_id =
      CreateNssCkaId(chaps_session.get(), key_pair.value());
  if (!cka_id) {
    return false;
  }
  if (!SetCkaId(chaps_session.get(), key_pair.value(), cka_id.get())) {
    return false;
  }

  out_private_key->reset(PK11_FindKeyByKeyID(slot, cka_id.get(), nullptr));
  if (!*out_private_key) {
    LOG(ERROR) << "Failed to find private key.";
    return false;
  }
  out_public_key->reset(SECKEY_ConvertToPublicKey(out_private_key->get()));
  if (!*out_public_key) {
    LOG(ERROR) << "Failed to extract public key.";
    return false;
  }
  return true;
}

std::unique_ptr<ChapsSlotSession> ChapsUtilImpl::GetChapsSlotSessionForSlot(
    PK11SlotInfo* slot) {
  if (!slot || (!is_chaps_provided_slot_for_testing_ &&
                !crypto::IsSlotProvidedByChaps(slot))) {
    return nullptr;
  }

  // Note that ChapsSlotSession(Factory) expects something else to have called
  // C_Initialize. It is a safe assumption that NSS has called C_Initialize for
  // chaps if |slot| is actually a chaps-provided slot, which is verified above.
  return chaps_slot_session_factory_->CreateChapsSlotSession(
      PK11_GetSlotID(slot));
}

}  // namespace chromeos