Source code
Revision control
Copy as Markdown
Other Tools
//
// Exciting constants we'll use for test cases below:
//
const kValidKeys = {
rfc: "JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=",
// Randomly generated key:
//
// {
// "crv": "Ed25519",
// "d": "MTodZiTA9CBsuIvSfO679TThkG3b7ce6R3sq_CdyVp4",
// "ext": true,
// "kty": "OKP",
// "x": "xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE"
// }
//
arbitrary: "xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE="
};
// As above in kValidKeys, but in JWK format (including the private key).
const kValidKeysJWK = {
rfc: {
"kty": "OKP",
"crv": "Ed25519",
"kid": "test-key-ed25519",
"d": "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU",
"x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
},
// Matching private key to arbitrary public key above.
arbitrary: {
"crv": "Ed25519",
"d": "MTodZiTA9CBsuIvSfO679TThkG3b7ce6R3sq_CdyVp4",
"ext": true,
"kty": "OKP",
"x": "xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE"
}
};
// A key with the right length that cannot be used to verify the HTTP response
// above.
const kInvalidKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
// Generated test expectations are more readable if we're using something
// other than a boolean.
const EXPECT_BLOCKED = "block";
const EXPECT_LOADED = "loaded";
const kAcceptSignature = "accept-signature";
// Given `{ digest: "...", body: "...", cors: true, type: "..." }`, generates
// the URL to a script resource that has the given characteristics.
let counter = 0;
function resourceURL(data, server_origin) {
counter++;
data.type ??= "application/javascript";
data.counter = counter;
let params = new URLSearchParams(data);
let result = new URL("/subresource-integrity/signatures/resource.py?" + params.toString(), server_origin ?? self.location.origin);
return result.href;
}
// Given a signature base (actually an arbitrary string) and a key in JWK
// format, generates a base64-encoded Ed25519 signature. Only available over
// HTTPS.
async function signSignatureBase(signatureBase, privateKeyJWK) {
assert_true(self.isSecureContext, "Signatures can only be generated in secure contexts.");
const privateKey = await crypto.subtle.importKey(
'jwk',
privateKeyJWK,
'Ed25519',
true, // extractable
['sign']
);
const encoder = new TextEncoder();
const messageBytes = encoder.encode(signatureBase);
const signatureBytes = await crypto.subtle.sign(
{ name: 'Ed25519' },
privateKey,
messageBytes
);
return btoa(String.fromCharCode(...new Uint8Array(signatureBytes)));
}
function generate_fetch_test(request_data, options, expectation, description) {
promise_test(test => {
const url = resourceURL(request_data, options.origin);
let fetch_options = {};
if (options.mode) {
fetch_options.mode = options.mode;
} else if (options.origin) {
fetch_options.mode = "cors";
}
if (options.integrity) {
fetch_options.integrity = options.integrity;
}
let fetcher = fetch(url, fetch_options);
if (expectation == EXPECT_LOADED) {
return fetcher.then(r => {
const expected_status = options.mode == "no-cors" ? 0 : (request_data.status ?? 200);
assert_equals(r.status, expected_status, `Response status is ${expected_status}.`);
// Verify `accept-signature`: if the invalid key is present, both a valid and invalid
// key were set. If just the valid key is present, that's the only key we should see
// in the header.
if (options.integrity?.includes(`ed25519-${kInvalidKey}`)) {
assert_equals(r.headers.get(kAcceptSignature),
`sig0=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri", sig1=("unencoded-digest";sf);keyid="${kInvalidKey}";tag="sri"`,
"`accept-signature` was set.");
} else if (options.integrity?.includes(`ed25519-${kValidKeys['rfc']}`)) {
assert_equals(r.headers.get(kAcceptSignature),
`sig0=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri"`,
"`accept-signature` was set.");
}
});
} else {
return promise_rejects_js(test, TypeError, fetcher);
}
}, "`fetch()`: " + description);
}
function generate_query_test(query, options, expectation, description) {
promise_test(test => {
let url = new URL("/subresource-integrity/signatures/query-resource.py?" + query, self.location);
let fetch_options = {};
if (options.mode) {
fetch_options.mode = options.mode;
}
if (options.integrity) {
fetch_options.integrity = options.integrity;
}
let fetcher = fetch(url, fetch_options);
if (expectation == EXPECT_LOADED) {
return fetcher.then(r => {
const expected_status = options.mode == "no-cors" ? 0 : 200;
assert_equals(r.status, expected_status, `Response status is ${expected_status}.`);
});
} else {
return promise_rejects_js(test, TypeError, fetcher);
}
}, "`fetch()`: " + description);
}
/*
* Script tests
*
* Metadata for a script which expects to execute correctly and a script that
* does not.
*/
const kScriptToExecute = {
body: "window.hello = `world`;",
hash: "PZJ+9CdAAIacg7wfUe4t/RkDQJVKM0mCZ2K7qiRhHFc=",
signatures: {
// ```
// "unencoded-digest";sf: sha-256=:PZJ+9CdAAIacg7wfUe4t/RkDQJVKM0mCZ2K7qiRhHFc=:
// "@signature-params": ("unencoded-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri"
// ```
rfc: "A1wOGCGrcfN34uMe2Umt7hJ6Su1MQFUL1QuT5nmk1R8I761eXUt2Zv4D5fOt1h1+4DlHPiA1FVwfJLbwlWnpBw==",
// ```
// "unencoded-digest";sf: sha-256=:PZJ+9CdAAIacg7wfUe4t/RkDQJVKM0mCZ2K7qiRhHFc=:
// "@signature-params": ("unencoded-digest";sf);keyid="xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE=";tag="sri"
// ```
arbitrary: "odk/ec9gO/DCcLPa1xSW1cSmB2s4XU3iDOxJAiod4v5/YBESjvwEJNAO9x4Frn/7rRIZ7sL5LwRNaymdHokOBQ=="
}
};
const kScriptToBlock = {
body: "assert_unreached(`This code should not execute.`);",
hash: "FUSFR1N3vTmSGbI7q9jaMbHq+ogNeBfpznOIufaIfpc=",
signatures: {
// ```
// "unencoded-digest";sf: sha-256=:FUSFR1N3vTmSGbI7q9jaMbHq+ogNeBfpznOIufaIfpc=:
// "@signature-params": ("unencoded-digest";sf);keyid="JrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=";tag="sri"
// ```
rfc: "bR3fU6kzmMLol4GIcgj19+It0GB0dlKrD4ssH+SCz0vTLAdT3zt6Kfq4V60NnDdn62XGNr20b0TEKtfcremcDw==",
// ```
// "unencoded-digest";sf: sha-256=:FUSFR1N3vTmSGbI7q9jaMbHq+ogNeBfpznOIufaIfpc=:
// "@signature-params": ("unencoded-digest";sf);keyid="xDnP380zcL4rJ76rXYjeHlfMyPZEOqpJYjsjEppbuXE";tag="sri"
// ```
arbitrary: "+5Iol+V65SW2qkpsTCyqYQJC4NZIsUGeNbO5kS9WdTboa9gg/nV6LwnySM02612YvPm++671nN9dBDJPYncuBA=="
}
};
// These constants use the metadata above to create dictionaries that can be
// passed into `generate_script_test` below.
const kUnsignedShouldExecute = { body: kScriptToExecute['body'] };
const kUnsignedShouldBlock = { body: kScriptToBlock['body'] };
const kSignedShouldExecute = {
body: kScriptToExecute['body'],
digest: `sha-256=:${kScriptToExecute['hash']}:`,
signatureInput: `signature=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri"`,
signature: `signature=:${kScriptToExecute['signatures']['rfc']}:`
};
const kSignedShouldBlock = {
body: kScriptToBlock['body'],
digest: `sha-256=:${kScriptToBlock['hash']}:`,
signatureInput: `signature=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri"`,
signature: `signature=:${kScriptToBlock['signatures']['rfc']}:`
};
const kMultiplySignedShouldExecute = {
body: kScriptToExecute['body'],
digest: `sha-256=:${kScriptToExecute['hash']}:`,
signatureInput: `signature1=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri", ` +
`signature2=("unencoded-digest";sf);keyid="${kValidKeys['arbitrary']}";tag="sri"`,
signature: `signature1=:${kScriptToExecute['signatures']['rfc']}:, ` +
`signature2=:${kScriptToExecute['signatures']['arbitrary']}:`
};
const kMultiplySignedShouldBlock = {
body: kScriptToBlock['body'],
digest: `sha-256=:${kScriptToBlock['hash']}:`,
signatureInput: `signature1=("unencoded-digest";sf);keyid="${kValidKeys['rfc']}";tag="sri", ` +
`signature2=("unencoded-digest";sf);keyid="${kValidKeys['arbitrary']}";tag="sri"`,
signature: `signature1=:${kScriptToBlock['signatures']['rfc']}:, ` +
`signature2=:${kScriptToBlock['signatures']['arbitrary']}:`
};
function generate_script_test(request_data, integrity, expectation, description) {
async_test(t => {
let s = document.createElement('script');
s.src = resourceURL(request_data);
s.integrity = integrity;
if (expectation == EXPECT_BLOCKED) {
s.onerror = t.step_func_done(e => {
assert_equals("error", e.type);
});
s.onload = t.unreached_func("Script should not execute.");
} else {
s.onload = t.step_func_done(e => {
assert_equals("load", e.type);
});
s.onerror = t.unreached_func("Script should not fail.");
}
document.body.appendChild(s);
}, "`<script>`: " + description);
}