Coverage Report

Created: 2025-03-04 07:22

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