Reland "iwa: Optimistically update IWAs when key rotation happens"
This is a reland of commit ec89d463d3901ff14afd0a4a87de7e62c31e7c4f
What changed: ScopedBundledIWA's destructor was never called which led
to a memory leak.
Original change's description:
> iwa: Optimistically update IWAs when key rotation happens
>
> This CL wires up IsolatedWebAppUpdateManager to listen to component
> installations and check for updates of affected IWAs accordingly.
>
> Same-version update attempts are now allowed if they might lead to the
> set of signing keys changing.
>
> Faulty updates (when the new bundle doesn't contain the rotated key) are
> simply ignored for now -- the exact handling is TBD.
>
> Bug: 353489152
> Change-Id: I16b00e7a5d51e653192ccff7e8726ba212281001
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5729755
> Reviewed-by: Robbie McElrath <rmcelrath@chromium.org>
> Commit-Queue: Andrew Rayskiy <greengrape@google.com>
> Cr-Commit-Position: refs/heads/main@{#1336147}
Bug: 353489152
Change-Id: I4f1c179a685deeb188d18de1159a686283c15d0f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5757202
Auto-Submit: Andrew Rayskiy <greengrape@google.com>
Commit-Queue: Andrew Rayskiy <greengrape@google.com>
Reviewed-by: Robbie McElrath <rmcelrath@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1336691}
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_apply_update_command.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_apply_update_command.cc
index ac34a7a2..b30a53d2 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_apply_update_command.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_apply_update_command.cc
@@ -138,13 +138,41 @@
GetMutableDebugValue().Set("pending_update_info",
pending_update_info_->AsDebugValue());
- if (isolation_data.version >= pending_update_info_->version) {
+ bool same_version_update_allowed_by_key_rotation = false;
+ switch (LookupRotatedKey(url_info_.web_bundle_id(), GetMutableDebugValue())) {
+ case KeyRotationLookupResult::kNoKeyRotation:
+ break;
+ case KeyRotationLookupResult::kKeyBlocked:
+ ReportFailure(
+ "The web bundle id for this app's bundle has been blocked by the key "
+ "distribution component.");
+ return;
+ case KeyRotationLookupResult::kKeyFound: {
+ KeyRotationData data =
+ GetKeyRotationData(url_info_.web_bundle_id(), isolation_data);
+ if (!data.pending_update_has_rk) {
+ ReportFailure(
+ "The update's integrity block data doesn't contain the required "
+ "public key as instructed by the key distribution component -- the "
+ "update won't succeed.");
+ return;
+ }
+ if (!data.current_installation_has_rk) {
+ same_version_update_allowed_by_key_rotation = true;
+ }
+ } break;
+ }
+
+ if (isolation_data.version > pending_update_info_->version ||
+ (isolation_data.version == pending_update_info_->version &&
+ !same_version_update_allowed_by_key_rotation)) {
ReportFailure(base::StrCat({"Installed app is already on version ",
isolation_data.version.GetString(),
". Cannot update to version ",
pending_update_info_->version.GetString()}));
return;
}
+
if (isolation_data.location.dev_mode() !=
pending_update_info_->location.dev_mode()) {
std::stringstream s;
@@ -281,17 +309,17 @@
: std::move(next_step_callback);
ScopedRegistryUpdate update = lock_->sync_bridge().BeginUpdate(
- // We don't really care whether committing the update succeeds or fails.
- // However, we want to wait for the write of the database to disk, so that
- // a potential crash during that write happens before the
- // to-be-implemented cleanup system for no longer referenced Web Bundles
- // kicks in.
+ // We don't really care whether committing the update succeeds or
+ // fails. However, we want to wait for the write of the database to
+ // disk, so that a potential crash during that write happens before
+ // the to-be-implemented cleanup system for no longer referenced Web
+ // Bundles kicks in.
base::IgnoreArgs<bool>(std::move(update_callback)));
WebApp* web_app = update->UpdateApp(url_info_.app_id());
- // This command might fail because the app is no longer installed, or because
- // it does not have `WebApp::IsolationData` or
+ // This command might fail because the app is no longer installed, or
+ // because it does not have `WebApp::IsolationData` or
// `WebApp::IsolationData::PendingUpdateInfo`, in which case there is no
// pending update info for us to delete.
if (!web_app || !web_app->isolation_data().has_value() ||
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_command_helper.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_command_helper.cc
index 7297cd58..ddca8eb 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_command_helper.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_command_helper.cc
@@ -9,6 +9,7 @@
#include <string>
#include <vector>
+#include "base/base64.h"
#include "base/check_op.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
@@ -26,6 +27,7 @@
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_features.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_source.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_integrity_block_data.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_storage_location.h"
@@ -33,6 +35,7 @@
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_validator.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_version.h"
+#include "chrome/browser/web_applications/isolated_web_apps/key_distribution/iwa_key_distribution_info_provider.h"
#include "chrome/browser/web_applications/isolated_web_apps/pending_install_info.h"
#include "chrome/browser/web_applications/web_app_icon_operations.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
@@ -137,6 +140,14 @@
return result == webapps::WebAppUrlLoaderResult::kUrlLoaded;
}
+bool IntegrityBlockDataHasRotatedKey(
+ base::optional_ref<const IsolatedWebAppIntegrityBlockData>
+ integrity_block_data,
+ base::span<const uint8_t> rotated_key) {
+ return integrity_block_data &&
+ integrity_block_data->HasPublicKey(rotated_key);
+}
+
} // namespace
void CleanupLocationIfOwned(const base::FilePath& profile_dir,
@@ -222,6 +233,59 @@
return *iwa;
}
+KeyRotationLookupResult LookupRotatedKey(
+ const web_package::SignedWebBundleId& web_bundle_id,
+ base::optional_ref<base::Value::Dict> debug_log) {
+ auto log_rotated_key = [&](const std::string& value) {
+ if (debug_log) {
+ debug_log->Set("rotated_key", value);
+ }
+ };
+
+ const auto* kr_info =
+ IwaKeyDistributionInfoProvider::GetInstance()->GetKeyRotationInfo(
+ web_bundle_id.id());
+ if (!kr_info) {
+ return KeyRotationLookupResult::kNoKeyRotation;
+ }
+
+ if (!kr_info->public_key) {
+ log_rotated_key("<disabled>");
+ return KeyRotationLookupResult::kKeyBlocked;
+ }
+ log_rotated_key(base::Base64Encode(*kr_info->public_key));
+ return KeyRotationLookupResult::kKeyFound;
+}
+
+KeyRotationData GetKeyRotationData(
+ const web_package::SignedWebBundleId& web_bundle_id,
+ const WebApp::IsolationData& isolation_data) {
+ const auto* kr_info =
+ IwaKeyDistributionInfoProvider::GetInstance()->GetKeyRotationInfo(
+ web_bundle_id.id());
+ CHECK(kr_info && kr_info->public_key)
+ << "`GetKeyRotationData()` must only be called if `LookupRotatedKey()` "
+ "has previously reported `KeyRotationLookupResult::kKeyFound`.";
+
+ const auto& rotated_key = *kr_info->public_key;
+
+ // Checks whether `rotated_key` is contained in
+ // `isolation_data.integrity_block_data`.
+ const bool current_installation_has_rk = IntegrityBlockDataHasRotatedKey(
+ isolation_data.integrity_block_data, rotated_key);
+ const auto& pending_update = isolation_data.pending_update_info();
+
+ // Checks whether `rotated_key` is contained in
+ // `isolation_data.pending_update_info.integrity_block_data`.
+ const bool pending_update_has_rk =
+ pending_update && IntegrityBlockDataHasRotatedKey(
+ pending_update->integrity_block_data, rotated_key);
+
+ return {.rotated_key = raw_ref(rotated_key),
+ .current_installation_has_rk = current_installation_has_rk,
+ .pending_update_has_rk = pending_update_has_rk};
+}
+
// static
std::unique_ptr<content::WebContents>
IsolatedWebAppInstallCommandHelper::CreateIsolatedWebAppWebContents(
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_command_helper.h b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_command_helper.h
index abbc878..e0aaff5 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_command_helper.h
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_command_helper.h
@@ -15,10 +15,12 @@
#include "base/functional/callback_forward.h"
#include "base/memory/weak_ptr.h"
#include "base/types/expected.h"
+#include "base/types/optional_ref.h"
#include "base/version.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_install_utils.h"
+#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_integrity_block.h"
#include "components/webapps/browser/installable/installable_logging.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom-forward.h"
@@ -68,6 +70,38 @@
GetIsolatedWebAppById(const WebAppRegistrar& registrar,
const webapps::AppId& iwa_id);
+enum class KeyRotationLookupResult { kNoKeyRotation, kKeyFound, kKeyBlocked };
+
+// Queries the `IwaKeyDistributionInfoProvider` whether there's
+// `KeyRotationInfo` associated with the given `web_bundle_id`.
+// * If there's no key found, returns `kNoKeyRotation`.
+// * If the rotated key is null, reflects this in `debug_log` and returns
+// `kKeyBlocked`.
+// * Otherwise, writes the key data into `debug_log` and returns `kKeyFound.`
+KeyRotationLookupResult LookupRotatedKey(
+ const web_package::SignedWebBundleId& web_bundle_id,
+ base::optional_ref<base::Value::Dict> debug_log = std::nullopt);
+
+// Provides the key rotation data associated with a particular IWA.
+struct KeyRotationData {
+ const raw_ref<const std::vector<uint8_t>> rotated_key;
+
+ // Tells whether the current app installation contains the rotated key
+ // (`iwa.isolation_data.integrity_block_data`).
+ bool current_installation_has_rk;
+
+ // Tells whether the pending update (if any) for the app contains the rotated
+ // key (`iwa.isolation_data.pending_update_info.integrity_block_data`).
+ bool pending_update_has_rk;
+};
+
+// Computes the key rotation data as outlined above.
+// This function must only be called if the result of invoking
+// `LookupRotatedKey()` has yielded `kKeyFound` (will CHECK otherwise).
+KeyRotationData GetKeyRotationData(
+ const web_package::SignedWebBundleId& web_bundle_id,
+ const WebApp::IsolationData& isolation_data);
+
// This is a helper class that contains methods which are shared between both
// install and update commands.
class IsolatedWebAppInstallCommandHelper {
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_prepare_apply_update_browsertest.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_prepare_apply_update_browsertest.cc
index ba5927d..e990938 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_prepare_apply_update_browsertest.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_prepare_apply_update_browsertest.cc
@@ -9,8 +9,10 @@
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/gmock_expected_support.h"
+#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/threading/thread_restrictions.h"
+#include "chrome/browser/component_updater/iwa_key_distribution_component_installer.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/install_isolated_web_app_command.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_apply_update_command.h"
@@ -20,6 +22,7 @@
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/integrity_block_data_matcher.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
+#include "chrome/browser/web_applications/isolated_web_apps/test/key_distribution/test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/test_signed_web_bundle_builder.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
@@ -35,11 +38,13 @@
namespace web_app {
namespace {
+using base::test::ErrorIs;
using base::test::HasValue;
using base::test::ValueIs;
using ::testing::_;
using ::testing::Eq;
using ::testing::Field;
+using ::testing::HasSubstr;
using ::testing::IsTrue;
class IsolatedWebAppInstallPrepareApplyUpdateCommandBrowserTest
@@ -120,6 +125,9 @@
}
bool is_dev_mode_ = GetParam();
+
+ base::test::ScopedFeatureList features_{
+ component_updater::kIwaKeyDistributionComponent};
};
IN_PROC_BROWSER_TEST_P(
@@ -198,6 +206,96 @@
test::GetDefaultEcdsaP256KeyPair().public_key))));
}
+IN_PROC_BROWSER_TEST_P(
+ IsolatedWebAppInstallPrepareApplyUpdateCommandBrowserTest,
+ SucceedsSameVersionWithKeyRotation) {
+ auto web_bundle_id = test::GetDefaultEd25519WebBundleId();
+ SetTrustedWebBundleIdsForTesting({web_bundle_id});
+
+ base::Version version("1.0.0");
+
+ // IWA signed by a Ed25519 key.
+ auto iwa =
+ IsolatedWebAppBuilder(ManifestBuilder()
+ .SetName("installed app")
+ .SetVersion(version.GetString()))
+ .BuildBundle(web_bundle_id, {test::GetDefaultEd25519KeyPair()});
+
+ // Same-version IWA signed by a Ecdsa P-256 key.
+ auto update_iwa =
+ IsolatedWebAppBuilder(ManifestBuilder()
+ .SetName("updated app")
+ .SetVersion(version.GetString()))
+ .BuildBundle(web_bundle_id, {test::GetDefaultEcdsaP256KeyPair()});
+
+ auto ed25519_pk = test::GetDefaultEd25519KeyPair().public_key;
+ auto ecdsa_p256_pk = test::GetDefaultEcdsaP256KeyPair().public_key;
+
+ // Step 1: Install `iwa` and validate web app data.
+ ASSERT_OK_AND_ASSIGN(auto install_result,
+ Install(web_bundle_id, iwa->path(), version));
+
+ ASSERT_THAT(
+ GetIsolatedWebAppFor(web_bundle_id),
+ test::IwaIs(Eq("installed app"),
+ test::IsolationDataIs(
+ install_result.location, version,
+ /*controlled_frame_partitions=*/_,
+ /*pending_update_info=*/std::nullopt,
+ /*integrity_block_data=*/
+ test::IntegrityBlockDataPublicKeysAre(ed25519_pk))));
+
+ // Step 2: Ensure that update fails without key rotation.
+ ASSERT_THAT(
+ PrepareAndStoreUpdateInfo(web_bundle_id, update_iwa->path(), version),
+ ErrorIs(_));
+
+ // Step 3: point `web_bundle_id` (which is Ed25519-based) to the default
+ // Ecdsa P-256 public key via the Key Distribution Component. This should
+ // allow us to perform a same-version update to replace the underlying
+ // bundle signed by a corrupted key.
+ EXPECT_THAT(
+ test::InstallIwaKeyDistributionComponent(
+ base::Version("0.1.0"), web_bundle_id.id(), ecdsa_p256_pk.bytes()),
+ HasValue());
+
+ // Step 4: Prepare the update based on `update_iwa` and validate pending info.
+ ASSERT_OK_AND_ASSIGN(
+ auto prep_store_update_result,
+ PrepareAndStoreUpdateInfo(web_bundle_id, update_iwa->path(), version));
+ ASSERT_THAT(
+ prep_store_update_result,
+ Field(&IsolatedWebAppUpdatePrepareAndStoreCommandSuccess::update_version,
+ Eq(version)));
+
+ ASSERT_THAT(
+ GetIsolatedWebAppFor(web_bundle_id),
+ test::IwaIs(Eq("installed app"),
+ test::IsolationDataIs(
+ install_result.location, version,
+ /*controlled_frame_partitions=*/_,
+ /*pending_update_info=*/
+ test::PendingUpdateInfoIs(
+ prep_store_update_result.location, version,
+ test::IntegrityBlockDataPublicKeysAre(ecdsa_p256_pk)),
+ /*integrity_block_data=*/
+ test::IntegrityBlockDataPublicKeysAre(ed25519_pk))));
+
+ // Step 5: Apply the update and ensure that pending info has been
+ // successfully transferred.
+ ASSERT_THAT(ApplyUpdate(web_bundle_id), HasValue());
+
+ ASSERT_THAT(
+ GetIsolatedWebAppFor(web_bundle_id),
+ test::IwaIs(Eq("updated app"),
+ test::IsolationDataIs(
+ prep_store_update_result.location, version,
+ /*controlled_frame_partitions=*/_,
+ /*pending_update_info=*/std::nullopt,
+ /*integrity_block_data=*/
+ test::IntegrityBlockDataPublicKeysAre(ecdsa_p256_pk))));
+}
+
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
IsolatedWebAppInstallPrepareApplyUpdateCommandBrowserTest,
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_integrity_block_data.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_integrity_block_data.cc
index d31dd22..172b91c 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_integrity_block_data.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_integrity_block_data.cc
@@ -193,4 +193,20 @@
})));
}
+bool IsolatedWebAppIntegrityBlockData::HasPublicKey(
+ base::span<const uint8_t> public_key) const {
+ return base::ranges::any_of(signatures(), [&](const auto& signature_info) {
+ return absl::visit(
+ base::Overloaded{
+ [&](const auto& signature_info) {
+ return base::ranges::equal(signature_info.public_key().bytes(),
+ public_key);
+ },
+ [](const web_package::SignedWebBundleSignatureInfoUnknown&) {
+ return false;
+ }},
+ signature_info);
+ });
+}
+
} // namespace web_app
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_integrity_block_data.h b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_integrity_block_data.h
index d46e357..10a1debe9 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_integrity_block_data.h
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_integrity_block_data.h
@@ -38,6 +38,8 @@
return signatures_;
}
+ bool HasPublicKey(base::span<const uint8_t> public_key) const;
+
base::Value AsDebugValue() const;
private:
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_prepare_and_store_update_command.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_prepare_and_store_update_command.cc
index 5a3da67a..f5dbbc2 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_prepare_and_store_update_command.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_prepare_and_store_update_command.cc
@@ -29,7 +29,6 @@
#include "chrome/browser/web_applications/commands/web_app_command.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_features.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_command_helper.h"
-#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_integrity_block_data.h"
#include "chrome/browser/web_applications/isolated_web_apps/pending_install_info.h"
#include "chrome/browser/web_applications/locks/app_lock.h"
#include "chrome/browser/web_applications/web_app.h"
@@ -169,12 +168,31 @@
GetIsolatedWebAppById(lock_->registrar(), url_info_.app_id()),
[&](const std::string& error) { ReportFailure(error); });
const auto& isolation_data = *iwa.isolation_data();
-
installed_version_ = isolation_data.version;
GetMutableDebugValue().Set("installed_version",
installed_version_->GetString());
- if (expected_version_.has_value() &&
- *expected_version_ <= *installed_version_) {
+
+ switch (LookupRotatedKey(url_info_.web_bundle_id(), GetMutableDebugValue())) {
+ case KeyRotationLookupResult::kNoKeyRotation:
+ break;
+ case KeyRotationLookupResult::kKeyFound: {
+ KeyRotationData data =
+ GetKeyRotationData(url_info_.web_bundle_id(), isolation_data);
+ rotated_key_ = *data.rotated_key;
+ if (!data.current_installation_has_rk) {
+ same_version_update_allowed_by_key_rotation_ = true;
+ }
+ } break;
+ case KeyRotationLookupResult::kKeyBlocked:
+ ReportFailure(
+ "The web bundle id for this app's bundle has been blocked by the key "
+ "distribution component.");
+ return;
+ }
+
+ if (expected_version_ && (*expected_version_ < *installed_version_ ||
+ (*expected_version_ == *installed_version_ &&
+ !same_version_update_allowed_by_key_rotation_))) {
ReportFailure(base::StrCat({"Installed app is already on version ",
installed_version_->GetString(),
". Cannot update to version ",
@@ -245,6 +263,13 @@
if (integrity_block) {
integrity_block_data_ =
IsolatedWebAppIntegrityBlockData::FromIntegrityBlock(*integrity_block);
+ if (rotated_key_ && !integrity_block_data_->HasPublicKey(*rotated_key_)) {
+ ReportFailure(
+ "The update's integrity block data doesn't contain the required "
+ "public key as instructed by the key distribution component -- the "
+ "update won't succeed.");
+ return;
+ }
}
// TODO(cmfcmf): Maybe we should log somewhere when the storage partition is
@@ -300,7 +325,9 @@
CHECK_EQ(*expected_version_, install_info.isolated_web_app_version);
}
- if (install_info.isolated_web_app_version <= *installed_version_) {
+ if (install_info.isolated_web_app_version < *installed_version_ ||
+ (install_info.isolated_web_app_version == *installed_version_ &&
+ !same_version_update_allowed_by_key_rotation_)) {
ReportFailure(base::StrCat(
{"Installed app is already on version ",
installed_version_->GetString(), ". Cannot update to version ",
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_prepare_and_store_update_command.h b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_prepare_and_store_update_command.h
index 26a7479..f23c97ce 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_prepare_and_store_update_command.h
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_prepare_and_store_update_command.h
@@ -208,6 +208,10 @@
// The inferred integrity block data of the update bundle being processed.
std::optional<IsolatedWebAppIntegrityBlockData> integrity_block_data_;
+ bool same_version_update_allowed_by_key_rotation_ = false;
+ // Key Rotation data for this IWA.
+ std::optional<std::vector<uint8_t>> rotated_key_;
+
std::optional<IwaSourceWithModeAndFileOp> update_source_;
std::optional<IwaSourceWithMode> destination_location_;
std::optional<IsolatedWebAppStorageLocation> destination_storage_location_;
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_discovery_task.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_discovery_task.cc
index 4ba72ff..2518d43 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_discovery_task.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_discovery_task.cc
@@ -21,6 +21,7 @@
#include "base/types/expected_macros.h"
#include "base/version.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_downloader.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_command_helper.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_prepare_and_store_update_command.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/update_manifest/update_manifest.h"
@@ -180,27 +181,52 @@
[&](const std::string&) { FailWith(Error::kIwaNotInstalled); });
const auto& isolation_data = *iwa.isolation_data();
base::Version currently_installed_version = isolation_data.version;
-
debug_log_.Set("currently_installed_version",
currently_installed_version.GetString());
- if (isolation_data.pending_update_info() &&
- isolation_data.pending_update_info()->version ==
- latest_version_entry->version()) {
- // If we already have a pending update for this version, stop. However, we
- // do allow overwriting a pending update with a different pending update
- // version.
+ const auto& pending_update = isolation_data.pending_update_info();
+
+ bool same_version_update_allowed_by_key_rotation = false;
+ bool pending_info_overwrite_allowed_by_key_rotation = false;
+ switch (LookupRotatedKey(url_info_.web_bundle_id(), debug_log_)) {
+ case KeyRotationLookupResult::kNoKeyRotation:
+ break;
+ case KeyRotationLookupResult::kKeyFound: {
+ KeyRotationData data =
+ GetKeyRotationData(url_info_.web_bundle_id(), isolation_data);
+ if (!data.current_installation_has_rk) {
+ same_version_update_allowed_by_key_rotation = true;
+ }
+ if (!data.pending_update_has_rk) {
+ pending_info_overwrite_allowed_by_key_rotation = true;
+ }
+ } break;
+ case KeyRotationLookupResult::kKeyBlocked: {
+ FailWith(Error::kUpdateManifestNoApplicableVersion);
+ return;
+ }
+ }
+
+ if (pending_update &&
+ pending_update->version == latest_version_entry->version() &&
+ !pending_info_overwrite_allowed_by_key_rotation) {
+ // If we already have a pending update for this version, stop. However,
+ // we do allow overwriting a pending update with a different pending
+ // update version or if there's a chance that this will yield a bundle
+ // signed by a rotated key.
SucceedWith(Success::kUpdateAlreadyPending);
return;
}
// Since this task is not holding any `WebAppLock`s, there is no guarantee
- // that the installed version of the IWA won't change in the time between now
- // and when we schedule the `IsolatedWebAppUpdatePrepareAndStoreCommand`. This
- // is not an issue, as `IsolatedWebAppUpdatePrepareAndStoreCommand` will
- // re-check that the new version is indeed newer than the currently installed
- // version.
- if (currently_installed_version >= latest_version_entry->version()) {
+ // that the installed version of the IWA won't change in the time between
+ // now and when we schedule the
+ // `IsolatedWebAppUpdatePrepareAndStoreCommand`. This is not an issue, as
+ // `IsolatedWebAppUpdatePrepareAndStoreCommand` will re-check that the new
+ // version is indeed newer than the currently installed version.
+ if (currently_installed_version > latest_version_entry->version() ||
+ (currently_installed_version == latest_version_entry->version() &&
+ !same_version_update_allowed_by_key_rotation)) {
// Never downgrade apps for now.
SucceedWith(Success::kNoUpdateFound);
return;
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager.cc
index 3068782d..33532f1 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager.cc
@@ -134,7 +134,7 @@
// Similar to extensions, we don't do any automatic updates in guest
// sessions.
!profile.IsGuestSession() &&
- // Web Apps are not a thing in off the record profiles, but have this
+ // Web Apps are not a thing in off the record profiles, but have
// here just in case - we also wouldn't want to automatically update
// IWAs in incognito windows.
!profile.IsOffTheRecord() &&
@@ -163,6 +163,8 @@
has_started_ = true;
install_manager_observation_.Observe(&provider_->install_manager());
+ key_distribution_info_observation_.Observe(
+ IwaKeyDistributionInfoProvider::GetInstance());
if (!IsAnyIwaInstalled()) {
// If no IWA is installed, then we do not need to regularly check for
@@ -353,6 +355,45 @@
std::move(callback)));
}
+void IsolatedWebAppUpdateManager::OnComponentUpdateSuccess(
+ const base::Version& component_version) {
+ // The corresponding observer is added during `Start()`.
+ CHECK(has_started_);
+
+ if (!automatic_updates_enabled_) {
+ return;
+ }
+
+ // Queue updates for all apps affected by key rotation.
+ for (const WebApp& app : provider_->registrar_unsafe().GetApps()) {
+ if (!app.isolation_data()) {
+ continue;
+ }
+ const auto& isolation_data = *app.isolation_data();
+
+ auto url_info = IsolatedWebAppUrlInfo::Create(app.manifest_id());
+ if (!url_info.has_value()) {
+ continue;
+ }
+
+ auto result = LookupRotatedKey(url_info->web_bundle_id());
+ // If the rotated key is null, there's no point in updating the
+ // app (as the update won't succeed anyway).
+ if (result != KeyRotationLookupResult::kKeyFound) {
+ continue;
+ }
+ KeyRotationData data =
+ GetKeyRotationData(url_info->web_bundle_id(), isolation_data);
+ // If either the bundle or the pending update already includes the rotated
+ // key, there's no need to rush with updates.
+ if (data.current_installation_has_rk || data.pending_update_has_rk) {
+ continue;
+ }
+
+ MaybeDiscoverUpdatesForApp(app.app_id());
+ }
+}
+
bool IsolatedWebAppUpdateManager::IsAnyIwaInstalled() {
for (const WebApp& app : provider_->registrar_unsafe().GetApps()) {
if (app.isolation_data().has_value()) {
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager.h b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager.h
index df13ca3a..68dabe3 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager.h
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager.h
@@ -28,6 +28,7 @@
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_apply_task.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_apply_waiter.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_discovery_task.h"
+#include "chrome/browser/web_applications/isolated_web_apps/key_distribution/iwa_key_distribution_info_provider.h"
#include "chrome/browser/web_applications/web_app_install_manager_observer.h"
#include "components/webapps/common/web_app_id.h"
@@ -78,7 +79,9 @@
//
// TODO(crbug.com/40274187): Consider only executing update discovery tasks when
// the user is not on a metered/paid internet connection.
-class IsolatedWebAppUpdateManager : public WebAppInstallManagerObserver {
+class IsolatedWebAppUpdateManager
+ : public WebAppInstallManagerObserver,
+ public IwaKeyDistributionInfoProvider::Observer {
public:
explicit IsolatedWebAppUpdateManager(
Profile& profile,
@@ -244,6 +247,10 @@
base::Value::List update_apply_results_log_;
};
+ // IwaKeyDistributionInfoProvider::Observer:
+ void OnComponentUpdateSuccess(
+ const base::Version& component_version) override;
+
bool IsAnyIwaInstalled();
// Queues new update discovery tasks and returns the number of new tasks that
@@ -345,6 +352,10 @@
base::ScopedObservation<WebAppInstallManager, WebAppInstallManagerObserver>
install_manager_observation_{this};
+ base::ScopedObservation<IwaKeyDistributionInfoProvider,
+ IwaKeyDistributionInfoProvider::Observer>
+ key_distribution_info_observation_{this};
+
class LocalDevModeUpdateDiscoverer;
std::unique_ptr<LocalDevModeUpdateDiscoverer>
local_dev_mode_update_discoverer_;
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager_browsertest.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager_browsertest.cc
index e3652242..58a64071 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager_browsertest.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager_browsertest.cc
@@ -7,6 +7,7 @@
#include <optional>
#include <string_view>
+#include "base/base64.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/scoped_observation.h"
@@ -20,6 +21,7 @@
#include "base/test/test_future.h"
#include "base/test/test_timeouts.h"
#include "base/types/expected.h"
+#include "chrome/browser/component_updater/iwa_key_distribution_component_installer.h"
#include "chrome/browser/prefs/session_startup_pref.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
@@ -31,7 +33,9 @@
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_constants.h"
+#include "chrome/browser/web_applications/isolated_web_apps/test/integrity_block_data_matcher.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
+#include "chrome/browser/web_applications/isolated_web_apps/test/key_distribution/test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/test_signed_web_bundle_builder.h"
#include "chrome/browser/web_applications/test/web_app_icon_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test_observers.h"
@@ -164,9 +168,6 @@
web_package::SignedWebBundleId GetWebBundleId() const {
return test::GetDefaultEd25519WebBundleId();
}
- GURL GetUpdateManifestUrl() const {
- return update_server_mixin_.GetUpdateManifestUrl(GetWebBundleId());
- }
const WebApp* GetIsolatedWebApp(const webapps::AppId& app_id) {
return provider().registrar_unsafe().GetAppById(app_id);
@@ -198,10 +199,8 @@
profile()->GetPrefs()->SetList(
prefs::kIsolatedWebAppInstallForceList,
base::Value::List().Append(
- base::Value::Dict()
- .Set(kPolicyWebBundleIdKey, GetWebBundleId().id())
- .Set(kPolicyUpdateManifestUrlKey,
- GetUpdateManifestUrl().spec())));
+ update_server_mixin_.CreateForceInstallPolicyEntry(
+ GetWebBundleId())));
web_app::WebAppTestInstallObserver(browser()->profile())
.BeginListeningAndWait({GetAppId()});
@@ -238,10 +237,8 @@
profile()->GetPrefs()->SetList(
prefs::kIsolatedWebAppInstallForceList,
base::Value::List().Append(
- base::Value::Dict()
- .Set(kPolicyWebBundleIdKey, GetWebBundleId().id())
- .Set(kPolicyUpdateManifestUrlKey,
- GetUpdateManifestUrl().spec())));
+ update_server_mixin_.CreateForceInstallPolicyEntry(
+ GetWebBundleId())));
web_app::WebAppTestInstallObserver(browser()->profile())
.BeginListeningAndWait({GetAppId()});
@@ -297,10 +294,8 @@
profile()->GetPrefs()->SetList(
prefs::kIsolatedWebAppInstallForceList,
base::Value::List().Append(
- base::Value::Dict()
- .Set(kPolicyWebBundleIdKey, GetWebBundleId().id())
- .Set(kPolicyUpdateManifestUrlKey,
- GetUpdateManifestUrl().spec())));
+ update_server_mixin_.CreateForceInstallPolicyEntry(
+ GetWebBundleId())));
SessionStartupPref pref(SessionStartupPref::LAST);
SessionStartupPref::SetStartupPref(profile(), pref);
@@ -358,5 +353,91 @@
}
#endif // !BUILDFLAG(IS_CHROMEOS_LACROS)
+class IsolatedWebAppUpdateManagerWithKeyRotationBrowserTest
+ : public IsolatedWebAppBrowserTestHarness {
+ public:
+ IsolatedWebAppUpdateManagerWithKeyRotationBrowserTest() {
+ scoped_feature_list_.InitWithFeatures(
+ {features::kIsolatedWebAppAutomaticUpdates,
+ component_updater::kIwaKeyDistributionComponent},
+ {});
+ }
+
+ const WebApp* GetIsolatedWebApp(const webapps::AppId& app_id) {
+ return provider().registrar_unsafe().GetAppById(app_id);
+ }
+
+ protected:
+ void AddBundleSignedBy(
+ const web_package::WebBundleSigner::KeyPair& key_pair) {
+ update_server_mixin_.AddBundle(
+ IsolatedWebAppBuilder(
+ ManifestBuilder().SetName("app-1.0.0").SetVersion("1.0.0"))
+ .BuildBundle(web_bundle_id_, {key_pair}));
+ }
+
+ IsolatedWebAppUpdateServerMixin update_server_mixin_{&mixin_host_};
+ base::test::ScopedFeatureList scoped_feature_list_;
+
+ web_package::SignedWebBundleId web_bundle_id_ =
+ test::GetDefaultEd25519WebBundleId();
+};
+
+IN_PROC_BROWSER_TEST_F(IsolatedWebAppUpdateManagerWithKeyRotationBrowserTest,
+ Succeeds) {
+ auto app_id =
+ IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(web_bundle_id_)
+ .app_id();
+
+ // Add a bundle with version 1.0.0 signed by the original key corresponding to
+ // `web_bundle_id_`.
+ AddBundleSignedBy(test::GetDefaultEd25519KeyPair());
+
+ profile()->GetPrefs()->SetList(
+ prefs::kIsolatedWebAppInstallForceList,
+ base::Value::List().Append(
+ update_server_mixin_.CreateForceInstallPolicyEntry(web_bundle_id_)));
+
+ web_app::WebAppTestInstallObserver(browser()->profile())
+ .BeginListeningAndWait({app_id});
+
+ EXPECT_THAT(
+ GetIsolatedWebApp(app_id),
+ test::IwaIs(Eq("app-1.0.0"),
+ test::IsolationDataIs(
+ /*location=*/_, Eq(base::Version("1.0.0")),
+ /*controlled_frame_partitions=*/_,
+ /*pending_update_info=*/Eq(std::nullopt),
+ /*integrity_block_data=*/
+ test::IntegrityBlockDataPublicKeysAre(
+ test::GetDefaultEd25519KeyPair().public_key))));
+
+ // Add a bundle with version 1.0.0 signed by a rotated key.
+ AddBundleSignedBy(test::GetDefaultEcdsaP256KeyPair());
+
+ WebAppTestManifestUpdatedObserver manifest_updated_observer(
+ &provider().install_manager());
+ manifest_updated_observer.BeginListening({app_id});
+ // Key rotation should trigger a discovery in the update manager.
+ EXPECT_THAT(
+ test::InstallIwaKeyDistributionComponent(
+ base::Version("0.1.0"), test::GetDefaultEd25519WebBundleId().id(),
+ test::GetDefaultEcdsaP256KeyPair().public_key.bytes()),
+ HasValue());
+ manifest_updated_observer.Wait();
+
+ // The app's integrity block data must be different now due to an update.
+ EXPECT_THAT(
+ GetIsolatedWebApp(app_id),
+ test::IwaIs(Eq("app-1.0.0"),
+ test::IsolationDataIs(
+ /*location=*/_, Eq(base::Version("1.0.0")),
+ /*controlled_frame_partitions=*/_,
+ /*pending_update_info=*/Eq(std::nullopt),
+ /*integrity_block_data=*/
+ test::IntegrityBlockDataPublicKeysAre(
+ test::GetDefaultEcdsaP256KeyPair().public_key))));
+}
+
} // namespace
} // namespace web_app
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.cc
index 41cd670..2b80552 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.cc
@@ -11,6 +11,7 @@
#include "base/strings/string_split.h"
#include "base/types/expected_macros.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
+#include "chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_constants.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "net/http/http_status_code.h"
@@ -45,6 +46,15 @@
base::StrCat({"/", web_bundle_id.id(), "/", kUpdateManifestFileName}));
}
+base::Value::Dict
+IsolatedWebAppUpdateServerMixin::CreateForceInstallPolicyEntry(
+ const web_package::SignedWebBundleId& web_bundle_id) const {
+ return base::Value::Dict()
+ .Set(kPolicyWebBundleIdKey, web_bundle_id.id())
+ .Set(kPolicyUpdateManifestUrlKey,
+ GetUpdateManifestUrl(web_bundle_id).spec());
+}
+
void IsolatedWebAppUpdateServerMixin::AddBundle(
std::unique_ptr<BundledIsolatedWebApp> bundle) {
auto* bundle_ptr = bundle.get();
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.h b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.h
index 39a412f0..2759762 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.h
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_server_mixin.h
@@ -36,6 +36,11 @@
GURL GetUpdateManifestUrl(
const web_package::SignedWebBundleId& web_bundle_id) const;
+ // Generates a policy entry that can be appended to
+ // `prefs::kIsolatedWebAppInstallForceList` in order to force-install the IWA.
+ base::Value::Dict CreateForceInstallPolicyEntry(
+ const web_package::SignedWebBundleId& web_bundle_id) const;
+
// Adds a bundle to the update server and starts tracking it in the
// corresponding update manifest.
void AddBundle(std::unique_ptr<BundledIsolatedWebApp> bundle);
diff --git a/chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h b/chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h
index c0e875b..41419e4 100644
--- a/chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h
+++ b/chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h
@@ -133,7 +133,7 @@
const base::FilePath path,
ManifestBuilder manifest_builder);
- ~BundledIsolatedWebApp();
+ virtual ~BundledIsolatedWebApp();
const base::FilePath& path() const { return path_; }
@@ -168,7 +168,7 @@
const std::vector<uint8_t> serialized_bundle,
ManifestBuilder manifest_builder);
- ~ScopedBundledIsolatedWebApp();
+ ~ScopedBundledIsolatedWebApp() override;
private:
ScopedBundledIsolatedWebApp(
diff --git a/chrome/browser/web_applications/isolated_web_apps/test/key_distribution/test_utils.cc b/chrome/browser/web_applications/isolated_web_apps/test/key_distribution/test_utils.cc
index a86a5ec0..aacf648af 100644
--- a/chrome/browser/web_applications/isolated_web_apps/test/key_distribution/test_utils.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/test/key_distribution/test_utils.cc
@@ -76,11 +76,11 @@
base::expected<void, IwaKeyDistributionInfoProvider::ComponentUpdateError>
UpdateKeyDistributionInfo(const base::Version& version,
- const IwaKeyDistribution& kr_proto) {
+ const IwaKeyDistribution& kd_proto) {
base::ScopedTempDir component_install_dir;
CHECK(component_install_dir.CreateUniqueTempDir());
auto path = component_install_dir.GetPath().AppendASCII("krc");
- CHECK(base::WriteFile(path, kr_proto.SerializeAsString()));
+ CHECK(base::WriteFile(path, kd_proto.SerializeAsString()));
return UpdateKeyDistributionInfo(version, path);
}
@@ -103,7 +103,12 @@
base::expected<void, IwaKeyDistributionInfoProvider::ComponentUpdateError>
InstallIwaKeyDistributionComponent(const base::Version& version,
- const IwaKeyDistribution& kr_proto) {
+ const IwaKeyDistribution& kd_proto) {
+ CHECK(base::FeatureList::IsEnabled(
+ component_updater::kIwaKeyDistributionComponent))
+ << "The `IwaKeyDistribution` feature must be enabled for the component "
+ "installation to succeed.";
+
using Installer =
component_updater::IwaKeyDistributionComponentInstallerPolicy;
base::ScopedAllowBlockingForTesting allow_blocking;
@@ -125,7 +130,7 @@
CHECK(base::CreateDirectory(install_dir));
CHECK(base::WriteFile(install_dir.Append(Installer::kDataFileName),
- kr_proto.SerializeAsString()));
+ kd_proto.SerializeAsString()));
// Write a manifest file. This is needed for component updater to detect any
// existing component on disk.
@@ -146,4 +151,24 @@
return result;
}
+base::expected<void, IwaKeyDistributionInfoProvider::ComponentUpdateError>
+InstallIwaKeyDistributionComponent(
+ const base::Version& version,
+ const std::string& web_bundle_id,
+ std::optional<base::span<const uint8_t>> expected_key) {
+ IwaKeyRotations::KeyRotationInfo kr_info;
+ if (expected_key) {
+ kr_info.set_expected_key(base::Base64Encode(*expected_key));
+ }
+
+ IwaKeyRotations key_rotations;
+ key_rotations.mutable_key_rotations()->emplace(web_bundle_id,
+ std::move(kr_info));
+
+ IwaKeyDistribution key_distribution;
+ *key_distribution.mutable_key_rotation_data() = std::move(key_rotations);
+
+ return InstallIwaKeyDistributionComponent(version, key_distribution);
+}
+
} // namespace web_app::test
diff --git a/chrome/browser/web_applications/isolated_web_apps/test/key_distribution/test_utils.h b/chrome/browser/web_applications/isolated_web_apps/test/key_distribution/test_utils.h
index 4e3f5d5c5..ff3044a 100644
--- a/chrome/browser/web_applications/isolated_web_apps/test/key_distribution/test_utils.h
+++ b/chrome/browser/web_applications/isolated_web_apps/test/key_distribution/test_utils.h
@@ -23,10 +23,10 @@
const base::FilePath& path);
// Synchronously updates the key distribution info provider with the given
-// `kr_proto`.
+// `kd_proto`.
base::expected<void, IwaKeyDistributionInfoProvider::ComponentUpdateError>
UpdateKeyDistributionInfo(const base::Version& version,
- const IwaKeyDistribution& kr_proto);
+ const IwaKeyDistribution& kd_proto);
// Synchronously updates the key distribution info provider with a protobuf
// that maps `web_bundle_id` to `expected_key`. If `expected_key` is a nullopt,
@@ -37,13 +37,21 @@
const std::string& web_bundle_id,
std::optional<base::span<const uint8_t>> expected_key);
-// Writes `kr_proto` into `DIR_COMPONENT_USER/IwaKeyDistribution/{version}` and
+// Writes `kd_proto` into `DIR_COMPONENT_USER/IwaKeyDistribution/{version}` and
// triggers the registration process with the component updater. The directory
// is deleted once IwaKeyDistributionInfoProvider has processed the update
// (regardless of the outcome).
base::expected<void, IwaKeyDistributionInfoProvider::ComponentUpdateError>
InstallIwaKeyDistributionComponent(const base::Version& version,
- const IwaKeyDistribution& kr_proto);
+ const IwaKeyDistribution& kd_proto);
+
+// A shortcut for the above function that populates only the key rotation part
+// of the proto.
+base::expected<void, IwaKeyDistributionInfoProvider::ComponentUpdateError>
+InstallIwaKeyDistributionComponent(
+ const base::Version& version,
+ const std::string& web_bundle_id,
+ std::optional<base::span<const uint8_t>> expected_key);
} // namespace web_app::test