blob: 3d8bd04a9e0feb67bb90295eb3934683ba897795 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.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/task/sequenced_task_runner.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_response_reader.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 "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 "third_party/abseil-cpp/absl/types/variant.h"
namespace web_app {
namespace {
using testing::ElementsAre;
using testing::Eq;
using testing::IsFalse;
using testing::IsTrue;
using testing::NotNull;
using testing::StartsWith;
using testing::VariantWith;
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_;
};
class IsolatedWebAppResponseReaderFactoryTest : 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);
factory_ = std::make_unique<IsolatedWebAppResponseReaderFactory>(
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;
CHECK(temp_dir_.CreateUniqueTempDir());
CHECK(CreateTemporaryFileInDir(temp_dir_.GetPath(), &web_bundle_path_));
CHECK_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 { factory_.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<IsolatedWebAppResponseReaderFactory> factory_;
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 ReaderResult =
base::expected<std::unique_ptr<IsolatedWebAppResponseReader>,
IsolatedWebAppResponseReaderFactory::Error>;
class IsolatedWebAppResponseReaderFactoryIntegrityBlockParserErrorTest
: public IsolatedWebAppResponseReaderFactoryTest,
public ::testing::WithParamInterface<
std::pair<web_package::mojom::BundleParseErrorType,
IsolatedWebAppResponseReaderFactory::
ReadIntegrityBlockAndMetadataStatus>> {};
TEST_P(IsolatedWebAppResponseReaderFactoryIntegrityBlockParserErrorTest,
TestIntegrityBlockParserError) {
base::HistogramTester histogram_tester;
base::test::TestFuture<ReaderResult> reader_future;
factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
/*skip_signature_verification=*/false,
reader_future.GetCallback());
auto error = web_package::mojom::BundleIntegrityBlockParseError::New();
error->type = GetParam().first;
error->message = "test error";
parser_factory_->RunIntegrityBlockCallback(nullptr, error->Clone());
ReaderResult result = reader_future.Take();
ASSERT_THAT(result.has_value(), IsFalse());
auto* actual_error =
absl::get_if<web_package::mojom::BundleIntegrityBlockParseErrorPtr>(
&result.error());
ASSERT_THAT(actual_error, NotNull());
EXPECT_THAT((*actual_error)->type, Eq(error->type));
EXPECT_THAT((*actual_error)->message, Eq(error->message));
histogram_tester.ExpectBucketCount(
"WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus", GetParam().second,
1);
}
INSTANTIATE_TEST_SUITE_P(
All,
IsolatedWebAppResponseReaderFactoryIntegrityBlockParserErrorTest,
::testing::Values(
std::make_pair(
web_package::mojom::BundleParseErrorType::kParserInternalError,
IsolatedWebAppResponseReaderFactory::
ReadIntegrityBlockAndMetadataStatus::
kIntegrityBlockParserInternalError),
std::make_pair(web_package::mojom::BundleParseErrorType::kVersionError,
IsolatedWebAppResponseReaderFactory::
ReadIntegrityBlockAndMetadataStatus::
kIntegrityBlockParserVersionError),
std::make_pair(web_package::mojom::BundleParseErrorType::kFormatError,
IsolatedWebAppResponseReaderFactory::
ReadIntegrityBlockAndMetadataStatus::
kIntegrityBlockParserFormatError)));
TEST_F(IsolatedWebAppResponseReaderFactoryTest,
TestInvalidIntegrityBlockContents) {
base::HistogramTester histogram_tester;
factory_ = std::make_unique<IsolatedWebAppResponseReaderFactory>(
std::make_unique<FakeIsolatedWebAppValidator>("test error"),
base::BindRepeating(
[]() -> std::unique_ptr<
web_package::SignedWebBundleSignatureVerifier> {
return std::make_unique<FakeSignatureVerifier>(absl::nullopt);
}));
base::test::TestFuture<ReaderResult> reader_future;
factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
/*skip_signature_verification=*/false,
reader_future.GetCallback());
FulfillIntegrityBlock();
ReaderResult result = reader_future.Take();
ASSERT_THAT(result.has_value(), IsFalse());
auto* actual_error = absl::get_if<IntegrityBlockError>(&result.error());
ASSERT_THAT(actual_error, NotNull());
EXPECT_THAT(actual_error->message, Eq("test error"));
histogram_tester.ExpectBucketCount(
"WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
IsolatedWebAppResponseReaderFactory::ReadIntegrityBlockAndMetadataStatus::
kIntegrityBlockValidationError,
1);
}
class IsolatedWebAppResponseReaderFactorySignatureVerificationErrorTest
: public IsolatedWebAppResponseReaderFactoryTest,
public ::testing::WithParamInterface<
std::tuple<web_package::SignedWebBundleSignatureVerifier::Error,
bool>> {
public:
IsolatedWebAppResponseReaderFactorySignatureVerificationErrorTest()
: IsolatedWebAppResponseReaderFactoryTest(),
error_(std::get<0>(GetParam())),
skip_signature_verification_(std::get<1>(GetParam())) {}
protected:
web_package::SignedWebBundleSignatureVerifier::Error error_;
bool skip_signature_verification_;
};
TEST_P(IsolatedWebAppResponseReaderFactorySignatureVerificationErrorTest,
SignatureVerificationError) {
base::HistogramTester histogram_tester;
factory_ = std::make_unique<IsolatedWebAppResponseReaderFactory>(
std::make_unique<FakeIsolatedWebAppValidator>(absl::nullopt),
base::BindRepeating(
[](web_package::SignedWebBundleSignatureVerifier::Error error)
-> std::unique_ptr<
web_package::SignedWebBundleSignatureVerifier> {
return std::make_unique<FakeSignatureVerifier>(error);
},
error_));
base::test::TestFuture<ReaderResult> reader_future;
factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
skip_signature_verification_,
reader_future.GetCallback());
FulfillIntegrityBlock();
// When signature verification is skipped, then the signature verification
// error seeded above should not be triggered.
if (skip_signature_verification_) {
FulfillMetadata();
ReaderResult result = reader_future.Take();
EXPECT_THAT(result.has_value(), IsTrue());
histogram_tester.ExpectBucketCount(
"WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
IsolatedWebAppResponseReaderFactory::
ReadIntegrityBlockAndMetadataStatus::kSignatureVerificationError,
0);
} else {
ReaderResult result = reader_future.Take();
ASSERT_THAT(result.has_value(), IsFalse());
auto* actual_error =
absl::get_if<web_package::SignedWebBundleSignatureVerifier::Error>(
&result.error());
ASSERT_THAT(actual_error, NotNull());
EXPECT_THAT(actual_error->message, Eq(error_.message));
histogram_tester.ExpectBucketCount(
"WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
IsolatedWebAppResponseReaderFactory::
ReadIntegrityBlockAndMetadataStatus::kSignatureVerificationError,
1);
}
}
INSTANTIATE_TEST_SUITE_P(
All,
IsolatedWebAppResponseReaderFactorySignatureVerificationErrorTest,
::testing::Combine(
::testing::Values(web_package::SignedWebBundleSignatureVerifier::Error::
ForInternalError("internal error"),
web_package::SignedWebBundleSignatureVerifier::Error::
ForInvalidSignature("invalid signature")),
// skip_signature_verification
::testing::Bool()));
class IsolatedWebAppResponseReaderFactoryMetadataParserErrorTest
: public IsolatedWebAppResponseReaderFactoryTest,
public ::testing::WithParamInterface<
std::pair<web_package::mojom::BundleParseErrorType,
IsolatedWebAppResponseReaderFactory::
ReadIntegrityBlockAndMetadataStatus>> {};
TEST_P(IsolatedWebAppResponseReaderFactoryMetadataParserErrorTest,
TestMetadataParserError) {
base::HistogramTester histogram_tester;
base::test::TestFuture<ReaderResult> reader_future;
factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
/*skip_signature_verification=*/false,
reader_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,
error->Clone());
ReaderResult result = reader_future.Take();
ASSERT_THAT(result.has_value(), IsFalse());
auto* actual_error =
absl::get_if<web_package::mojom::BundleMetadataParseErrorPtr>(
&result.error());
ASSERT_THAT(actual_error, NotNull());
EXPECT_THAT((*actual_error)->type, Eq(error->type));
EXPECT_THAT((*actual_error)->message, Eq(error->message));
histogram_tester.ExpectBucketCount(
"WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus", GetParam().second,
1);
}
INSTANTIATE_TEST_SUITE_P(
All,
IsolatedWebAppResponseReaderFactoryMetadataParserErrorTest,
::testing::Values(
std::make_pair(
web_package::mojom::BundleParseErrorType::kParserInternalError,
IsolatedWebAppResponseReaderFactory::
ReadIntegrityBlockAndMetadataStatus::
kMetadataParserInternalError),
std::make_pair(web_package::mojom::BundleParseErrorType::kVersionError,
IsolatedWebAppResponseReaderFactory::
ReadIntegrityBlockAndMetadataStatus::
kMetadataParserVersionError),
std::make_pair(web_package::mojom::BundleParseErrorType::kFormatError,
IsolatedWebAppResponseReaderFactory::
ReadIntegrityBlockAndMetadataStatus::
kMetadataParserFormatError)));
TEST_F(IsolatedWebAppResponseReaderFactoryTest, TestInvalidMetadataPrimaryUrl) {
base::HistogramTester histogram_tester;
base::test::TestFuture<ReaderResult> reader_future;
factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
/*skip_signature_verification=*/false,
reader_future.GetCallback());
FulfillIntegrityBlock();
auto metadata = metadata_->Clone();
metadata->primary_url = kUrl;
parser_factory_->RunMetadataCallback(integrity_block_->size,
std::move(metadata));
ReaderResult result = reader_future.Take();
ASSERT_THAT(result.has_value(), IsFalse());
auto* actual_error = absl::get_if<MetadataError>(&result.error());
ASSERT_THAT(actual_error, NotNull());
EXPECT_THAT(actual_error->message,
StartsWith("Primary URL must not be present"));
histogram_tester.ExpectBucketCount(
"WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
IsolatedWebAppResponseReaderFactory::ReadIntegrityBlockAndMetadataStatus::
kMetadataValidationError,
1);
}
TEST_F(IsolatedWebAppResponseReaderFactoryTest,
TestInvalidMetadataInvalidExchange) {
base::test::TestFuture<ReaderResult> reader_future;
factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
/*skip_signature_verification=*/false,
reader_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));
ReaderResult result = reader_future.Take();
ASSERT_THAT(result.has_value(), IsFalse());
auto* actual_error = absl::get_if<MetadataError>(&result.error());
ASSERT_THAT(actual_error, NotNull());
EXPECT_THAT(actual_error->message,
StartsWith("The URL of an exchange is invalid"));
}
} // namespace
} // namespace web_app