| // 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 "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.h" |
| |
| #include <memory> |
| |
| #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/functional/callback_helpers.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/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 "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_trust_checker.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_validator.h" |
| #include "chrome/browser/web_applications/test/signed_web_bundle_utils.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/prefs/testing_pref_service.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/signed_web_bundles/signed_web_bundle_signature_verifier.h" |
| #include "components/web_package/test_support/mock_web_bundle_parser_factory.h" |
| #include "content/public/common/content_features.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 "third_party/abseil-cpp/absl/types/optional.h" |
| #include "url/url_constants.h" |
| |
| namespace web_app { |
| |
| namespace { |
| |
| using testing::ElementsAre; |
| |
| 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}; |
| |
| class FakeIsolatedWebAppValidator : public IsolatedWebAppValidator { |
| public: |
| explicit FakeIsolatedWebAppValidator( |
| absl::optional<std::string> integrity_block_error) |
| : IsolatedWebAppValidator(std::make_unique<IsolatedWebAppTrustChecker>( |
| TestingPrefServiceSimple())), |
| integrity_block_error_(integrity_block_error) {} |
| |
| void ValidateIntegrityBlock( |
| const web_package::SignedWebBundleId& web_bundle_id, |
| const web_package::SignedWebBundleIntegrityBlock& integrity_block, |
| base::OnceCallback<void(absl::optional<std::string>)> callback) override { |
| std::move(callback).Run(integrity_block_error_); |
| } |
| |
| private: |
| absl::optional<std::string> integrity_block_error_; |
| }; |
| |
| class FakeSignatureVerifier |
| : public web_package::SignedWebBundleSignatureVerifier { |
| public: |
| explicit FakeSignatureVerifier( |
| absl::optional<web_package::SignedWebBundleSignatureVerifier::Error> |
| error, |
| base::RepeatingClosure on_verify_signatures = base::DoNothing()) |
| : error_(error), on_verify_signatures_(on_verify_signatures) {} |
| |
| void VerifySignatures( |
| scoped_refptr<web_package::SharedFile> file, |
| web_package::SignedWebBundleIntegrityBlock integrity_block, |
| SignatureVerificationCallback callback) override { |
| on_verify_signatures_.Run(); |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), error_)); |
| } |
| |
| private: |
| absl::optional<web_package::SignedWebBundleSignatureVerifier::Error> error_; |
| base::RepeatingClosure on_verify_signatures_; |
| }; |
| |
| } // namespace |
| |
| class IsolatedWebAppReaderRegistryTest : public ::testing::Test { |
| protected: |
| void SetUp() override { |
| scoped_feature_list_.InitAndEnableFeature(features::kIsolatedWebApps); |
| |
| 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); |
| |
| web_package::mojom::BundleIntegrityBlockSignatureStackEntryPtr |
| signature_stack_entry = |
| web_package::mojom::BundleIntegrityBlockSignatureStackEntry::New(); |
| signature_stack_entry->public_key = web_package::Ed25519PublicKey::Create( |
| base::make_span(kEd25519PublicKey)); |
| signature_stack_entry->signature = web_package::Ed25519Signature::Create( |
| base::make_span(kEd25519Signature)); |
| |
| 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); |
| |
| registry_ = std::make_unique<IsolatedWebAppReaderRegistry>( |
| std::make_unique<FakeIsolatedWebAppValidator>(absl::nullopt), |
| base::BindRepeating( |
| []() -> std::unique_ptr< |
| web_package::SignedWebBundleSignatureVerifier> { |
| return std::make_unique<FakeSignatureVerifier>(absl::nullopt); |
| })); |
| |
| std::string test_file_data = kResponseBody; |
| EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| EXPECT_TRUE( |
| CreateTemporaryFileInDir(temp_dir_.GetPath(), &web_bundle_path_)); |
| EXPECT_EQ( |
| test_file_data.size(), |
| static_cast<size_t>(base::WriteFile( |
| web_bundle_path_, test_file_data.data(), test_file_data.size()))); |
| |
| in_process_data_decoder_.service() |
| .SetWebBundleParserFactoryBinderForTesting(base::BindRepeating( |
| &web_package::MockWebBundleParserFactory::AddReceiver, |
| base::Unretained(parser_factory_.get()))); |
| } |
| |
| void TearDown() override { registry_.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()); |
| } |
| |
| base::test::TaskEnvironment 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<absl::optional<GURL>> |
| on_create_parser_future_; |
| |
| 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_; |
| }; |
| |
| using ReadResult = |
| base::expected<IsolatedWebAppReaderRegistry::Response, |
| IsolatedWebAppReaderRegistry::ReadResponseError>; |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestSingleRequest) { |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, kWebBundleId, resource_request, |
| read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| ReadResult result = read_response_future.Take(); |
| ASSERT_TRUE(result.has_value()) << result.error().message; |
| EXPECT_EQ(result->head()->response_code, 200); |
| |
| GURL expected_parser_base_url( |
| base::StrCat({chrome::kIsolatedAppScheme, url::kStandardSchemeSeparator, |
| kWebBundleId.id()})); |
| EXPECT_EQ(expected_parser_base_url, on_create_parser_future_.Take()); |
| |
| histogram_tester.ExpectBucketCount( |
| "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus", |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus:: |
| kSuccess, |
| 1); |
| |
| std::string response_body = ReadAndFulfillResponseBody( |
| result->head()->payload_length, |
| base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody, |
| base::Unretained(&*result))); |
| EXPECT_EQ(kResponseBody, response_body); |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, |
| TestSingleRequestWithQueryAndFragment) { |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl.Resolve("/?bar=baz#foo"); |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, kWebBundleId, resource_request, |
| read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| ReadResult result = read_response_future.Take(); |
| ASSERT_TRUE(result.has_value()) << result.error().message; |
| EXPECT_EQ(result->head()->response_code, 200); |
| |
| std::string response_body = ReadAndFulfillResponseBody( |
| result->head()->payload_length, |
| base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody, |
| base::Unretained(&*result))); |
| EXPECT_EQ(kResponseBody, response_body); |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, |
| TestReadingResponseAfterSignedWebBundleReaderIsDeleted) { |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, kWebBundleId, resource_request, |
| read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| FulfillResponse(resource_request); |
| |
| ReadResult result = read_response_future.Take(); |
| ASSERT_TRUE(result.has_value()) << result.error().message; |
| EXPECT_EQ(result->head()->response_code, 200); |
| |
| // Delete the registry so that the `SignedWebBundleReader`, which `result` |
| // holds onto weakly, is deleted, which should make `result->ReadBody()` |
| // fail with `net::ERR_FAILED`. |
| registry_.reset(); |
| |
| base::test::TestFuture<net::Error> error_future; |
| ReadResponseBody( |
| result->head()->payload_length, |
| base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody, |
| base::Unretained(&*result)), |
| error_future.GetCallback()); |
| EXPECT_EQ(net::ERR_FAILED, error_future.Take()); |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestRequestToNonExistingResponse) { |
| 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_, kWebBundleId, resource_request, |
| read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| FulfillMetadata(); |
| |
| ReadResult result = read_response_future.Take(); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ( |
| result.error().type, |
| IsolatedWebAppReaderRegistry::ReadResponseError::Type::kResponseNotFound); |
| EXPECT_EQ(result.error().message, |
| "Failed to read response: The Web Bundle does not contain a " |
| "response for the provided URL: " |
| "isolated-app://" |
| "aaaaaaacaibaaaaaaaaaaaaaaiaaeaaaaaaaaaaaaabaeaqaaaaaaaic/foo"); |
| |
| histogram_tester.ExpectBucketCount( |
| "WebApp.Isolated.ReadResponseHeadStatus", |
| IsolatedWebAppReaderRegistry::ReadResponseHeadStatus:: |
| kResponseNotFoundError, |
| 1); |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestSignedWebBundleReaderLifetime) { |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| size_t num_signature_verifications = 0; |
| registry_ = std::make_unique<IsolatedWebAppReaderRegistry>( |
| std::make_unique<FakeIsolatedWebAppValidator>(absl::nullopt), |
| base::BindLambdaForTesting( |
| [&]() -> std::unique_ptr< |
| web_package::SignedWebBundleSignatureVerifier> { |
| return std::make_unique<FakeSignatureVerifier>( |
| absl::nullopt, base::BindLambdaForTesting( |
| [&]() { ++num_signature_verifications; })); |
| })); |
| |
| // Verify that the cache cleanup timer has not yet started. |
| EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0ul) |
| << (task_environment_.DescribeCurrentTasks(), |
| "Pending Tasks have been logged."); |
| |
| { |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, 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); |
| |
| ReadResult result = read_response_future.Take(); |
| ASSERT_TRUE(result.has_value()) << result.error().message; |
| EXPECT_EQ(result->head()->response_code, 200); |
| } |
| |
| EXPECT_EQ(num_signature_verifications, 1ul); |
| |
| // Verify that the cache cleanup timer has started. |
| EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 1ul) |
| << (task_environment_.DescribeCurrentTasks(), |
| "Pending Tasks have been logged."); |
| |
| { |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, kWebBundleId, resource_request, |
| read_response_future.GetCallback()); |
| |
| // Notably, no `FulfillIntegrityBlock` or `FulfillMetadata` here, since the |
| // `SignedWebBundleReader` should still be cached. |
| FulfillResponse(resource_request); |
| |
| ReadResult result = read_response_future.Take(); |
| ASSERT_TRUE(result.has_value()) << result.error().message; |
| EXPECT_EQ(result->head()->response_code, 200); |
| } |
| |
| EXPECT_EQ(num_signature_verifications, 1ul); |
| |
| // Verify that the cache cleanup timer is still running. |
| EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 1ul) |
| << (task_environment_.DescribeCurrentTasks(), |
| "Pending Tasks have been logged."); |
| |
| // 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_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0ul) |
| << (task_environment_.DescribeCurrentTasks(), |
| "Pending Tasks have been logged."); |
| |
| { |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, 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); |
| |
| ReadResult result = read_response_future.Take(); |
| ASSERT_TRUE(result.has_value()) << result.error().message; |
| EXPECT_EQ(result->head()->response_code, 200); |
| } |
| |
| // Signatures should not have been verified again, since we only verify them |
| // once per session per file path. |
| EXPECT_EQ(num_signature_verifications, 1ul); |
| |
| // Verify that the cache cleanup timer has started again. |
| EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 1ul) |
| << (task_environment_.DescribeCurrentTasks(), |
| "Pending Tasks have been logged."); |
| } |
| |
| class IsolatedWebAppReaderRegistryIntegrityBlockParserErrorTest |
| : public IsolatedWebAppReaderRegistryTest, |
| public ::testing::WithParamInterface<std::pair< |
| web_package::mojom::BundleParseErrorType, |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus>> { |
| }; |
| |
| 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_, 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(); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ(result.error().type, |
| IsolatedWebAppReaderRegistry::ReadResponseError::Type::kOtherError); |
| EXPECT_EQ(result.error().message, |
| "Failed to parse integrity block: test error"); |
| |
| histogram_tester.ExpectBucketCount( |
| "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus", GetParam().second, |
| 1); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| IsolatedWebAppReaderRegistryIntegrityBlockParserErrorTest, |
| ::testing::Values( |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kParserInternalError, |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus:: |
| kIntegrityBlockParserInternalError), |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kVersionError, |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus:: |
| kIntegrityBlockParserVersionError), |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kFormatError, |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus:: |
| kIntegrityBlockParserFormatError))); |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestInvalidIntegrityBlockContents) { |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| registry_ = std::make_unique<IsolatedWebAppReaderRegistry>( |
| std::make_unique<FakeIsolatedWebAppValidator>("test error"), |
| base::BindRepeating( |
| []() -> std::unique_ptr< |
| web_package::SignedWebBundleSignatureVerifier> { |
| return std::make_unique<FakeSignatureVerifier>(absl::nullopt); |
| })); |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, kWebBundleId, resource_request, |
| read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| |
| ReadResult result = read_response_future.Take(); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ(result.error().type, |
| IsolatedWebAppReaderRegistry::ReadResponseError::Type::kOtherError); |
| EXPECT_EQ(result.error().message, |
| "Failed to validate integrity block: test error"); |
| |
| histogram_tester.ExpectBucketCount( |
| "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus", |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus:: |
| kIntegrityBlockValidationError, |
| 1); |
| } |
| |
| class IsolatedWebAppReaderRegistrySignatureVerificationErrorTest |
| : public IsolatedWebAppReaderRegistryTest, |
| public ::testing::WithParamInterface< |
| web_package::SignedWebBundleSignatureVerifier::Error> {}; |
| |
| TEST_P(IsolatedWebAppReaderRegistrySignatureVerificationErrorTest, |
| SignatureVerificationError) { |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| registry_ = std::make_unique<IsolatedWebAppReaderRegistry>( |
| std::make_unique<FakeIsolatedWebAppValidator>(absl::nullopt), |
| base::BindRepeating( |
| []() -> std::unique_ptr< |
| web_package::SignedWebBundleSignatureVerifier> { |
| return std::make_unique<FakeSignatureVerifier>(GetParam()); |
| })); |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, kWebBundleId, resource_request, |
| read_response_future.GetCallback()); |
| |
| FulfillIntegrityBlock(); |
| |
| ReadResult result = read_response_future.Take(); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ(result.error().type, |
| IsolatedWebAppReaderRegistry::ReadResponseError::Type::kOtherError); |
| EXPECT_EQ(result.error().message, |
| base::StringPrintf("Failed to verify signatures: %s", |
| GetParam().message.c_str())); |
| |
| histogram_tester.ExpectBucketCount( |
| "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus", |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus:: |
| kSignatureVerificationError, |
| 1); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| IsolatedWebAppReaderRegistrySignatureVerificationErrorTest, |
| ::testing::Values( |
| web_package::SignedWebBundleSignatureVerifier::Error::ForInternalError( |
| "internal error"), |
| web_package::SignedWebBundleSignatureVerifier::Error:: |
| ForInvalidSignature("invalid signature"))); |
| |
| class IsolatedWebAppReaderRegistryMetadataParserErrorTest |
| : public IsolatedWebAppReaderRegistryTest, |
| public ::testing::WithParamInterface<std::pair< |
| web_package::mojom::BundleParseErrorType, |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus>> { |
| }; |
| |
| TEST_P(IsolatedWebAppReaderRegistryMetadataParserErrorTest, |
| TestMetadataParserError) { |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, 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(); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ(result.error().type, |
| IsolatedWebAppReaderRegistry::ReadResponseError::Type::kOtherError); |
| EXPECT_EQ(result.error().message, "Failed to parse metadata: test error"); |
| |
| histogram_tester.ExpectBucketCount( |
| "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus", GetParam().second, |
| 1); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| IsolatedWebAppReaderRegistryMetadataParserErrorTest, |
| ::testing::Values( |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kParserInternalError, |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus:: |
| kMetadataParserInternalError), |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kVersionError, |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus:: |
| kMetadataParserVersionError), |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kFormatError, |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus:: |
| kMetadataParserFormatError))); |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestInvalidMetadataPrimaryUrl) { |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, 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(); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ(result.error().type, |
| IsolatedWebAppReaderRegistry::ReadResponseError::Type::kOtherError); |
| EXPECT_EQ(result.error().message, |
| base::StringPrintf("Failed to validate metadata: Primary URL must " |
| "not be present, but was %s", |
| kUrl.spec().c_str())); |
| |
| histogram_tester.ExpectBucketCount( |
| "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus", |
| IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus:: |
| kMetadataValidationError, |
| 1); |
| } |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestInvalidMetadataInvalidExchange) { |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, 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(); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ(result.error().type, |
| IsolatedWebAppReaderRegistry::ReadResponseError::Type::kOtherError); |
| EXPECT_EQ(result.error().message, |
| "Failed to validate metadata: The URL of an exchange is invalid: " |
| "The host of isolated-app:// URLs must be a valid Signed Web " |
| "Bundle ID (got foo): The signed web bundle ID must be exactly 56 " |
| "characters long, but was 3 characters long."); |
| } |
| |
| class IsolatedWebAppReaderRegistryResponseHeadParserErrorTest |
| : public IsolatedWebAppReaderRegistryTest, |
| public ::testing::WithParamInterface< |
| std::pair<web_package::mojom::BundleParseErrorType, |
| IsolatedWebAppReaderRegistry::ReadResponseHeadStatus>> {}; |
| |
| TEST_P(IsolatedWebAppReaderRegistryResponseHeadParserErrorTest, |
| TestResponseHeadParserError) { |
| base::HistogramTester histogram_tester; |
| |
| network::ResourceRequest resource_request; |
| resource_request.url = kUrl; |
| |
| base::test::TestFuture<ReadResult> read_response_future; |
| registry_->ReadResponse(web_bundle_path_, 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(); |
| ASSERT_FALSE(result.has_value()); |
| EXPECT_EQ(result.error().type, |
| IsolatedWebAppReaderRegistry::ReadResponseError::Type::kOtherError); |
| EXPECT_EQ(result.error().message, |
| "Failed to parse response head: test error"); |
| |
| histogram_tester.ExpectBucketCount("WebApp.Isolated.ReadResponseHeadStatus", |
| GetParam().second, 1); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| IsolatedWebAppReaderRegistryResponseHeadParserErrorTest, |
| ::testing::Values( |
| std::make_pair( |
| web_package::mojom::BundleParseErrorType::kParserInternalError, |
| IsolatedWebAppReaderRegistry::ReadResponseHeadStatus:: |
| kResponseHeadParserInternalError), |
| std::make_pair(web_package::mojom::BundleParseErrorType::kFormatError, |
| IsolatedWebAppReaderRegistry::ReadResponseHeadStatus:: |
| kResponseHeadParserFormatError))); |
| |
| TEST_F(IsolatedWebAppReaderRegistryTest, TestConcurrentRequests) { |
| 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_, 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_, 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); |
| { |
| ReadResult result = read_response_future_1.Take(); |
| ASSERT_TRUE(result.has_value()) << result.error().message; |
| EXPECT_EQ(result->head()->response_code, 200); |
| |
| std::string response_body = ReadAndFulfillResponseBody( |
| result->head()->payload_length, |
| base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody, |
| base::Unretained(&*result))); |
| EXPECT_EQ(kResponseBody, response_body); |
| } |
| |
| FulfillResponse(resource_request); |
| { |
| ReadResult result = read_response_future_2.Take(); |
| ASSERT_TRUE(result.has_value()) << result.error().message; |
| EXPECT_EQ(result->head()->response_code, 200); |
| |
| std::string response_body = ReadAndFulfillResponseBody( |
| result->head()->payload_length, |
| base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody, |
| base::Unretained(&*result))); |
| EXPECT_EQ(kResponseBody, response_body); |
| } |
| |
| base::test::TestFuture<ReadResult> read_response_future_3; |
| registry_->ReadResponse(web_bundle_path_, 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); |
| { |
| ReadResult result = read_response_future_3.Take(); |
| ASSERT_TRUE(result.has_value()) << result.error().message; |
| EXPECT_EQ(result->head()->response_code, 200); |
| |
| std::string response_body = ReadAndFulfillResponseBody( |
| result->head()->payload_length, |
| base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody, |
| base::Unretained(&*result))); |
| EXPECT_EQ(kResponseBody, response_body); |
| } |
| } |
| |
| // TODO(crbug.com/1365853): 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 |