blob: 4cde838708c38de8ed58d8c7566dec94a5bea708 [file] [log] [blame]
// 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.
#include "chrome/browser/policy/messaging_layer/upload/configuration_file_controller.h"
#include <algorithm>
#include <cstdint>
#include <limits>
#include <optional>
#include <string_view>
#include <vector>
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/byte_conversions.h"
#include "base/strings/string_view_util.h"
#include "chrome/browser/policy/messaging_layer/upload/record_upload_request_builder.h"
#include "components/reporting/encryption/primitives.h"
#include "components/reporting/encryption/verification.h"
#include "components/reporting/proto/synced/record.pb.h"
#include "components/reporting/util/status.h"
#include "components/version_info/version_info.h"
namespace reporting {
namespace {
// Checks if two lists contain the same destinations.
bool DestinationListsEqual(ListOfBlockedDestinations list_a,
ListOfBlockedDestinations list_b) {
if (list_a.destinations_size() != list_b.destinations_size()) {
return false;
}
return std::equal(list_a.destinations().begin(), list_a.destinations().end(),
list_b.destinations().begin());
}
} // namespace
// Only used in testing. Feature for testing the configuration file in our
// automated tests. Not exposed on the UI.
BASE_FEATURE(kReportingConfigurationFileTestSignature,
base::FEATURE_DISABLED_BY_DEFAULT);
ConfigurationFileController::~ConfigurationFileController() = default;
// The default constructor used for this class takes as a parameter a callback
// that sends information to missive after being verified by this class if
// needed. It also creates the signature verifier that will be used to verify
// the signature of the configuration file using the well-known prod key stored
// in Chrome.
ConfigurationFileController::ConfigurationFileController(
UploadClient::UpdateConfigInMissiveCallback update_config_in_missive_cb)
: ConfigurationFileController(std::move(update_config_in_missive_cb),
ListOfBlockedDestinations(),
version_info::GetMajorVersionNumberAsInt()) {}
int32_t ConfigurationFileController::HandleConfigurationFile(
reporting::ConfigFile config_file) {
// Sanity check to verify that the experiment is enabled.
if (!base::FeatureList::IsEnabled(kShouldRequestConfigurationFile)) {
// Returns -1 since this is not an actual error with the file provided.
return kFeatureDisabled;
}
// Check if we got the same version that we were using already.
// This might happen if the server returns the configuration file two times
// with the same version because of two requests made close to each other, we
// just ignore them if that's the case and return the current version. We also
// just ignore the config file if the version is negative.
if (config_file.version() == current_config_file_version_ ||
config_file.version() < 0) {
return current_config_file_version_;
}
// Verify signature.
const auto signature_verification_status = VerifySignature(config_file);
if (!signature_verification_status.ok()) {
base::UmaHistogramEnumeration(
"Browser.ERP.ConfigFileSignatureVerificationError",
signature_verification_status.code(), error::Code::MAX_VALUE);
return kConfigurationFileCorrupted;
}
// Check if we should send the list of blocked destinations to missive.
if (HandleBlockedEventConfigs(config_file.blocked_event_configs())) {
update_config_in_missive_cb_.Run(destinations_list_);
}
// If we reached here it means that we just got a new version of the config
// file from the server so we update the version being sent on the payload and
// the one stored by this class.
current_config_file_version_ = config_file.version();
return current_config_file_version_;
}
Status ConfigurationFileController::VerifySignature(ConfigFile config_file) {
// We don't check the signature in our tests. This flag is only used inside
// the tast tests.
if (base::FeatureList::IsEnabled(kReportingConfigurationFileTestSignature)) {
return Status::StatusOK();
}
// Sanity checks, this should never fail.
if (!config_file.has_version()) {
return Status{error::INVALID_ARGUMENT,
"Missing version information for config file"};
}
if (!config_file.has_config_file_signature()) {
return Status{error::INVALID_ARGUMENT,
"Missing signature information for config file"};
}
// Verify the value signed on the server using the big-endian representation
// of the configuration file version.
return verifier_.Verify(
base::as_string_view(base::U32ToBigEndian(config_file.version())),
config_file.config_file_signature());
}
// static
std::unique_ptr<ConfigurationFileController>
ConfigurationFileController::CreateForTesting(
UploadClient::UpdateConfigInMissiveCallback update_config_in_missive_cb,
ListOfBlockedDestinations destinations_list,
int os_version) {
return base::WrapUnique(new ConfigurationFileController(
std::move(update_config_in_missive_cb), std::move(destinations_list),
os_version));
}
ConfigurationFileController::ConfigurationFileController(
UploadClient::UpdateConfigInMissiveCallback update_config_in_missive_cb,
ListOfBlockedDestinations destinations_list,
int os_version)
: update_config_in_missive_cb_(std::move(update_config_in_missive_cb)),
verifier_(SignatureVerifier(SignatureVerifier::VerificationKey())),
destinations_list_(std::move(destinations_list)),
current_os_version_(os_version) {}
bool ConfigurationFileController::HandleBlockedEventConfigs(
google::protobuf::RepeatedPtrField<EventConfig> blocked_event_configs) {
// If the incoming list is empty and the stored list is also empty
// we return false to signal to the parent function that it should not
// send the list to missive.
if (blocked_event_configs.empty() &&
destinations_list_.destinations().empty()) {
return false;
}
// If the configuration fetched from the server is empty and the stored list
// is not empty we return true to signal to the parent function that it
// should update the list in missive to an empty one.
if (blocked_event_configs.empty() &&
!destinations_list_.destinations().empty()) {
destinations_list_ = ListOfBlockedDestinations();
return true;
}
ListOfBlockedDestinations current_list;
// Check all the destinations, if they have version information
// we check whether it should be blocked or not.
for (const auto& current_event : blocked_event_configs) {
// If there is no minimum/maximum release version specified we add it
// directly since it should be blocked on all versions.
if (!current_event.has_minimum_release_version()) {
current_list.add_destinations(current_event.destination());
continue;
}
// Check if it should be blocked for the current version or not.
std::optional<int32_t> maximum_version;
maximum_version =
current_event.has_maximum_release_version()
? std::optional<int32_t>(current_event.maximum_release_version())
: std::nullopt;
if (ShouldBeBlocked(current_event.minimum_release_version(),
maximum_version)) {
current_list.add_destinations(current_event.destination());
}
}
// Compare to see if the destinations list that we have right now is the same
// as the one that is incoming from the server, if not then we swap the lists
// and we notify the parent function that it should send the new list to
// missive.
if (DestinationListsEqual(current_list, destinations_list_)) {
return false;
}
destinations_list_ = std::move(current_list);
return true;
}
bool ConfigurationFileController::ShouldBeBlocked(
int32_t minimum_version,
std::optional<int32_t> maximum_version) const {
if (maximum_version.has_value()) {
return current_os_version_ >= minimum_version &&
current_os_version_ <= maximum_version.value();
}
return current_os_version_ >= minimum_version;
}
} // namespace reporting