// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chromeos/components/kcer/kcer_nss/kcer_token_impl_nss.h"
#include <certdb.h>
#include <keythi.h>
#include <pkcs11.h>
#include <secerr.h>
#include <stdint.h>
#include <optional>
#include <queue>
#include <string>
#include <string_view>
#include <vector>
#include "base/check_is_test.h"
#include "base/compiler_specific.h"
#include "base/functional/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/bind_post_task.h"
#include "base/task/thread_pool.h"
#include "build/chromeos_buildflags.h"
#include "chromeos/components/kcer/cert_cache.h"
#include "chromeos/components/kcer/chaps/high_level_chaps_client.h"
#include "chromeos/components/kcer/helpers/key_helper.h"
#include "chromeos/components/kcer/helpers/pkcs12_validator.h"
#include "chromeos/components/kcer/kcer_histograms.h"
#include "chromeos/components/kcer/kcer_token.h"
#include "chromeos/components/kcer/kcer_utils.h"
#include "chromeos/components/kcer/key_permissions.pb.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "crypto/nss_key_util.h"
#include "crypto/scoped_nss_types.h"
#include "net/base/net_errors.h"
#include "net/cert/cert_database.h"
#include "net/cert/scoped_nss_types.h"
#include "net/cert/x509_certificate.h"
#include "net/cert/x509_util_nss.h"
#include "third_party/cros_system_api/constants/pkcs11_custom_attributes.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chromeos/ash/components/chaps_util/chaps_util.h"
#endif
// General pattern for implementing KcerToken methods:
// * The received callbacks for the results must already be bound to correct
// task runners (see base::BindPostTask), so it's not needed to manually post
// them on any particular task runner.
// * Each method must advance task queue on completion. This is important
// because before initialization and during other tasks all new tasks are stored
// in the queue. It can only progress when a previous tasks advances the queue
// or a new task arrives.
// * The most convenient way to advance the queue is to call
// BlockQueueGetUnblocker() and attach the result to the original callback. This
// will advance the queue automatically after the callback is called (or even
// discarded).
// * It's also better to keep the task queue blocked during the execution of a
// callback in case of additional requests from it.
namespace kcer::internal {
namespace {
void RunUnblocker(base::ScopedClosureRunner unblocker) {
unblocker.RunAndReset();
}
// Returns a vector containing bytes from `value` or an empty vector if `value`
// is nullptr.
std::vector<uint8_t> SECItemToBytes(const crypto::ScopedSECItem& value) {
return value ? std::vector<uint8_t>(value->data, value->data + value->len)
: std::vector<uint8_t>();
}
void CleanUpAndDestroyKeys(crypto::ScopedSECKEYPublicKey public_key,
crypto::ScopedSECKEYPrivateKey private_key) {
// Clean up generated keys. PK11_DeleteTokenPrivateKey and
// PK11_DeleteTokenPublicKey are documented to also destroy the passed
// SECKEYPublicKey/SECKEYPrivateKey structures.
if (private_key) {
PK11_DeleteTokenPrivateKey(/*privKey=*/private_key.release(),
/*force=*/false);
}
if (public_key) {
PK11_DeleteTokenPublicKey(/*pubKey=*/public_key.release());
}
}
base::expected<crypto::ScopedSECKEYPrivateKey, Error>
GetSECKEYPrivateKeyByPkcs11Id(const crypto::ScopedPK11Slot& slot,
const std::vector<uint8_t>& pkcs11_id) {
SECItem sec_key_id;
sec_key_id.data = const_cast<uint8_t*>(pkcs11_id.data());
sec_key_id.len = pkcs11_id.size();
crypto::ScopedSECKEYPrivateKey private_key(
PK11_FindKeyByKeyID(slot.get(), &sec_key_id, /*wincx=*/nullptr));
if (!private_key) {
return base::unexpected(Error::kKeyNotFound);
}
return private_key;
}
// Returns ScopedSECKEYPrivateKey if the key was found.
// Returns Error::kKeyNotFound if the key was not found.
// Returns some other error in case of other problems.
base::expected<crypto::ScopedSECKEYPrivateKey, Error> GetSECKEYPrivateKey(
const crypto::ScopedPK11Slot& slot,
const PrivateKeyHandle& key) {
std::vector<uint8_t> pkcs11_id = key.GetPkcs11IdInternal().value();
if (pkcs11_id.empty()) {
CHECK(!key.GetSpkiInternal()->empty());
pkcs11_id = SECItemToBytes(crypto::MakeNssIdFromSpki(
base::make_span(key.GetSpkiInternal().value())));
}
if (pkcs11_id.empty()) {
return base::unexpected(Error::kFailedToGetKeyId);
}
return GetSECKEYPrivateKeyByPkcs11Id(slot, pkcs11_id);
}
void GenerateRsaKeyOnWorkerThread(Token token,
crypto::ScopedPK11Slot slot,
RsaModulusLength modulus_length_bits,
bool hardware_backed,
Kcer::GenerateKeyCallback callback) {
uint32_t modulus_length_bits_uint =
static_cast<uint32_t>(modulus_length_bits);
bool key_gen_success = false;
crypto::ScopedSECKEYPublicKey public_key;
crypto::ScopedSECKEYPrivateKey private_key;
if (hardware_backed) {
key_gen_success = crypto::GenerateRSAKeyPairNSS(
slot.get(), modulus_length_bits_uint, /*permanent=*/true, &public_key,
&private_key);
} else {
#if BUILDFLAG(IS_CHROMEOS_ASH)
auto chaps_util = chromeos::ChapsUtil::Create();
key_gen_success = chaps_util->GenerateSoftwareBackedRSAKey(
slot.get(), modulus_length_bits_uint, &public_key, &private_key);
#else
return std::move(callback).Run(base::unexpected(Error::kNotImplemented));
#endif
}
if (!key_gen_success) {
CleanUpAndDestroyKeys(std::move(public_key), std::move(private_key));
return std::move(callback).Run(
base::unexpected(Error::kFailedToGenerateKey));
}
DCHECK(public_key && private_key);
crypto::ScopedSECItem public_key_der(
SECKEY_EncodeDERSubjectPublicKeyInfo(public_key.get()));
if (!public_key_der) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToExportPublicKey));
}
Pkcs11Id pkcs11_id(SECItemToBytes(crypto::ScopedSECItem(
PK11_MakeIDFromPubKey(&public_key->u.rsa.modulus))));
PublicKeySpki public_key_spki(SECItemToBytes(std::move(public_key_der)));
return std::move(callback).Run(
PublicKey(token, std::move(pkcs11_id), std::move(public_key_spki)));
}
void GenerateEcKeyOnWorkerThread(Token token,
crypto::ScopedPK11Slot slot,
EllipticCurve curve,
bool hardware_backed,
Kcer::GenerateKeyCallback callback) {
bool key_gen_success = false;
crypto::ScopedSECKEYPublicKey public_key;
crypto::ScopedSECKEYPrivateKey private_key;
if (curve != EllipticCurve::kP256) {
return std::move(callback).Run(base::unexpected(Error::kNotSupported));
}
if (hardware_backed) {
key_gen_success = crypto::GenerateECKeyPairNSS(
slot.get(), SEC_OID_ANSIX962_EC_PRIME256V1, /*permanent=*/true,
&public_key, &private_key);
} else {
// Shouldn't be needed yet, will be implemented in non-NSS version of
// Kcer.
return std::move(callback).Run(base::unexpected(Error::kNotImplemented));
}
if (!key_gen_success) {
CleanUpAndDestroyKeys(std::move(public_key), std::move(private_key));
return std::move(callback).Run(
base::unexpected(Error::kFailedToGenerateKey));
}
crypto::ScopedSECItem public_key_der(
SECKEY_EncodeDERSubjectPublicKeyInfo(public_key.get()));
if (!public_key_der) {
CleanUpAndDestroyKeys(std::move(public_key), std::move(private_key));
return std::move(callback).Run(
base::unexpected(Error::kFailedToExportPublicKey));
}
Pkcs11Id pkcs11_id(SECItemToBytes(crypto::ScopedSECItem(
PK11_MakeIDFromPubKey(&public_key->u.ec.publicValue))));
return std::move(callback).Run(
PublicKey(token, std::move(pkcs11_id),
PublicKeySpki(SECItemToBytes(std::move(public_key_der)))));
}
void ImportKeyOnWorkerThread(Token token,
crypto::ScopedPK11Slot slot,
Pkcs8PrivateKeyInfoDer pkcs8_private_key_info_der,
Kcer::ImportKeyCallback callback) {
// TODO(miersh): The import method uses
// PK11_ImportDERPrivateKeyInfoAndReturnKey inside, which doesn't seem to
// create a public key object for the imported key. This causes the ListKeys
// method to not see the key. This should be fixed in Kcer-without-NSS.
crypto::ScopedSECKEYPrivateKey imported_private_key =
crypto::ImportNSSKeyFromPrivateKeyInfo(slot.get(),
pkcs8_private_key_info_der.value(),
/*permanent=*/true);
if (!imported_private_key) {
return std::move(callback).Run(base::unexpected(Error::kFailedToImportKey));
}
crypto::ScopedSECKEYPublicKey public_key(
SECKEY_ConvertToPublicKey(imported_private_key.get()));
if (!public_key) {
CleanUpAndDestroyKeys(std::move(public_key),
std::move(imported_private_key));
return std::move(callback).Run(
base::unexpected(Error::kFailedToExportPublicKey));
}
crypto::ScopedSECItem public_key_der(
SECKEY_EncodeDERSubjectPublicKeyInfo(public_key.get()));
if (!public_key_der) {
CleanUpAndDestroyKeys(std::move(public_key),
std::move(imported_private_key));
return std::move(callback).Run(
base::unexpected(Error::kFailedToEncodePublicKey));
}
Pkcs11Id pkcs11_id(
SECItemToBytes(crypto::MakeNssIdFromPublicKey(public_key.get())));
return std::move(callback).Run(
PublicKey(token, std::move(pkcs11_id),
PublicKeySpki(SECItemToBytes(std::move(public_key_der)))));
}
void ImportCertOnWorkerThread(
crypto::ScopedPK11Slot slot,
CertDer cert_der,
base::OnceCallback<void(bool /*did_modify*/,
base::expected<void, Error> /*result*/)> callback) {
net::ScopedCERTCertificateList certs =
net::x509_util::CreateCERTCertificateListFromBytes(
*cert_der, net::X509Certificate::FORMAT_AUTO);
if (certs.empty() || (certs.size() != 1)) {
return std::move(callback).Run(
/*did_modify=*/false, base::unexpected(Error::kInvalidCertificate));
}
CERTCertificate* cert = certs[0].get();
CK_OBJECT_HANDLE key;
crypto::ScopedPK11Slot key_slot(PK11_KeyForCertExists(cert, &key, nullptr));
if (key_slot != slot) {
return std::move(callback).Run(
/*did_modify=*/false, base::unexpected(Error::kKeyNotFound));
}
if (int res = net::x509_util::ImportUserCert(cert, std::move(key_slot));
res != net::OK) {
LOG(ERROR) << "Failed to import certificate, error: " << res;
return std::move(callback).Run(
/*did_modify=*/false,
base::unexpected(Error::kFailedToImportCertificate));
}
return std::move(callback).Run(/*did_modify=*/true, {});
}
// Returns true if |public_key| is relevant as a "platform key" that should be
// visible to chrome extensions / subsystems.
bool ShouldIncludePublicKey(SECKEYPublicKey* public_key) {
crypto::ScopedSECItem cka_id(SECITEM_AllocItem(/*arena=*/nullptr,
/*item=*/nullptr,
/*len=*/0));
if (PK11_ReadRawAttribute(
/*objType=*/PK11_TypePubKey, public_key, CKA_ID, cka_id.get()) !=
SECSuccess) {
return false;
}
std::string_view cka_id_str(reinterpret_cast<char*>(cka_id->data),
cka_id->len);
// Only keys generated/stored by extensions/Chrome should be visible to
// extensions. Oemcrypto stores its key in the TPM, but that should not
// be exposed. Look at exposing additional attributes or changing the slot
// that Oemcrypto stores keys, so that it can be done based on properties
// of the key. See https://crbug/com/1136396
if (cka_id_str == "arc-oemcrypto") {
VLOG(0) << "Filtered out arc-oemcrypto public key.";
return false;
}
return true;
}
void ListKeysOnWorkerThread(Token token,
crypto::ScopedPK11Slot slot,
KcerToken::TokenListKeysCallback callback) {
std::vector<PublicKey> result;
crypto::ScopedSECKEYPublicKeyList public_keys(
PK11_ListPublicKeysInSlot(slot.get(), /*nickname=*/nullptr));
if (!public_keys) {
return std::move(callback).Run(std::move(result));
}
for (SECKEYPublicKeyListNode* node = PUBKEY_LIST_HEAD(public_keys);
!PUBKEY_LIST_END(node, public_keys); node = PUBKEY_LIST_NEXT(node)) {
if (!ShouldIncludePublicKey(node->key)) {
continue;
}
Pkcs11Id pkcs11_id(
SECItemToBytes(crypto::MakeNssIdFromPublicKey(node->key)));
base::expected<crypto::ScopedSECKEYPrivateKey, Error> private_key =
GetSECKEYPrivateKeyByPkcs11Id(slot, pkcs11_id.value());
if (!private_key.has_value()) {
// Do not list key pairs without private keys, they might be left behind
// when NSS fails to delete a public key.
continue;
}
crypto::ScopedSECItem public_key_der(
SECKEY_EncodeDERSubjectPublicKeyInfo(node->key));
if (!public_key_der || (public_key_der->len == 0)) {
LOG(WARNING) << "Could not encode subject public key info.";
continue;
}
result.emplace_back(
token, std::move(pkcs11_id),
PublicKeySpki(SECItemToBytes(std::move(public_key_der))));
}
std::move(callback).Run(std::move(result));
}
void ListCertsOnWorkerThread(
crypto::ScopedPK11Slot slot,
base::OnceCallback<void(net::ScopedCERTCertificateList)> callback) {
crypto::ScopedCERTCertList cert_list(PK11_ListCertsInSlot(slot.get()));
net::ScopedCERTCertificateList result;
if (cert_list) {
for (CERTCertListNode* node = CERT_LIST_HEAD(cert_list);
!CERT_LIST_END(node, cert_list); node = CERT_LIST_NEXT(node)) {
if (!CERT_IsUserCert(node->cert)) {
continue;
}
result.push_back(net::x509_util::DupCERTCertificate(node->cert));
}
}
std::move(callback).Run(std::move(result));
}
void RemoveKeyAndCertsOnWorkerThread(
crypto::ScopedPK11Slot slot,
PrivateKeyHandle key,
base::OnceCallback<void(bool /*did_modify*/,
base::expected<void, Error> /*result*/)> callback) {
base::expected<crypto::ScopedSECKEYPrivateKey, Error> private_key =
GetSECKEYPrivateKey(slot, key);
if (!private_key.has_value()) {
return std::move(callback).Run(/*did_modify=*/false,
base::unexpected(private_key.error()));
}
crypto::ScopedSECKEYPrivateKey& sec_private_key = private_key.value();
// There can be multiple certs that need to be deleted. The loop continues
// until no more certs are found or too many errors received.
int errors_allowed = 5;
int certs_removed = 0;
while (errors_allowed > 0) {
net::ScopedCERTCertificate cert(
PK11_GetCertFromPrivateKey(sec_private_key.get()));
if (!cert) {
break;
}
if (SEC_DeletePermCertificate(cert.get()) != SECSuccess) {
--errors_allowed;
continue;
}
++certs_removed;
}
if (errors_allowed == 0) {
return std::move(callback).Run(
/*did_modify=*/(certs_removed > 0),
base::unexpected(Error::kFailedToRemoveCertificate));
}
crypto::ScopedSECKEYPublicKey public_key(
SECKEY_ConvertToPublicKey(sec_private_key.get()));
if (PK11_DeleteTokenPrivateKey(/*privKey=*/sec_private_key.release(),
/*force=*/false) != SECSuccess) {
return std::move(callback).Run(
/*did_modify=*/(certs_removed > 0),
base::unexpected(Error::kFailedToRemovePrivateKey));
}
// TODO(crbug.com/40700534): NSS tends to fail the deletion of the public key,
// ignore the result for now and make sure it works properly in the non-NSS
// version of Kcer.
PK11_DeleteTokenPublicKey(/*privKey=*/public_key.release());
// Success.
return std::move(callback).Run(
/*did_modify=*/(certs_removed > 0), {});
}
void RemoveCertOnWorkerThread(
crypto::ScopedPK11Slot slot,
scoped_refptr<const Cert> cert,
base::OnceCallback<void(bool /*did_modify*/,
base::expected<void, Error> /*result*/)> callback) {
net::ScopedCERTCertificate nss_cert =
net::x509_util::CreateCERTCertificateFromX509Certificate(
cert->GetX509Cert().get());
if (!nss_cert) {
return std::move(callback).Run(
/*did_modify=*/false, base::unexpected(Error::kInvalidCertificate));
}
if (SEC_DeletePermCertificate(nss_cert.get()) != SECSuccess) {
return std::move(callback).Run(
/*did_modify=*/false,
base::unexpected(Error::kFailedToRemoveCertificate));
}
// TODO(miersh): Currently RemoveCertOnWorkerThread() returns "success" even
// when SEC_DeletePermCertificate doesn't find the certificate. This is
// acceptable for a "remove" method, but it might be useful to change it
// after NSS is not used for Kcer.
std::move(callback).Run(/*did_modify=*/true, {});
}
void DoesPrivateKeyExistOnWorkerThread(crypto::ScopedPK11Slot slot,
PrivateKeyHandle key,
Kcer::DoesKeyExistCallback callback) {
base::expected<crypto::ScopedSECKEYPrivateKey, Error> private_key =
GetSECKEYPrivateKey(slot, key);
if (private_key.has_value()) {
return std::move(callback).Run(true);
}
if ((private_key.error() == Error::kKeyNotFound) ||
(private_key.error() == Error::kTokenIsNotAvailable)) {
return std::move(callback).Run(false);
}
return std::move(callback).Run(base::unexpected(private_key.error()));
}
bool DoesKeySupportSigningScheme(
SigningScheme kcer_signing_scheme,
const crypto::ScopedSECKEYPrivateKey& sec_private_key) {
switch (kcer_signing_scheme) {
case SigningScheme::kRsaPkcs1Sha1:
case SigningScheme::kRsaPkcs1Sha256:
case SigningScheme::kRsaPkcs1Sha384:
case SigningScheme::kRsaPkcs1Sha512:
case SigningScheme::kRsaPssRsaeSha256:
case SigningScheme::kRsaPssRsaeSha384:
case SigningScheme::kRsaPssRsaeSha512:
return (sec_private_key->keyType == ::KeyType::rsaKey);
case SigningScheme::kEcdsaSecp256r1Sha256:
case SigningScheme::kEcdsaSecp384r1Sha384:
case SigningScheme::kEcdsaSecp521r1Sha512:
return (sec_private_key->keyType == ::KeyType::ecKey);
}
}
// TODO(b/244408117): This method is mostly a copy of Sign() from
// net/ssl/ssl_platform_key_nss.cc . The original method should be replaced
// during the upcoming refactoring to remove direct usages of NSS.
void SignOnWorkerThread(crypto::ScopedPK11Slot slot,
PrivateKeyHandle key,
SigningScheme kcer_signing_scheme,
DataToSign data_to_sign,
Kcer::SignCallback callback) {
base::expected<crypto::ScopedSECKEYPrivateKey, Error> private_key =
GetSECKEYPrivateKey(slot, key);
if (!private_key.has_value()) {
return std::move(callback).Run(base::unexpected(private_key.error()));
}
const crypto::ScopedSECKEYPrivateKey& sec_private_key = private_key.value();
if (!DoesKeySupportSigningScheme(kcer_signing_scheme, sec_private_key)) {
return std::move(callback).Run(
base::unexpected(Error::kKeyDoesNotSupportSigningScheme));
}
uint16_t ssl_algorithm = static_cast<uint16_t>(kcer_signing_scheme);
const EVP_MD* digest_method =
SSL_get_signature_algorithm_digest(ssl_algorithm);
uint8_t digest[EVP_MAX_MD_SIZE];
unsigned digest_len;
if (!digest_method ||
!EVP_Digest(data_to_sign->data(), data_to_sign->size(), digest,
&digest_len, digest_method, nullptr)) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToSignFailedToDigest));
}
SECItem digest_item;
digest_item.data = digest;
digest_item.len = digest_len;
CK_MECHANISM_TYPE mechanism = PK11_MapSignKeyType(sec_private_key->keyType);
SECItem param = {siBuffer, nullptr, 0};
CK_RSA_PKCS_PSS_PARAMS pss_params;
bssl::UniquePtr<uint8_t> free_digest_info;
if (SSL_is_signature_algorithm_rsa_pss(ssl_algorithm)) {
switch (EVP_MD_type(digest_method)) {
case NID_sha256:
pss_params.hashAlg = CKM_SHA256;
pss_params.mgf = CKG_MGF1_SHA256;
break;
case NID_sha384:
pss_params.hashAlg = CKM_SHA384;
pss_params.mgf = CKG_MGF1_SHA384;
break;
case NID_sha512:
pss_params.hashAlg = CKM_SHA512;
pss_params.mgf = CKG_MGF1_SHA512;
break;
default:
return std::move(callback).Run(
base::unexpected(Error::kUnexpectedSigningScheme));
}
// Use the hash length for the salt length.
pss_params.sLen = EVP_MD_size(digest_method);
mechanism = CKM_RSA_PKCS_PSS;
param.data = reinterpret_cast<unsigned char*>(&pss_params);
param.len = sizeof(pss_params);
} else if (SSL_get_signature_algorithm_key_type(ssl_algorithm) ==
EVP_PKEY_RSA) {
// PK11_SignWithMechanism expects the caller to prepend the DigestInfo for
// PKCS #1.
int hash_nid =
EVP_MD_type(SSL_get_signature_algorithm_digest(ssl_algorithm));
int is_alloced;
size_t prefix_len;
if (!RSA_add_pkcs1_prefix(&digest_item.data, &prefix_len, &is_alloced,
hash_nid, digest_item.data, digest_item.len)) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToSignFailedToAddPrefix));
}
digest_item.len = prefix_len;
if (is_alloced) {
free_digest_info.reset(digest_item.data);
}
}
std::vector<uint8_t> signature;
{
const int len = PK11_SignatureLen(sec_private_key.get());
if (len <= 0) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToSignFailedToGetSignatureLength));
}
signature.resize(len);
SECItem signature_item;
signature_item.data = signature.data();
signature_item.len = signature.size();
SECStatus rv =
PK11_SignWithMechanism(sec_private_key.get(), mechanism, ¶m,
&signature_item, &digest_item);
if (rv != SECSuccess) {
return std::move(callback).Run(base::unexpected(Error::kFailedToSign));
}
signature.resize(signature_item.len);
}
// NSS emits raw ECDSA signatures, but BoringSSL expects a DER-encoded
// ECDSA-Sig-Value.
if (SSL_get_signature_algorithm_key_type(ssl_algorithm) == EVP_PKEY_EC) {
if (signature.size() % 2 != 0) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToSignBadSignatureLength));
}
size_t order_len = signature.size() / 2;
// Convert the RAW ECDSA signature to a DER-encoded ECDSA-Sig-Value.
bssl::UniquePtr<ECDSA_SIG> sig(ECDSA_SIG_new());
if (!sig || !BN_bin2bn(signature.data(), order_len, sig->r) ||
!BN_bin2bn(signature.data() + order_len, order_len, sig->s)) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToDerEncode));
}
{
const int len = i2d_ECDSA_SIG(sig.get(), nullptr);
if (len <= 0) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToSignBadSignatureLength));
}
signature.resize(len);
}
{
uint8_t* ptr = signature.data();
const int len = i2d_ECDSA_SIG(sig.get(), &ptr);
if (len <= 0) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToDerEncode));
}
signature.resize(len);
}
}
return std::move(callback).Run(Signature(std::move(signature)));
}
// Checks whether |input_length| is lower or equal to the maximum input length
// for a RSA PKCS#1 v1.5 signature generated using |private_key| with PK11_Sign.
// Returns false if |input_length| is too large.
// If the maximum input length can not be determined (which is possible because
// it queries the PKCS#11 module), returns true and logs a warning.
bool CheckRsaPkcs1SignDigestInputLength(SECKEYPrivateKey* private_key,
size_t input_length) {
// For RSA keys, PK11_Sign will perform PKCS#1 v1.5 padding, which needs at
// least 11 bytes. RSA Sign can process an input of max. modulus length.
// Thus the maximum input length for the sign operation is
// |modulus_length - 11|.
int modulus_length_bytes = PK11_GetPrivateModulusLen(private_key);
if (modulus_length_bytes <= 0) {
LOG(WARNING) << "Could not determine modulus length";
return true;
}
size_t max_input_length_after_padding =
static_cast<size_t>(modulus_length_bytes);
// PKCS#1 v1.5 Padding needs at least this many bytes.
size_t kMinPaddingLength = 11u;
return input_length + kMinPaddingLength <= max_input_length_after_padding;
}
// Performs "raw" PKCS1 v1.5 padding + signing by calling PK11_Sign on |key|.
// TODO(b/244408117): This method is mostly a copy of
// `PlatformKeysService::SignRSAPKCS1Raw` from platform_keys_service_nss.cc .
// The original method should be replaced during the upcoming refactoring to
// remove direct usages of NSS.
void SignRsaPkcs1RawOnWorkerThread(crypto::ScopedPK11Slot slot,
PrivateKeyHandle key,
DigestWithPrefix digest_with_prefix,
Kcer::SignCallback callback) {
base::expected<crypto::ScopedSECKEYPrivateKey, Error> private_key =
GetSECKEYPrivateKey(slot, key);
if (!private_key.has_value()) {
return std::move(callback).Run(base::unexpected(private_key.error()));
}
const crypto::ScopedSECKEYPrivateKey& sec_private_key = private_key.value();
if (sec_private_key->keyType != ::KeyType::rsaKey) {
return std::move(callback).Run(
base::unexpected(Error::kKeyDoesNotSupportSigningScheme));
}
static_assert(
sizeof(*digest_with_prefix->data()) == sizeof(char),
"Can't reinterpret data if its characters are not 8 bit large.");
SECItem input = {siBuffer,
const_cast<unsigned char*>(digest_with_prefix->data()),
static_cast<unsigned int>(digest_with_prefix->size())};
// Compute signature of hash.
int signature_len = PK11_SignatureLen(sec_private_key.get());
if (signature_len <= 0) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToSignBadSignatureLength));
}
std::vector<unsigned char> signature(signature_len);
SECItem signature_output = {siBuffer, signature.data(),
static_cast<unsigned int>(signature.size())};
if (PK11_Sign(sec_private_key.get(), &signature_output, &input) !=
SECSuccess) {
// Input size is checked after a failure - obtaining max input size
// involves extracting key modulus length which is not a free operation, so
// don't bother if signing succeeded.
// Note: It would be better if this could be determined from some library
// return code (e.g. PORT_GetError), but this was not possible with
// NSS+chaps at this point.
if (!CheckRsaPkcs1SignDigestInputLength(sec_private_key.get(),
digest_with_prefix->size())) {
return std::move(callback).Run(base::unexpected(Error::kInputTooLong));
}
return std::move(callback).Run(base::unexpected(Error::kFailedToSign));
}
return std::move(callback).Run(Signature(std::move(signature)));
}
void GetKeyPermissionsOnWorkerThread(
KeyPermissionsAttributeId key_permissions_attribute_id,
crypto::ScopedPK11Slot slot,
PrivateKeyHandle key,
Kcer::GetKeyPermissionsCallback callback) {
base::expected<crypto::ScopedSECKEYPrivateKey, Error> private_key =
GetSECKEYPrivateKey(slot, key);
if (!private_key.has_value()) {
return std::move(callback).Run(base::unexpected(private_key.error()));
}
const crypto::ScopedSECKEYPrivateKey& sec_private_key = private_key.value();
crypto::ScopedSECItem key_permissions_attribute(
SECITEM_AllocItem(/*arena=*/nullptr,
/*item=*/nullptr,
/*len=*/0));
SECStatus status = PK11_ReadRawAttribute(
/*objType=*/PK11_TypePrivKey, sec_private_key.get(),
key_permissions_attribute_id.value(), key_permissions_attribute.get());
if (status != SECSuccess) {
// CKR_ATTRIBUTE_TYPE_INVALID is a cryptoki function return value which is
// returned by Chaps if the attribute was not set before for the key. NSS
// maps this error to SEC_ERROR_BAD_DATA. This error is captured here so
// as not to return an |error| in cases of retrieving unset key attributes
// and to return nullopt |attribute_value| instead.
int error = PORT_GetError();
if (error == SEC_ERROR_BAD_DATA) {
return std::move(callback).Run(std::nullopt);
} else {
return std::move(callback).Run(
base::unexpected(Error::kFailedToReadAttribute));
}
}
chaps::KeyPermissions key_permissions;
if (!key_permissions.ParseFromArray(key_permissions_attribute->data,
key_permissions_attribute->len)) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToParseKeyPermissions));
}
return std::move(callback).Run(key_permissions);
}
void GetCertProvisioningIdOnWorkerThread(
CertProvisioningIdAttributeId cert_prov_attribute_id,
crypto::ScopedPK11Slot slot,
PrivateKeyHandle key,
Kcer::GetCertProvisioningProfileIdCallback callback) {
base::expected<crypto::ScopedSECKEYPrivateKey, Error> private_key =
GetSECKEYPrivateKey(slot, key);
if (!private_key.has_value()) {
return std::move(callback).Run(base::unexpected(private_key.error()));
}
const crypto::ScopedSECKEYPrivateKey& sec_private_key = private_key.value();
crypto::ScopedSECItem cert_prov_attribute(SECITEM_AllocItem(/*arena=*/nullptr,
/*item=*/nullptr,
/*len=*/0));
SECStatus status = PK11_ReadRawAttribute(
/*objType=*/PK11_TypePrivKey, sec_private_key.get(),
cert_prov_attribute_id.value(), cert_prov_attribute.get());
if (status != SECSuccess) {
// CKR_ATTRIBUTE_TYPE_INVALID is a cryptoki function return value which is
// returned by Chaps if the attribute was not set before for the key. NSS
// maps this error to SEC_ERROR_BAD_DATA. This error is captured here so
// as not to return an |error| in cases of retrieving unset key attributes
// and to return nullopt |attribute_value| instead.
int error = PORT_GetError();
if (error == SEC_ERROR_BAD_DATA) {
return std::move(callback).Run(std::nullopt);
} else {
return std::move(callback).Run(
base::unexpected(Error::kFailedToReadAttribute));
}
}
return std::move(callback).Run(
std::string(cert_prov_attribute->data,
cert_prov_attribute->data + cert_prov_attribute->len));
}
void GetTokenInfoOnWorkerThread(crypto::ScopedPK11Slot slot,
Kcer::GetTokenInfoCallback callback) {
TokenInfo token_info;
token_info.pkcs11_id = PK11_GetSlotID(slot.get());
token_info.token_name = PK11_GetSlotName(slot.get());
token_info.module_name = PK11_GetModule(slot.get())->commonName;
return std::move(callback).Run(std::move(token_info));
}
void GetKeyInfoOnWorkerThread(crypto::ScopedPK11Slot slot,
PrivateKeyHandle key,
Kcer::GetKeyInfoCallback callback) {
KeyInfo key_info;
base::expected<crypto::ScopedSECKEYPrivateKey, Error> private_key =
GetSECKEYPrivateKey(slot, key);
if (!private_key.has_value()) {
return std::move(callback).Run(base::unexpected(private_key.error()));
}
const crypto::ScopedSECKEYPrivateKey& sec_private_key = private_key.value();
constexpr CK_ATTRIBUTE_TYPE kKeyInSoftware = CKA_VENDOR_DEFINED + 5;
// All keys in chaps without the attribute are hardware backed.
key_info.is_hardware_backed = !PK11_HasAttributeSet(
slot.get(), sec_private_key->pkcs11ID, kKeyInSoftware,
/*haslock=*/PR_FALSE);
switch (SECKEY_GetPrivateKeyType(sec_private_key.get())) {
case rsaKey:
key_info.key_type = KeyType::kRsa;
break;
case ecKey:
key_info.key_type = KeyType::kEcc;
break;
default:
return std::move(callback).Run(base::unexpected(Error::kUnknownKeyType));
}
key_info.supported_signing_schemes = GetSupportedSigningSchemes(
PK11_DoesMechanism(slot.get(), CKM_RSA_PKCS_PSS), key_info.key_type);
char* nickname = PK11_GetPrivateKeyNickname(sec_private_key.get());
if (nickname) {
key_info.nickname = nickname;
PORT_Free(nickname);
}
return std::move(callback).Run(std::move(key_info));
}
void SetKeyNicknameOnWorkerThread(crypto::ScopedPK11Slot slot,
PrivateKeyHandle key,
std::string nickname,
Kcer::StatusCallback callback) {
base::expected<crypto::ScopedSECKEYPrivateKey, Error> private_key =
GetSECKEYPrivateKey(slot, key);
if (!private_key.has_value()) {
return std::move(callback).Run(base::unexpected(private_key.error()));
}
if (PK11_SetPrivateKeyNickname(private_key.value().get(), nickname.c_str()) !=
SECSuccess) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToWriteAttribute));
}
return std::move(callback).Run({});
}
void SetKeyPermissionsOnWorkerThread(KeyPermissionsAttributeId attribute_id,
crypto::ScopedPK11Slot slot,
PrivateKeyHandle key,
chaps::KeyPermissions key_permissions,
Kcer::StatusCallback callback) {
base::expected<crypto::ScopedSECKEYPrivateKey, Error> private_key =
GetSECKEYPrivateKey(slot, key);
if (!private_key.has_value()) {
return std::move(callback).Run(base::unexpected(private_key.error()));
}
std::vector<uint8_t> serialized_permissions;
serialized_permissions.resize(key_permissions.ByteSizeLong());
key_permissions.SerializeToArray(serialized_permissions.data(),
serialized_permissions.size());
SECItem attribute_value;
attribute_value.data = serialized_permissions.data();
attribute_value.len = serialized_permissions.size();
if (SECStatus res = PK11_WriteRawAttribute(
/*objType=*/PK11_TypePrivKey, private_key.value().get(),
attribute_id.value(), &attribute_value);
res != SECSuccess) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToWriteAttribute));
}
return std::move(callback).Run({});
}
void SetCertProvisioningProfileIdOnWorkerThread(
CertProvisioningIdAttributeId attribute_id,
crypto::ScopedPK11Slot slot,
PrivateKeyHandle key,
std::string cert_prov_id,
Kcer::StatusCallback callback) {
base::expected<crypto::ScopedSECKEYPrivateKey, Error> private_key =
GetSECKEYPrivateKey(slot, key);
if (!private_key.has_value()) {
return std::move(callback).Run(base::unexpected(private_key.error()));
}
SECItem attribute_value;
attribute_value.data = reinterpret_cast<uint8_t*>(cert_prov_id.data());
attribute_value.len = cert_prov_id.size();
if (SECStatus res = PK11_WriteRawAttribute(
/*objType=*/PK11_TypePrivKey, private_key.value().get(),
attribute_id.value(), &attribute_value);
res != SECSuccess) {
return std::move(callback).Run(
base::unexpected(Error::kFailedToWriteAttribute));
}
return std::move(callback).Run({});
}
scoped_refptr<const Cert> BuildKcerCert(
Token token,
const net::ScopedCERTCertificate& nss_cert) {
Pkcs11Id id_bytes(SECItemToBytes(crypto::MakeNssIdFromSpki(base::make_span(
nss_cert->derPublicKey.data, nss_cert->derPublicKey.len))));
std::string nickname;
if (nss_cert->nickname) {
nickname = nss_cert->nickname;
}
return base::MakeRefCounted<Cert>(
token, std::move(id_bytes), std::move(nickname),
net::x509_util::CreateX509CertificateFromCERTCertificate(nss_cert.get()));
}
} // namespace
KcerTokenImplNss::KcerTokenImplNss(Token token,
HighLevelChapsClient* chaps_client)
: token_(token),
kcer_utils_(std::make_unique<KcerTokenUtils>(token, chaps_client)) {}
KcerTokenImplNss::~KcerTokenImplNss() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
net::CertDatabase::GetInstance()->RemoveObserver(this);
content::GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE,
std::move(kcer_utils_));
}
void KcerTokenImplNss::InitializeForNss(crypto::ScopedPK11Slot nss_slot) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (nss_slot) {
kcer_utils_->Initialize(
SessionChapsClient::SlotId(PK11_GetSlotID(nss_slot.get())));
slot_ = std::move(nss_slot);
net::CertDatabase::GetInstance()->AddObserver(this);
} else {
state_ = State::kInitializationFailed;
}
// This is supposed to be the first time the task queue is unblocked, no
// other tasks should be already running.
UnblockQueueProcessNextTask();
}
base::WeakPtr<KcerToken> KcerTokenImplNss::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void KcerTokenImplNss::OnClientCertStoreChanged() {
state_ = State::kCacheOutdated;
// If task queue is not blocked, trigger cache update immediately.
if (!is_blocked_) {
UnblockQueueProcessNextTask();
}
}
void KcerTokenImplNss::GenerateRsaKey(RsaModulusLength modulus_length_bits,
bool hardware_backed,
Kcer::GenerateKeyCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
}
if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::GenerateRsaKey, weak_factory_.GetWeakPtr(),
modulus_length_bits, hardware_backed, std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&GenerateRsaKeyOnWorkerThread, token_,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
modulus_length_bits, hardware_backed,
std::move(unblocking_callback)));
}
void KcerTokenImplNss::GenerateEcKey(EllipticCurve curve,
bool hardware_backed,
Kcer::GenerateKeyCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
} else if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::GenerateEcKey, weak_factory_.GetWeakPtr(), curve,
hardware_backed, std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&GenerateEcKeyOnWorkerThread, token_,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
curve, hardware_backed, std::move(unblocking_callback)));
}
void KcerTokenImplNss::ImportKey(
Pkcs8PrivateKeyInfoDer pkcs8_private_key_info_der,
Kcer::ImportKeyCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
} else if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::ImportKey, weak_factory_.GetWeakPtr(),
std::move(pkcs8_private_key_info_der), std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&ImportKeyOnWorkerThread, token_,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(pkcs8_private_key_info_der),
std::move(unblocking_callback)));
}
void KcerTokenImplNss::ImportCertFromBytes(CertDer cert_der,
Kcer::StatusCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
}
if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::ImportCertFromBytes, weak_factory_.GetWeakPtr(),
std::move(cert_der), std::move(callback)));
}
// Block task queue, attach queue unblocking and notification sending to the
// callback.
auto wrapped_callback = base::BindPostTask(
content::GetIOThreadTaskRunner({}),
base::BindOnce(&KcerTokenImplNss::OnCertsModified,
weak_factory_.GetWeakPtr(),
std::move(callback).Then(BlockQueueGetUnblocker())));
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&ImportCertOnWorkerThread,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(cert_der), std::move(wrapped_callback)));
}
//==============================================================================
void KcerTokenImplNss::ImportPkcs12Cert(Pkcs12Blob pkcs12_blob,
std::string password,
bool hardware_backed,
bool mark_as_migrated,
Kcer::StatusCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
}
if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::ImportPkcs12Cert, weak_factory_.GetWeakPtr(),
std::move(pkcs12_blob), std::move(password), hardware_backed,
mark_as_migrated, std::move(callback)));
}
RecordKcerPkcs12ImportUmaEvent(
KcerPkcs12ImportEvent::AttemptedPkcs12ChapsImport);
// Block task queue, attach queue unblocking and notification sending to the
// callback.
auto wrapped_callback = base::BindPostTask(
content::GetIOThreadTaskRunner({}),
base::BindOnce(&KcerTokenImplNss::OnCertsModified,
weak_factory_.GetWeakPtr(),
std::move(callback).Then(BlockQueueGetUnblocker())));
Pkcs12Reader pkcs12_reader;
KeyData key_data;
bssl::UniquePtr<STACK_OF(X509)> certs;
Pkcs12ReaderStatusCode get_key_and_cert_status =
pkcs12_reader.GetPkcs12KeyAndCerts(pkcs12_blob.value(), password,
key_data.key, certs);
if (get_key_and_cert_status != Pkcs12ReaderStatusCode::kSuccess) {
return std::move(wrapped_callback)
.Run(
/*did_modify=*/false, base::unexpected(ConvertPkcs12ParsingError(
get_key_and_cert_status)));
}
Pkcs12ReaderStatusCode enrich_key_data_result =
pkcs12_reader.EnrichKeyData(key_data);
if ((enrich_key_data_result != Pkcs12ReaderStatusCode::kSuccess) ||
key_data.cka_id_value.empty()) {
return std::move(wrapped_callback)
.Run(/*did_modify=*/false, base::unexpected(Error::kFailedToGetKeyId));
}
std::vector<CertData> certs_data;
Pkcs12ReaderStatusCode prepare_certs_status = ValidateAndPrepareCertData(
cert_cache_, pkcs12_reader, std::move(certs), key_data, certs_data);
if (prepare_certs_status == Pkcs12ReaderStatusCode::kAlreadyExists) {
return std::move(wrapped_callback)
.Run(/*did_modify=*/false, base::unexpected(Error::kAlreadyExists));
}
if ((prepare_certs_status != Pkcs12ReaderStatusCode::kSuccess) ||
certs_data.empty()) {
return std::move(wrapped_callback)
.Run(/*did_modify=*/false, base::unexpected(Error::kInvalidPkcs12));
}
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&KcerTokenUtils::ImportPkcs12,
base::Unretained(kcer_utils_.get()), std::move(key_data),
std::move(certs_data), hardware_backed, mark_as_migrated,
std::move(wrapped_callback)));
}
//==============================================================================
void KcerTokenImplNss::ExportPkcs12Cert(scoped_refptr<const Cert> cert,
Kcer::ExportPkcs12Callback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
// TODO(244408716): Implement.
}
void KcerTokenImplNss::RemoveKeyAndCerts(PrivateKeyHandle key,
Kcer::StatusCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
}
if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::RemoveKeyAndCerts, weak_factory_.GetWeakPtr(),
std::move(key), std::move(callback)));
}
// Block task queue, attach queue unblocking and notification sending to the
// callback.
auto wrapped_callback = base::BindPostTask(
content::GetIOThreadTaskRunner({}),
base::BindOnce(&KcerTokenImplNss::OnCertsModified,
weak_factory_.GetWeakPtr(),
std::move(callback).Then(BlockQueueGetUnblocker())));
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&RemoveKeyAndCertsOnWorkerThread,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(key), std::move(wrapped_callback)));
}
void KcerTokenImplNss::RemoveCert(scoped_refptr<const Cert> cert,
Kcer::StatusCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
} else if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::RemoveCert, weak_factory_.GetWeakPtr(),
std::move(cert), std::move(callback)));
}
// Block task queue, attach queue unblocking and notification sending to the
// callback.
auto wrapped_callback = base::BindPostTask(
content::GetIOThreadTaskRunner({}),
base::BindOnce(&KcerTokenImplNss::OnCertsModified,
weak_factory_.GetWeakPtr(),
std::move(callback).Then(BlockQueueGetUnblocker())));
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&RemoveCertOnWorkerThread,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(cert), std::move(wrapped_callback)));
}
void KcerTokenImplNss::ListKeys(TokenListKeysCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
} else if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(&KcerTokenImplNss::ListKeys,
weak_factory_.GetWeakPtr(),
std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&ListKeysOnWorkerThread, token_,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(unblocking_callback)));
}
void KcerTokenImplNss::ListCerts(TokenListCertsCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
}
if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(&KcerTokenImplNss::ListCerts,
weak_factory_.GetWeakPtr(),
std::move(callback)));
}
return std::move(callback)
.Then(BlockQueueGetUnblocker())
.Run(cert_cache_.GetAllCerts());
}
void KcerTokenImplNss::DoesPrivateKeyExist(
PrivateKeyHandle key,
Kcer::DoesKeyExistCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
} else if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::DoesPrivateKeyExist, weak_factory_.GetWeakPtr(),
std::move(key), std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&DoesPrivateKeyExistOnWorkerThread,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(key), std::move(unblocking_callback)));
}
void KcerTokenImplNss::Sign(PrivateKeyHandle key,
SigningScheme signing_scheme,
DataToSign data,
Kcer::SignCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
}
if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::Sign, weak_factory_.GetWeakPtr(), std::move(key),
signing_scheme, std::move(data), std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&SignOnWorkerThread,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(key), signing_scheme, std::move(data),
std::move(unblocking_callback)));
}
void KcerTokenImplNss::SignRsaPkcs1Raw(PrivateKeyHandle key,
DigestWithPrefix digest_with_prefix,
Kcer::SignCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
}
if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::SignRsaPkcs1Raw, weak_factory_.GetWeakPtr(),
std::move(key), std::move(digest_with_prefix), std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&SignRsaPkcs1RawOnWorkerThread,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(key), std::move(digest_with_prefix),
std::move(unblocking_callback)));
}
void KcerTokenImplNss::GetTokenInfo(Kcer::GetTokenInfoCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
}
if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(&KcerTokenImplNss::GetTokenInfo,
weak_factory_.GetWeakPtr(),
std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&GetTokenInfoOnWorkerThread,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(unblocking_callback)));
}
void KcerTokenImplNss::GetKeyInfo(PrivateKeyHandle key,
Kcer::GetKeyInfoCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
} else if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::GetKeyInfo, weak_factory_.GetWeakPtr(),
std::move(key), std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&GetKeyInfoOnWorkerThread,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(key), std::move(unblocking_callback)));
}
void KcerTokenImplNss::GetKeyPermissions(
PrivateKeyHandle key,
Kcer::GetKeyPermissionsCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
} else if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::GetKeyPermissions, weak_factory_.GetWeakPtr(),
std::move(key), std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&GetKeyPermissionsOnWorkerThread,
GetKeyPermissionsAttributeId(),
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(key), std::move(unblocking_callback)));
}
void KcerTokenImplNss::GetCertProvisioningProfileId(
PrivateKeyHandle key,
Kcer::GetCertProvisioningProfileIdCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
} else if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::GetCertProvisioningProfileId,
weak_factory_.GetWeakPtr(), std::move(key), std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&GetCertProvisioningIdOnWorkerThread,
GetCertProvisioningIdAttributeId(),
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(key), std::move(unblocking_callback)));
}
void KcerTokenImplNss::SetKeyNickname(PrivateKeyHandle key,
std::string nickname,
Kcer::StatusCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
} else if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::SetKeyNickname, weak_factory_.GetWeakPtr(),
std::move(key), std::move(nickname), std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&SetKeyNicknameOnWorkerThread,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(key), std::move(nickname),
std::move(unblocking_callback)));
}
void KcerTokenImplNss::SetKeyPermissions(PrivateKeyHandle key,
chaps::KeyPermissions key_permissions,
Kcer::StatusCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
} else if (is_blocked_) {
return task_queue_.push_back(base::BindOnce(
&KcerTokenImplNss::SetKeyPermissions, weak_factory_.GetWeakPtr(),
std::move(key), std::move(key_permissions), std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&SetKeyPermissionsOnWorkerThread,
GetKeyPermissionsAttributeId(),
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(key), std::move(key_permissions),
std::move(unblocking_callback)));
}
void KcerTokenImplNss::SetCertProvisioningProfileId(
PrivateKeyHandle key,
std::string profile_id,
Kcer::StatusCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kInitializationFailed) [[unlikely]] {
return HandleInitializationFailed(std::move(callback));
} else if (is_blocked_) {
return task_queue_.push_back(
base::BindOnce(&KcerTokenImplNss::SetCertProvisioningProfileId,
weak_factory_.GetWeakPtr(), std::move(key),
std::move(profile_id), std::move(callback)));
}
// Block task queue, attach unblocking task to the callback.
auto unblocking_callback = std::move(callback).Then(BlockQueueGetUnblocker());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&SetCertProvisioningProfileIdOnWorkerThread,
GetCertProvisioningIdAttributeId(),
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(key), std::move(profile_id),
std::move(unblocking_callback)));
}
// `did_modify` indicates whether a modification was actually made (which should
// trigger a notification).
void KcerTokenImplNss::OnCertsModified(Kcer::StatusCallback callback,
bool did_modify,
base::expected<void, Error> result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (did_modify) {
net::CertDatabase::GetInstance()->NotifyObserversClientCertStoreChanged();
}
// The Notify... above will post a task to invalidate the cache. Calling the
// original callback for a request will automatically trigger updating cache
// and executing the next request. Post a task with the original callback
// (instead of calling it synchronously), so the cache update and the next
// request happen after the notification.
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), std::move(result)));
}
base::OnceClosure KcerTokenImplNss::BlockQueueGetUnblocker() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
CHECK(!is_blocked_);
is_blocked_ = true;
auto unblock_on_io = base::BindPostTask(
content::GetIOThreadTaskRunner({}),
base::BindOnce(&KcerTokenImplNss::UnblockQueueProcessNextTask,
weak_factory_.GetWeakPtr()));
// `unblocker` is executed either manually or on destruction.
base::ScopedClosureRunner unblocker(std::move(unblock_on_io));
// Pack `unblocker` into a closure, so it can be attached to a callback.
return base::BindOnce(&RunUnblocker, std::move(unblocker));
}
void KcerTokenImplNss::UnblockQueueProcessNextTask() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
is_blocked_ = false;
if (state_ == State::kCacheOutdated) {
return UpdateCache();
}
if (task_queue_.empty()) {
return;
}
base::OnceClosure next_task = std::move(task_queue_.front());
task_queue_.pop_front();
std::move(next_task).Run();
}
void KcerTokenImplNss::UpdateCache() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
// Block task queue, create callback for the current sequence.
auto callback =
base::BindPostTask(content::GetIOThreadTaskRunner({}),
base::BindOnce(&KcerTokenImplNss::UpdateCacheWithCerts,
weak_factory_.GetWeakPtr())
.Then(BlockQueueGetUnblocker()));
state_ = State::kCacheUpdating;
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&ListCertsOnWorkerThread,
crypto::ScopedPK11Slot(PK11_ReferenceSlot(slot_.get())),
std::move(callback)));
}
void KcerTokenImplNss::UpdateCacheWithCerts(
net::ScopedCERTCertificateList new_certs) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == State::kCacheOutdated) {
// If the status switched from kUpdating, then new update happened since
// the cache started to update, `new_certs` might already be outdated.
// Skip re-building the cache and try again.
return;
}
std::vector<scoped_refptr<const Cert>> new_cache;
// For every cert that was found, either take it from the previous cache or
// create a kcer::Cert object for it.
for (const net::ScopedCERTCertificate& new_cert : new_certs) {
const base::span<const uint8_t> cert_span(
net::x509_util::CERTCertificateAsSpan(new_cert.get()));
scoped_refptr<const Cert> cert = cert_cache_.FindCert(cert_span);
if (!cert) {
cert = BuildKcerCert(token_, new_cert);
}
new_cache.push_back(std::move(cert));
}
// Rebuilding the cache implicitly removes all the certs that are not in the
// permanent storage anymore. The certs themself will be fully destroyed
// when the last ref-counting reference to them is destroyed.
cert_cache_ = CertCache(new_cache);
state_ = State::kCacheUpToDate;
}
template <typename T>
void KcerTokenImplNss::HandleInitializationFailed(
base::OnceCallback<void(base::expected<T, Error>)> callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
std::move(callback).Run(base::unexpected(Error::kTokenInitializationFailed));
// Multiple tasks might be handled in a row, schedule the next task
// asynchronously to not overload the stack and not occupy the thread for
// too long.
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&KcerTokenImplNss::UnblockQueueProcessNextTask,
weak_factory_.GetWeakPtr()));
}
void KcerTokenImplNss::SetAttributeTranslationForTesting(bool is_enabled) {
CHECK_IS_TEST();
translate_attributes_for_testing_ = is_enabled;
}
KeyPermissionsAttributeId KcerTokenImplNss::GetKeyPermissionsAttributeId()
const {
if (translate_attributes_for_testing_) [[unlikely]] {
CHECK_IS_TEST();
return KeyPermissionsAttributeId(CKA_END_DATE);
}
return KeyPermissionsAttributeId(
pkcs11_custom_attributes::kCkaChromeOsKeyPermissions);
}
CertProvisioningIdAttributeId
KcerTokenImplNss::GetCertProvisioningIdAttributeId() const {
if (translate_attributes_for_testing_) [[unlikely]] {
CHECK_IS_TEST();
return CertProvisioningIdAttributeId(CKA_START_DATE);
}
return CertProvisioningIdAttributeId(
pkcs11_custom_attributes::kCkaChromeOsBuiltinProvisioningProfileId);
}
} // namespace kcer::internal