/* * Copyright (c) 2023, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include namespace Web::SRI { constexpr Array supported_hash_functions { // These are sorted by strength, low to high. // NOTE: We are specifically told to refuse MD5 and SHA1. // https://w3c.github.io/webappsec-subresource-integrity/#hash-functions "sha256"sv, "sha384"sv, "sha512"sv, }; // https://w3c.github.io/webappsec-subresource-integrity/#getprioritizedhashfunction static StringView get_prioritized_hash_function(StringView a, StringView b) { if (a == b) return ""sv; auto a_priority = supported_hash_functions.first_index_of(a).value(); auto b_priority = supported_hash_functions.first_index_of(b).value(); if (a_priority > b_priority) return a; return b; } // https://w3c.github.io/webappsec-subresource-integrity/#apply-algorithm-to-response ErrorOr apply_algorithm_to_bytes(StringView algorithm, ByteBuffer const& bytes) { // NOTE: The steps are duplicated here because each hash algorithm returns a different result type. if (algorithm == "sha256"sv) { // 1. Let result be the result of applying algorithm to bytes. auto result = Crypto::Hash::SHA256::hash(bytes); // 2. Return the result of base64 encoding result. return encode_base64(result.bytes()); } if (algorithm == "sha384"sv) { // 1. Let result be the result of applying algorithm to bytes. auto result = Crypto::Hash::SHA384::hash(bytes); // 2. Return the result of base64 encoding result. return encode_base64(result.bytes()); } if (algorithm == "sha512"sv) { // 1. Let result be the result of applying algorithm to bytes. auto result = Crypto::Hash::SHA512::hash(bytes); // 2. Return the result of base64 encoding result. return encode_base64(result.bytes()); } VERIFY_NOT_REACHED(); } // https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata ErrorOr> parse_metadata(StringView metadata) { // 1. Let result be the empty set. Vector result; // 2. For each item returned by splitting metadata on spaces: TRY(metadata.for_each_split_view(' ', SplitBehavior::Nothing, [&](StringView item) -> ErrorOr { // 1. Let hash-with-opt-token-list be the result of splitting item on U+003F (?). auto hash_with_opt_token_list = item.split_view('?'); // 2. Let hash-expression be hash-with-opt-token-list[0]. auto hash_expression = hash_with_opt_token_list[0]; // 3. Let base64-value be the empty string. StringView base64_value; // 4. Let hash-expr-token-list be the result of splitting hash-expression on U+002D (-). auto hash_expr_token_list = hash_expression.split_view('-'); // 5. Let algorithm be hash-expr-token-list[0]. auto algorithm = hash_expr_token_list[0]; // 6. If hash-expr-token-list[1] exists, set base64-value to hash-expr-token-list[1]. if (hash_expr_token_list.size() >= 1) base64_value = hash_expr_token_list[1]; // 7. If algorithm is not a hash function recognized by the user agent, continue. if (!supported_hash_functions.contains_slow(algorithm)) return {}; // 8. Let metadata be the ordered map «["alg" → algorithm, "val" → base64-value]». // Note: Since no options are defined (see the §3.1 Integrity metadata), a corresponding entry is not set in metadata. // If options are defined in a future version, hash-with-opt-token-list[1] can be utilized as options. auto metadata = Metadata { .algorithm = TRY(String::from_utf8(algorithm)), .base64_value = TRY(String::from_utf8(base64_value)), .options = {}, }; // 9. Append metadata to result. TRY(result.try_append(move(metadata))); return {}; })); // 3. Return result. return result; } // https://w3c.github.io/webappsec-subresource-integrity/#get-the-strongest-metadata ErrorOr> get_strongest_metadata_from_set(Vector const& set) { // 1. Let result be the empty set and strongest be the empty string. Vector result; Optional strongest; // 2. For each item in set: for (auto const& item : set) { // 1. If result is the empty set, add item to result and set strongest to item, skip to the next item. if (result.is_empty()) { TRY(result.try_append(item)); strongest = item; continue; } // 2. Let currentAlgorithm be the alg component of strongest. auto& current_algorithm = strongest->algorithm; // 3. Let newAlgorithm be the alg component of item. auto& new_algorithm = item.algorithm; // 4. If the result of getPrioritizedHashFunction(currentAlgorithm, newAlgorithm) is the empty string, add item to result. auto prioritized_hash_function = get_prioritized_hash_function(current_algorithm, new_algorithm); if (prioritized_hash_function.is_empty()) { TRY(result.try_append(item)); } // If the result is newAlgorithm, set strongest to item, set result to the empty set, and add item to result. else if (prioritized_hash_function == new_algorithm) { strongest = item; result.clear_with_capacity(); TRY(result.try_append(item)); } } // 3. Return result. return result; } // https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist ErrorOr do_bytes_match_metadata_list(ByteBuffer const& bytes, StringView metadata_list) { // 1. Let parsedMetadata be the result of parsing metadataList. auto parsed_metadata = TRY(parse_metadata(metadata_list)); // 2. If parsedMetadata is empty set, return true. if (parsed_metadata.is_empty()) return true; // 3. Let metadata be the result of getting the strongest metadata from parsedMetadata. auto metadata = TRY(get_strongest_metadata_from_set(parsed_metadata)); // 4. For each item in metadata: for (auto const& item : metadata) { // 1. Let algorithm be the item["alg"]. auto& algorithm = item.algorithm; // 2. Let expectedValue be the item["val"]. auto& expected_value = item.base64_value; // 3. Let actualValue be the result of applying algorithm to bytes. auto actual_value = TRY(apply_algorithm_to_bytes(algorithm, bytes)); // 4. If actualValue is a case-sensitive match for expectedValue, return true. if (actual_value == expected_value) return true; } // 5. Return false. return false; } }