Skip to content

Commit

Permalink
variations: Add SeedStore override for CrOS.
Browse files Browse the repository at this point in the history
CrOS early boot needs to have its own notion of what the "safe seed" is
(and when to use it), so override the VariationsSeedStore class's
LoadSafeSeed method to load a distinct seed from the one in local state.

BUG=b:263975722
TEST=included unit tests

Change-Id: I87c5135bb6438056f5018c722c888c430da8c35d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4685411
Commit-Queue: Miriam Zimmerman <mutexlox@chromium.org>
Reviewed-by: Ian Barkley-Yeung <iby@chromium.org>
Reviewed-by: Ilya Sherman <isherman@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1187997}
  • Loading branch information
mutexlox authored and Chromium LUCI CQ committed Aug 24, 2023
1 parent 62cce61 commit ba0105e
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 22 deletions.
4 changes: 4 additions & 0 deletions components/variations/cros_evaluate_seed/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ source_set("evaluate_seed_lib") {
"cros_safe_seed_manager.h",
"cros_variations_field_trial_creator.cc",
"cros_variations_field_trial_creator.h",
"early_boot_seed_store.cc",
"early_boot_seed_store.h",
"evaluate_seed.cc",
"evaluate_seed.h",
]
Expand All @@ -41,6 +43,7 @@ source_set("unit_tests") {
testonly = true
sources = [
"cros_safe_seed_manager_unittest.cc",
"early_boot_seed_store_unittest.cc",
"evaluate_seed_unittest.cc",
]
deps = [
Expand All @@ -50,6 +53,7 @@ source_set("unit_tests") {
"//build:branding_buildflags",
"//build/config/chromebox_for_meetings:buildflags",
"//chromeos/ash/components/dbus/featured:proto",
"//components/prefs:test_support",
"//components/test:test_support",
"//components/variations",
"//components/variations/service:service",
Expand Down
19 changes: 13 additions & 6 deletions components/variations/cros_evaluate_seed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,23 @@ each early-boot experiment should be in, as well as any parameters for the
experiment. It lives here so that it is trivial to keep the code in sync between
ChromeOS's platform layer and chrome.

It will be built alongside ash, and use the same seed it uses in `Local State`
in normal operation.
It will be built alongside ash.

It will be executed primarily by `featured`, which lives in
`//platform2/featured/`.

In safe seed cases, the **platform** code (featured) will determine whether to
use the safe (or null) seed, and pass that information along with the value of
the safe seed to use along to this code.

`evaluate_seed` will write a serialized version of the computed state to stdout,
along with a representation of the seed used for computation (for purposes of
determining whether the seed can be marked as "safe").

## Seed usage

When the device starts up, `featured` will exec `evaluate_seed`, which will by
default use whatever the latest seed in `/home/chronos/Local State` is. The
state computed by `evaluate_seed` will be cached in a tmpfs until the next
reboot, so even if ash later downloads and applies different seeds,
`evaluate_seed` will not re-evaluate the seed until the next device reboot.

For disaster recovery, featured will determine whether to use the safe (or null)
seed, and pass that information along with the value of the safe seed to use
along to `evaluate_seed`.
45 changes: 45 additions & 0 deletions components/variations/cros_evaluate_seed/early_boot_seed_store.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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 "components/variations/cros_evaluate_seed/early_boot_seed_store.h"

#include "base/logging.h"
#include "base/time/time.h"
#include "components/variations/client_filterable_state.h"
#include "third_party/abseil-cpp/absl/types/optional.h"

namespace variations::cros_early_boot::evaluate_seed {

EarlyBootSeedStore::EarlyBootSeedStore(
PrefService* local_state,
const absl::optional<featured::SeedDetails>& safe_seed_details)
: VariationsSeedStore(local_state), safe_seed_details_(safe_seed_details) {}

EarlyBootSeedStore::~EarlyBootSeedStore() = default;

bool EarlyBootSeedStore::LoadSafeSeed(VariationsSeed* seed,
ClientFilterableState* client_state) {
// We require that evaluate_seed's command line specified a safe seed in order
// to use the safe seed.
CHECK(safe_seed_details_.has_value());
absl::optional<VerifySignatureResult> verify_signature_result;
if (VerifyAndParseSeed(seed, safe_seed_details_->compressed_data(),
safe_seed_details_->signature(),
&verify_signature_result) !=
LoadSeedResult::kSuccess) {
return false;
}

client_state->reference_date =
base::Time::FromJavaTime(safe_seed_details_->date());
client_state->locale = safe_seed_details_->locale();
client_state->permanent_consistency_country =
safe_seed_details_->permanent_consistency_country();
client_state->session_consistency_country =
safe_seed_details_->session_consistency_country();

return true;
}

} // namespace variations::cros_early_boot::evaluate_seed
51 changes: 51 additions & 0 deletions components/variations/cros_evaluate_seed/early_boot_seed_store.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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.

#ifndef COMPONENTS_VARIATIONS_CROS_EVALUATE_SEED_EARLY_BOOT_SEED_STORE_H_
#define COMPONENTS_VARIATIONS_CROS_EVALUATE_SEED_EARLY_BOOT_SEED_STORE_H_

#include "base/time/time.h"
#include "chromeos/ash/components/dbus/featured/featured.pb.h"
#include "components/variations/client_filterable_state.h"
#include "components/variations/proto/variations_seed.pb.h"
#include "components/variations/variations_seed_store.h"

class PrefService;

namespace variations::cros_early_boot::evaluate_seed {

// VariationsSeedStore that uses a safe seed specific to early-boot ChromeOS.
//
// While early-boot experiments share a seed with non-early-boot experiments and
// use the same code to load them from |local_state|, they do *not* share a safe
// seed, since a seed could be safe for Chromium without being safe for
// early-boot ChromeOS.
class EarlyBootSeedStore : public VariationsSeedStore {
public:
// Construct an EarlyBootSeedStore, using |local_state| for the normal
// seed and |safe_seed_details| (which may be nullopt if we are not in safe
// seed mode) for the safe seed.
EarlyBootSeedStore(
PrefService* local_state,
const absl::optional<featured::SeedDetails>& safe_seed_details);

EarlyBootSeedStore(const EarlyBootSeedStore&) = delete;
EarlyBootSeedStore& operator=(const EarlyBootSeedStore&) = delete;

~EarlyBootSeedStore() override;

// Populate the given |seed| and |client_state| with the safe seed state as
// specified in the constructor.
// Unlike the base class version, DOES NOT modify local state or have any side
// effects.
bool LoadSafeSeed(VariationsSeed* seed,
ClientFilterableState* client_state) override;

private:
const absl::optional<featured::SeedDetails> safe_seed_details_;
};

} // namespace variations::cros_early_boot::evaluate_seed

#endif // COMPONENTS_VARIATIONS_CROS_EVALUATE_SEED_EARLY_BOOT_SEED_STORE_H_
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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 "components/variations/cros_evaluate_seed/early_boot_seed_store.h"

#include <string>

#include "base/test/scoped_command_line.h"
#include "base/time/time.h"
#include "chromeos/ash/components/dbus/featured/featured.pb.h"
#include "components/prefs/testing_pref_service.h"
#include "components/variations/client_filterable_state.h"
#include "components/variations/proto/variations_seed.pb.h"
#include "components/variations/variations_switches.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace variations::cros_early_boot::evaluate_seed {

namespace {

MATCHER_P(EqualsProto,
message,
"Match a proto Message equal to the matcher's argument.") {
std::string expected_serialized, actual_serialized;
message.SerializeToString(&expected_serialized);
arg.SerializeToString(&actual_serialized);
return expected_serialized == actual_serialized;
}

// Populates |seed| with simple test data. The resulting seed will contain one
// study called "test", which contains one experiment called "abc" with
// probability weight 100.
VariationsSeed CreateTestSeed() {
VariationsSeed seed;
Study* study = seed.add_study();
study->set_name("test");
study->set_default_experiment_name("abc");
Study_Experiment* experiment = study->add_experiment();
experiment->set_name("abc");
experiment->set_probability_weight(100);
seed.set_serial_number("123");
return seed;
}

TEST(EarlyBootSeedStoreTest, LoadSafeSeed) {
const VariationsSeed safe_seed = CreateTestSeed();

featured::SeedDetails safe_seed_details;
safe_seed_details.set_date(123456789);
safe_seed_details.set_locale("xx-YY");
safe_seed_details.set_permanent_consistency_country("us");
safe_seed_details.set_session_consistency_country("ca");

std::string serialized_seed;
ASSERT_TRUE(safe_seed.SerializeToString(&serialized_seed));
safe_seed_details.set_compressed_data(serialized_seed);

TestingPrefServiceSimple prefs;
VariationsSeedStore::RegisterPrefs(prefs.registry());

// Allow empty signature.
base::test::ScopedCommandLine scoped_command_line;
scoped_command_line.GetProcessCommandLine()->AppendSwitch(
switches::kAcceptEmptySeedSignatureForTesting);

EarlyBootSeedStore store(&prefs, safe_seed_details);

VariationsSeed actual_seed;
ClientFilterableState actual_client_state(
/*is_enterprise_function=*/base::BindOnce([] { return false; }),
/*google_groups_function=*/base::BindOnce(
[] { return base::flat_set<uint64_t>(); }));
EXPECT_TRUE(store.LoadSafeSeed(&actual_seed, &actual_client_state));

EXPECT_THAT(actual_seed, EqualsProto(safe_seed));
EXPECT_EQ(base::Time::FromJavaTime(safe_seed_details.date()),
actual_client_state.reference_date);
EXPECT_EQ(safe_seed_details.locale(), actual_client_state.locale);
EXPECT_EQ(safe_seed_details.permanent_consistency_country(),
actual_client_state.permanent_consistency_country);
EXPECT_EQ(safe_seed_details.session_consistency_country(),
actual_client_state.session_consistency_country);
}

TEST(EarlyBootSeedStoreTest, LoadSafeSeed_Unspecified) {
TestingPrefServiceSimple prefs;
VariationsSeedStore::RegisterPrefs(prefs.registry());

EarlyBootSeedStore store(&prefs, absl::nullopt);
VariationsSeed actual_seed;
ClientFilterableState actual_client_state(
/*is_enterprise_function=*/base::BindOnce([] { return false; }),
/*google_groups_function=*/base::BindOnce(
[] { return base::flat_set<uint64_t>(); }));
EXPECT_DEATH(store.LoadSafeSeed(&actual_seed, &actual_client_state),
"safe_seed_details_");
}

TEST(EarlyBootSeedStoreTest, LoadSafeSeed_Invalid) {
featured::SeedDetails safe_seed_details;
safe_seed_details.set_compressed_data("bad");

TestingPrefServiceSimple prefs;
VariationsSeedStore::RegisterPrefs(prefs.registry());

// Allow empty signature.
base::test::ScopedCommandLine scoped_command_line;
scoped_command_line.GetProcessCommandLine()->AppendSwitch(
switches::kAcceptEmptySeedSignatureForTesting);

EarlyBootSeedStore store(&prefs, safe_seed_details);

VariationsSeed actual_seed;
ClientFilterableState actual_client_state(
/*is_enterprise_function=*/base::BindOnce([] { return false; }),
/*google_groups_function=*/base::BindOnce(
[] { return base::flat_set<uint64_t>(); }));
EXPECT_FALSE(store.LoadSafeSeed(&actual_seed, &actual_client_state));
}

} // namespace
} // namespace variations::cros_early_boot::evaluate_seed
53 changes: 38 additions & 15 deletions components/variations/variations_seed_store.cc
Original file line number Diff line number Diff line change
Expand Up @@ -485,44 +485,67 @@ void VariationsSeedStore::ImportInitialSeed(
}
#endif // BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)

LoadSeedResult VariationsSeedStore::VerifyAndParseSeed(
VariationsSeed* seed,
const std::string& seed_data,
const std::string& base64_seed_signature,
absl::optional<VerifySignatureResult>* verify_signature_result) {
// TODO(crbug/1335082): get rid of |signature_verification_enabled_| and only
// support switches::kAcceptEmptySeedSignatureForTesting.
if (signature_verification_enabled_ &&
!AcceptEmptySeedSignatureForTesting(base64_seed_signature)) {
*verify_signature_result =
VerifySeedSignature(seed_data, base64_seed_signature);
if (*verify_signature_result != VerifySignatureResult::VALID_SIGNATURE) {
return LoadSeedResult::kInvalidSignature;
}
}

if (!seed->ParseFromString(seed_data)) {
return LoadSeedResult::kCorruptProtobuf;
}

return LoadSeedResult::kSuccess;
}

LoadSeedResult VariationsSeedStore::LoadSeedImpl(
SeedType seed_type,
VariationsSeed* seed,
std::string* seed_data,
std::string* base64_seed_signature) {
LoadSeedResult read_result = ReadSeedData(seed_type, seed_data);
if (read_result != LoadSeedResult::kSuccess)
if (read_result != LoadSeedResult::kSuccess) {
return read_result;
}

*base64_seed_signature = local_state_->GetString(
seed_type == SeedType::LATEST ? prefs::kVariationsSeedSignature
: prefs::kVariationsSafeSeedSignature);
// TODO(crbug/1335082): get rid of |signature_verification_enabled_| and only
// support switches::kAcceptEmptySeedSignatureForTesting.
if (signature_verification_enabled_ &&
!AcceptEmptySeedSignatureForTesting(*base64_seed_signature)) {
const VerifySignatureResult result =
VerifySeedSignature(*seed_data, *base64_seed_signature);

absl::optional<VerifySignatureResult> verify_signature_result;
LoadSeedResult result = VerifyAndParseSeed(
seed, *seed_data, *base64_seed_signature, &verify_signature_result);
if (verify_signature_result.has_value()) {
VerifySignatureResult signature_result = verify_signature_result.value();
if (seed_type == SeedType::LATEST) {
UMA_HISTOGRAM_ENUMERATION("Variations.LoadSeedSignature", result,
UMA_HISTOGRAM_ENUMERATION("Variations.LoadSeedSignature",
signature_result,
VerifySignatureResult::ENUM_SIZE);
} else {
UMA_HISTOGRAM_ENUMERATION(
"Variations.SafeMode.LoadSafeSeed.SignatureValidity", result,
VerifySignatureResult::ENUM_SIZE);
"Variations.SafeMode.LoadSafeSeed.SignatureValidity",
signature_result, VerifySignatureResult::ENUM_SIZE);
}
if (result != VerifySignatureResult::VALID_SIGNATURE) {
if (signature_result != VerifySignatureResult::VALID_SIGNATURE) {
ClearPrefs(seed_type);
return LoadSeedResult::kInvalidSignature;
}
}

if (!seed->ParseFromString(*seed_data)) {
if (result == LoadSeedResult::kCorruptProtobuf) {
ClearPrefs(seed_type);
return LoadSeedResult::kCorruptProtobuf;
}

return LoadSeedResult::kSuccess;
return result;
}

LoadSeedResult VariationsSeedStore::ReadSeedData(SeedType seed_type,
Expand Down
13 changes: 12 additions & 1 deletion components/variations/variations_seed_store.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "components/variations/metrics.h"
#include "components/variations/proto/variations_seed.pb.h"
#include "components/variations/seed_response.h"
#include "third_party/abseil-cpp/absl/types/optional.h"

class PrefService;
class PrefRegistrySimple;
Expand Down Expand Up @@ -101,7 +102,8 @@ class COMPONENT_EXPORT(VARIATIONS) VariationsSeedStore {
// Side effect: Upon failing to read or validate the safe seed, clears all
// of the safe seed pref values.
//
// Virtual for testing.
// Virtual for testing and for early-boot CrOS experiments to use a different
// safe seed.
[[nodiscard]] virtual bool LoadSafeSeed(VariationsSeed* seed,
ClientFilterableState* client_state);

Expand Down Expand Up @@ -154,6 +156,15 @@ class COMPONENT_EXPORT(VARIATIONS) VariationsSeedStore {
const std::string& seed_bytes,
const std::string& base64_seed_signature);

protected:
// Verify an already-loaded |seed_data| along with its |base64_seed_signature|
// and, if verification passes, parse it into |*seed|.
[[nodiscard]] LoadSeedResult VerifyAndParseSeed(
VariationsSeed* seed,
const std::string& seed_data,
const std::string& base64_seed_signature,
absl::optional<VerifySignatureResult>* verify_signature_result);

private:
FRIEND_TEST_ALL_PREFIXES(VariationsSeedStoreTest, VerifySeedSignature);
FRIEND_TEST_ALL_PREFIXES(VariationsSeedStoreTest, ApplyDeltaPatch);
Expand Down

0 comments on commit ba0105e

Please sign in to comment.