SRI.cpp 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. /*
  2. * Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/Array.h>
  7. #include <AK/Base64.h>
  8. #include <AK/Vector.h>
  9. #include <LibCrypto/Hash/SHA2.h>
  10. #include <LibWeb/SRI/SRI.h>
  11. namespace Web::SRI {
  12. constexpr Array supported_hash_functions {
  13. // These are sorted by strength, low to high.
  14. // NOTE: We are specifically told to refuse MD5 and SHA1.
  15. // https://w3c.github.io/webappsec-subresource-integrity/#hash-functions
  16. "sha256"sv,
  17. "sha384"sv,
  18. "sha512"sv,
  19. };
  20. // https://w3c.github.io/webappsec-subresource-integrity/#getprioritizedhashfunction
  21. static StringView get_prioritized_hash_function(StringView a, StringView b)
  22. {
  23. if (a == b)
  24. return ""sv;
  25. auto a_priority = supported_hash_functions.first_index_of(a).value();
  26. auto b_priority = supported_hash_functions.first_index_of(b).value();
  27. if (a_priority > b_priority)
  28. return a;
  29. return b;
  30. }
  31. // https://w3c.github.io/webappsec-subresource-integrity/#apply-algorithm-to-response
  32. ErrorOr<String> apply_algorithm_to_bytes(StringView algorithm, ByteBuffer const& bytes)
  33. {
  34. // NOTE: The steps are duplicated here because each hash algorithm returns a different result type.
  35. if (algorithm == "sha256"sv) {
  36. // 1. Let result be the result of applying algorithm to bytes.
  37. auto result = Crypto::Hash::SHA256::hash(bytes);
  38. // 2. Return the result of base64 encoding result.
  39. return encode_base64(result.bytes());
  40. }
  41. if (algorithm == "sha384"sv) {
  42. // 1. Let result be the result of applying algorithm to bytes.
  43. auto result = Crypto::Hash::SHA384::hash(bytes);
  44. // 2. Return the result of base64 encoding result.
  45. return encode_base64(result.bytes());
  46. }
  47. if (algorithm == "sha512"sv) {
  48. // 1. Let result be the result of applying algorithm to bytes.
  49. auto result = Crypto::Hash::SHA512::hash(bytes);
  50. // 2. Return the result of base64 encoding result.
  51. return encode_base64(result.bytes());
  52. }
  53. VERIFY_NOT_REACHED();
  54. }
  55. // https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
  56. ErrorOr<Vector<Metadata>> parse_metadata(StringView metadata)
  57. {
  58. // 1. Let result be the empty set.
  59. Vector<Metadata> result;
  60. // 2. For each item returned by splitting metadata on spaces:
  61. TRY(metadata.for_each_split_view(' ', SplitBehavior::Nothing, [&](StringView item) -> ErrorOr<void> {
  62. // 1. Let hash-with-opt-token-list be the result of splitting item on U+003F (?).
  63. auto hash_with_opt_token_list = item.split_view('?');
  64. // 2. Let hash-expression be hash-with-opt-token-list[0].
  65. auto hash_expression = hash_with_opt_token_list[0];
  66. // 3. Let base64-value be the empty string.
  67. StringView base64_value;
  68. // 4. Let hash-expr-token-list be the result of splitting hash-expression on U+002D (-).
  69. auto hash_expr_token_list = hash_expression.split_view('-');
  70. // 5. Let algorithm be hash-expr-token-list[0].
  71. auto algorithm = hash_expr_token_list[0];
  72. // 6. If hash-expr-token-list[1] exists, set base64-value to hash-expr-token-list[1].
  73. if (hash_expr_token_list.size() >= 1)
  74. base64_value = hash_expr_token_list[1];
  75. // 7. If algorithm is not a hash function recognized by the user agent, continue.
  76. if (!supported_hash_functions.contains_slow(algorithm))
  77. return {};
  78. // 8. Let metadata be the ordered map «["alg" → algorithm, "val" → base64-value]».
  79. // Note: Since no options are defined (see the §3.1 Integrity metadata), a corresponding entry is not set in metadata.
  80. // If options are defined in a future version, hash-with-opt-token-list[1] can be utilized as options.
  81. auto metadata = Metadata {
  82. .algorithm = TRY(String::from_utf8(algorithm)),
  83. .base64_value = TRY(String::from_utf8(base64_value)),
  84. .options = {},
  85. };
  86. // 9. Append metadata to result.
  87. TRY(result.try_append(move(metadata)));
  88. return {};
  89. }));
  90. // 3. Return result.
  91. return result;
  92. }
  93. // https://w3c.github.io/webappsec-subresource-integrity/#get-the-strongest-metadata
  94. ErrorOr<Vector<Metadata>> get_strongest_metadata_from_set(Vector<Metadata> const& set)
  95. {
  96. // 1. Let result be the empty set and strongest be the empty string.
  97. Vector<Metadata> result;
  98. Optional<Metadata> strongest;
  99. // 2. For each item in set:
  100. for (auto const& item : set) {
  101. // 1. If result is the empty set, add item to result and set strongest to item, skip to the next item.
  102. if (result.is_empty()) {
  103. TRY(result.try_append(item));
  104. strongest = item;
  105. continue;
  106. }
  107. // 2. Let currentAlgorithm be the alg component of strongest.
  108. auto& current_algorithm = strongest->algorithm;
  109. // 3. Let newAlgorithm be the alg component of item.
  110. auto& new_algorithm = item.algorithm;
  111. // 4. If the result of getPrioritizedHashFunction(currentAlgorithm, newAlgorithm) is the empty string, add item to result.
  112. auto prioritized_hash_function = get_prioritized_hash_function(current_algorithm, new_algorithm);
  113. if (prioritized_hash_function.is_empty()) {
  114. TRY(result.try_append(item));
  115. }
  116. // If the result is newAlgorithm, set strongest to item, set result to the empty set, and add item to result.
  117. else if (prioritized_hash_function == new_algorithm) {
  118. strongest = item;
  119. result.clear_with_capacity();
  120. TRY(result.try_append(item));
  121. }
  122. }
  123. // 3. Return result.
  124. return result;
  125. }
  126. // https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist
  127. ErrorOr<bool> do_bytes_match_metadata_list(ByteBuffer const& bytes, StringView metadata_list)
  128. {
  129. // 1. Let parsedMetadata be the result of parsing metadataList.
  130. auto parsed_metadata = TRY(parse_metadata(metadata_list));
  131. // 2. If parsedMetadata is empty set, return true.
  132. if (parsed_metadata.is_empty())
  133. return true;
  134. // 3. Let metadata be the result of getting the strongest metadata from parsedMetadata.
  135. auto metadata = TRY(get_strongest_metadata_from_set(parsed_metadata));
  136. // 4. For each item in metadata:
  137. for (auto const& item : metadata) {
  138. // 1. Let algorithm be the item["alg"].
  139. auto& algorithm = item.algorithm;
  140. // 2. Let expectedValue be the item["val"].
  141. auto& expected_value = item.base64_value;
  142. // 3. Let actualValue be the result of applying algorithm to bytes.
  143. auto actual_value = TRY(apply_algorithm_to_bytes(algorithm, bytes));
  144. // 4. If actualValue is a case-sensitive match for expectedValue, return true.
  145. if (actual_value == expected_value)
  146. return true;
  147. }
  148. // 5. Return false.
  149. return false;
  150. }
  151. }