web_bundle_parser_unittest.cc 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112
  1. // Copyright 2019 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "components/web_package/web_bundle_parser.h"
  5. #include "base/files/file_path.h"
  6. #include "base/files/file_util.h"
  7. #include "base/path_service.h"
  8. #include "base/test/task_environment.h"
  9. #include "base/test/test_future.h"
  10. #include "components/cbor/writer.h"
  11. #include "components/web_package/mojom/web_bundle_parser.mojom-forward.h"
  12. #include "components/web_package/mojom/web_bundle_parser.mojom.h"
  13. #include "components/web_package/test_support/signed_web_bundles/web_bundle_signer.h"
  14. #include "components/web_package/web_bundle_builder.h"
  15. #include "mojo/public/cpp/bindings/receiver_set.h"
  16. #include "testing/gtest/include/gtest/gtest.h"
  17. #include "third_party/abseil-cpp/absl/types/optional.h"
  18. #include "third_party/boringssl/src/include/openssl/curve25519.h"
  19. namespace web_package {
  20. namespace {
  21. constexpr char kPrimaryUrl[] = "https://test.example.com/";
  22. constexpr char kValidityUrl[] =
  23. "https://test.example.org/resource.validity.msg";
  24. const uint64_t kSignatureDate = 1564272000; // 2019-07-28T00:00:00Z
  25. const uint64_t kSignatureDuration = 7 * 24 * 60 * 60;
  26. std::string GetTestFileContents(const base::FilePath& path) {
  27. base::FilePath test_data_dir;
  28. base::PathService::Get(base::DIR_SOURCE_ROOT, &test_data_dir);
  29. test_data_dir = test_data_dir.Append(
  30. base::FilePath(FILE_PATH_LITERAL("components/test/data/web_package")));
  31. std::string contents;
  32. EXPECT_TRUE(base::ReadFileToString(test_data_dir.Append(path), &contents));
  33. return contents;
  34. }
  35. class TestDataSource : public mojom::BundleDataSource {
  36. public:
  37. explicit TestDataSource(const base::FilePath& path,
  38. const bool is_random_access_context = false)
  39. : data_(GetTestFileContents(path)),
  40. is_random_access_context_(is_random_access_context) {}
  41. explicit TestDataSource(const std::vector<uint8_t>& data,
  42. const bool is_random_access_context = false)
  43. : data_(reinterpret_cast<const char*>(data.data()), data.size()),
  44. is_random_access_context_(is_random_access_context) {}
  45. void Read(uint64_t offset, uint64_t length, ReadCallback callback) override {
  46. if (offset >= data_.size()) {
  47. std::move(callback).Run(absl::nullopt);
  48. return;
  49. }
  50. const uint8_t* start =
  51. reinterpret_cast<const uint8_t*>(data_.data()) + offset;
  52. uint64_t available_length = std::min(length, data_.size() - offset);
  53. std::move(callback).Run(
  54. std::vector<uint8_t>(start, start + available_length));
  55. }
  56. void Length(LengthCallback callback) override {
  57. EXPECT_TRUE(is_random_access_context_);
  58. std::move(callback).Run(data_.size());
  59. }
  60. void IsRandomAccessContext(IsRandomAccessContextCallback callback) override {
  61. std::move(callback).Run(is_random_access_context_);
  62. }
  63. base::StringPiece GetPayload(const mojom::BundleResponsePtr& response) {
  64. return base::StringPiece(data_).substr(response->payload_offset,
  65. response->payload_length);
  66. }
  67. void AddReceiver(mojo::PendingReceiver<mojom::BundleDataSource> receiver) {
  68. receivers_.Add(this, std::move(receiver));
  69. }
  70. private:
  71. std::string data_;
  72. bool is_random_access_context_;
  73. mojo::ReceiverSet<mojom::BundleDataSource> receivers_;
  74. };
  75. using ParseSignedBundleIntegrityBlockResult =
  76. std::pair<mojom::BundleIntegrityBlockPtr,
  77. mojom::BundleIntegrityBlockParseErrorPtr>;
  78. ParseSignedBundleIntegrityBlockResult ParseSignedBundleIntegrityBlock(
  79. TestDataSource* data_source,
  80. const GURL& base_url = GURL()) {
  81. mojo::PendingRemote<mojom::BundleDataSource> source_remote;
  82. data_source->AddReceiver(source_remote.InitWithNewPipeAndPassReceiver());
  83. mojo::PendingRemote<mojom::WebBundleParser> parser_remote;
  84. WebBundleParser parser_impl(parser_remote.InitWithNewPipeAndPassReceiver(),
  85. std::move(source_remote), base_url);
  86. mojom::WebBundleParser& parser = parser_impl;
  87. base::test::TestFuture<mojom::BundleIntegrityBlockPtr,
  88. mojom::BundleIntegrityBlockParseErrorPtr>
  89. integrity_block_future;
  90. parser.ParseIntegrityBlock(integrity_block_future.GetCallback());
  91. ParseSignedBundleIntegrityBlockResult result = integrity_block_future.Take();
  92. EXPECT_TRUE((result.first && !result.second) ||
  93. (!result.first && result.second));
  94. return result;
  95. }
  96. using ParseUnsignedBundleResult =
  97. std::pair<mojom::BundleMetadataPtr, mojom::BundleMetadataParseErrorPtr>;
  98. ParseUnsignedBundleResult ParseUnsignedBundle(TestDataSource* data_source,
  99. const GURL& base_url = GURL(),
  100. int64_t offset = -1) {
  101. mojo::PendingRemote<mojom::BundleDataSource> source_remote;
  102. data_source->AddReceiver(source_remote.InitWithNewPipeAndPassReceiver());
  103. mojo::PendingRemote<mojom::WebBundleParser> parser_remote;
  104. WebBundleParser parser_impl(parser_remote.InitWithNewPipeAndPassReceiver(),
  105. std::move(source_remote), base_url);
  106. mojom::WebBundleParser& parser = parser_impl;
  107. base::test::TestFuture<mojom::BundleMetadataPtr,
  108. mojom::BundleMetadataParseErrorPtr>
  109. future;
  110. parser.ParseMetadata(offset, future.GetCallback());
  111. ParseUnsignedBundleResult result = future.Take();
  112. EXPECT_TRUE((result.first && !result.second) ||
  113. (!result.first && result.second));
  114. return result;
  115. }
  116. void ExpectFormatError(ParseUnsignedBundleResult result) {
  117. ASSERT_TRUE(result.second);
  118. EXPECT_EQ(result.second->type, mojom::BundleParseErrorType::kFormatError);
  119. }
  120. // Finds the response for |url|.
  121. mojom::BundleResponseLocationPtr FindResponse(
  122. const mojom::BundleMetadataPtr& metadata,
  123. const GURL& url) {
  124. const auto item = metadata->requests.find(url);
  125. if (item == metadata->requests.end())
  126. return nullptr;
  127. return item->second.Clone();
  128. }
  129. mojom::BundleResponsePtr ParseResponse(
  130. TestDataSource* data_source,
  131. const mojom::BundleResponseLocationPtr& location,
  132. const GURL& base_url = GURL()) {
  133. mojo::PendingRemote<mojom::BundleDataSource> source_remote;
  134. data_source->AddReceiver(source_remote.InitWithNewPipeAndPassReceiver());
  135. mojo::PendingRemote<mojom::WebBundleParser> parser_remote;
  136. WebBundleParser parser_impl(parser_remote.InitWithNewPipeAndPassReceiver(),
  137. std::move(source_remote), base_url);
  138. mojom::WebBundleParser& parser = parser_impl;
  139. base::test::TestFuture<mojom::BundleResponsePtr,
  140. mojom::BundleResponseParseErrorPtr>
  141. future;
  142. parser.ParseResponse(location->offset, location->length,
  143. future.GetCallback());
  144. return std::get<0>(future.Take());
  145. }
  146. cbor::Value CreateByteString(base::StringPiece s) {
  147. return cbor::Value(base::as_bytes(base::make_span(s)));
  148. }
  149. std::string AsString(const std::vector<uint8_t>& data) {
  150. return std::string(reinterpret_cast<const char*>(data.data()), data.size());
  151. }
  152. std::vector<uint8_t> CreateSmallBundle() {
  153. web_package::WebBundleBuilder builder;
  154. builder.AddExchange(kPrimaryUrl,
  155. {{":status", "200"}, {"content-type", "text/plain"}},
  156. "payload");
  157. return builder.CreateBundle();
  158. }
  159. struct SignedWebBundleAndKeys {
  160. std::vector<uint8_t> bundle;
  161. std::vector<WebBundleSigner::KeyPair> key_pairs;
  162. };
  163. WebBundleSigner::KeyPair CreateKeys() {
  164. std::vector<uint8_t> public_key(ED25519_PUBLIC_KEY_LEN);
  165. std::vector<uint8_t> private_key(ED25519_PRIVATE_KEY_LEN);
  166. ED25519_keypair(public_key.data(), private_key.data());
  167. return WebBundleSigner::KeyPair(public_key, private_key);
  168. }
  169. SignedWebBundleAndKeys SignBundle(
  170. const std::vector<uint8_t>& unsigned_bundle,
  171. WebBundleSigner::ErrorForTesting error_for_testing =
  172. WebBundleSigner::ErrorForTesting::kNoError,
  173. size_t num_signatures = 1) {
  174. std::vector<WebBundleSigner::KeyPair> key_pairs;
  175. for (size_t i = 0; i < num_signatures; ++i) {
  176. key_pairs.push_back(CreateKeys());
  177. }
  178. return {
  179. WebBundleSigner::SignBundle(unsigned_bundle, key_pairs,
  180. error_for_testing),
  181. key_pairs,
  182. };
  183. }
  184. void CheckIfSignatureStackEntryIsValid(
  185. mojom::BundleIntegrityBlockSignatureStackEntryPtr& entry,
  186. const std::vector<uint8_t>& public_key) {
  187. EXPECT_EQ(entry->public_key, public_key);
  188. EXPECT_EQ(entry->signature.size(), 64ul);
  189. // The signature should also be present at the very end of
  190. // `complete_entry_cbor`.
  191. EXPECT_TRUE(
  192. std::equal(entry->signature.begin(), entry->signature.end(),
  193. entry->complete_entry_cbor.end() - entry->signature.size()));
  194. // The attributes should also be part of `complete_entry_cbor`.
  195. EXPECT_NE(
  196. std::search(entry->complete_entry_cbor.begin(),
  197. entry->complete_entry_cbor.end(),
  198. entry->attributes_cbor.begin(), entry->attributes_cbor.end()),
  199. entry->complete_entry_cbor.end());
  200. // The attributes should contain the public key.
  201. EXPECT_NE(
  202. std::search(entry->attributes_cbor.begin(), entry->attributes_cbor.end(),
  203. public_key.begin(), public_key.end()),
  204. entry->attributes_cbor.end());
  205. }
  206. } // namespace
  207. class WebBundleParserTest : public testing::Test {
  208. private:
  209. base::test::TaskEnvironment task_environment_;
  210. };
  211. TEST_F(WebBundleParserTest, WrongMagic) {
  212. WebBundleBuilder builder;
  213. std::vector<uint8_t> bundle = builder.CreateBundle();
  214. bundle[3] ^= 1;
  215. TestDataSource data_source(bundle);
  216. mojom::BundleMetadataParseErrorPtr error =
  217. ParseUnsignedBundle(&data_source).second;
  218. ASSERT_TRUE(error);
  219. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kFormatError);
  220. }
  221. TEST_F(WebBundleParserTest, UnknownVersion) {
  222. WebBundleBuilder builder;
  223. std::vector<uint8_t> bundle = builder.CreateBundle();
  224. // Modify the version string from "b2\0\0" to "q2\0\0".
  225. ASSERT_EQ(bundle[11], 'b');
  226. bundle[11] = 'q';
  227. TestDataSource data_source(bundle);
  228. mojom::BundleMetadataParseErrorPtr error =
  229. ParseUnsignedBundle(&data_source).second;
  230. ASSERT_TRUE(error);
  231. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kVersionError);
  232. }
  233. TEST_F(WebBundleParserTest, SectionLengthsTooLarge) {
  234. WebBundleBuilder builder;
  235. std::string too_long_section_name(8192, 'x');
  236. builder.AddSection(too_long_section_name, cbor::Value(0));
  237. TestDataSource data_source(builder.CreateBundle());
  238. ExpectFormatError(ParseUnsignedBundle(&data_source));
  239. }
  240. TEST_F(WebBundleParserTest, DuplicateSectionName) {
  241. WebBundleBuilder builder;
  242. builder.AddSection("foo", cbor::Value(0));
  243. builder.AddSection("foo", cbor::Value(0));
  244. TestDataSource data_source(builder.CreateBundle());
  245. ExpectFormatError(ParseUnsignedBundle(&data_source));
  246. }
  247. TEST_F(WebBundleParserTest, InvalidRequestURL) {
  248. WebBundleBuilder builder;
  249. builder.AddExchange("", {{":status", "200"}, {"content-type", "text/plain"}},
  250. "payload");
  251. TestDataSource data_source(builder.CreateBundle());
  252. ExpectFormatError(ParseUnsignedBundle(&data_source));
  253. }
  254. TEST_F(WebBundleParserTest, RequestURLIsNotUTF8) {
  255. WebBundleBuilder builder(BundleVersion::kB2,
  256. /*allow_invalid_utf8_strings_for_testing*/ true);
  257. builder.AddExchange("https://test.example.com/\xcc",
  258. {{":status", "200"}, {"content-type", "text/plain"}},
  259. "payload");
  260. TestDataSource data_source(builder.CreateBundle());
  261. ExpectFormatError(ParseUnsignedBundle(&data_source));
  262. }
  263. // TODO(crbug.com/966753): Revisit this once
  264. // https://github.com/WICG/webpackage/issues/468 is resolved.
  265. TEST_F(WebBundleParserTest, RequestURLHasNonStandardScheme) {
  266. WebBundleBuilder builder;
  267. builder.AddExchange("foo://bar",
  268. {{":status", "200"}, {"content-type", "text/plain"}},
  269. "payload");
  270. TestDataSource data_source(builder.CreateBundle());
  271. ASSERT_TRUE(ParseUnsignedBundle(&data_source).first);
  272. }
  273. TEST_F(WebBundleParserTest, RequestURLHasIsolatedAppScheme) {
  274. WebBundleBuilder builder;
  275. builder.AddExchange("isolated-app://foo",
  276. {{":status", "200"}, {"content-type", "text/plain"}},
  277. "payload");
  278. TestDataSource data_source(builder.CreateBundle());
  279. ASSERT_TRUE(ParseUnsignedBundle(&data_source).first);
  280. }
  281. TEST_F(WebBundleParserTest, RequestURLHasCredentials) {
  282. WebBundleBuilder builder;
  283. builder.AddExchange("https://user:passwd@test.example.com/",
  284. {{":status", "200"}, {"content-type", "text/plain"}},
  285. "payload");
  286. TestDataSource data_source(builder.CreateBundle());
  287. ExpectFormatError(ParseUnsignedBundle(&data_source));
  288. }
  289. TEST_F(WebBundleParserTest, RequestURLHasFragment) {
  290. WebBundleBuilder builder;
  291. builder.AddExchange("https://test.example.com/#fragment",
  292. {{":status", "200"}, {"content-type", "text/plain"}},
  293. "payload");
  294. TestDataSource data_source(builder.CreateBundle());
  295. ExpectFormatError(ParseUnsignedBundle(&data_source));
  296. }
  297. TEST_F(WebBundleParserTest, RequestURLIsValidUuidInPackage) {
  298. const char uuid_in_package[] =
  299. "uuid-in-package:f81d4fae-7dec-11d0-a765-00a0c91e6bf6";
  300. WebBundleBuilder builder;
  301. builder.AddExchange(uuid_in_package,
  302. {{":status", "200"}, {"content-type", "text/plain"}},
  303. "payload");
  304. TestDataSource data_source(builder.CreateBundle());
  305. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  306. ASSERT_TRUE(metadata);
  307. ASSERT_EQ(metadata->requests.size(), 1u);
  308. auto location = FindResponse(metadata, GURL(uuid_in_package));
  309. ASSERT_TRUE(location);
  310. }
  311. TEST_F(WebBundleParserTest, NoStatusInResponseHeaders) {
  312. WebBundleBuilder builder;
  313. builder.AddExchange("https://test.example.com/",
  314. {{"content-type", "text/plain"}},
  315. "payload"); // ":status" is missing.
  316. TestDataSource data_source(builder.CreateBundle());
  317. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  318. ASSERT_TRUE(metadata);
  319. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  320. ASSERT_TRUE(location);
  321. ASSERT_FALSE(ParseResponse(&data_source, location));
  322. }
  323. TEST_F(WebBundleParserTest, InvalidResponseStatus) {
  324. WebBundleBuilder builder;
  325. builder.AddExchange("https://test.example.com/",
  326. {{":status", "0200"}, {"content-type", "text/plain"}},
  327. "payload");
  328. TestDataSource data_source(builder.CreateBundle());
  329. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  330. ASSERT_TRUE(metadata);
  331. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  332. ASSERT_TRUE(location);
  333. ASSERT_FALSE(ParseResponse(&data_source, location));
  334. }
  335. TEST_F(WebBundleParserTest, ExtraPseudoInResponseHeaders) {
  336. WebBundleBuilder builder;
  337. builder.AddExchange(
  338. "https://test.example.com/",
  339. {{":status", "200"}, {":foo", ""}, {"content-type", "text/plain"}},
  340. "payload");
  341. TestDataSource data_source(builder.CreateBundle());
  342. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  343. ASSERT_TRUE(metadata);
  344. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  345. ASSERT_TRUE(location);
  346. ASSERT_FALSE(ParseResponse(&data_source, location));
  347. }
  348. TEST_F(WebBundleParserTest, UpperCaseCharacterInHeaderName) {
  349. WebBundleBuilder builder;
  350. builder.AddExchange("https://test.example.com/",
  351. {{":status", "200"}, {"Content-Type", "text/plain"}},
  352. "payload");
  353. TestDataSource data_source(builder.CreateBundle());
  354. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  355. ASSERT_TRUE(metadata);
  356. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  357. ASSERT_TRUE(location);
  358. ASSERT_FALSE(ParseResponse(&data_source, location));
  359. }
  360. TEST_F(WebBundleParserTest, InvalidHeaderValue) {
  361. WebBundleBuilder builder;
  362. builder.AddExchange("https://test.example.com/",
  363. {{":status", "200"}, {"content-type", "\n"}}, "payload");
  364. TestDataSource data_source(builder.CreateBundle());
  365. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  366. ASSERT_TRUE(metadata);
  367. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  368. ASSERT_TRUE(location);
  369. ASSERT_FALSE(ParseResponse(&data_source, location));
  370. }
  371. TEST_F(WebBundleParserTest, NoContentTypeWithNonEmptyContent) {
  372. WebBundleBuilder builder;
  373. builder.AddExchange("https://test.example.com/", {{":status", "200"}},
  374. "payload");
  375. TestDataSource data_source(builder.CreateBundle());
  376. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  377. ASSERT_TRUE(metadata);
  378. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  379. ASSERT_TRUE(location);
  380. ASSERT_FALSE(ParseResponse(&data_source, location));
  381. }
  382. TEST_F(WebBundleParserTest, NoContentTypeWithEmptyContent) {
  383. WebBundleBuilder builder;
  384. builder.AddExchange("https://test.example.com/", {{":status", "301"}}, "");
  385. TestDataSource data_source(builder.CreateBundle());
  386. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  387. ASSERT_TRUE(metadata);
  388. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  389. ASSERT_TRUE(location);
  390. ASSERT_TRUE(ParseResponse(&data_source, location));
  391. }
  392. TEST_F(WebBundleParserTest, AllKnownSectionInCritical) {
  393. WebBundleBuilder builder;
  394. builder.AddExchange("https://test.example.com/",
  395. {{":status", "200"}, {"content-type", "text/plain"}},
  396. "payload");
  397. cbor::Value::ArrayValue critical_section;
  398. critical_section.emplace_back("index");
  399. critical_section.emplace_back("critical");
  400. critical_section.emplace_back("responses");
  401. builder.AddSection("critical", cbor::Value(critical_section));
  402. TestDataSource data_source(builder.CreateBundle());
  403. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  404. ASSERT_TRUE(metadata);
  405. }
  406. TEST_F(WebBundleParserTest, UnknownSectionInCritical) {
  407. WebBundleBuilder builder;
  408. builder.AddExchange("https://test.example.com/",
  409. {{":status", "200"}, {"content-type", "text/plain"}},
  410. "payload");
  411. cbor::Value::ArrayValue critical_section;
  412. critical_section.emplace_back("unknown_section_name");
  413. builder.AddSection("critical", cbor::Value(critical_section));
  414. TestDataSource data_source(builder.CreateBundle());
  415. ExpectFormatError(ParseUnsignedBundle(&data_source));
  416. }
  417. TEST_F(WebBundleParserTest, EmptySignaturesSection) {
  418. WebBundleBuilder builder;
  419. builder.AddExchange("https://test.example.com/",
  420. {{":status", "200"}, {"content-type", "text/plain"}},
  421. "payload");
  422. // WebBundleBuilder omits signatures section if empty, so create it
  423. // ourselves.
  424. cbor::Value::ArrayValue signatures_section;
  425. signatures_section.emplace_back(cbor::Value::ArrayValue()); // authorities
  426. signatures_section.emplace_back(
  427. cbor::Value::ArrayValue()); // vouched-subsets
  428. builder.AddSection("signatures", cbor::Value(signatures_section));
  429. TestDataSource data_source(builder.CreateBundle());
  430. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  431. ASSERT_TRUE(metadata);
  432. EXPECT_TRUE(metadata->authorities.empty());
  433. EXPECT_TRUE(metadata->vouched_subsets.empty());
  434. }
  435. TEST_F(WebBundleParserTest, SignaturesSection) {
  436. WebBundleBuilder builder;
  437. builder.AddExchange("https://test.example.com/",
  438. {{":status", "200"}, {"content-type", "text/plain"}},
  439. "payload");
  440. // Create a signatures section with some dummy data.
  441. cbor::Value::MapValue authority;
  442. authority.emplace("cert", CreateByteString("[cert]"));
  443. authority.emplace("ocsp", CreateByteString("[ocsp]"));
  444. authority.emplace("sct", CreateByteString("[sct]"));
  445. builder.AddAuthority(std::move(authority));
  446. cbor::Value signed_bytes = builder.CreateEncodedSigned(
  447. kValidityUrl, "[auth-sha256]", kSignatureDate,
  448. kSignatureDate + kSignatureDuration, "https://test.example.com/",
  449. "[header-sha256]", "[payload-integrity]");
  450. cbor::Value::MapValue vouched_subset;
  451. vouched_subset.emplace("authority", 0);
  452. vouched_subset.emplace("sig", CreateByteString("[sig]"));
  453. vouched_subset.emplace("signed", signed_bytes.Clone());
  454. builder.AddVouchedSubset(std::move(vouched_subset));
  455. TestDataSource data_source(builder.CreateBundle());
  456. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  457. ASSERT_TRUE(metadata);
  458. ASSERT_EQ(metadata->authorities.size(), 1u);
  459. EXPECT_EQ(AsString(metadata->authorities[0]->cert), "[cert]");
  460. ASSERT_TRUE(metadata->authorities[0]->ocsp.has_value());
  461. EXPECT_EQ(AsString(*metadata->authorities[0]->ocsp), "[ocsp]");
  462. ASSERT_TRUE(metadata->authorities[0]->sct.has_value());
  463. EXPECT_EQ(AsString(*metadata->authorities[0]->sct), "[sct]");
  464. ASSERT_EQ(metadata->vouched_subsets.size(), 1u);
  465. EXPECT_EQ(metadata->vouched_subsets[0]->authority, 0u);
  466. EXPECT_EQ(AsString(metadata->vouched_subsets[0]->sig), "[sig]");
  467. EXPECT_EQ(AsString(metadata->vouched_subsets[0]->raw_signed),
  468. signed_bytes.GetBytestringAsString());
  469. const auto& parsed_signed = metadata->vouched_subsets[0]->parsed_signed;
  470. EXPECT_EQ(parsed_signed->validity_url, kValidityUrl);
  471. EXPECT_EQ(AsString(parsed_signed->auth_sha256), "[auth-sha256]");
  472. EXPECT_EQ(parsed_signed->date, kSignatureDate);
  473. EXPECT_EQ(parsed_signed->expires, kSignatureDate + kSignatureDuration);
  474. EXPECT_EQ(parsed_signed->subset_hashes.size(), 1u);
  475. const auto& hashes =
  476. parsed_signed->subset_hashes[GURL("https://test.example.com/")];
  477. ASSERT_TRUE(hashes);
  478. EXPECT_EQ(hashes->variants_value, "");
  479. ASSERT_EQ(hashes->resource_integrities.size(), 1u);
  480. EXPECT_EQ(AsString(hashes->resource_integrities[0]->header_sha256),
  481. "[header-sha256]");
  482. EXPECT_EQ(hashes->resource_integrities[0]->payload_integrity_header,
  483. "[payload-integrity]");
  484. }
  485. TEST_F(WebBundleParserTest, MultipleSignatures) {
  486. WebBundleBuilder builder;
  487. builder.AddExchange("https://test.example.com/",
  488. {{":status", "200"}, {"content-type", "text/plain"}},
  489. "payload");
  490. // Create a signatures section with some dummy data.
  491. cbor::Value::MapValue authority1;
  492. authority1.emplace("cert", CreateByteString("[cert1]"));
  493. authority1.emplace("ocsp", CreateByteString("[ocsp]"));
  494. authority1.emplace("sct", CreateByteString("[sct]"));
  495. builder.AddAuthority(std::move(authority1));
  496. cbor::Value::MapValue authority2;
  497. authority2.emplace("cert", CreateByteString("[cert2]"));
  498. builder.AddAuthority(std::move(authority2));
  499. cbor::Value signed_bytes1 = builder.CreateEncodedSigned(
  500. kValidityUrl, "[auth-sha256]", kSignatureDate,
  501. kSignatureDate + kSignatureDuration, "https://test.example.com/",
  502. "[header-sha256]", "[payload-integrity]");
  503. cbor::Value::MapValue vouched_subset1;
  504. vouched_subset1.emplace("authority", 0);
  505. vouched_subset1.emplace("sig", CreateByteString("[sig1]"));
  506. vouched_subset1.emplace("signed", signed_bytes1.Clone());
  507. builder.AddVouchedSubset(std::move(vouched_subset1));
  508. cbor::Value signed_bytes2 = builder.CreateEncodedSigned(
  509. kValidityUrl, "[auth-sha256-2]", kSignatureDate,
  510. kSignatureDate + kSignatureDuration, "https://test.example.org/",
  511. "[header-sha256-2]", "[payload-integrity-2]");
  512. cbor::Value::MapValue vouched_subset2;
  513. vouched_subset2.emplace("authority", 1);
  514. vouched_subset2.emplace("sig", CreateByteString("[sig2]"));
  515. vouched_subset2.emplace("signed", signed_bytes2.Clone());
  516. builder.AddVouchedSubset(std::move(vouched_subset2));
  517. TestDataSource data_source(builder.CreateBundle());
  518. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  519. ASSERT_TRUE(metadata);
  520. ASSERT_EQ(metadata->authorities.size(), 2u);
  521. EXPECT_EQ(AsString(metadata->authorities[0]->cert), "[cert1]");
  522. EXPECT_TRUE(metadata->authorities[0]->ocsp.has_value());
  523. EXPECT_TRUE(metadata->authorities[0]->sct.has_value());
  524. EXPECT_EQ(AsString(metadata->authorities[1]->cert), "[cert2]");
  525. EXPECT_FALSE(metadata->authorities[1]->ocsp.has_value());
  526. EXPECT_FALSE(metadata->authorities[1]->sct.has_value());
  527. ASSERT_EQ(metadata->vouched_subsets.size(), 2u);
  528. EXPECT_EQ(metadata->vouched_subsets[0]->authority, 0u);
  529. EXPECT_EQ(AsString(metadata->vouched_subsets[0]->sig), "[sig1]");
  530. EXPECT_EQ(AsString(metadata->vouched_subsets[0]->raw_signed),
  531. signed_bytes1.GetBytestringAsString());
  532. EXPECT_EQ(metadata->vouched_subsets[1]->authority, 1u);
  533. EXPECT_EQ(AsString(metadata->vouched_subsets[1]->sig), "[sig2]");
  534. EXPECT_EQ(AsString(metadata->vouched_subsets[1]->raw_signed),
  535. signed_bytes2.GetBytestringAsString());
  536. }
  537. TEST_F(WebBundleParserTest, ParseGoldenFile) {
  538. TestDataSource data_source(base::FilePath(FILE_PATH_LITERAL("hello_b2.wbn")));
  539. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  540. ASSERT_TRUE(metadata);
  541. ASSERT_EQ(metadata->requests.size(), 4u);
  542. EXPECT_EQ(metadata->primary_url, "https://test.example.org/");
  543. std::map<std::string, mojom::BundleResponsePtr> responses;
  544. for (const auto& item : metadata->requests) {
  545. auto location = FindResponse(metadata, item.first);
  546. ASSERT_TRUE(location);
  547. auto resp = ParseResponse(&data_source, location);
  548. ASSERT_TRUE(resp);
  549. responses[item.first.spec()] = std::move(resp);
  550. }
  551. ASSERT_TRUE(responses["https://test.example.org/"]);
  552. EXPECT_EQ(responses["https://test.example.org/"]->response_code, 200);
  553. EXPECT_EQ(
  554. responses["https://test.example.org/"]->response_headers["content-type"],
  555. "text/html; charset=utf-8");
  556. EXPECT_EQ(data_source.GetPayload(responses["https://test.example.org/"]),
  557. GetTestFileContents(
  558. base::FilePath(FILE_PATH_LITERAL("hello/index.html"))));
  559. EXPECT_TRUE(responses["https://test.example.org/index.html"]);
  560. EXPECT_TRUE(responses["https://test.example.org/manifest.webmanifest"]);
  561. EXPECT_TRUE(responses["https://test.example.org/script.js"]);
  562. }
  563. TEST_F(WebBundleParserTest, ParseFileWithVouchedSubsets) {
  564. TestDataSource data_source(
  565. base::FilePath(FILE_PATH_LITERAL("hello_vouched_subsets.wbn")));
  566. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  567. ASSERT_TRUE(metadata);
  568. EXPECT_EQ(metadata->authorities.size(), 1u);
  569. ASSERT_EQ(metadata->vouched_subsets.size(), 1u);
  570. EXPECT_EQ(metadata->vouched_subsets[0]->authority, 0u);
  571. const auto& parsed_signed = metadata->vouched_subsets[0]->parsed_signed;
  572. EXPECT_EQ(parsed_signed->validity_url, kValidityUrl);
  573. EXPECT_EQ(parsed_signed->date, kSignatureDate);
  574. EXPECT_EQ(parsed_signed->expires, kSignatureDate + kSignatureDuration);
  575. EXPECT_EQ(parsed_signed->subset_hashes.size(), metadata->requests.size());
  576. const auto& hashes =
  577. parsed_signed->subset_hashes[GURL("https://test.example.org/")];
  578. ASSERT_TRUE(hashes);
  579. EXPECT_EQ(hashes->variants_value, "");
  580. ASSERT_EQ(hashes->resource_integrities.size(), 1u);
  581. EXPECT_EQ(hashes->resource_integrities[0]->payload_integrity_header,
  582. "digest/mi-sha256-03");
  583. }
  584. TEST_F(WebBundleParserTest, SingleEntry) {
  585. WebBundleBuilder builder;
  586. builder.AddPrimaryURL(kPrimaryUrl);
  587. builder.AddExchange("https://test.example.com/",
  588. {{":status", "200"}, {"content-type", "text/plain"}},
  589. "payload");
  590. TestDataSource data_source(builder.CreateBundle());
  591. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  592. ASSERT_TRUE(metadata);
  593. ASSERT_EQ(metadata->version, mojom::BundleFormatVersion::kB2);
  594. ASSERT_EQ(metadata->requests.size(), 1u);
  595. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  596. ASSERT_TRUE(location);
  597. auto response = ParseResponse(&data_source, location);
  598. ASSERT_TRUE(response);
  599. EXPECT_EQ(response->response_code, 200);
  600. EXPECT_EQ(response->response_headers.size(), 1u);
  601. EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  602. EXPECT_EQ(data_source.GetPayload(response), "payload");
  603. EXPECT_EQ(metadata->primary_url, kPrimaryUrl);
  604. }
  605. TEST_F(WebBundleParserTest, NoPrimaryUrlSingleEntry) {
  606. WebBundleBuilder builder;
  607. builder.AddExchange("https://test.example.com/",
  608. {{":status", "200"}, {"content-type", "text/plain"}},
  609. "payload");
  610. TestDataSource data_source(builder.CreateBundle());
  611. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  612. ASSERT_TRUE(metadata);
  613. ASSERT_EQ(metadata->requests.size(), 1u);
  614. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  615. ASSERT_TRUE(location);
  616. auto response = ParseResponse(&data_source, location);
  617. ASSERT_TRUE(response);
  618. EXPECT_EQ(response->response_code, 200);
  619. EXPECT_EQ(response->response_headers.size(), 1u);
  620. EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  621. EXPECT_EQ(data_source.GetPayload(response), "payload");
  622. EXPECT_TRUE(metadata->primary_url.is_empty());
  623. }
  624. TEST_F(WebBundleParserTest, RelativeURL) {
  625. WebBundleBuilder builder;
  626. builder.AddPrimaryURL("path/to/primary_url");
  627. builder.AddExchange("path/to/file.txt",
  628. {{":status", "200"}, {"content-type", "text/plain"}},
  629. "payload");
  630. TestDataSource data_source(builder.CreateBundle());
  631. const GURL base_url("https://test.example.com/dir/test.wbn");
  632. mojom::BundleMetadataPtr metadata =
  633. ParseUnsignedBundle(&data_source, base_url).first;
  634. EXPECT_EQ(metadata->primary_url,
  635. "https://test.example.com/dir/path/to/primary_url");
  636. ASSERT_TRUE(metadata);
  637. ASSERT_EQ(metadata->requests.size(), 1u);
  638. auto location = FindResponse(
  639. metadata, GURL("https://test.example.com/dir/path/to/file.txt"));
  640. ASSERT_TRUE(location);
  641. auto response = ParseResponse(&data_source, location, base_url);
  642. ASSERT_TRUE(response);
  643. EXPECT_EQ(response->response_code, 200);
  644. EXPECT_EQ(response->response_headers.size(), 1u);
  645. EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  646. EXPECT_EQ(data_source.GetPayload(response), "payload");
  647. }
  648. TEST_F(WebBundleParserTest, RandomAccessContextWithAutomaticOffset) {
  649. std::vector<uint8_t> bundle = CreateSmallBundle();
  650. TestDataSource data_source(bundle, /*is_random_access_context=*/true);
  651. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  652. ASSERT_TRUE(metadata);
  653. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  654. ASSERT_TRUE(location);
  655. auto response = ParseResponse(&data_source, location);
  656. ASSERT_TRUE(response);
  657. EXPECT_EQ(response->response_code, 200);
  658. EXPECT_EQ(response->response_headers.size(), 1u);
  659. EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  660. EXPECT_EQ(data_source.GetPayload(response), "payload");
  661. }
  662. TEST_F(WebBundleParserTest, RandomAccessContextWithFixedCorrectOffset) {
  663. std::vector<uint8_t> bundle = CreateSmallBundle();
  664. TestDataSource data_source(bundle, /*is_random_access_context=*/true);
  665. mojom::BundleMetadataPtr metadata =
  666. ParseUnsignedBundle(&data_source, GURL(), 0).first;
  667. ASSERT_TRUE(metadata);
  668. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  669. ASSERT_TRUE(location);
  670. auto response = ParseResponse(&data_source, location);
  671. ASSERT_TRUE(response);
  672. EXPECT_EQ(response->response_code, 200);
  673. EXPECT_EQ(response->response_headers.size(), 1u);
  674. EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  675. EXPECT_EQ(data_source.GetPayload(response), "payload");
  676. }
  677. TEST_F(WebBundleParserTest, RandomAccessContextWithFixedIncorrectOffset) {
  678. std::vector<uint8_t> bundle = CreateSmallBundle();
  679. TestDataSource data_source(bundle, /*is_random_access_context=*/true);
  680. mojom::BundleMetadataParseErrorPtr error =
  681. ParseUnsignedBundle(&data_source, GURL(), 1).second;
  682. ASSERT_TRUE(error);
  683. }
  684. TEST_F(WebBundleParserTest,
  685. RandomAccessContextPrependedDataWithAutomaticOffset) {
  686. std::vector<uint8_t> bundle = CreateSmallBundle();
  687. bundle.insert(bundle.begin(),
  688. {'o', 't', 'h', 'e', 'r', ' ', 'd', 'a', 't', 'a'});
  689. TestDataSource data_source(bundle, /*is_random_access_context=*/true);
  690. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  691. ASSERT_TRUE(metadata);
  692. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  693. ASSERT_TRUE(location);
  694. auto response = ParseResponse(&data_source, location);
  695. ASSERT_TRUE(response);
  696. EXPECT_EQ(response->response_code, 200);
  697. EXPECT_EQ(response->response_headers.size(), 1u);
  698. EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  699. EXPECT_EQ(data_source.GetPayload(response), "payload");
  700. }
  701. TEST_F(WebBundleParserTest,
  702. RandomAccessContextPrependedDataWithFixedCorrectOffset) {
  703. std::vector<uint8_t> bundle = CreateSmallBundle();
  704. bundle.insert(bundle.begin(),
  705. {'o', 't', 'h', 'e', 'r', ' ', 'd', 'a', 't', 'a'});
  706. TestDataSource data_source(bundle, /*is_random_access_context=*/true);
  707. mojom::BundleMetadataPtr metadata =
  708. ParseUnsignedBundle(&data_source, GURL(), 10).first;
  709. ASSERT_TRUE(metadata);
  710. auto location = FindResponse(metadata, GURL("https://test.example.com/"));
  711. ASSERT_TRUE(location);
  712. auto response = ParseResponse(&data_source, location);
  713. ASSERT_TRUE(response);
  714. EXPECT_EQ(response->response_code, 200);
  715. EXPECT_EQ(response->response_headers.size(), 1u);
  716. EXPECT_EQ(response->response_headers["content-type"], "text/plain");
  717. EXPECT_EQ(data_source.GetPayload(response), "payload");
  718. }
  719. TEST_F(WebBundleParserTest,
  720. RandomAccessContextPrependedDataWithFixedIncorrectOffset) {
  721. std::vector<uint8_t> bundle = CreateSmallBundle();
  722. bundle.insert(bundle.begin(),
  723. {'o', 't', 'h', 'e', 'r', ' ', 'd', 'a', 't', 'a'});
  724. TestDataSource data_source(bundle, /*is_random_access_context=*/true);
  725. mojom::BundleMetadataParseErrorPtr error =
  726. ParseUnsignedBundle(&data_source, GURL(), 3).second;
  727. ASSERT_TRUE(error);
  728. }
  729. TEST_F(WebBundleParserTest, RandomAccessContextLengthSmallerThanWebBundle) {
  730. std::vector<uint8_t> bundle = CreateSmallBundle();
  731. std::vector<uint8_t> invalid_length = {0, 0, 0, 0, 0, 0, 0, 10};
  732. std::copy(invalid_length.begin(), invalid_length.end(), bundle.end() - 8);
  733. TestDataSource data_source(bundle, /*is_random_access_context=*/true);
  734. ExpectFormatError(ParseUnsignedBundle(&data_source));
  735. }
  736. TEST_F(WebBundleParserTest, RandomAccessContextFileSmallerThanLengthField) {
  737. std::vector<uint8_t> bundle = {1, 2, 3, 4};
  738. TestDataSource data_source(bundle, /*is_random_access_context=*/true);
  739. ExpectFormatError(ParseUnsignedBundle(&data_source));
  740. }
  741. TEST_F(WebBundleParserTest, RandomAccessContextLengthBiggerThanFile) {
  742. std::vector<uint8_t> bundle = CreateSmallBundle();
  743. std::vector<uint8_t> invalid_length = {0xff, 0, 0, 0, 0, 0, 0, 0};
  744. std::copy(invalid_length.begin(), invalid_length.end(), bundle.end() - 8);
  745. TestDataSource data_source(bundle, /*is_random_access_context=*/true);
  746. ExpectFormatError(ParseUnsignedBundle(&data_source));
  747. }
  748. // TODO(crbug.com/969596): Add a test case that loads a wbn file with variants,
  749. // once gen-bundle supports variants.
  750. // This test verifies that even if a bundle is signed, it is still readable as
  751. // an unsigned bundle in random-access contexts, since the `length` field of the
  752. // web bundle can be used to find the start of the unsigned bundle.
  753. TEST_F(WebBundleParserTest, SignedBundleMetadataOnlyInRandomAccessContexts) {
  754. auto bundle_and_keys = SignBundle(CreateSmallBundle());
  755. TestDataSource data_source(bundle_and_keys.bundle, true);
  756. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  757. EXPECT_TRUE(metadata);
  758. }
  759. // This test verifies that when a bundle is signed, it can not be read as an
  760. // unsigned bundle in non-random-access contexts, since the `length` field of
  761. // the web bundle can't be used then.
  762. TEST_F(WebBundleParserTest, SignedBundleMetadataOnlyInNonRandomAccessContexts) {
  763. auto bundle_and_keys = SignBundle(CreateSmallBundle());
  764. TestDataSource data_source(bundle_and_keys.bundle, false);
  765. mojom::BundleMetadataPtr metadata = ParseUnsignedBundle(&data_source).first;
  766. EXPECT_FALSE(metadata);
  767. }
  768. TEST_F(WebBundleParserTest, SignedBundleIntegrityBlockIsParsedCorrectly) {
  769. auto unsigned_bundle = CreateSmallBundle();
  770. auto bundle_and_keys = SignBundle(unsigned_bundle);
  771. TestDataSource data_source(bundle_and_keys.bundle);
  772. auto [integrity_block, error] = ParseSignedBundleIntegrityBlock(&data_source);
  773. EXPECT_TRUE(integrity_block) << error->message;
  774. // The size of the integrity block should be exactly equal to the size
  775. // difference between a signed and an unsigned bundle.
  776. EXPECT_EQ(integrity_block->size,
  777. bundle_and_keys.bundle.size() - unsigned_bundle.size());
  778. // There should be exactly one signature stack entry, corresponding to the
  779. // public key that was used to sign the web bundle.
  780. EXPECT_EQ(integrity_block->signature_stack.size(), 1ul);
  781. auto& entry = integrity_block->signature_stack[0];
  782. CheckIfSignatureStackEntryIsValid(entry,
  783. bundle_and_keys.key_pairs[0].public_key);
  784. }
  785. TEST_F(WebBundleParserTest,
  786. SignedBundleIntegrityBlockIsParsedCorrectlyWithTwoSignatures) {
  787. auto unsigned_bundle = CreateSmallBundle();
  788. auto bundle_and_keys = SignBundle(
  789. unsigned_bundle, WebBundleSigner::ErrorForTesting::kNoError, 2);
  790. TestDataSource data_source(bundle_and_keys.bundle);
  791. auto [integrity_block, error] = ParseSignedBundleIntegrityBlock(&data_source);
  792. EXPECT_TRUE(integrity_block) << error->message;
  793. // The size of the integrity block should be exactly equal to the size
  794. // difference between a signed and an unsigned bundle.
  795. EXPECT_EQ(integrity_block->size,
  796. bundle_and_keys.bundle.size() - unsigned_bundle.size());
  797. // There should be exactly one signature stack entry, corresponding to the
  798. // public key that was used to sign the web bundle.
  799. EXPECT_EQ(integrity_block->signature_stack.size(), 2ul);
  800. CheckIfSignatureStackEntryIsValid(integrity_block->signature_stack[0],
  801. bundle_and_keys.key_pairs[0].public_key);
  802. CheckIfSignatureStackEntryIsValid(integrity_block->signature_stack[1],
  803. bundle_and_keys.key_pairs[1].public_key);
  804. }
  805. TEST_F(WebBundleParserTest, SignedBundleWrongMagic) {
  806. WebBundleBuilder builder;
  807. std::vector<uint8_t> unsigned_bundle = builder.CreateBundle();
  808. auto bundle_and_keys = SignBundle(unsigned_bundle);
  809. bundle_and_keys.bundle[3] ^= 1;
  810. TestDataSource data_source(bundle_and_keys.bundle);
  811. mojom::BundleIntegrityBlockParseErrorPtr error =
  812. ParseSignedBundleIntegrityBlock(&data_source).second;
  813. ASSERT_TRUE(error);
  814. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kFormatError);
  815. EXPECT_EQ(error->message, "Wrong array size or magic bytes.");
  816. }
  817. TEST_F(WebBundleParserTest, SignedBundleUnknownVersion) {
  818. WebBundleBuilder builder;
  819. std::vector<uint8_t> unsigned_bundle = builder.CreateBundle();
  820. auto bundle_and_keys = SignBundle(unsigned_bundle);
  821. // Modify the version string from "1b\0\0" to "1q\0\0".
  822. ASSERT_EQ(bundle_and_keys.bundle[12], 'b');
  823. bundle_and_keys.bundle[12] = 'q';
  824. TestDataSource data_source(bundle_and_keys.bundle);
  825. mojom::BundleIntegrityBlockParseErrorPtr error =
  826. ParseSignedBundleIntegrityBlock(&data_source).second;
  827. ASSERT_TRUE(error);
  828. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kVersionError)
  829. << error->message;
  830. }
  831. TEST_F(WebBundleParserTest, SignedBundleEmptySignatureStack) {
  832. WebBundleBuilder builder;
  833. std::vector<uint8_t> unsigned_bundle = builder.CreateBundle();
  834. std::vector<uint8_t> signed_bundle(
  835. *cbor::Writer::Write(WebBundleSigner::CreateIntegrityBlock({})));
  836. signed_bundle.insert(signed_bundle.end(), unsigned_bundle.begin(),
  837. unsigned_bundle.end());
  838. TestDataSource data_source(signed_bundle);
  839. mojom::BundleIntegrityBlockParseErrorPtr error =
  840. ParseSignedBundleIntegrityBlock(&data_source).second;
  841. ASSERT_TRUE(error);
  842. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kFormatError);
  843. EXPECT_EQ(error->message,
  844. "The signature stack must contain one or two signatures (developer "
  845. "+ potentially distributor signature).");
  846. }
  847. TEST_F(WebBundleParserTest, SignedBundleSignatureStackWithThreeEntries) {
  848. WebBundleBuilder builder;
  849. std::vector<uint8_t> unsigned_bundle = builder.CreateBundle();
  850. auto bundle_and_keys = SignBundle(
  851. unsigned_bundle, WebBundleSigner::ErrorForTesting::kNoError, 3);
  852. TestDataSource data_source(bundle_and_keys.bundle);
  853. mojom::BundleIntegrityBlockParseErrorPtr error =
  854. ParseSignedBundleIntegrityBlock(&data_source).second;
  855. ASSERT_TRUE(error);
  856. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kFormatError);
  857. EXPECT_EQ(error->message,
  858. "The signature stack must contain one or two signatures (developer "
  859. "+ potentially distributor signature).");
  860. }
  861. TEST_F(WebBundleParserTest, SignedBundleWrongSignatureLength) {
  862. auto unsigned_bundle = CreateSmallBundle();
  863. auto [public_key, private_key] = CreateKeys();
  864. auto bundle_and_keys =
  865. SignBundle(unsigned_bundle,
  866. WebBundleSigner::ErrorForTesting::kInvalidSignatureLength);
  867. TestDataSource data_source(bundle_and_keys.bundle);
  868. auto error = ParseSignedBundleIntegrityBlock(&data_source).second;
  869. ASSERT_TRUE(error);
  870. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kFormatError);
  871. EXPECT_EQ(error->message,
  872. "The signature does not have the correct length, expected 64 "
  873. "bytes.");
  874. }
  875. TEST_F(WebBundleParserTest, SignedBundleWrongSignatureStackEntryLength) {
  876. auto unsigned_bundle = CreateSmallBundle();
  877. auto [public_key, private_key] = CreateKeys();
  878. auto bundle_and_keys = SignBundle(
  879. unsigned_bundle,
  880. WebBundleSigner::ErrorForTesting::kAdditionalSignatureStackEntryElement);
  881. TestDataSource data_source(bundle_and_keys.bundle);
  882. auto error = ParseSignedBundleIntegrityBlock(&data_source).second;
  883. ASSERT_TRUE(error);
  884. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kFormatError);
  885. EXPECT_EQ(error->message,
  886. "Each signature stack entry must contain exactly two elements.");
  887. }
  888. TEST_F(WebBundleParserTest, SignedBundleWrongAdditionalAttribute) {
  889. auto unsigned_bundle = CreateSmallBundle();
  890. auto bundle_and_keys =
  891. SignBundle(unsigned_bundle, WebBundleSigner::ErrorForTesting::
  892. kAdditionalSignatureStackEntryAttribute);
  893. TestDataSource data_source(bundle_and_keys.bundle);
  894. auto error = ParseSignedBundleIntegrityBlock(&data_source).second;
  895. ASSERT_TRUE(error);
  896. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kFormatError);
  897. EXPECT_EQ(
  898. error->message,
  899. "A signature stack entry's attributes must be a map with one element.");
  900. }
  901. TEST_F(WebBundleParserTest, SignedBundleNoPublicKeyAttribute) {
  902. auto unsigned_bundle = CreateSmallBundle();
  903. auto bundle_and_keys =
  904. SignBundle(unsigned_bundle, WebBundleSigner::ErrorForTesting::
  905. kNoPublicKeySignatureStackEntryAttribute);
  906. TestDataSource data_source(bundle_and_keys.bundle);
  907. auto error = ParseSignedBundleIntegrityBlock(&data_source).second;
  908. ASSERT_TRUE(error);
  909. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kFormatError);
  910. EXPECT_EQ(
  911. error->message,
  912. "A signature stack entry's attributes must be a map with one element.");
  913. }
  914. TEST_F(WebBundleParserTest, SignedBundleWrongPublicKeyAttribute) {
  915. auto unsigned_bundle = CreateSmallBundle();
  916. auto bundle_and_keys = SignBundle(
  917. unsigned_bundle,
  918. WebBundleSigner::ErrorForTesting::kWrongSignatureStackEntryAttributeName);
  919. TestDataSource data_source(bundle_and_keys.bundle);
  920. auto error = ParseSignedBundleIntegrityBlock(&data_source).second;
  921. ASSERT_TRUE(error);
  922. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kFormatError);
  923. EXPECT_EQ(
  924. error->message,
  925. "The signature stack entry's attribute must have 'ed25519PublicKey' as "
  926. "its key.");
  927. }
  928. TEST_F(WebBundleParserTest, SignedBundleWrongPublicKeyLength) {
  929. auto unsigned_bundle = CreateSmallBundle();
  930. auto bundle_and_keys =
  931. SignBundle(unsigned_bundle,
  932. WebBundleSigner::ErrorForTesting::kInvalidPublicKeyLength);
  933. TestDataSource data_source(bundle_and_keys.bundle);
  934. auto error = ParseSignedBundleIntegrityBlock(&data_source).second;
  935. ASSERT_TRUE(error);
  936. EXPECT_EQ(error->type, mojom::BundleParseErrorType::kFormatError);
  937. EXPECT_EQ(error->message,
  938. "The public key does not have the correct length, expected 32 "
  939. "bytes.");
  940. }
  941. } // namespace web_package