blob: aaa6ea245479e236bad0e1bbdb1d5882af11a798 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/web_package/signed_exchange_envelope.h"
#include <string_view>
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/path_service.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "content/browser/web_package/signed_exchange_consts.h"
#include "content/browser/web_package/signed_exchange_prologue.h"
#include "content/public/common/content_paths.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
namespace {
const char kSignatureString[] =
"sig1;"
" sig=*MEUCIQDXlI2gN3RNBlgFiuRNFpZXcDIaUpX6HIEwcZEc0cZYLAIga9DsVOMM+"
"g5YpwEBdGW3sS+bvnmAJJiSMwhuBdqp5UY=*;"
" integrity=\"mi-draft2\";"
" validity-url=\"https://test.example.org/resource.validity.1511128380\";"
" cert-url=\"https://example.com/oldcerts\";"
" cert-sha256=*W7uB969dFW3Mb5ZefPS9Tq5ZbH5iSmOILpjv2qEArmI=*;"
" date=1511128380; expires=1511733180";
cbor::Value CBORByteString(const char* str) {
return cbor::Value(str, cbor::Value::Type::BYTE_STRING);
}
std::optional<SignedExchangeEnvelope> GenerateHeaderAndParse(
SignedExchangeVersion version,
std::string_view fallback_url,
std::string_view signature,
const std::map<const char*, const char*>& response_map) {
cbor::Value::MapValue response_cbor_map;
for (auto& pair : response_map)
response_cbor_map[CBORByteString(pair.first)] = CBORByteString(pair.second);
DCHECK_EQ(version, SignedExchangeVersion::kB3);
auto serialized =
cbor::Writer::Write(cbor::Value(std::move(response_cbor_map)));
return SignedExchangeEnvelope::Parse(
version, signed_exchange_utils::URLWithRawString(fallback_url), signature,
UNSAFE_TODO(base::span(serialized->data(), serialized->size())),
nullptr /* devtools_proxy */);
}
} // namespace
class SignedExchangeEnvelopeTest
: public ::testing::TestWithParam<SignedExchangeVersion> {};
TEST_P(SignedExchangeEnvelopeTest, ParseGoldenFile) {
base::FilePath test_sxg_path;
base::PathService::Get(content::DIR_TEST_DATA, &test_sxg_path);
test_sxg_path =
test_sxg_path.AppendASCII("sxg").AppendASCII("test.example.org_test.sxg");
std::string contents;
ASSERT_TRUE(base::ReadFileToString(test_sxg_path, &contents));
base::span<const uint8_t> contents_bytes = base::as_byte_span(contents);
ASSERT_GT(contents.size(),
signed_exchange_prologue::BeforeFallbackUrl::kEncodedSizeInBytes);
signed_exchange_prologue::BeforeFallbackUrl prologue_a =
signed_exchange_prologue::BeforeFallbackUrl::Parse(
UNSAFE_TODO(base::span(contents_bytes.data(),
signed_exchange_prologue::BeforeFallbackUrl::
kEncodedSizeInBytes)),
nullptr /* devtools_proxy */);
ASSERT_GT(contents.size(),
signed_exchange_prologue::BeforeFallbackUrl::kEncodedSizeInBytes +
prologue_a.ComputeFallbackUrlAndAfterLength());
signed_exchange_prologue::FallbackUrlAndAfter prologue_b =
signed_exchange_prologue::FallbackUrlAndAfter::Parse(
UNSAFE_TODO(base::span(
contents_bytes
.subspan(signed_exchange_prologue::BeforeFallbackUrl::
kEncodedSizeInBytes)
.data(),
prologue_a.ComputeFallbackUrlAndAfterLength())),
prologue_a, nullptr /* devtools_proxy */);
size_t signature_header_field_offset =
signed_exchange_prologue::BeforeFallbackUrl::kEncodedSizeInBytes +
prologue_a.ComputeFallbackUrlAndAfterLength();
std::string_view signature_header_field(
UNSAFE_TODO(contents.data() + signature_header_field_offset),
prologue_b.signature_header_field_length());
const auto cbor_bytes = UNSAFE_TODO(
base::span(contents_bytes
.subspan(signature_header_field_offset +
prologue_b.signature_header_field_length())
.data(),
prologue_b.cbor_header_length()));
const std::optional<SignedExchangeEnvelope> envelope =
SignedExchangeEnvelope::Parse(
SignedExchangeVersion::kB3, prologue_b.fallback_url(),
signature_header_field, cbor_bytes, nullptr /* devtools_proxy */);
ASSERT_TRUE(envelope.has_value());
EXPECT_EQ(envelope->request_url().url,
GURL("https://test.example.org/test/"));
EXPECT_EQ(envelope->response_code(), static_cast<net::HttpStatusCode>(200u));
EXPECT_EQ(envelope->response_headers().size(), 4u);
EXPECT_EQ(envelope->response_headers().find("content-encoding")->second,
"mi-sha256-03");
}
TEST_P(SignedExchangeEnvelopeTest, ValidHeader) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{{kStatusKey, "200"}, {"content-type", "text/html"}, {"digest", "foo"}});
ASSERT_TRUE(header.has_value());
EXPECT_EQ(header->request_url().url, GURL("https://test.example.org/test/"));
EXPECT_EQ(header->response_code(), static_cast<net::HttpStatusCode>(200u));
EXPECT_EQ(header->response_headers().size(), 3u);
EXPECT_EQ(header->response_headers().find("content-type")->second,
"text/html");
EXPECT_EQ(header->response_headers().find("digest")->second, "foo");
EXPECT_EQ(header->response_headers().find("x-content-type-options")->second,
"nosniff"); // Injected by SignedExchangeEnvelope.
}
TEST_P(SignedExchangeEnvelopeTest, InformationalResponseCode) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{
{kStatusKey, "100"},
{"content-type", "text/html"},
});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, RelativeURL) {
auto header = GenerateHeaderAndParse(GetParam(), "test/", kSignatureString,
{
{kStatusKey, "200"},
{"content-type", "text/html"},
});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, HttpURLShouldFail) {
auto header = GenerateHeaderAndParse(
GetParam(), "http://test.example.org/test/", kSignatureString,
{
{kStatusKey, "200"},
{"content-type", "text/html"},
});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, RedirectStatusShouldFail) {
auto header =
GenerateHeaderAndParse(GetParam(), "https://test.example.org/test/",
kSignatureString, {{kStatusKey, "302"}});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, Status300ShouldFail) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{{kStatusKey, "300"}, // 300 is not a redirect status.
{"content-type", "text/html"}});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, StatefulResponseHeader) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{
{kStatusKey, "200"},
{"content-type", "text/html"},
{"set-cookie", "foo=bar"},
});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, UppercaseResponseMap) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{{kStatusKey, "200"},
{"content-type", "text/html"},
{"Content-Length", "123"}});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, InvalidValidityURLHeader) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test2.example.org/test/", kSignatureString,
{{kStatusKey, "200"}, {"content-type", "text/html"}});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, NoContentType) {
auto header =
GenerateHeaderAndParse(GetParam(), "https://test.example.org/test/",
kSignatureString, {{kStatusKey, "200"}});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, XContentTypeOptionsShouldBeOverwritten) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{{kStatusKey, "200"},
{"content-type", "text/html"},
{"x-content-type-options", "foo"}});
ASSERT_TRUE(header.has_value());
EXPECT_EQ(header->response_headers().find("x-content-type-options")->second,
"nosniff");
}
TEST_P(SignedExchangeEnvelopeTest, InnerResponseIsSXG) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{{kStatusKey, "200"},
{"content-type", "application/signed-exchange;v=b3"}});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, CacheControlNoStore) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{
{kStatusKey, "200"},
{"content-type", "text/html"},
{"cache-control", "no-store"},
});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, CacheControlSecondValueIsNoStore) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{
{kStatusKey, "200"},
{"content-type", "text/html"},
{"cache-control", "max-age=300, no-store"},
});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, CacheControlPrivateWithValue) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{
{kStatusKey, "200"},
{"content-type", "text/html"},
{"cache-control", "private=foo"},
});
ASSERT_FALSE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, CacheControlNoStoreInQuotedString) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{
{kStatusKey, "200"},
{"content-type", "text/html"},
{"cache-control", "foo=\"300, no-store\""},
{"digest", "foo"},
});
ASSERT_TRUE(header.has_value());
}
TEST_P(SignedExchangeEnvelopeTest, CacheControlParseError) {
auto header = GenerateHeaderAndParse(
GetParam(), "https://test.example.org/test/", kSignatureString,
{
{kStatusKey, "200"},
{"content-type", "text/html"},
{"cache-control", "max-age=\"abc"},
});
ASSERT_FALSE(header.has_value());
}
INSTANTIATE_TEST_SUITE_P(SignedExchangeEnvelopeTests,
SignedExchangeEnvelopeTest,
::testing::Values(SignedExchangeVersion::kB3));
} // namespace content