helper.js (9406B)
1 // 2 // Exciting constants we'll use for test cases below: 3 // 4 const kValidKeys = { 5 // https://www.rfc-editor.org/rfc/rfc9421.html#name-example-ed25519-test-key 6 rfc: "JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=", 7 8 // Randomly generated key: 9 // 10 // { 11 // "crv": "Ed25519", 12 // "d": "MTodZiTA9CBsuIvSfO679TThkG3b7ce6R3sq_CdyVp4", 13 // "ext": true, 14 // "kty": "OKP", 15 // "x": "xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE" 16 // } 17 // 18 arbitrary: "xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE=" 19 }; 20 21 // As above in kValidKeys, but in JWK format (including the private key). 22 const kValidKeysJWK = { 23 // https://www.rfc-editor.org/rfc/rfc9421.html#name-example-ed25519-test-key 24 rfc: { 25 "kty": "OKP", 26 "crv": "Ed25519", 27 "kid": "test-key-ed25519", 28 "d": "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU", 29 "x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs" 30 }, 31 32 // Matching private key to arbitrary public key above. 33 arbitrary: { 34 "crv": "Ed25519", 35 "d": "MTodZiTA9CBsuIvSfO679TThkG3b7ce6R3sq_CdyVp4", 36 "ext": true, 37 "kty": "OKP", 38 "x": "xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE" 39 } 40 }; 41 42 // A key with the right length that cannot be used to verify the HTTP response 43 // above. 44 const kInvalidKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; 45 46 // Generated test expectations are more readable if we're using something 47 // other than a boolean. 48 const EXPECT_BLOCKED = "block"; 49 const EXPECT_LOADED = "loaded"; 50 51 const kAcceptSignature = "accept-signature"; 52 53 // Given `{ digest: "...", body: "...", cors: true, type: "..." }`, generates 54 // the URL to a script resource that has the given characteristics. 55 let counter = 0; 56 function resourceURL(data, server_origin) { 57 counter++; 58 data.type ??= "application/javascript"; 59 data.counter = counter; 60 let params = new URLSearchParams(data); 61 let result = new URL("/subresource-integrity/signatures/resource.py?" + params.toString(), server_origin ?? self.location.origin); 62 return result.href; 63 } 64 65 // Given a signature base (actually an arbitrary string) and a key in JWK 66 // format, generates a base64-encoded Ed25519 signature. Only available over 67 // HTTPS. 68 async function signSignatureBase(signatureBase, privateKeyJWK) { 69 assert_true(self.isSecureContext, "Signatures can only be generated in secure contexts."); 70 const privateKey = await crypto.subtle.importKey( 71 'jwk', 72 privateKeyJWK, 73 'Ed25519', 74 true, // extractable 75 ['sign'] 76 ); 77 78 const encoder = new TextEncoder(); 79 const messageBytes = encoder.encode(signatureBase); 80 81 const signatureBytes = await crypto.subtle.sign( 82 { name: 'Ed25519' }, 83 privateKey, 84 messageBytes 85 ); 86 87 return btoa(String.fromCharCode(...new Uint8Array(signatureBytes))); 88 } 89 90 function generate_fetch_test(request_data, options, expectation, description) { 91 promise_test(test => { 92 const url = resourceURL(request_data, options.origin); 93 let fetch_options = {}; 94 if (options.mode) { 95 fetch_options.mode = options.mode; 96 } else if (options.origin) { 97 fetch_options.mode = "cors"; 98 } 99 if (options.integrity) { 100 fetch_options.integrity = options.integrity; 101 } 102 103 let fetcher = fetch(url, fetch_options); 104 if (expectation == EXPECT_LOADED) { 105 return fetcher.then(r => { 106 const expected_status = options.mode == "no-cors" ? 0 : (request_data.status ?? 200); 107 assert_equals(r.status, expected_status, `Response status is ${expected_status}.`); 108 109 // Verify `accept-signature`: if the invalid key is present, both a valid and invalid 110 // key were set. If just the valid key is present, that's the only key we should see 111 // in the header. 112 if (options.integrity?.includes(`ed25519-${kInvalidKey}`)) { 113 assert_equals(r.headers.get(kAcceptSignature), 114 `sig0=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="ed25519-integrity", sig1=("unencoded-digest";sf);keyid="${kInvalidKey}";tag="ed25519-integrity"`, 115 "`accept-signature` was set."); 116 } else if (options.integrity?.includes(`ed25519-${kValidKeys['rfc']}`)) { 117 assert_equals(r.headers.get(kAcceptSignature), 118 `sig0=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="ed25519-integrity"`, 119 "`accept-signature` was set."); 120 } 121 }); 122 } else { 123 return promise_rejects_js(test, TypeError, fetcher); 124 } 125 }, "`fetch()`: " + description); 126 } 127 128 function generate_query_test(query, options, expectation, description) { 129 promise_test(test => { 130 let url = new URL("/subresource-integrity/signatures/query-resource.py?" + query, self.location); 131 132 let fetch_options = {}; 133 if (options.mode) { 134 fetch_options.mode = options.mode; 135 } 136 if (options.integrity) { 137 fetch_options.integrity = options.integrity; 138 } 139 140 let fetcher = fetch(url, fetch_options); 141 if (expectation == EXPECT_LOADED) { 142 return fetcher.then(r => { 143 const expected_status = options.mode == "no-cors" ? 0 : 200; 144 assert_equals(r.status, expected_status, `Response status is ${expected_status}.`); 145 }); 146 } else { 147 return promise_rejects_js(test, TypeError, fetcher); 148 } 149 }, "`fetch()`: " + description); 150 } 151 152 /* 153 * Script tests 154 * 155 * Metadata for a script which expects to execute correctly and a script that 156 * does not. 157 */ 158 const kScriptToExecute = { 159 body: "window.hello = `world`;", 160 hash: "PZJ+9CdAAIacg7wfUe4t/RkDQJVKM0mCZ2K7qiRhHFc=", 161 162 signatures: { 163 // ``` 164 // "unencoded-digest";sf: sha-256=:PZJ+9CdAAIacg7wfUe4t/RkDQJVKM0mCZ2K7qiRhHFc=: 165 // "@signature-params": ("unencoded-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri" 166 // ``` 167 rfc: "A1wOGCGrcfN34uMe2Umt7hJ6Su1MQFUL1QuT5nmk1R8I761eXUt2Zv4D5fOt1h1+4DlHPiA1FVwfJLbwlWnpBw==", 168 169 // ``` 170 // "unencoded-digest";sf: sha-256=:PZJ+9CdAAIacg7wfUe4t/RkDQJVKM0mCZ2K7qiRhHFc=: 171 // "@signature-params": ("unencoded-digest";sf);keyid="xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE=";tag="sri" 172 // ``` 173 arbitrary: "odk/ec9gO/DCcLPa1xSW1cSmB2s4XU3iDOxJAiod4v5/YBESjvwEJNAO9x4Frn/7rRIZ7sL5LwRNaymdHokOBQ==" 174 } 175 }; 176 177 const kScriptToBlock = { 178 body: "assert_unreached(`This code should not execute.`);", 179 hash: "FUSFR1N3vTmSGbI7q9jaMbHq+ogNeBfpznOIufaIfpc=", 180 181 signatures: { 182 // ``` 183 // "unencoded-digest";sf: sha-256=:FUSFR1N3vTmSGbI7q9jaMbHq+ogNeBfpznOIufaIfpc=: 184 // "@signature-params": ("unencoded-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri" 185 // ``` 186 rfc: "bR3fU6kzmMLol4GIcgj19+It0GB0dlKrD4ssH+SCz0vTLAdT3zt6Kfq4V60NnDdn62XGNr20b0TEKtfcremcDw==", 187 188 // ``` 189 // "unencoded-digest";sf: sha-256=:FUSFR1N3vTmSGbI7q9jaMbHq+ogNeBfpznOIufaIfpc=: 190 // "@signature-params": ("unencoded-digest";sf);keyid="xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE";tag="sri" 191 // ``` 192 arbitrary: "+5Iol+V65SW2qkpsTCyqYQJC4NZIsUGeNbO5kS9WdTboa9gg/nV6LwnySM02612YvPm++671nN9dBDJPYncuBA==" 193 } 194 }; 195 196 // These constants use the metadata above to create dictionaries that can be 197 // passed into `generate_script_test` below. 198 const kUnsignedShouldExecute = { body: kScriptToExecute['body'] }; 199 const kUnsignedShouldBlock = { body: kScriptToBlock['body'] }; 200 const kSignedShouldExecute = { 201 body: kScriptToExecute['body'], 202 digest: `sha-256=:${kScriptToExecute['hash']}:`, 203 signatureInput: `signature=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri"`, 204 signature: `signature=:${kScriptToExecute['signatures']['rfc']}:` 205 }; 206 const kSignedShouldBlock = { 207 body: kScriptToBlock['body'], 208 digest: `sha-256=:${kScriptToBlock['hash']}:`, 209 signatureInput: `signature=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri"`, 210 signature: `signature=:${kScriptToBlock['signatures']['rfc']}:` 211 }; 212 const kMultiplySignedShouldExecute = { 213 body: kScriptToExecute['body'], 214 digest: `sha-256=:${kScriptToExecute['hash']}:`, 215 signatureInput: `signature1=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri", ` + 216 `signature2=("unencoded-digest";sf);keyid="${kValidKeys['arbitrary']}";tag="sri"`, 217 signature: `signature1=:${kScriptToExecute['signatures']['rfc']}:, ` + 218 `signature2=:${kScriptToExecute['signatures']['arbitrary']}:` 219 }; 220 const kMultiplySignedShouldBlock = { 221 body: kScriptToBlock['body'], 222 digest: `sha-256=:${kScriptToBlock['hash']}:`, 223 signatureInput: `signature1=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri", ` + 224 `signature2=("unencoded-digest";sf);keyid="${kValidKeys['arbitrary']}";tag="sri"`, 225 signature: `signature1=:${kScriptToBlock['signatures']['rfc']}:, ` + 226 `signature2=:${kScriptToBlock['signatures']['arbitrary']}:` 227 }; 228 229 function generate_script_test(request_data, integrity, expectation, description) { 230 async_test(t => { 231 let s = document.createElement('script'); 232 s.src = resourceURL(request_data); 233 s.integrity = integrity; 234 if (expectation == EXPECT_BLOCKED) { 235 s.onerror = t.step_func_done(e => { 236 assert_equals("error", e.type); 237 }); 238 s.onload = t.unreached_func("Script should not execute."); 239 } else { 240 s.onload = t.step_func_done(e => { 241 assert_equals("load", e.type); 242 }); 243 s.onerror = t.unreached_func("Script should not fail."); 244 } 245 document.body.appendChild(s); 246 }, "`<script>`: " + description); 247 }