chromium/chrome/browser/extensions/api/enterprise_platform_keys/enterprise_platform_keys_api.cc

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

#include "chrome/browser/extensions/api/enterprise_platform_keys/enterprise_platform_keys_api.h"

#include <stdint.h>

#include <string>
#include <utility>
#include <vector>

#include "base/values.h"
#include "chrome/browser/chromeos/platform_keys/extension_platform_keys_service.h"
#include "chrome/browser/chromeos/platform_keys/extension_platform_keys_service_factory.h"
#include "chrome/browser/chromeos/platform_keys/platform_keys.h"
#include "chrome/browser/extensions/api/platform_keys/platform_keys_api.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/enterprise_platform_keys.h"
#include "chrome/common/extensions/api/enterprise_platform_keys_internal.h"
#include "chrome/common/pref_names.h"
#include "chromeos/crosapi/mojom/keystore_service.mojom.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "extensions/browser/extension_function.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest.h"

#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/lacros/lacros_service.h"
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/crosapi/keystore_service_ash.h"
#include "chrome/browser/ash/crosapi/keystore_service_factory_ash.h"
#endif  // #if BUILDFLAG(IS_CHROMEOS_ASH)

namespace extensions {

namespace {

namespace api_epk = api::enterprise_platform_keys;
namespace api_epki = api::enterprise_platform_keys_internal;
using crosapi::mojom::KeystoreService;

#if BUILDFLAG(IS_CHROMEOS_LACROS)
const char kUnsupportedByAsh[] = "Not implemented.";
const char kUnsupportedProfile[] = "Not available.";
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)
const char kExtensionDoesNotHavePermission[] =
    "The extension does not have permission to call this function.";
const char kChromeOsEcdsaUnsupported[] =
    "Installed ChromeOS version does not support ECDSA.";

crosapi::mojom::KeystoreService* GetKeystoreService(
    content::BrowserContext* browser_context) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  // TODO(b/191958380): Lift the restriction when *.platformKeys.* APIs are
  // implemented for secondary profiles in Lacros.
  CHECK(Profile::FromBrowserContext(browser_context)->IsMainProfile())
      << "Attempted to use an incorrect profile. Please file a bug at "
         "https://bugs.chromium.org/ if this happens.";
  return chromeos::LacrosService::Get()->GetRemote<KeystoreService>().get();
#endif  // #if BUILDFLAG(IS_CHROMEOS_LACROS)

#if BUILDFLAG(IS_CHROMEOS_ASH)
  return crosapi::KeystoreServiceFactoryAsh::GetForBrowserContext(
      browser_context);
#endif  // #if BUILDFLAG(IS_CHROMEOS_ASH)
}

// Performs common crosapi validation. These errors are not caused by the
// extension so they are considered recoverable. Returns an error message on
// error, or empty string on success. |min_version| is the minimum version of
// the ash implementation of KeystoreService necessary to support this
// extension. |context| is the browser context in which the extension is hosted.
std::string ValidateCrosapi(int min_version, content::BrowserContext* context) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  chromeos::LacrosService* service = chromeos::LacrosService::Get();
  if (!service || !service->IsAvailable<crosapi::mojom::KeystoreService>())
    return kUnsupportedByAsh;

  int version = service->GetInterfaceVersion<KeystoreService>();
  if (version < min_version)
    return kUnsupportedByAsh;

  // These APIs are used in security-sensitive contexts. We need to ensure that
  // the user for ash is the same as the user for lacros. We do this by
  // restricting the API to the default profile, which is guaranteed to be the
  // same user.
  if (!Profile::FromBrowserContext(context)->IsMainProfile())
    return kUnsupportedProfile;
#endif  // #if BUILDFLAG(IS_CHROMEOS_LACROS)

  return "";
}

std::optional<crosapi::mojom::KeystoreType> KeystoreTypeFromString(
    const std::string& input) {
  if (input == "user")
    return crosapi::mojom::KeystoreType::kUser;
  if (input == "system")
    return crosapi::mojom::KeystoreType::kDevice;
  return std::nullopt;
}

// Validates that |token_id| is well-formed. Converts |token_id| into the output
// parameter |keystore|. Only populated on success. Returns an empty string on
// success and an error message on error. A validation error should result in
// extension termination.
std::string ValidateInput(const std::string& token_id,
                          crosapi::mojom::KeystoreType* keystore) {
  std::optional<crosapi::mojom::KeystoreType> keystore_type =
      KeystoreTypeFromString(token_id);
  if (!keystore_type)
    return platform_keys::kErrorInvalidToken;

  *keystore = keystore_type.value();
  return "";
}
}  // namespace

namespace platform_keys {

void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterListPref(prefs::kAttestationExtensionAllowlist);
}

bool IsExtensionAllowed(Profile* profile, const Extension* extension) {
  if (Manifest::IsComponentLocation(extension->location())) {
    // Note: For this to even be called, the component extension must also be
    // allowed in chrome/common/extensions/api/_permission_features.json
    return true;
  }
  const base::Value::List& list =
      profile->GetPrefs()->GetList(prefs::kAttestationExtensionAllowlist);
  base::Value value(extension->id());
  return base::Contains(list, value);
}

}  // namespace platform_keys

//------------------------------------------------------------------------------

EnterprisePlatformKeysInternalGenerateKeyFunction::
    ~EnterprisePlatformKeysInternalGenerateKeyFunction() = default;

ExtensionFunction::ResponseAction
EnterprisePlatformKeysInternalGenerateKeyFunction::Run() {
  std::optional<api_epki::GenerateKey::Params> params =
      api_epki::GenerateKey::Params::Create(args());

#if BUILDFLAG(IS_CHROMEOS_LACROS)
  // TODO(b/191958380): Lift the restriction when *.platformKeys.* APIs are
  // implemented for secondary profiles in Lacros.
  if (!Profile::FromBrowserContext(browser_context())->IsMainProfile())
    return RespondNow(Error(kUnsupportedProfile));
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

  EXTENSION_FUNCTION_VALIDATE(params);
  std::optional<chromeos::platform_keys::TokenId> platform_keys_token_id =
      platform_keys::ApiIdToPlatformKeysTokenId(params->token_id);
  if (!platform_keys_token_id)
    return RespondNow(Error(platform_keys::kErrorInvalidToken));

  chromeos::ExtensionPlatformKeysService* service =
      chromeos::ExtensionPlatformKeysServiceFactory::GetForBrowserContext(
          browser_context());
  DCHECK(service);

  if (params->algorithm.name == "RSASSA-PKCS1-v1_5") {
    // TODO(pneubeck): Add support for unsigned integers to IDL.
    EXTENSION_FUNCTION_VALIDATE(params->algorithm.modulus_length &&
                                *(params->algorithm.modulus_length) >= 0);
    service->GenerateRSAKey(
        platform_keys_token_id.value(), *(params->algorithm.modulus_length),
        params->software_backed, extension_id(),
        base::BindOnce(
            &EnterprisePlatformKeysInternalGenerateKeyFunction::OnGeneratedKey,
            this));
  } else if (params->algorithm.name == "ECDSA") {
    EXTENSION_FUNCTION_VALIDATE(params->algorithm.named_curve);
    service->GenerateECKey(
        platform_keys_token_id.value(), *(params->algorithm.named_curve),
        extension_id(),
        base::BindOnce(
            &EnterprisePlatformKeysInternalGenerateKeyFunction::OnGeneratedKey,
            this));
  } else {
    NOTREACHED_IN_MIGRATION();
    EXTENSION_FUNCTION_VALIDATE(false);
  }
  return RespondLater();
}

void EnterprisePlatformKeysInternalGenerateKeyFunction::OnGeneratedKey(
    std::vector<uint8_t> public_key_der,
    std::optional<crosapi::mojom::KeystoreError> error) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!error) {
    Respond(ArgumentList(
        api_epki::GenerateKey::Results::Create(std::move(public_key_der))));
  } else {
    Respond(
        Error(chromeos::platform_keys::KeystoreErrorToString(error.value())));
  }
}

//------------------------------------------------------------------------------

ExtensionFunction::ResponseAction
EnterprisePlatformKeysGetCertificatesFunction::Run() {
  std::optional<api_epk::GetCertificates::Params> params =
      api_epk::GetCertificates::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  std::string error = ValidateCrosapi(
      KeystoreService::kGetCertificatesMinVersion, browser_context());
  if (!error.empty()) {
    return RespondNow(Error(error));
  }

  crosapi::mojom::KeystoreType keystore;
  error = ValidateInput(params->token_id, &keystore);
  if (!error.empty()) {
    return RespondNow(Error(error));
  }

  auto c = base::BindOnce(
      &EnterprisePlatformKeysGetCertificatesFunction::OnGetCertificates, this);
  GetKeystoreService(browser_context())
      ->GetCertificates(keystore, std::move(c));
  return RespondLater();
}

void EnterprisePlatformKeysGetCertificatesFunction::OnGetCertificates(
    crosapi::mojom::GetCertificatesResultPtr result) {
  if (result->is_error()) {
    Respond(Error(
        chromeos::platform_keys::KeystoreErrorToString(result->get_error())));
    return;
  }
  DCHECK(result->is_certificates());

  base::Value::List client_certs;
  for (std::vector<uint8_t>& cert : result->get_certificates()) {
    client_certs.Append(base::Value(std::move(cert)));
  }

  base::Value::List results;
  results.Append(std::move(client_certs));
  Respond(ArgumentList(std::move(results)));
}

//------------------------------------------------------------------------------

ExtensionFunction::ResponseAction
EnterprisePlatformKeysImportCertificateFunction::Run() {
  std::optional<api_epk::ImportCertificate::Params> params =
      api_epk::ImportCertificate::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  std::string error = ValidateCrosapi(
      KeystoreService::kAddCertificateMinVersion, browser_context());
  if (!error.empty()) {
    return RespondNow(Error(error));
  }

  crosapi::mojom::KeystoreType keystore;
  error = ValidateInput(params->token_id, &keystore);
  EXTENSION_FUNCTION_VALIDATE(error.empty());

  auto c = base::BindOnce(
      &EnterprisePlatformKeysImportCertificateFunction::OnAddCertificate, this);
  GetKeystoreService(browser_context())
      ->AddCertificate(keystore, params->certificate, std::move(c));
  return RespondLater();
}

void EnterprisePlatformKeysImportCertificateFunction::OnAddCertificate(
    bool is_error,
    crosapi::mojom::KeystoreError error_code) {
  if (!is_error) {
    Respond(NoArguments());
  } else {
    Respond(Error(chromeos::platform_keys::KeystoreErrorToString(error_code)));
  }
}

//------------------------------------------------------------------------------

ExtensionFunction::ResponseAction
EnterprisePlatformKeysRemoveCertificateFunction::Run() {
  std::optional<api_epk::RemoveCertificate::Params> params =
      api_epk::RemoveCertificate::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  std::string error = ValidateCrosapi(
      KeystoreService::kRemoveCertificateMinVersion, browser_context());
  if (!error.empty()) {
    return RespondNow(Error(error));
  }

  crosapi::mojom::KeystoreType keystore;
  error = ValidateInput(params->token_id, &keystore);
  EXTENSION_FUNCTION_VALIDATE(error.empty());

  auto c = base::BindOnce(
      &EnterprisePlatformKeysRemoveCertificateFunction::OnRemoveCertificate,
      this);
  GetKeystoreService(browser_context())
      ->RemoveCertificate(keystore, params->certificate, std::move(c));
  return RespondLater();
}

void EnterprisePlatformKeysRemoveCertificateFunction::OnRemoveCertificate(
    bool is_error,
    crosapi::mojom::KeystoreError error) {
  if (!is_error) {
    Respond(NoArguments());
  } else {
    Respond(Error(chromeos::platform_keys::KeystoreErrorToString(error)));
  }
}

//------------------------------------------------------------------------------

ExtensionFunction::ResponseAction
EnterprisePlatformKeysInternalGetTokensFunction::Run() {
  EXTENSION_FUNCTION_VALIDATE(args().empty());

  std::string error = ValidateCrosapi(KeystoreService::kGetKeyStoresMinVersion,
                                      browser_context());
  if (!error.empty()) {
    return RespondNow(Error(error));
  }

  auto c = base::BindOnce(
      &EnterprisePlatformKeysInternalGetTokensFunction::OnGetKeyStores, this);
  GetKeystoreService(browser_context())->GetKeyStores(std::move(c));
  return RespondLater();
}

void EnterprisePlatformKeysInternalGetTokensFunction::OnGetKeyStores(
    crosapi::mojom::GetKeyStoresResultPtr result) {
  if (result->is_error()) {
    Respond(Error(
        chromeos::platform_keys::KeystoreErrorToString(result->get_error())));
    return;
  }
  DCHECK(result->is_key_stores());

  std::vector<std::string> key_stores;
  using KeystoreType = crosapi::mojom::KeystoreType;
  for (KeystoreType keystore_type : result->get_key_stores()) {
    if (!crosapi::mojom::IsKnownEnumValue(keystore_type)) {
      continue;
    }

    switch (keystore_type) {
      case KeystoreType::kUser:
        key_stores.push_back("user");
        break;
      case KeystoreType::kDevice:
        key_stores.push_back("system");
        break;
    }
  }
  Respond(ArgumentList(api_epki::GetTokens::Results::Create(key_stores)));
}

//------------------------------------------------------------------------------

ExtensionFunction::ResponseAction
EnterprisePlatformKeysChallengeMachineKeyFunction::Run() {
  std::optional<api_epk::ChallengeMachineKey::Params> params =
      api_epk::ChallengeMachineKey::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  const std::string error = ValidateCrosapi(
      KeystoreService::kChallengeAttestationOnlyKeystoreMinVersion,
      browser_context());
  if (!error.empty())
    return RespondNow(Error(error));

  if (!platform_keys::IsExtensionAllowed(
          Profile::FromBrowserContext(browser_context()), extension())) {
    return RespondNow(Error(kExtensionDoesNotHavePermission));
  }

  auto c = base::BindOnce(&EnterprisePlatformKeysChallengeMachineKeyFunction::
                              OnChallengeAttestationOnlyKeystore,
                          this);
  GetKeystoreService(browser_context())
      ->ChallengeAttestationOnlyKeystore(
          crosapi::mojom::KeystoreType::kDevice, params->challenge,
          /*migrate=*/params->register_key ? *params->register_key : false,
          crosapi::mojom::KeystoreSigningAlgorithmName::kRsassaPkcs115,
          std::move(c));
  return RespondLater();
}

void EnterprisePlatformKeysChallengeMachineKeyFunction::
    OnChallengeAttestationOnlyKeystore(
        crosapi::mojom::ChallengeAttestationOnlyKeystoreResultPtr result) {
  // It's possible the browser context was destroyed during the async work.
  // If that happens, bail.
  if (!browser_context()) {
    return;
  }
  if (result->is_error_message()) {
    Respond(Error(result->get_error_message()));
    return;
  }
  DCHECK(result->is_challenge_response());

  Respond(ArgumentList(api_epk::ChallengeMachineKey::Results::Create(
      result->get_challenge_response())));
}

//------------------------------------------------------------------------------

ExtensionFunction::ResponseAction
EnterprisePlatformKeysChallengeUserKeyFunction::Run() {
  std::optional<api_epk::ChallengeUserKey::Params> params =
      api_epk::ChallengeUserKey::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  const std::string error = ValidateCrosapi(
      KeystoreService::kChallengeAttestationOnlyKeystoreMinVersion,
      browser_context());
  if (!error.empty())
    return RespondNow(Error(error));

  if (!platform_keys::IsExtensionAllowed(
          Profile::FromBrowserContext(browser_context()), extension())) {
    return RespondNow(Error(kExtensionDoesNotHavePermission));
  }

  auto c = base::BindOnce(&EnterprisePlatformKeysChallengeUserKeyFunction::
                              OnChallengeAttestationOnlyKeystore,
                          this);
  GetKeystoreService(browser_context())
      ->ChallengeAttestationOnlyKeystore(
          crosapi::mojom::KeystoreType::kUser, params->challenge,
          /*migrate=*/params->register_key,
          crosapi::mojom::KeystoreSigningAlgorithmName::kRsassaPkcs115,
          std::move(c));
  return RespondLater();
}

void EnterprisePlatformKeysChallengeUserKeyFunction::
    OnChallengeAttestationOnlyKeystore(
        crosapi::mojom::ChallengeAttestationOnlyKeystoreResultPtr result) {
  // It's possible the browser context was destroyed during the async work.
  // If that happens, bail.
  if (!browser_context()) {
    return;
  }
  if (result->is_error_message()) {
    Respond(Error(result->get_error_message()));
    return;
  }
  DCHECK(result->is_challenge_response());
  Respond(ArgumentList(api_epk::ChallengeUserKey::Results::Create(
      result->get_challenge_response())));
}

//------------------------------------------------------------------------------

const uint64_t kChallengeKeystoreAlgorithmParameterMinVersion = 17;

ExtensionFunction::ResponseAction
EnterprisePlatformKeysChallengeKeyFunction::Run() {
  std::optional<api_epk::ChallengeKey::Params> params =
      api_epk::ChallengeKey::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  std::string error = ValidateCrosapi(
      KeystoreService::kChallengeAttestationOnlyKeystoreMinVersion,
      browser_context());
  if (!error.empty()) {
    return RespondNow(Error(std::move(error)));
  }

  if (!platform_keys::IsExtensionAllowed(
          Profile::FromBrowserContext(browser_context()), extension())) {
    return RespondNow(Error(kExtensionDoesNotHavePermission));
  }

  crosapi::mojom::KeystoreType keystore_type =
      crosapi::mojom::KeystoreType::kDevice;
  EXTENSION_FUNCTION_VALIDATE(params->options.scope !=
                              api::enterprise_platform_keys::Scope::kNone);
  switch (params->options.scope) {
    case api::enterprise_platform_keys::Scope::kUser:
      keystore_type = crosapi::mojom::KeystoreType::kUser;
      break;
    case api::enterprise_platform_keys::Scope::kMachine:
      keystore_type = crosapi::mojom::KeystoreType::kDevice;
      break;
    case api::enterprise_platform_keys::Scope::kNone:
      NOTREACHED_IN_MIGRATION();
  }

  // Default to RSA when not registering a key.
  crosapi::mojom::KeystoreSigningAlgorithmName algorithm =
      crosapi::mojom::KeystoreSigningAlgorithmName::kRsassaPkcs115;
  if (params->options.register_key.has_value()) {
    EXTENSION_FUNCTION_VALIDATE(
        params->options.register_key->algorithm !=
        api::enterprise_platform_keys::Algorithm::kNone);
    switch (params->options.register_key->algorithm) {
      case api::enterprise_platform_keys::Algorithm::kRsa:
        algorithm =
            crosapi::mojom::KeystoreSigningAlgorithmName::kRsassaPkcs115;
        break;
      case api::enterprise_platform_keys::Algorithm::kEcdsa: {
        // Older versions of Ash default to RSA. If ECDSA is specified but the
        // Keystore would use RSA instead, return an error.
        const std::string version_error = ValidateCrosapi(
            kChallengeKeystoreAlgorithmParameterMinVersion, browser_context());
        if (!version_error.empty()) {
          return RespondNow(Error(kChromeOsEcdsaUnsupported));
        }
        algorithm = crosapi::mojom::KeystoreSigningAlgorithmName::kEcdsa;
        break;
      }
      case api::enterprise_platform_keys::Algorithm::kNone:
        NOTREACHED_IN_MIGRATION();
    }
  }

  auto callback = base::BindOnce(&EnterprisePlatformKeysChallengeKeyFunction::
                                     OnChallengeAttestationOnlyKeystore,
                                 this);
  GetKeystoreService(browser_context())
      ->ChallengeAttestationOnlyKeystore(
          keystore_type, params->options.challenge,
          /*migrate=*/params->options.register_key.has_value(), algorithm,
          std::move(callback));

  return RespondLater();
}

void EnterprisePlatformKeysChallengeKeyFunction::
    OnChallengeAttestationOnlyKeystore(
        crosapi::mojom::ChallengeAttestationOnlyKeystoreResultPtr result) {
  // It's possible the browser context was destroyed during the async work.
  // If that happens, bail.
  if (!browser_context()) {
    return;
  }
  if (result->is_error_message()) {
    Respond(Error(result->get_error_message()));
    return;
  }
  DCHECK(result->is_challenge_response());
  Respond(ArgumentList(api_epk::ChallengeKey::Results::Create(
      result->get_challenge_response())));
}

}  // namespace extensions