| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| #include <optional> |
| |
| #include "base/auto_reset.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/gmock_expected_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/repeating_test_future.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "base/types/expected.h" |
| #include "components/web_package/mojom/web_bundle_parser.mojom.h" |
| #include "components/web_package/signed_web_bundles/ed25519_public_key.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/web_package/test_support/mock_web_bundle_parser_factory.h" |
| #include "components/web_package/test_support/signed_web_bundles/key_pair.h" |
| #include "components/web_package/test_support/signed_web_bundles/signature_verifier_test_utils.h" |
| #include "components/webapps/isolated_web_apps/error/uma_logging.h" |
| #include "components/webapps/isolated_web_apps/identity/iwa_identity_validator.h" |
| #include "components/webapps/isolated_web_apps/reading/response_reader.h" |
| #include "components/webapps/isolated_web_apps/reading/response_reader_factory.h" |
| #include "components/webapps/isolated_web_apps/reading/response_reader_registry.h" |
| #include "components/webapps/isolated_web_apps/reading/signed_web_bundle_reader.h" |
| #include "components/webapps/isolated_web_apps/reading/validator.h" |
| #include "components/webapps/isolated_web_apps/test_support/signed_web_bundle_utils.h" |
| #include "components/webapps/isolated_web_apps/test_support/signing_keys.h" |
| #include "components/webapps/isolated_web_apps/test_support/test_iwa_client.h" |
| #include "components/webapps/isolated_web_apps/test_support/test_signed_web_bundle_builder.h" |
| #include "components/webapps/isolated_web_apps/types/iwa_origin.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "content/public/test/test_browser_context.h" |
| #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/url_constants.h" |
| |
| namespace web_app { |
| |
| namespace { |
| |
| using base::test::ErrorIs; |
| using base::test::HasValue; |
| using base::test::RunOnceCallback; |
| using testing::_; |
| using testing::AllOf; |
| using testing::ElementsAre; |
| using testing::Field; |
| using testing::HasSubstr; |
| using testing::Return; |
| |
| using ReadResponseError = IsolatedWebAppReaderRegistry::ReadResponseError; |
| using VerifierError = web_package::SignedWebBundleSignatureVerifier::Error; |
| |
| constexpr uint8_t kEd25519PublicKey[32] = {0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, |
| 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, |
| 0, 0, 0, 0, 2, 2, 2, 0, 0, 0}; |
| |
| constexpr uint8_t kEd25519Signature[64] = { |
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, |
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 7, 0, 0, 0, 0, |
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 0, 0}; |
| |
| } // namespace |
| |
| class IsolatedWebAppReaderRegistryTest : public ::testing::Test { |
| protected: |
| void SetUp() override { |
| scoped_feature_list_.InitAndEnableFeature(features::kIsolatedWebApps); |
| IwaIdentityValidator::CreateSingleton(); |
| ON_CALL(iwa_client_, ValidateTrust(_, kWebBundleId, _)) |
| .WillByDefault(Return(base::ok())); |
| |
| browser_context_ = std::make_unique<content::TestBrowserContext>(); |
| |
| parser_factory_ = std::make_unique<web_package::MockWebBundleParserFactory>( |
| on_create_parser_future_.GetCallback()); |
| |
| response_ = web_package::mojom::BundleResponse::New(); |
| response_->response_code = 200; |
| response_->payload_offset = 0; |
| response_->payload_length = sizeof(kResponseBody) - 1; |
| |
| base::flat_map<GURL, web_package::mojom::BundleResponseLocationPtr> |
| requests; |
| requests.insert( |
| {kUrl, web_package::mojom::BundleResponseLocation::New( |
| response_->payload_offset, response_->payload_length)}); |
| |
| metadata_ = web_package::mojom::BundleMetadata::New(); |
| metadata_->requests = std::move(requests); |
| |
| auto signature_info_ed25519 = |
| web_package::mojom::SignatureInfoEd25519::New(); |
| signature_info_ed25519->public_key = |
| web_package::Ed25519PublicKey::Create(base::span(kEd25519PublicKey)); |
| signature_info_ed25519->signature = |
| web_package::Ed25519Signature::Create(base::span(kEd25519Signature)); |
| |
| web_package::mojom::BundleIntegrityBlockSignatureStackEntryPtr |
| signature_stack_entry = |
| web_package::mojom::BundleIntegrityBlockSignatureStackEntry::New(); |
| signature_stack_entry->signature_info = |
| web_package::mojom::SignatureInfo::NewEd25519( |
| std::move(signature_info_ed25519)); |
| |
| std::vector<web_package::mojom::BundleIntegrityBlockSignatureStackEntryPtr> |
| signature_stack; |
| signature_stack.push_back(std::move(signature_stack_entry)); |
| |
| integrity_block_ = web_package::mojom::BundleIntegrityBlock::New(); |
| integrity_block_->size = 42; |
| integrity_block_->signature_stack = std::move(signature_stack); |
| integrity_block_->attributes = |
| web_package::test::GetAttributesForSignedWebBundleId(kWebBundleId.id()); |
| |
| registry_ = std::make_unique<IsolatedWebAppReaderRegistry>( |
| browser_context_.get(), |
| std::make_unique<IsolatedWebAppResponseReaderFactory>( |
| browser_context_.get())); |
| |
| EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| EXPECT_TRUE( |
| CreateTemporaryFileInDir(temp_dir_.GetPath(), &web_bundle_path_)); |
| EXPECT_TRUE(base::WriteFile(web_bundle_path_, kResponseBody)); |
| |
| in_process_data_decoder_.SetWebBundleParserFactoryBinder( |
| base::BindRepeating( |
| &web_package::MockWebBundleParserFactory::AddReceiver, |
| base::Unretained(parser_factory_.get()))); |
| } |
| |
| void TearDown() override { |
| registry_.reset(); |
| browser_context_.reset(); |
| } |
| |
| void FulfillIntegrityBlock() { |
| parser_factory_->RunIntegrityBlockCallback(integrity_block_->Clone()); |
| } |
| |
| void FulfillMetadata() { |
| parser_factory_->RunMetadataCallback(integrity_block_->size, |
| metadata_->Clone()); |
| } |
| |
| void FulfillResponse(const network::ResourceRequest& resource_request) { |
| parser_factory_->RunResponseCallback( |
| web_package::mojom::BundleResponseLocation::New( |
| response_->payload_offset, response_->payload_length), |
| response_->Clone()); |
| } |
| |
| test::MockIwaClient& iwa_client() { return iwa_client_; } |
| |
| content::BrowserTaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| base::test::ScopedFeatureList scoped_feature_list_; |
| data_decoder::test::InProcessDataDecoder in_process_data_decoder_; |
| base::ScopedTempDir temp_dir_; |
| base::FilePath web_bundle_path_; |
| base::test::RepeatingTestFuture<std::optional<GURL>> on_create_parser_future_; |
| std::unique_ptr<content::TestBrowserContext> browser_context_; |
| |
| const web_package::SignedWebBundleId kWebBundleId = |
| *web_package::SignedWebBundleId::Create( |
| "aaaaaaacaibaaaaaaaaaaaaaaiaaeaaaaaaaaaaaaabaeaqaaaaaaaic"); |
| const GURL kUrl = GURL("isolated-app://" + kWebBundleId.id()); |
| |
| constexpr static char kResponseBody[] = "test"; |
| |
| constexpr static char kInvalidIsolatedWebAppUrl[] = "isolated-app://foo/"; |
| |
| std::unique_ptr<IsolatedWebAppReaderRegistry> registry_; |
| std::unique_ptr<web_package::MockWebBundleParserFactory> parser_factory_; |
| web_package::mojom::BundleIntegrityBlockPtr integrity_block_; |
| web_package::mojom::BundleMetadataPtr metadata_; |
| web_package::mojom::BundleResponsePtr response_; |
| testing::StrictMock<web_package::test::MockSignatureVerifier> |
| signature_verifier_; |
| base::AutoReset<web_package::SignedWebBundleSignatureVerifier*> |
| reset_signature_verifier_ = |
| web_app::SignedWebBundleReader::SetSignatureVerifierForTesting( |
| &signature_verifier_); |
| testing::NiceMock<test::MockIwaClient> iwa_client_; |
| }; |
| |
| using ReadResult = |
| base::expected<IsolatedWebAppResponseReader::Response, ReadResponseError>; |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestSingleRequest) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| |
| EXPECT_EQ(IwaOrigin(kWebBundleId).origin().GetURL(), |
| on_create_parser_future_.Take()); |
| |
| histogram_tester.ExpectBucketCount( |
| ToSuccessHistogramName("WebApp.Isolated.SwbnFileUsability"), |
| /*success*/ 1, 1); |
| |
| std::string response_body = ReadAndFulfillResponseBody( |
| response.head()->payload_length, |
| base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody, |
| base::Unretained(&response))); |
| EXPECT_EQ(kResponseBody, response_body); |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, |
| ReadResponseWhenBundleIsNoLongerTrusted) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| { |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| EXPECT_THAT(read_response_future.Take(), HasValue()); |
| } |
| |
| testing::Mock::VerifyAndClearExpectations(&iwa_client()); |
| ON_CALL(iwa_client(), ValidateTrust(_, _, _)) |
| .WillByDefault(Return(base::unexpected("public key(s) are not trusted"))); |
| |
| { |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future.GetCallback()); |
| |
| EXPECT_THAT(read_response_future.Take(), |
| ErrorIs(Field(&ReadResponseError::message, |
| HasSubstr("public key(s) are not trusted")))); |
| } |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, |
| TestSingleRequestWithQueryAndFragment) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl.Resolve("/?bar=baz#foo"); |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| |
| std::string response_body = ReadAndFulfillResponseBody( |
| response.head()->payload_length, |
| base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody, |
| base::Unretained(&response))); |
| EXPECT_EQ(kResponseBody, response_body); |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestMixedDevModeAndProdModeRequests) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| // First, simulate a successful parsing of the integrity block, and read a |
| // response. |
| { |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future.GetCallback()); |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| } |
| |
| { |
| // A request to the already opened bundle should still succeed. |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future.GetCallback()); |
| FulfillResponse(resource_request); |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| } |
| |
| // Now revoke trust for this bundle and clear the cache. New requests will |
| // fail from now on. |
| testing::Mock::VerifyAndClearExpectations(&iwa_client()); |
| ON_CALL(iwa_client(), ValidateTrust(_, _, _)) |
| .WillByDefault(Return(base::unexpected("public key(s) are not trusted"))); |
| base::test::TestFuture<void> close_future; |
| |
| registry_->ClearCacheForPath(web_bundle_path_, close_future.GetCallback()); |
| EXPECT_TRUE(close_future.Wait()); |
| { |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future.GetCallback()); |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| EXPECT_THAT(read_response_future.Take(), |
| testing::Not(base::test::HasValue())); |
| } |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, |
| TestReadingResponseAfterSignedWebBundleReaderIsDeleted) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| |
| // Delete the registry so that the `SignedWebBundleReader`, which `response` |
| // holds onto weakly, is deleted, which should make `response.ReadBody()` |
| // fail with `net::ERR_FAILED`. |
| registry_.reset(); |
| |
| base::test::TestFuture<net::Error> error_future; |
| ReadResponseBody( |
| response.head()->payload_length, |
| base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody, |
| base::Unretained(&response)), |
| error_future.GetCallback()); |
| EXPECT_EQ(net::ERR_FAILED, error_future.Take()); |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestRequestToNonExistingResponse) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = GURL(kUrl.spec() + "foo"); |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| |
| ReadResult result = read_response_future.Take(); |
| EXPECT_THAT( |
| result, |
| ErrorIs(AllOf( |
| Field(&ReadResponseError::type, |
| ReadResponseError::Type::kResponseNotFound), |
| Field( |
| &ReadResponseError::message, |
| "Failed to read response from Signed Web Bundle: The Web Bundle " |
| "does not contain a response for the provided URL: " |
| "isolated-app://" |
| "aaaaaaacaibaaaaaaaaaaaaaaiaaeaaaaaaaaaaaaabaeaqaaaaaaaic/" |
| "foo")))); |
| |
| histogram_tester.ExpectBucketCount( |
| ToErrorHistogramName("WebApp.Isolated.ReadResponseHead"), |
| IsolatedWebAppReaderRegistry::ReadResponseHeadError:: |
| kResponseNotFoundError, |
| 1); |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestSignedWebBundleReaderLifetime) { |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| // signatures only verified once per session even for multiple calls |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| |
| // Verify that the cache cleanup timer has not yet started. |
| EXPECT_FALSE(registry_->IsCleanupTimerRunningForTesting()); |
| |
| { |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future.GetCallback()); |
| |
| // `SignedWebBundleReader`s should not be evicted from the cache while they |
| // are still parsing integrity block and metadata, thus the following two |
| // calls to fast forward time should not have any effect. |
| task_environment_.FastForwardBy(base::Hours(1)); |
| |
| FulfillIntegrityBlock(); |
| |
| task_environment_.FastForwardBy(base::Hours(1)); |
| |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| } |
| |
| // Verify that the cache cleanup timer has started. |
| EXPECT_TRUE(registry_->IsCleanupTimerRunningForTesting()); |
| |
| { |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future.GetCallback()); |
| |
| // Notably, no `FulfillIntegrityBlock` or `FulfillMetadata` here, since the |
| // `SignedWebBundleReader` should still be cached. |
| FulfillResponse(resource_request); |
| |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| } |
| |
| // Verify that the cache cleanup timer is still running. |
| EXPECT_TRUE(registry_->IsCleanupTimerRunningForTesting()); |
| |
| // After some time has passed, the `SignedWebBundleReader` should be evicted |
| // from the cache. |
| task_environment_.FastForwardBy(base::Hours(1)); |
| |
| // Verify that the cache cleanup timer has stopped, given that the cache is |
| // now empty again. |
| EXPECT_FALSE(registry_->IsCleanupTimerRunningForTesting()); |
| |
| { |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future.GetCallback()); |
| |
| // Since the SignedWebBundleReader has been evicted from cache, integrity |
| // block and metadata have to be read again. |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| } |
| |
| // Verify that the cache cleanup timer has started again. |
| EXPECT_TRUE(registry_->IsCleanupTimerRunningForTesting()); |
| } |
| |
| class IsolatedWebAppReaderRegistryIntegrityBlockParserErrorTest |
| : public IsolatedWebAppReaderRegistryTest, |
| public ::testing::WithParamInterface< |
| std::pair<web_package::mojom::BundleParseErrorType, |
| UnusableSwbnFileError::Error>> {}; |
| |
| TEST_P(IsolatedWebAppReaderRegistryIntegrityBlockParserErrorTest, |
| TestIntegrityBlockParserError) { |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| auto error = web_package::mojom::BundleIntegrityBlockParseError::New(); |
| error->type = GetParam().first; |
| error->message = "test error"; |
| parser_factory_->RunIntegrityBlockCallback(nullptr, std::move(error)); |
| |
| ReadResult result = read_response_future.Take(); |
| EXPECT_THAT( |
| result, |
| ErrorIs(AllOf( |
| Field(&ReadResponseError::type, ReadResponseError::Type::kOtherError), |
| Field(&ReadResponseError::message, |
| "Failed to parse integrity block: test error")))); |
| |
| histogram_tester.ExpectBucketCount( |
| ToErrorHistogramName("WebApp.Isolated.SwbnFileUsability"), |
| GetParam().second, 1); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| IsolatedWebAppReaderRegistryIntegrityBlockParserErrorTest, |
| ::testing::Values( |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kParserInternalError, |
| UnusableSwbnFileError::Error::kIntegrityBlockParserInternalError), |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kVersionError, |
| UnusableSwbnFileError::Error::kIntegrityBlockParserVersionError), |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kFormatError, |
| UnusableSwbnFileError::Error::kIntegrityBlockParserFormatError))); |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestInvalidIntegrityBlockContents) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| auto integrity_block = integrity_block_->Clone(); |
| // Simulate a failed validation by returning a different ID. |
| integrity_block->attributes = |
| web_package::test::GetAttributesForSignedWebBundleId( |
| web_package::test::GetDefaultEcdsaP256WebBundleId().id()); |
| parser_factory_->RunIntegrityBlockCallback(std::move(integrity_block)); |
| FulfillMetadata(); |
| |
| ReadResult result = read_response_future.Take(); |
| EXPECT_THAT( |
| result, |
| ErrorIs(AllOf( |
| Field(&ReadResponseError::type, ReadResponseError::Type::kOtherError), |
| Field(&ReadResponseError::message, |
| testing::HasSubstr("Failed to validate integrity block"))))); |
| |
| histogram_tester.ExpectBucketCount( |
| ToErrorHistogramName("WebApp.Isolated.SwbnFileUsability"), |
| UnusableSwbnFileError::Error::kIntegrityBlockValidationError, 1); |
| } |
| |
| class IsolatedWebAppReaderRegistrySignatureVerificationErrorTest |
| : public IsolatedWebAppReaderRegistryTest, |
| public ::testing::WithParamInterface<VerifierError> {}; |
| |
| TEST_P(IsolatedWebAppReaderRegistrySignatureVerificationErrorTest, |
| SignatureVerificationError) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::unexpected(GetParam()))); |
| #endif |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // On ChromeOS, signatures are only verified at installation-time, thus the |
| // `web_package::test::FakeSignatureVerifier` set up above will never be |
| // called. |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| EXPECT_THAT(read_response_future.Take(), HasValue()); |
| |
| histogram_tester.ExpectBucketCount( |
| ToSuccessHistogramName("WebApp.Isolated.SwbnFileUsability"), |
| /*success*/ 1, 1); |
| #else |
| ReadResult result = read_response_future.Take(); |
| EXPECT_THAT( |
| result, |
| ErrorIs(AllOf( |
| Field(&ReadResponseError::type, ReadResponseError::Type::kOtherError), |
| Field(&ReadResponseError::message, |
| base::StringPrintf("Failed to verify signatures: %s", |
| GetParam().message.c_str()))))); |
| |
| histogram_tester.ExpectBucketCount( |
| ToErrorHistogramName("WebApp.Isolated.SwbnFileUsability"), |
| UnusableSwbnFileError::Error::kSignatureVerificationError, 1); |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| IsolatedWebAppReaderRegistrySignatureVerificationErrorTest, |
| ::testing::Values(VerifierError::ForInternalError("internal error"), |
| VerifierError::ForInvalidSignature("invalid signature"))); |
| |
| class IsolatedWebAppReaderRegistryMetadataParserErrorTest |
| : public IsolatedWebAppReaderRegistryTest, |
| public ::testing::WithParamInterface< |
| std::pair<web_package::mojom::BundleParseErrorType, |
| UnusableSwbnFileError::Error>> {}; |
| |
| TEST_P(IsolatedWebAppReaderRegistryMetadataParserErrorTest, |
| TestMetadataParserError) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| auto error = web_package::mojom::BundleMetadataParseError::New(); |
| error->message = "test error"; |
| error->type = GetParam().first; |
| parser_factory_->RunMetadataCallback(integrity_block_->size, nullptr, |
| std::move(error)); |
| |
| ReadResult result = read_response_future.Take(); |
| EXPECT_THAT(result, |
| ErrorIs(AllOf(Field(&ReadResponseError::type, |
| ReadResponseError::Type::kOtherError), |
| Field(&ReadResponseError::message, |
| "Failed to parse metadata: test error")))); |
| |
| histogram_tester.ExpectBucketCount( |
| ToErrorHistogramName("WebApp.Isolated.SwbnFileUsability"), |
| GetParam().second, 1); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| IsolatedWebAppReaderRegistryMetadataParserErrorTest, |
| ::testing::Values( |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kParserInternalError, |
| UnusableSwbnFileError::Error::kMetadataParserInternalError), |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kVersionError, |
| UnusableSwbnFileError::Error::kMetadataParserVersionError), |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kFormatError, |
| UnusableSwbnFileError::Error::kMetadataParserFormatError))); |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestInvalidMetadataPrimaryUrl) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| auto metadata = metadata_->Clone(); |
| metadata->primary_url = kUrl; |
| parser_factory_->RunMetadataCallback(integrity_block_->size, |
| std::move(metadata)); |
| |
| ReadResult result = read_response_future.Take(); |
| EXPECT_THAT( |
| result, |
| ErrorIs(AllOf( |
| Field(&ReadResponseError::type, ReadResponseError::Type::kOtherError), |
| Field(&ReadResponseError::message, |
| base::StringPrintf( |
| "Failed to validate metadata: Primary URL must " |
| "not be present, but was %s", |
| kUrl.spec().c_str()))))); |
| |
| histogram_tester.ExpectBucketCount( |
| ToErrorHistogramName("WebApp.Isolated.SwbnFileUsability"), |
| UnusableSwbnFileError::Error::kMetadataValidationError, 1); |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestInvalidMetadataInvalidExchange) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| auto metadata = metadata_->Clone(); |
| metadata->requests.insert_or_assign( |
| GURL(kInvalidIsolatedWebAppUrl), |
| web_package::mojom::BundleResponseLocation::New()); |
| parser_factory_->RunMetadataCallback(integrity_block_->size, |
| std::move(metadata)); |
| |
| ReadResult result = read_response_future.Take(); |
| EXPECT_THAT( |
| result, |
| ErrorIs(AllOf( |
| Field(&ReadResponseError::type, ReadResponseError::Type::kOtherError), |
| Field(&ReadResponseError::message, |
| HasSubstr( |
| "Failed to validate metadata: The URL of an exchange is " |
| "invalid"))))); |
| } |
| |
| class IsolatedWebAppReaderRegistryResponseHeadParserErrorTest |
| : public IsolatedWebAppReaderRegistryTest, |
| public ::testing::WithParamInterface< |
| std::pair<web_package::mojom::BundleParseErrorType, |
| IsolatedWebAppReaderRegistry::ReadResponseHeadError>> {}; |
| |
| TEST_P(IsolatedWebAppReaderRegistryResponseHeadParserErrorTest, |
| TestResponseHeadParserError) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| |
| auto error = web_package::mojom::BundleResponseParseError::New(); |
| error->message = "test error"; |
| error->type = GetParam().first; |
| parser_factory_->RunResponseCallback( |
| web_package::mojom::BundleResponseLocation::New( |
| response_->payload_offset, response_->payload_length), |
| nullptr, std::move(error)); |
| |
| ReadResult result = read_response_future.Take(); |
| EXPECT_THAT( |
| result, |
| ErrorIs(AllOf( |
| Field(&ReadResponseError::type, ReadResponseError::Type::kOtherError), |
| Field(&ReadResponseError::message, |
| "Failed to parse response head: test error")))); |
| |
| histogram_tester.ExpectBucketCount( |
| ToErrorHistogramName("WebApp.Isolated.ReadResponseHead"), |
| GetParam().second, 1); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| IsolatedWebAppReaderRegistryResponseHeadParserErrorTest, |
| ::testing::Values( |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kParserInternalError, |
| IsolatedWebAppReaderRegistry::ReadResponseHeadError:: |
| kResponseHeadParserInternalError), |
| std::make_pair(web_package::mojom::BundleParseErrorType::kFormatError, |
| IsolatedWebAppReaderRegistry::ReadResponseHeadError:: |
| kResponseHeadParserFormatError))); |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestConcurrentRequests) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| using ReaderCacheState = IsolatedWebAppReaderRegistry::ReaderCacheState; |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| // Simulate two simultaneous requests for the same web bundle |
| base::test::TestFuture<ReadResult> read_response_future_1; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future_1.GetCallback()); |
| |
| histogram_tester.GetAllSamples("WebApp.Isolated.ResponseReaderCacheState"), |
| ElementsAre(base::Bucket(ReaderCacheState::kNotCached, 1), |
| base::Bucket(ReaderCacheState::kCachedReady, 0), |
| base::Bucket(ReaderCacheState::kCachedPending, 0)); |
| |
| base::test::TestFuture<ReadResult> read_response_future_2; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future_2.GetCallback()); |
| |
| histogram_tester.GetAllSamples("WebApp.Isolated.ResponseReaderCacheState"), |
| ElementsAre(base::Bucket(ReaderCacheState::kNotCached, 1), |
| base::Bucket(ReaderCacheState::kCachedReady, 0), |
| base::Bucket(ReaderCacheState::kCachedPending, 1)); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| { |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future_1.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| |
| std::string response_body = ReadAndFulfillResponseBody( |
| response.head()->payload_length, |
| base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody, |
| base::Unretained(&response))); |
| EXPECT_EQ(kResponseBody, response_body); |
| } |
| |
| FulfillResponse(resource_request); |
| { |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future_2.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| |
| std::string response_body = ReadAndFulfillResponseBody( |
| response.head()->payload_length, |
| base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody, |
| base::Unretained(&response))); |
| EXPECT_EQ(kResponseBody, response_body); |
| } |
| |
| base::test::TestFuture<ReadResult> read_response_future_3; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future_3.GetCallback()); |
| |
| histogram_tester.GetAllSamples("WebApp.Isolated.ResponseReaderCacheState"), |
| ElementsAre(base::Bucket(ReaderCacheState::kNotCached, 1), |
| base::Bucket(ReaderCacheState::kCachedReady, 1), |
| base::Bucket(ReaderCacheState::kCachedPending, 1)); |
| |
| FulfillResponse(resource_request); |
| { |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future_3.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| |
| std::string response_body = ReadAndFulfillResponseBody( |
| response.head()->payload_length, |
| base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody, |
| base::Unretained(&response))); |
| EXPECT_EQ(kResponseBody, response_body); |
| } |
| } |
| |
| // Check that we can close the cached reader that keeps |
| // the signed web bundle file opened. |
| TEST_F(IsolatedWebAppReaderRegistryTest, Close) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| |
| base::test::TestFuture<void> close_future; |
| registry_->ClearCacheForPath(web_bundle_path_, close_future.GetCallback()); |
| ASSERT_TRUE(close_future.Wait()); |
| |
| base::test::TestFuture<net::Error> error_future; |
| ReadResponseBody( |
| response.head()->payload_length, |
| base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody, |
| base::Unretained(&response)), |
| error_future.GetCallback()); |
| EXPECT_EQ(net::ERR_FAILED, error_future.Take()); |
| |
| ASSERT_TRUE(base::DeleteFile(web_bundle_path_)); |
| } |
| |
| // Check the case when the close request is coming while the reader |
| // is being created. |
| TEST_F(IsolatedWebAppReaderRegistryTest, CloseOnArrival) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, read_response_future.GetCallback()); |
| |
| base::test::TestFuture<void> close_future; |
| registry_->ClearCacheForPath(web_bundle_path_, close_future.GetCallback()); |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| |
| ReadResult result = read_response_future.Take(); |
| EXPECT_THAT(result, ErrorIs(AllOf(Field(&ReadResponseError::type, |
| ReadResponseError::Type::kOtherError), |
| Field(&ReadResponseError::message, |
| "The bundle is waiting to close")))); |
| |
| ASSERT_TRUE(close_future.Wait()); |
| |
| ASSERT_TRUE(base::DeleteFile(web_bundle_path_)); |
| } |
| |
| // Closing unopened signed web bundle should not cause problems. |
| TEST_F(IsolatedWebAppReaderRegistryTest, CloseEmpty) { |
| base::test::TestFuture<void> close_future; |
| registry_->ClearCacheForPath(web_bundle_path_, close_future.GetCallback()); |
| |
| ASSERT_TRUE(close_future.Wait()); |
| } |
| |
| // Reopen of the closed file should work. |
| TEST_F(IsolatedWebAppReaderRegistryTest, OpenCloseOpen) { |
| #if !BUILDFLAG(IS_CHROMEOS) |
| EXPECT_CALL(signature_verifier_, VerifySignatures) |
| .WillOnce(RunOnceCallback<2>(base::ok())); |
| #endif |
| // Open the signed web bundle for the first time. |
| { |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| resource_request, |
| read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response response, |
| read_response_future.Take()); |
| EXPECT_EQ(response.head()->response_code, 200); |
| } |
| |
| // Close the file. |
| { |
| base::test::TestFuture<void> close_future; |
| registry_->ClearCacheForPath(web_bundle_path_, close_future.GetCallback()); |
| ASSERT_TRUE(close_future.Wait()); |
| } |
| |
| // After closing we should be able to reopen the signed web bundle without any |
| // issues. |
| { |
| network::ResourceRequest new_resource_request; |
| new_resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> new_read_response_future; |
| registry_->ReadResponse(web_bundle_path_, /*dev_mode=*/false, kWebBundleId, |
| new_resource_request, |
| new_read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(new_resource_request); |
| ASSERT_OK_AND_ASSIGN(IsolatedWebAppResponseReader::Response new_response, |
| new_read_response_future.Take()); |
| EXPECT_EQ(new_response.head()->response_code, 200); |
| } |
| } |
| |
| // TODO(crbug.com/40239531): Add a test that checks the behavior when |
| // `SignedWebBundleReader`s for two different Web Bundle IDs are requested |
| // concurrently. Testing this is currently not possible, since running two |
| // `MockWebBundleParser`s at the same time is not yet possible. |
| |
| } // namespace web_app |