chromium/chrome/browser/chromeos/platform_keys/extension_key_permissions_service.cc

// Copyright 2020 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/chromeos/platform_keys/extension_key_permissions_service.h"

#include <stdint.h>

#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/base64.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/values.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/ash/platform_keys/key_permissions/key_permissions_service_impl.h"
#include "chrome/browser/chromeos/platform_keys/platform_keys.h"
#include "chromeos/crosapi/mojom/keystore_service.mojom.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/policy_namespace.h"
#include "components/policy/core/common/policy_service.h"
#include "components/policy/policy_constants.h"
#include "extensions/browser/state_store.h"

#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chrome/browser/profiles/profile.h"
#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 chromeos::platform_keys {

namespace {
const char kStateStoreSPKI[] = "SPKI";
const char kStateStoreSignOnce[] = "signOnce";
const char kStateStoreSignUnlimited[] = "signUnlimited";

const char kPolicyAllowCorporateKeyUsage[] = "allowCorporateKeyUsage";

bool ContainsTag(uint64_t tags, crosapi::mojom::KeyTag value) {
  return tags & static_cast<uint64_t>(value);
}

const base::Value::Dict* GetKeyPermissionsMap(
    policy::PolicyService* const profile_policies) {
  if (!profile_policies)
    return nullptr;

  const policy::PolicyMap& policies = profile_policies->GetPolicies(
      policy::PolicyNamespace(policy::POLICY_DOMAIN_CHROME, std::string()));
  const base::Value* policy_value =
      policies.GetValue(policy::key::kKeyPermissions, base::Value::Type::DICT);
  if (!policy_value) {
    DVLOG(1) << "KeyPermissions policy is not set";
    return nullptr;
  }
  return policy_value->GetIfDict();
}

bool GetCorporateKeyUsageFromPref(
    const base::Value::Dict* key_permissions_for_ext) {
  if (!key_permissions_for_ext)
    return false;

  const base::Value* allow_corporate_key_usage =
      key_permissions_for_ext->Find(kPolicyAllowCorporateKeyUsage);
  if (!allow_corporate_key_usage || !allow_corporate_key_usage->is_bool())
    return false;
  return allow_corporate_key_usage->GetBool();
}

// Returns true if the extension with id |extension_id| is allowed to use
// corporate usage keys by policy in |profile_policies|.
bool PolicyAllowsCorporateKeyUsageForExtension(
    const std::string& extension_id,
    policy::PolicyService* const profile_policies) {
  if (!profile_policies)
    return false;

  const base::Value::Dict* key_permissions_map =
      GetKeyPermissionsMap(profile_policies);
  if (!key_permissions_map)
    return false;

  const base::Value::Dict* key_permissions_for_ext =
      key_permissions_map->FindDict(extension_id);
  if (!key_permissions_for_ext)
    return false;

  bool allow_corporate_key_usage =
      GetCorporateKeyUsageFromPref(key_permissions_for_ext);

  VLOG_IF(2, allow_corporate_key_usage)
      << "Policy allows usage of corporate keys by extension " << extension_id;
  return allow_corporate_key_usage;
}

// Returns appropriate KeystoreService for |browser_context|.
//
// KeystoreService is expected to always outlive ExtensionKeyPermissionsService
// because ExtensionKeyPermissionsService instances are owned by
// ExtensionPlatformKeysService and:
//
// For Lacros-Chrome it returns a remote mojo implementation owned by
// LacrosService (that is created before the start of the main loop and should
// outlive ExtensionPlatformKeysService).
//
// For Ash-Chrome the factory can return:
// * an instance owned by CrosapiManager (that is created before profiles and
// should outlive ExtensionPlatformKeysService)
// * or an appropriate keyed service that will always exist
// during ExtensionPlatformKeysService lifetime (because of KeyedService
// dependencies).
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<crosapi::mojom::KeystoreService>()
      .get();
#endif  // #if BUILDFLAG(IS_CHROMEOS_LACROS)

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

}  // namespace

ExtensionKeyPermissionsService::ExtensionKeyPermissionsService(
    const std::string& extension_id,
    extensions::StateStore* extensions_state_store,
    base::Value::List state_store_value,
    policy::PolicyService* profile_policies,
    content::BrowserContext* browser_context)
    : extension_id_(extension_id),
      extensions_state_store_(extensions_state_store),
      profile_policies_(profile_policies),
      keystore_service_(GetKeystoreService(browser_context)) {
  DCHECK(extensions_state_store_);
  DCHECK(profile_policies_);
  DCHECK(keystore_service_);
  KeyEntriesFromState(state_store_value);
}

ExtensionKeyPermissionsService::~ExtensionKeyPermissionsService() = default;

ExtensionKeyPermissionsService::KeyEntry*
ExtensionKeyPermissionsService::GetStateStoreEntry(
    const std::string& public_key_spki_der_b64) {
  for (KeyEntry& entry : state_store_entries_) {
    // For every ASN.1 value there is exactly one DER encoding, so it is fine to
    // compare the DER (or its base64 encoding).
    if (entry.spki_b64 == public_key_spki_der_b64)
      return &entry;
  }

  state_store_entries_.emplace_back(public_key_spki_der_b64);
  return &state_store_entries_.back();
}

void ExtensionKeyPermissionsService::CanUseKeyForSigning(
    const std::vector<uint8_t>& public_key_spki_der,
    CanUseKeyForSigningCallback callback) {
  KeyEntry* matching_entry =
      GetStateStoreEntry(base::Base64Encode(public_key_spki_der));

  // In any case, we allow the generating extension to use the generated key a
  // single time for signing arbitrary data. The reason is, that the extension
  // usually has to sign a certification request containing the public key in
  // order to obtain a certificate for the key.
  // That means, once a certificate authority generated a certificate for the
  // key, the generating extension doesn't have access to the key anymore,
  // except if explicitly permitted by the administrator.
  if (matching_entry->sign_once) {
    std::move(callback).Run(/*allowed=*/true);
    return;
  }

  auto bound_callback = base::BindOnce(
      &ExtensionKeyPermissionsService::CanUseKeyForSigningWithFlags,
      weak_factory_.GetWeakPtr(), std::move(callback),
      matching_entry->sign_unlimited);
  keystore_service_->GetKeyTags(public_key_spki_der, std::move(bound_callback));
}

void ExtensionKeyPermissionsService::CanUseKeyForSigningWithFlags(
    CanUseKeyForSigningCallback callback,
    bool sign_unlimited_allowed,
    crosapi::mojom::GetKeyTagsResultPtr key_tags) {
  if (key_tags->is_error()) {
    LOG(ERROR) << "Failed to check if the key is corporate: "
               << KeystoreErrorToString(key_tags->get_error());
    std::move(callback).Run(/*allowed=*/false);
    return;
  }

  // Usage of corporate keys is solely determined by policy. The user must not
  // circumvent this decision.
  if (ContainsTag(key_tags->get_tags(), crosapi::mojom::KeyTag::kCorporate)) {
    std::move(callback).Run(/*allowed=*/PolicyAllowsCorporateKeyUsage());
    return;
  }

  // Only permissions for keys that are not designated for corporate usage are
  // determined by user decisions.
  std::move(callback).Run(sign_unlimited_allowed);
}

void ExtensionKeyPermissionsService::SetKeyUsedForSigning(
    const std::vector<uint8_t>& public_key_spki_der,
    SetKeyUsedForSigningCallback callback) {
  KeyEntry* matching_entry =
      GetStateStoreEntry(base::Base64Encode(public_key_spki_der));
  matching_entry->sign_once = false;
  WriteToStateStore();

  // Return success.
  std::move(callback).Run(/*is_error=*/false,
                          /*error=*/crosapi::mojom::KeystoreError::kUnknown);
}

void ExtensionKeyPermissionsService::RegisterKeyForCorporateUsage(
    const std::vector<uint8_t>& public_key_spki_der,
    RegisterKeyForCorporateUsageCallback callback) {
  KeyEntry* matching_entry =
      GetStateStoreEntry(base::Base64Encode(public_key_spki_der));

  if (matching_entry->sign_once) {
    VLOG(1) << "Key is already allowed for signing, skipping.";
    // Return success.
    std::move(callback).Run(/*is_error=*/false,
                            /*error=*/crosapi::mojom::KeystoreError::kUnknown);
    return;
  }

  matching_entry->sign_once = true;
  WriteToStateStore();

  keystore_service_->AddKeyTags(
      public_key_spki_der,
      static_cast<uint64_t>(crosapi::mojom::KeyTag::kCorporate),
      std::move(callback));
}

void ExtensionKeyPermissionsService::SetUserGrantedPermission(
    const std::vector<uint8_t>& public_key_spki_der,
    SetUserGrantedPermissionCallback callback) {
  keystore_service_->CanUserGrantPermissionForKey(
      public_key_spki_der,
      base::BindOnce(
          &ExtensionKeyPermissionsService::SetUserGrantedPermissionWithFlag,
          weak_factory_.GetWeakPtr(), public_key_spki_der,
          std::move(callback)));
}

void ExtensionKeyPermissionsService::SetUserGrantedPermissionWithFlag(
    const std::vector<uint8_t>& public_key_spki_der,
    SetUserGrantedPermissionCallback callback,
    bool can_user_grant_permission) {
  if (!can_user_grant_permission) {
    std::move(callback).Run(
        /*is_error=*/true,
        crosapi::mojom::KeystoreError::kGrantKeyPermissionForExtension);
    return;
  }

  KeyEntry* matching_entry =
      GetStateStoreEntry(base::Base64Encode(public_key_spki_der));

  if (matching_entry->sign_unlimited) {
    VLOG(1) << "Key is already allowed for signing, skipping.";
    // Return success.
    std::move(callback).Run(/*is_error=*/false,
                            /*error=*/crosapi::mojom::KeystoreError::kUnknown);
    return;
  }

  matching_entry->sign_unlimited = true;
  WriteToStateStore();
  // Return success.
  std::move(callback).Run(/*is_error=*/false,
                          /*error=*/crosapi::mojom::KeystoreError::kUnknown);
}

bool ExtensionKeyPermissionsService::PolicyAllowsCorporateKeyUsage() const {
  return PolicyAllowsCorporateKeyUsageForExtension(extension_id_,
                                                   profile_policies_);
}

void ExtensionKeyPermissionsService::WriteToStateStore() {
  extensions_state_store_->SetExtensionValue(
      extension_id_, kStateStorePlatformKeys, base::Value(KeyEntriesToState()));
}

void ExtensionKeyPermissionsService::KeyEntriesFromState(
    const base::Value::List& state) {
  state_store_entries_.clear();

  for (const auto& entry : state) {
    std::string spki_b64;
    if (entry.is_string()) {
      spki_b64 = entry.GetString();
      // This handles the case that the store contained a plain list of base64
      // and DER-encoded SPKIs from an older version of ChromeOS.
      KeyEntry new_entry(spki_b64);
      new_entry.sign_once = true;
      state_store_entries_.push_back(new_entry);
    } else if (entry.is_dict()) {
      const base::Value::Dict& dict_entry = entry.GetDict();
      const std::string* spki_b64_str = dict_entry.FindString(kStateStoreSPKI);
      if (spki_b64_str)
        spki_b64 = *spki_b64_str;
      KeyEntry new_entry(spki_b64);
      new_entry.sign_once = dict_entry.FindBool(kStateStoreSignOnce)
                                .value_or(new_entry.sign_once);
      new_entry.sign_unlimited = dict_entry.FindBool(kStateStoreSignUnlimited)
                                     .value_or(new_entry.sign_unlimited);
      state_store_entries_.push_back(new_entry);
    } else {
      LOG(ERROR) << "Found invalid entry of type " << entry.type()
                 << " in PlatformKeys state store.";
      continue;
    }
  }
}

base::Value::List ExtensionKeyPermissionsService::KeyEntriesToState() {
  base::Value::List new_state;
  for (const KeyEntry& entry : state_store_entries_) {
    // Drop entries that the extension doesn't have any permissions for anymore.
    if (!entry.sign_once && !entry.sign_unlimited)
      continue;

    base::Value::Dict new_entry;
    new_entry.Set(kStateStoreSPKI, entry.spki_b64);
    // Omit writing default values, namely |false|.
    if (entry.sign_once) {
      new_entry.Set(kStateStoreSignOnce, entry.sign_once);
    }
    if (entry.sign_unlimited) {
      new_entry.Set(kStateStoreSignUnlimited, entry.sign_unlimited);
    }
    new_state.Append(std::move(new_entry));
  }
  return new_state;
}

// static
std::vector<std::string>
ExtensionKeyPermissionsService::GetCorporateKeyUsageAllowedAppIds(
    policy::PolicyService* const profile_policies) {
  std::vector<std::string> permissions;

  const base::Value::Dict* key_permissions_service_map =
      GetKeyPermissionsMap(profile_policies);
  if (!key_permissions_service_map)
    return permissions;

  for (const auto item : *key_permissions_service_map) {
    const auto& app_id = item.first;
    const base::Value::Dict* key_permissions_service_for_app =
        item.second.GetIfDict();
    if (!key_permissions_service_for_app) {
      continue;
    }
    if (GetCorporateKeyUsageFromPref(key_permissions_service_for_app))
      permissions.push_back(app_id);
  }
  return permissions;
}

}  // namespace chromeos::platform_keys