Add private metric report encryption Implement functionality to fetch public key and encrypt private metrics report for DWA. from third_party protocol buffer dependencies. NO_IFTTT=dwa_egtest.mm will add support for PrivateMetricsFeature in a separate CL. Bug: 411369489 Change-Id: Ib4a1baae22064b4c7e8d9897ae8de979f2aef6f7 Binary-Size: Size increase is unavoidable because increase is largely Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6913363 Reviewed-by: Alexei Svitkine <asvitkine@chromium.org> Reviewed-by: Olivier Robin <olivierrobin@chromium.org> Commit-Queue: Jay Zhou <zhouzj@google.com> Cr-Commit-Position: refs/heads/main@{#1515492} NOKEYCHECK=True GitOrigin-RevId: cbc6209a5a8eb373d40b1b8f3cd461b8be9a621b
diff --git a/dwa/DEPS b/dwa/DEPS index b3a9001..115faf5 100644 --- a/dwa/DEPS +++ b/dwa/DEPS
@@ -1,4 +1,5 @@ include_rules = [ "+mojo/public/cpp/bindings", "+net/base/registry_controlled_domains", + "+third_party/federated_compute", ]
diff --git a/dwa/dwa_service.cc b/dwa/dwa_service.cc index 3f3fb52..cd6b8aa 100644 --- a/dwa/dwa_service.cc +++ b/dwa/dwa_service.cc
@@ -23,26 +23,10 @@ #include "components/metrics/metrics_pref_names.h" #include "components/prefs/pref_service.h" #include "components/version_info/version_info.h" +#include "third_party/federated_compute/src/fcp/confidentialcompute/crypto.h" namespace metrics::dwa { -namespace { - -// TODO(crbug.com/411369489): Encrypt private metric report. Current -// implementation only serializes the report and moves the serialized report -// into the encrypted field without actually encrypting it. -::private_metrics::EncryptedPrivateMetricReport EncryptPrivateMetricReport( - const ::private_metrics::PrivateMetricReport& report) { - std::string serialized_log; - report.SerializeToString(&serialized_log); - - ::private_metrics::EncryptedPrivateMetricReport encrypted_report; - *encrypted_report.mutable_encrypted_report() = std::move(serialized_log); - return encrypted_report; -} - -} // namespace - // Set of countries in the European Economic Area. Used by // RecordCoarseSystemInformation to set geo_designation fields in // CoarseSystemInfo. These will need to be manually updated using @@ -89,12 +73,24 @@ const size_t kMinLogQueueSizeBytes = 300 * 1024; // 300 KiB const size_t kMaxLogSizeBytes = 1024 * 1024; // 1 MiB -DwaService::DwaService(MetricsServiceClient* client, PrefService* local_state) +DwaService::DwaService( + MetricsServiceClient* client, + PrefService* local_state, + scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) : recorder_(DwaRecorder::Get()), client_(client), pref_service_(local_state), reporting_service_(client, local_state, GetLogStoreLimits()) { reporting_service_.Initialize(); + + // Set up the downloader to refresh the encryption public key. + data_upload_config_downloader_ = + std::make_unique<private_metrics::DataUploadConfigDownloader>( + url_loader_factory); + + encryption_public_key_verifier_ = + base::BindRepeating(&ValidateEncryptionPublicKey); + // Set up the scheduler for DwaService. auto rotate_callback = base::BindRepeating(&DwaService::RotateLog, self_ptr_factory_.GetWeakPtr()); @@ -154,6 +150,66 @@ reporting_service_.unsent_log_store()->Purge(); } +void DwaService::RefreshEncryptionPublicKey() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // Multiple calls to FetchDataUploadConfig() is acceptable because + // FetchDataUploadConfig() checks for an existing fetch and discards any + // additional attempts if one is already in progress. + data_upload_config_downloader_->FetchDataUploadConfig( + base::BindOnce(&DwaService::HandleEncryptionPublicKeyRefresh, + self_ptr_factory_.GetWeakPtr())); +} + +void DwaService::SetEncryptionPublicKeyForTesting( + std::string_view test_encryption_public_key) { + encryption_public_key_ = test_encryption_public_key; +} + +void DwaService::SetEncryptionPublicKeyVerifierForTesting( + const base::RepeatingCallback< + bool(const fcp::confidential_compute::OkpCwt&)>& + test_encryption_public_key_verifier) { + encryption_public_key_verifier_ = test_encryption_public_key_verifier; +} + +void DwaService::HandleEncryptionPublicKeyRefresh( + std::optional<fcp::confidentialcompute::DataUploadConfig> + maybe_data_upload_config) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // `maybe_data_upload_config` may be empty if the http + // request to fetch `fcp::confidentialcompute::DataUploadConfig` results in an + // empty response body or if the HTTP status is non-200. + // `maybe_data_upload_config` may also be empty if de-serialization of + // DataUploadConfig into a protocol buffer object fails. + // If HandleEncryptionPublicKeyRefresh() fails to update or refresh the public + // key in one of the described error scenarios, the method will maintain the + // previous public key value. + if (maybe_data_upload_config.has_value() && + // The field may be empty in the unexpected case that the protocol buffer + // is malformed. + maybe_data_upload_config.value().has_confidential_encryption_config()) { + auto encryption_public_key = maybe_data_upload_config.value() + .confidential_encryption_config() + .public_key(); + + // Validate the public key is a valid cwt before setting the in-memory + // field, `encryption_public_key_`. This function call is okay to use in + // main thread. The performance of decoding of CBOR web tokens (CWT) is + // generally considered to be more efficient than the decoding of JSON Web + // Token (JWT) decoding due to CBOR's binary nature. + auto cwt = fcp::confidential_compute::OkpCwt::Decode(encryption_public_key); + if (!cwt.ok() || !cwt->algorithm.has_value() || + !cwt->public_key.has_value() || + !encryption_public_key_verifier_.Run(*cwt)) { + return; + } + + encryption_public_key_ = encryption_public_key; + } +} + // static UnsentLogStore::UnsentLogStoreLimits DwaService::GetLogStoreLimits() { return UnsentLogStore::UnsentLogStoreLimits{ @@ -315,6 +371,59 @@ return k_anonymity_buckets; } +// static +std::optional<::private_metrics::EncryptedPrivateMetricReport> +DwaService::EncryptPrivateMetricReport( + const ::private_metrics::PrivateMetricReport& report, + std::string_view public_key, + const fcp::confidential_compute::OkpCwt& decoded_public_key) { + std::string serialized_log; + report.SerializeToString(&serialized_log); + + ::private_metrics::PrivateMetricReportHeader report_header; + report_header.set_key_id(decoded_public_key.public_key.value().key_id); + report_header.set_epoch_id(report.epoch_id()); + + std::string serialized_report_header; + report_header.SerializeToString(&serialized_report_header); + + // The messages are encrypted with AEAD using a per-message generated + // symmetric key and then the symmetric key is encrypted using HPKE with the + // public key. This function call is considered fast for use in main thread. + // TODO(crbug.com/444678679): Add UMA histogram to measure performance on + // Encrypt() for detecting any potential regressions or edge cases. + fcp::confidential_compute::MessageEncryptor message_encryptor; + auto result = message_encryptor.Encrypt(serialized_log, public_key, + serialized_report_header); + if (!result.ok()) { + return std::nullopt; + } + + ::private_metrics::EncryptedPrivateMetricReport encrypted_report; + *encrypted_report.mutable_encrypted_report() = + std::move(result.value().ciphertext); + encrypted_report.set_serialized_report_header(serialized_report_header); + *encrypted_report.mutable_report_header() = std::move(report_header); + encrypted_report.set_report_type( + ::private_metrics::EncryptedPrivateMetricReport::DWA); + return encrypted_report; +} + +// static +bool DwaService::ValidateEncryptionPublicKey( + const fcp::confidential_compute::OkpCwt& cwt) { + // In the unexpected case where the `cwt` is malformed and does not contain an + // expiration time, the key should not be used. + if (!cwt.expiration_time.has_value()) { + return false; + } + + // If the encryption key is close to expiration (12 hour buffer), the key + // should not be used. + auto public_key_expiration = cwt.expiration_time.value() - absl::Hours(12); + return public_key_expiration >= absl::Now(); +} + void DwaService::RotateLog() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); if (!reporting_service_.unsent_log_store()->has_unsent_logs()) { @@ -366,9 +475,31 @@ return; } + // If the encryption public key is empty, no new logs should be created. + if (encryption_public_key_.empty()) { + RefreshEncryptionPublicKey(); + return; + } + + auto cwt = fcp::confidential_compute::OkpCwt::Decode(encryption_public_key_); + if (!cwt.ok() || !cwt->algorithm.has_value() || + !cwt->public_key.has_value()) { + RefreshEncryptionPublicKey(); + return; + } + + if (!encryption_public_key_verifier_.Run(*cwt)) { + RefreshEncryptionPublicKey(); + return; + } + + // Build the private metric report. ::private_metrics::PrivateMetricReport report; report.set_ephemeral_id(GetEphemeralClientId(*pref_service_)); + uint64_t epoch_id = (base::Time::Now() - base::Time::UnixEpoch()).InDays(); + report.set_epoch_id(epoch_id); + std::vector<::dwa::DeidentifiedWebAnalyticsEvent> dwa_events = recorder_->TakeDwaEvents(); @@ -378,7 +509,7 @@ auto k_anonymity_buckets = BuildKAnonymityBuckets(dwa_event); // Since there are no k-anonymity buckets, the k-anonymity filter cannot be - // enforced. As such, the bucket should be dropped. + // enforced. As such, the event should be dropped. // TODO(crbug.com/432764678): Add UMA metric when dwa_event is dropped due // to empty k-anonymity buckets. if (k_anonymity_buckets.empty()) { @@ -392,11 +523,24 @@ *event->mutable_dwa_event() = std::move(dwa_event); } - ::private_metrics::EncryptedPrivateMetricReport encrypted_report = - EncryptPrivateMetricReport(report); + auto encrypted_report = + EncryptPrivateMetricReport(report, encryption_public_key_, cwt.value()); + if (!encrypted_report.has_value()) { + // The encrypted report should be dropped in the unexpected event that + // private metrics report encryption fails. + // TODO(crbug.com/444681539): Add UMA histogram to check when private + // metrics encryption fails. + return; + } std::string serialized_log; - encrypted_report.SerializeToString(&serialized_log); + if (!encrypted_report.value().SerializeToString(&serialized_log)) { + // The encrypted report should be dropped in the unexpected event that + // private metrics report serialization fails. + // TODO(crbug.com/444681539): Add UMA histogram to check when private + // metrics serialization fails. + return; + } LogMetadata metadata; reporting_service_.unsent_log_store()->StoreLog(serialized_log, metadata,
diff --git a/dwa/dwa_service.h b/dwa/dwa_service.h index 092333d..fef3832 100644 --- a/dwa/dwa_service.h +++ b/dwa/dwa_service.h
@@ -8,6 +8,7 @@ #include <cstdint> #include <memory> #include <optional> +#include <string> #include <vector> #include "base/memory/weak_ptr.h" @@ -15,10 +16,13 @@ #include "components/metrics/dwa/dwa_recorder.h" #include "components/metrics/metrics_rotation_scheduler.h" #include "components/metrics/metrics_service_client.h" +#include "components/metrics/private_metrics/data_upload_config_downloader.h" #include "components/metrics/private_metrics/private_metrics_reporting_service.h" #include "components/metrics/unsent_log_store.h" #include "components/prefs/pref_registry_simple.h" #include "components/prefs/pref_service.h" +#include "third_party/federated_compute/src/fcp/confidentialcompute/cose.h" +#include "third_party/federated_compute/src/fcp/protos/confidentialcompute/data_upload_config.pb.h" #include "third_party/metrics_proto/dwa/deidentified_web_analytics.pb.h" #include "third_party/metrics_proto/private_metrics/private_metrics.pb.h" @@ -28,7 +32,9 @@ // analytics events. class DwaService { public: - DwaService(MetricsServiceClient* client, PrefService* pref_service); + DwaService(MetricsServiceClient* client, + PrefService* pref_service, + scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory); DwaService(const DwaService&) = delete; DwaService& operator=(const DwaService&) = delete; @@ -43,6 +49,25 @@ // Clears all event and log data. void Purge(); + // Refresh the public key used to encrypt private metric reports. + void RefreshEncryptionPublicKey(); + + // Sets encryption public key used to encrypt private metrics reports as + // `test_encryption_public_key`. + void SetEncryptionPublicKeyForTesting( + std::string_view test_encryption_public_key); + + // Sets the value of encryption public key verifier, + // `encryption_public_key_verifier_`, to + // `test_encryption_public_key_verifier`. + // TODO(crbug.com/444679397): Remove this function and instead make + // ValidateEncryptionPublicKey() a virtual function that can be overridden in + // unit tests. + void SetEncryptionPublicKeyVerifierForTesting( + const base::RepeatingCallback< + bool(const fcp::confidential_compute::OkpCwt&)>& + test_encryption_public_key_verifier); + // Records coarse system profile into CoarseSystemInfo of the deidentified web // analytics report proto. static void RecordCoarseSystemInformation( @@ -77,7 +102,7 @@ // k-anonymity buckets for `dwa_event` as there would be no way to enforce the // k-anonymity filter without the k-anonymity buckets. For `dwa_event`, the // combination of `dwa_event.coarse_system_info`, `dwa_event.event_hash`, and - // `dwa_event.field_trials` builds the first k-anbonymity bucket because the + // `dwa_event.field_trials` builds the first k-anonymity bucket because the // combination describes an user invoking an action. We want to verify there // is a sufficient number of users who perform this action before allowing the // `dwa_event` past the k-anonymity filter. Similarly, @@ -90,6 +115,27 @@ static std::vector<uint64_t> BuildKAnonymityBuckets( const ::dwa::DeidentifiedWebAnalyticsEvent& dwa_event); + // Encrypts `report` using the public encryption key, `public_key`, which is + // accepted by the HPKE encryption method in //third_party/federated_compute. + // The encrypted private metrics report can only be decrypted in a trusted + // execution environment (TEE) because decryption keys are released + // execlusively to the TEE. This method accepts the field `decoded_public_key` + // as an optimization. The field `decoded_public_key` is the OkpCwt + // representation of `public_key`, which is parsed and validated before being + // passed to this method. This prevents the need to re-parse the public key to + // extract the key id, which is required when building the encrypted private + // metrics report. + static std::optional<::private_metrics::EncryptedPrivateMetricReport> + EncryptPrivateMetricReport( + const ::private_metrics::PrivateMetricReport& report, + std::string_view public_key, + const fcp::confidential_compute::OkpCwt& decoded_public_key); + + // Returns false if the public key `cwt` is expired or should not be used. + // Otherwise, returns true. + static bool ValidateEncryptionPublicKey( + const fcp::confidential_compute::OkpCwt& cwt); + // Register prefs from `dwa_pref_names.h`. static void RegisterPrefs(PrefRegistrySimple* registry); @@ -114,6 +160,12 @@ // Retrieves the storage parameters to control the reporting service. static UnsentLogStore::UnsentLogStoreLimits GetLogStoreLimits(); + // Handles updating the public key, `encryption_public_key_`, during a public + // key refresh. + void HandleEncryptionPublicKeyRefresh( + std::optional<fcp::confidentialcompute::DataUploadConfig> + maybe_data_upload_config); + // Manages on-device recording of events. raw_ptr<DwaRecorder> recorder_; @@ -129,6 +181,23 @@ // The scheduler for determining when uploads should happen. std::unique_ptr<MetricsRotationScheduler> scheduler_; + // The DataUploadConfig protocol buffer contains the public key required to + // encrypt private metric reports and the signed endorsements of the keys + // required to perform attestation verification. + std::unique_ptr<private_metrics::DataUploadConfigDownloader> + data_upload_config_downloader_; + + // The public key used to encrypt PrivateMetricReport protocol buffer. The + // public key is fetched on startup and refreshed once every 24 hours. + std::string encryption_public_key_; + + // A repeating callback used to verify the `encryption_public_key_`. The + // callback should return false if the public key is invalid, expired, or + // should not be used. + // Otherwise, the callback should return true. + base::RepeatingCallback<bool(const fcp::confidential_compute::OkpCwt&)> + encryption_public_key_verifier_; + // Weak pointers factory used to post task on different threads. All weak // pointers managed by this factory have the same lifetime as DwaService. base::WeakPtrFactory<DwaService> self_ptr_factory_{this};
diff --git a/dwa/dwa_service_unittest.cc b/dwa/dwa_service_unittest.cc index 8d853e7..2728a07 100644 --- a/dwa/dwa_service_unittest.cc +++ b/dwa/dwa_service_unittest.cc
@@ -17,6 +17,8 @@ #include "components/prefs/pref_service.h" #include "components/prefs/testing_pref_service.h" #include "testing/gtest/include/gtest/gtest.h" +#include "third_party/federated_compute/src/fcp/confidentialcompute/cose.h" +#include "third_party/federated_compute/src/fcp/confidentialcompute/crypto.h" namespace metrics::dwa { @@ -30,7 +32,8 @@ MetricsStateManager::RegisterPrefs(prefs_.registry()); DwaService::RegisterPrefs(prefs_.registry()); - scoped_feature_list_.InitAndEnableFeature(kDwaFeature); + scoped_feature_list_.InitWithFeatures( + {dwa::kDwaFeature, dwa::kPrivateMetricsFeature}, {}); } DwaServiceTest(const DwaServiceTest&) = delete; @@ -40,6 +43,15 @@ void TearDown() override { DwaRecorder::Get()->Purge(); } + void SetEncryptionPublicKeyForTesting(DwaService* dwa_service) { + fcp::confidential_compute::MessageDecryptor decryptor; + auto recipient_public_key = + decryptor.GetPublicKey([](absl::string_view) { return ""; }, 0); + dwa_service->SetEncryptionPublicKeyForTesting(recipient_public_key.value()); + dwa_service->SetEncryptionPublicKeyVerifierForTesting(base::BindRepeating( + [](const fcp::confidential_compute::OkpCwt&) -> bool { return true; })); + } + int GetPersistedLogCount() { if (base::FeatureList::IsEnabled(kPrivateMetricsFeature)) { return prefs_.GetList(private_metrics::prefs::kUnsentLogStoreName).size(); @@ -291,8 +303,62 @@ ASSERT_EQ(previous_bucket_value, k_anonymity_buckets.at(0)); } +TEST_F(DwaServiceTest, ValidateEncryptionPublicKey) { + fcp::confidential_compute::MessageDecryptor decryptor; + auto recipient_public_key = + decryptor.GetPublicKey([](absl::string_view) { return ""; }, 0); + ASSERT_TRUE(recipient_public_key.ok()); + + auto cwt = fcp::confidential_compute::OkpCwt::Decode(*recipient_public_key); + ASSERT_TRUE(cwt.ok()); + + // Validate DwaService::ValidateEncryptionPublicKey() returns false when key + // is already expired. + auto now = absl::Now(); + cwt->expiration_time = std::make_optional(now - absl::Hours(8)); + EXPECT_FALSE(DwaService::ValidateEncryptionPublicKey(*cwt)); + + // Validate DwaService::ValidateEncryptionPublicKey() returns false when key + // is close to expiring (less than 12 hours). + cwt->expiration_time = std::make_optional(now + absl::Hours(11)); + EXPECT_FALSE(DwaService::ValidateEncryptionPublicKey(*cwt)); + + // Validate DwaService::ValidateEncryptionPublicKey() returns true when key + // valid and far from expiring (more than 12 hours). + cwt->expiration_time = std::make_optional(now + absl::Hours(13)); + EXPECT_TRUE(DwaService::ValidateEncryptionPublicKey(*cwt)); +} + +TEST_F(DwaServiceTest, EncryptPrivateMetricReport) { + TestingPrefServiceSimple pref_service; + DwaService::RegisterPrefs(pref_service.registry()); + + fcp::confidential_compute::MessageDecryptor decryptor; + auto recipient_public_key = + decryptor.GetPublicKey([](absl::string_view) { return ""; }, 0); + auto decoded_public_key = + fcp::confidential_compute::OkpCwt::Decode(*recipient_public_key); + decoded_public_key->public_key.value().key_id = "key-id"; + + ::private_metrics::PrivateMetricReport report; + report.set_ephemeral_id(DwaService::GetEphemeralClientId(pref_service)); + auto epoch_id = (base::Time::Now() - base::Time::UnixEpoch()).InDays(); + report.set_epoch_id(epoch_id); + + auto encrypted_report = DwaService::EncryptPrivateMetricReport( + report, *recipient_public_key, *decoded_public_key); + ASSERT_TRUE(encrypted_report.has_value()); + + EXPECT_TRUE(encrypted_report->has_encrypted_report()); + EXPECT_TRUE(encrypted_report->has_serialized_report_header()); + EXPECT_TRUE(encrypted_report->has_report_header()); + EXPECT_TRUE(encrypted_report->has_report_type()); +} + TEST_F(DwaServiceEnvironmentTest, Flush) { - DwaService service(&client_, &prefs_); + DwaService service(&client_, &prefs_, nullptr); + SetEncryptionPublicKeyForTesting(&service); + histogram_tester_.ExpectTotalCount(kDwaInitSequenceHistogramName, /*expected_count=*/1); DwaRecorder::Get()->EnableRecording(); @@ -307,7 +373,9 @@ } TEST_F(DwaServiceEnvironmentTest, Purge) { - DwaService service(&client_, &prefs_); + DwaService service(&client_, &prefs_, nullptr); + SetEncryptionPublicKeyForTesting(&service); + histogram_tester_.ExpectTotalCount(kDwaInitSequenceHistogramName, /*expected_count=*/1); DwaRecorder::Get()->EnableRecording(); @@ -332,7 +400,9 @@ } TEST_F(DwaServiceEnvironmentTest, EnableDisableRecordingAndReporting) { - DwaService service(&client_, &prefs_); + DwaService service(&client_, &prefs_, nullptr); + SetEncryptionPublicKeyForTesting(&service); + histogram_tester_.ExpectTotalCount(kDwaInitSequenceHistogramName, /*expected_count=*/1); EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u); @@ -390,7 +460,9 @@ } TEST_F(DwaServiceEnvironmentTest, LogsRotatedPeriodically) { - DwaService service(&client_, &prefs_); + DwaService service(&client_, &prefs_, nullptr); + SetEncryptionPublicKeyForTesting(&service); + histogram_tester_.ExpectTotalCount(kDwaInitSequenceHistogramName, /*expected_count=*/1); DwaRecorder::Get()->EnableRecording();
diff --git a/private_metrics/data_upload_config_downloader.cc b/private_metrics/data_upload_config_downloader.cc index 49e187e..914c51b 100644 --- a/private_metrics/data_upload_config_downloader.cc +++ b/private_metrics/data_upload_config_downloader.cc
@@ -76,7 +76,7 @@ })"); inline constexpr char kDataUploadConfigGstaticUrl[] = - "https://www.gstatic.com/chrome/private-metrics/data-upload-config.pbtxt"; + "https://www.gstatic.com/chrome/private-metrics/data-upload-config.pb"; } // namespace std::unique_ptr<network::SimpleURLLoader> CreateSimpleURLLoader(
diff --git a/private_metrics/data_upload_config_downloader_unittest.cc b/private_metrics/data_upload_config_downloader_unittest.cc index 0642fb8..afe9c23 100644 --- a/private_metrics/data_upload_config_downloader_unittest.cc +++ b/private_metrics/data_upload_config_downloader_unittest.cc
@@ -19,7 +19,7 @@ namespace { inline constexpr char kDataUploadConfigGstaticUrl[] = - "https://www.gstatic.com/chrome/private-metrics/data-upload-config.pbtxt"; + "https://www.gstatic.com/chrome/private-metrics/data-upload-config.pb"; void AddValidDataUploadConfigResponse( network::TestURLLoaderFactory& test_url_loader_factory) {