/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "gtest/gtest.h" #include "mozilla/dom/IntegrityPolicyWAICT.h" #include "mozilla/dom/WAICTManifestBinding.h" #include "nsString.h" using namespace mozilla::dom; // Valid Manifests TEST(WAICTManifestValidation, ValidManifestBasic) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/assets/main.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } TEST(WAICTManifestValidation, ValidManifestWithMultipleHashes) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/assets/x.html": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=", "/assets/css/main.css": "zet5ebcBGt1+fr6F0vJbpOv7p4tV/fIbFH4AafxtBl0=", "/favicon.ico": "zbt5ebcBGt1+gr6F0vJbpOv7p4tV/fIbFH4AafxtBl0=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } TEST(WAICTManifestValidation, ValidManifestWithMixedCaseBase64) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": { "/script.js": "R4J9Yw07MpTfSq6zRYOv0aU8HFN2nQJQqmbQkl/SwCy=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } TEST(WAICTManifestValidation, ValidManifestWithOnlyHashes) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } TEST(WAICTManifestValidation, ValidManifestWithOnlyAnyHashes) { WAICTManifest manifest; nsCString json(R"JSON({ "any_hashes": [ "mVuswfW4XCBOWbx+QiKkPPQy+gTfr+i1sVADexgyN+8=", "H9OJUrESfT3SUlRpqAiDFEvqnnG2Sp9/eloyVMqxnnY=" ] })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } TEST(WAICTManifestValidation, ValidManifestWithBothAnyHashesAndHashes) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" }, "any_hashes": [ "mVuswfW4XCBOWbx+QiKkPPQy+gTfr+i1sVADexgyN+8=", "H9OJUrESfT3SUlRpqAiDFEvqnnG2Sp9/eloyVMqxnnY=", "0SsmrVFFC7wxU4QM5UeZeXBnyKlXTAzfkVsZXIrzabo=" ] })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } // We require that there is at least one hash anywhere, it's ok to have // empty arrays if the condition is met. TEST(WAICTManifestValidation, ValidManifestWithEmptyAnyHashes) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" }, "any_hashes": [] })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } // We allow duplicate hashes in any_hashes for performance reasons. // With large manifests (100k+ hashes), checking uniqueness would add // significant overhead (O(n²) or O(n) with hash set). Duplicates are // harmless - validation will just check the same hash multiple times. TEST(WAICTManifestValidation, ValidManifestWithDuplicateAnyHashes) { WAICTManifest manifest; nsCString json(R"JSON({ "any_hashes": [ "mVuswfW4XCBOWbx+QiKkPPQy+gTfr+i1sVADexgyN+8=", "H9OJUrESfT3SUlRpqAiDFEvqnnG2Sp9/eloyVMqxnnY=", "mVuswfW4XCBOWbx+QiKkPPQy+gTfr+i1sVADexgyN+8=" ] })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } TEST(WAICTManifestValidation, ValidManifestWithResourceDelimiter) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" }, "resource_delimiter": "/* DELIMITER */" })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } TEST(WAICTManifestValidation, ValidManifestWithTransparencyProof) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" }, "transparency_proof": "Lbzg/T0VD/HIUTRcTcU0/zbtSeT2302RKTc0VfDHIUTRcTcU0" })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } TEST(WAICTManifestValidation, ValidManifestWithAllOptionalFields) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" }, "bt-server": "https://bt.example.com", "any_hashes": ["mVuswfW4XCBOWbx+QiKkPPQy+gTfr+i1sVADexgyN+8="], "resource_delimiter": "/* MY DELIM */", "transparency_proof": "Lbzg/T0VD/HIUTRcTcU0/zbtSeT2302RKTc0Vf..." })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } // Invalid JSON TEST(WAICTManifestValidation, InvalidJSON_Malformed) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com" "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidJSON); } TEST(WAICTManifestValidation, InvalidJSON_NotAnObject) { WAICTManifest manifest; nsCString json = "\"just a string\""_ns; auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidJSON); } TEST(WAICTManifestValidation, InvalidJSON_Empty) { WAICTManifest manifest; nsCString json = ""_ns; auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidJSON); } // We require at least one hash to be present TEST(WAICTManifestValidation, MissingBothHashesAndAnyHashes) { WAICTManifest manifest; nsCString json(R"JSON({ })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::MissingHashes); } // We require at least one hash to be present TEST(WAICTManifestValidation, EmptyHashesAndNoAnyHashes) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": {} })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::MissingHashes); } TEST(WAICTManifestValidation, EmptyAnyHashesAndNoHashes) { WAICTManifest manifest; nsCString json(R"JSON({ "any_hashes": [] })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::MissingHashes); } TEST(WAICTManifestValidation, BothHashesAndAnyHashesEmpty) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": {}, "any_hashes": [] })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::MissingHashes); } // Invalid Hash Formats TEST(WAICTManifestValidation, InvalidHash_EmptyKey) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } TEST(WAICTManifestValidation, InvalidHash_EmptyValue) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } TEST(WAICTManifestValidation, InvalidHash_TooShort) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "hashhash" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } TEST(WAICTManifestValidation, InvalidHash_TooLong) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=extra" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } TEST(WAICTManifestValidation, InvalidHash_InvalidBase64Characters) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "r4j9yW07mpT@SQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } // = in the middle of base64 TEST(WAICTManifestValidation, InvalidHash_WrongPaddingPosition) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2N=jqQMBqKL/SWCY" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } // The next 2 come from the condition that base64 of sha256 is 43/44 chars TEST(WAICTManifestValidation, InvalidHash_43CharsWithPadding) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWC=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } TEST(WAICTManifestValidation, InvalidHash_44CharsWithoutPadding) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCYA" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } TEST(WAICTManifestValidation, InvalidHash_OneHashInvalid) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/valid.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=", "/invalid.js": "tooshort" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } // We are fine with an empty buffer, but empty string as a hash is considered as // an exception TEST(WAICTManifestValidation, InvalidAnyHash_EmptyString) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" }, "any_hashes": [""] })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } // The next is the same as for hashes TEST(WAICTManifestValidation, InvalidAnyHash_TooShort) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" }, "any_hashes": ["tooshort"] })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } TEST(WAICTManifestValidation, InvalidAnyHash_OneValidOneInvalid) { WAICTManifest manifest; nsCString json(R"JSON({ "bt-server": "https://bt.example.com", "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" }, "any_hashes": [ "mVuswfW4XCBOWbx+QiKkPPQy+gTfr+i1sVADexgyN+8=", "invalid" ] })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } // Potential attack: Memory exhaustion via extremely long hash values TEST(WAICTManifestValidation, InvalidHash_VeryLongHash) { WAICTManifest manifest; nsCString veryLongHash; for (int i = 0; i < 10000; i++) { veryLongHash.Append('A'); } nsCString json; json.Append(R"JSON({ "hashes": { "/script.js": ")JSON"); json.Append(veryLongHash); json.Append(R"JSON(" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } // Potential attack: Path traversal or injection via special characters TEST(WAICTManifestValidation, InvalidHash_SpecialCharactersInKey) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": { "../../../etc/passwd": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } // Potential attack: Memory exhaustion via extremely long keys TEST(WAICTManifestValidation, InvalidHash_VeryLongKey) { WAICTManifest manifest; nsCString veryLongKey; for (int i = 0; i < 10000; i++) { veryLongKey.Append("/very/long/path"); } nsCString json; json.Append(R"JSON({ "hashes": { ")JSON"); json.Append(veryLongKey); json.Append(R"JSON(": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::OK); } TEST(WAICTManifestValidation, InvalidHash_LeadingWhitespace) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": { "/script.js": " r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } TEST(WAICTManifestValidation, InvalidHash_TrailingWhitespace) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8Hfn2NqjqQMBqKL/SWCY= " } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } TEST(WAICTManifestValidation, InvalidHash_EmbeddedWhitespace) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8 Hfn2NqjqQMBqKL/SWCY=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidHashFormat); } // Null bytes are not valid in JSON strings, so the JSON parser rejects // this before we even reach hash validation TEST(WAICTManifestValidation, InvalidHash_NullByte) { WAICTManifest manifest; nsCString json(R"JSON({ "hashes": { "/script.js": "r4j9yW07mpTFSQ6ZRYOV0Au8)JSON"); json.Append('\0'); json.Append(R"JSON(Hfn2NqjqQMBqKL/SWCY=" } })JSON"); auto status = IntegrityPolicyWAICT::ValidateManifest(json, manifest); EXPECT_EQ(status, IntegrityPolicyWAICT::ManifestValidationStatus::InvalidJSON); }