summaryrefslogtreecommitdiffstats
path: root/services/crypto/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'services/crypto/tests/unit')
-rw-r--r--services/crypto/tests/unit/head_helpers.js78
-rw-r--r--services/crypto/tests/unit/test_crypto_crypt.js226
-rw-r--r--services/crypto/tests/unit/test_crypto_random.js52
-rw-r--r--services/crypto/tests/unit/test_jwcrypto.js51
-rw-r--r--services/crypto/tests/unit/test_load_modules.js12
-rw-r--r--services/crypto/tests/unit/test_utils_hawk.js346
-rw-r--r--services/crypto/tests/unit/test_utils_httpmac.js73
-rw-r--r--services/crypto/tests/unit/xpcshell.toml18
8 files changed, 856 insertions, 0 deletions
diff --git a/services/crypto/tests/unit/head_helpers.js b/services/crypto/tests/unit/head_helpers.js
new file mode 100644
index 0000000000..322f6761a9
--- /dev/null
+++ b/services/crypto/tests/unit/head_helpers.js
@@ -0,0 +1,78 @@
+/* import-globals-from ../../../common/tests/unit/head_helpers.js */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+try {
+ // In the context of xpcshell tests, there won't be a default AppInfo
+ // eslint-disable-next-line mozilla/use-services
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
+} catch (ex) {
+ // Make sure to provide the right OS so crypto loads the right binaries
+ var OS = "XPCShell";
+ if (mozinfo.os == "win") {
+ OS = "WINNT";
+ } else if (mozinfo.os == "mac") {
+ OS = "Darwin";
+ } else {
+ OS = "Linux";
+ }
+
+ const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+ );
+ updateAppInfo({
+ name: "XPCShell",
+ ID: "{3e3ba16c-1675-4e88-b9c8-afef81b3d2ef}",
+ version: "1",
+ platformVersion: "",
+ OS,
+ });
+}
+
+function base64UrlDecode(s) {
+ s = s.replace(/-/g, "+");
+ s = s.replace(/_/g, "/");
+
+ // Replace padding if it was stripped by the sender.
+ // See http://tools.ietf.org/html/rfc4648#section-4
+ switch (s.length % 4) {
+ case 0:
+ break; // No pad chars in this case
+ case 2:
+ s += "==";
+ break; // Two pad chars
+ case 3:
+ s += "=";
+ break; // One pad char
+ default:
+ throw new Error("Illegal base64url string!");
+ }
+
+ // With correct padding restored, apply the standard base64 decoder
+ return atob(s);
+}
+
+// Register resource alias. Normally done in SyncComponents.manifest.
+function addResourceAlias() {
+ const resProt = Services.io
+ .getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ let uri = Services.io.newURI("resource://gre/modules/services-crypto/");
+ resProt.setSubstitution("services-crypto", uri);
+}
+addResourceAlias();
+
+/**
+ * Print some debug message to the console. All arguments will be printed,
+ * separated by spaces.
+ *
+ * @param [arg0, arg1, arg2, ...]
+ * Any number of arguments to print out
+ * @usage _("Hello World") -> prints "Hello World"
+ * @usage _(1, 2, 3) -> prints "1 2 3"
+ */
+var _ = function (some, debug, text, to) {
+ print(Array.from(arguments).join(" "));
+};
diff --git a/services/crypto/tests/unit/test_crypto_crypt.js b/services/crypto/tests/unit/test_crypto_crypt.js
new file mode 100644
index 0000000000..8fadd307c7
--- /dev/null
+++ b/services/crypto/tests/unit/test_crypto_crypt.js
@@ -0,0 +1,226 @@
+const { WeaveCrypto } = ChromeUtils.importESModule(
+ "resource://services-crypto/WeaveCrypto.sys.mjs"
+);
+
+var cryptoSvc = new WeaveCrypto();
+
+add_task(async function test_key_memoization() {
+ let cryptoGlobal = cryptoSvc._getCrypto();
+ let oldImport = cryptoGlobal.subtle.importKey;
+ if (!oldImport) {
+ _("Couldn't swizzle crypto.subtle.importKey; returning.");
+ return;
+ }
+
+ let iv = cryptoSvc.generateRandomIV();
+ let key = await cryptoSvc.generateRandomKey();
+ let c = 0;
+ cryptoGlobal.subtle.importKey = function (
+ format,
+ keyData,
+ algo,
+ extractable,
+ usages
+ ) {
+ c++;
+ return oldImport.call(
+ cryptoGlobal.subtle,
+ format,
+ keyData,
+ algo,
+ extractable,
+ usages
+ );
+ };
+
+ // Encryption should cause a single counter increment.
+ Assert.equal(c, 0);
+ let cipherText = await cryptoSvc.encrypt("Hello, world.", key, iv);
+ Assert.equal(c, 1);
+ cipherText = await cryptoSvc.encrypt("Hello, world.", key, iv);
+ Assert.equal(c, 1);
+
+ // ... as should decryption.
+ await cryptoSvc.decrypt(cipherText, key, iv);
+ await cryptoSvc.decrypt(cipherText, key, iv);
+ await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(c, 2);
+
+ // Un-swizzle.
+ cryptoGlobal.subtle.importKey = oldImport;
+});
+
+// Just verify that it gets populated with the correct bytes.
+add_task(async function test_makeUint8Array() {
+ ChromeUtils.importESModule("resource://gre/modules/ctypes.sys.mjs");
+
+ let item1 = cryptoSvc.makeUint8Array("abcdefghi", false);
+ Assert.ok(item1);
+ for (let i = 0; i < 8; ++i) {
+ Assert.equal(item1[i], "abcdefghi".charCodeAt(i));
+ }
+});
+
+add_task(async function test_encrypt_decrypt() {
+ // First, do a normal run with expected usage... Generate a random key and
+ // iv, encrypt and decrypt a string.
+ var iv = cryptoSvc.generateRandomIV();
+ Assert.equal(iv.length, 24);
+
+ var key = await cryptoSvc.generateRandomKey();
+ Assert.equal(key.length, 44);
+
+ var mySecret = "bacon is a vegetable";
+ var cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ Assert.equal(cipherText.length, 44);
+
+ var clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(clearText.length, 20);
+
+ // Did the text survive the encryption round-trip?
+ Assert.equal(clearText, mySecret);
+ Assert.notEqual(cipherText, mySecret); // just to be explicit
+
+ // Do some more tests with a fixed key/iv, to check for reproducable results.
+ key = "St1tFCor7vQEJNug/465dQ==";
+ iv = "oLjkfrLIOnK2bDRvW4kXYA==";
+
+ _("Testing small IV.");
+ mySecret = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=";
+ let shortiv = "YWJj";
+ let err;
+ try {
+ await cryptoSvc.encrypt(mySecret, key, shortiv);
+ } catch (ex) {
+ err = ex;
+ }
+ Assert.ok(!!err);
+
+ _("Testing long IV.");
+ let longiv = "gsgLRDaxWvIfKt75RjuvFWERt83FFsY2A0TW+0b2iVk=";
+ try {
+ await cryptoSvc.encrypt(mySecret, key, longiv);
+ } catch (ex) {
+ err = ex;
+ }
+ Assert.ok(!!err);
+
+ // Test small input sizes
+ mySecret = "";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "OGQjp6mK1a3fs9k9Ml4L3w==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "x";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "96iMl4vhOxFUW/lVHHzVqg==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "xx";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "olpPbETRYROCSqFWcH2SWg==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "xxx";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "rRbpHGyVSZizLX/x43Wm+Q==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "xxxx";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "HeC7miVGDcpxae9RmiIKAw==");
+ Assert.equal(clearText, mySecret);
+
+ // Test non-ascii input
+ // ("testuser1" using similar-looking glyphs)
+ mySecret = String.fromCharCode(355, 277, 349, 357, 533, 537, 101, 345, 185);
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "Pj4ixByXoH3SU3JkOXaEKPgwRAWplAWFLQZkpJd5Kr4=");
+ Assert.equal(clearText, mySecret);
+
+ // Tests input spanning a block boundary (AES block size is 16 bytes)
+ mySecret = "123456789012345";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "e6c5hwphe45/3VN/M0bMUA==");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "1234567890123456";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "V6aaOZw8pWlYkoIHNkhsP1JOIQF87E2vTUvBUQnyV04=");
+ Assert.equal(clearText, mySecret);
+
+ mySecret = "12345678901234567";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "V6aaOZw8pWlYkoIHNkhsP5GvxWJ9+GIAS6lXw+5fHTI=");
+ Assert.equal(clearText, mySecret);
+
+ key = "iz35tuIMq4/H+IYw2KTgow==";
+ iv = "TJYrvva2KxvkM8hvOIvWp3==";
+ mySecret = "i like pie";
+
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "DLGx8BWqSCLGG7i/xwvvxg==");
+ Assert.equal(clearText, mySecret);
+
+ key = "c5hG3YG+NC61FFy8NOHQak1ZhMEWO79bwiAfar2euzI=";
+ iv = "gsgLRDaxWvIfKt75RjuvFW==";
+ mySecret = "i like pie";
+
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ clearText = await cryptoSvc.decrypt(cipherText, key, iv);
+ Assert.equal(cipherText, "o+ADtdMd8ubzNWurS6jt0Q==");
+ Assert.equal(clearText, mySecret);
+
+ key = "St1tFCor7vQEJNug/465dQ==";
+ iv = "oLjkfrLIOnK2bDRvW4kXYA==";
+ mySecret = "does thunder read testcases?";
+ cipherText = await cryptoSvc.encrypt(mySecret, key, iv);
+ Assert.equal(cipherText, "T6fik9Ros+DB2ablH9zZ8FWZ0xm/szSwJjIHZu7sjPs=");
+
+ var badkey = "badkeybadkeybadkeybadk==";
+ var badiv = "badivbadivbadivbadivbad=";
+ var badcipher = "crapinputcrapinputcrapinputcrapinputcrapinp=";
+ var failure;
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(cipherText, badkey, iv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(cipherText, key, badiv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(cipherText, badkey, badiv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+
+ try {
+ failure = false;
+ clearText = await cryptoSvc.decrypt(badcipher, key, iv);
+ } catch (e) {
+ failure = true;
+ }
+ Assert.ok(failure);
+});
diff --git a/services/crypto/tests/unit/test_crypto_random.js b/services/crypto/tests/unit/test_crypto_random.js
new file mode 100644
index 0000000000..bd913c81e8
--- /dev/null
+++ b/services/crypto/tests/unit/test_crypto_random.js
@@ -0,0 +1,52 @@
+const { WeaveCrypto } = ChromeUtils.importESModule(
+ "resource://services-crypto/WeaveCrypto.sys.mjs"
+);
+
+var cryptoSvc = new WeaveCrypto();
+
+add_task(async function test_crypto_random() {
+ if (this.gczeal) {
+ _("Running crypto random tests with gczeal(2).");
+ gczeal(2);
+ }
+
+ // Test salt generation.
+ var salt;
+
+ salt = cryptoSvc.generateRandomBytes(0);
+ Assert.equal(salt.length, 0);
+ salt = cryptoSvc.generateRandomBytes(1);
+ Assert.equal(salt.length, 4);
+ salt = cryptoSvc.generateRandomBytes(2);
+ Assert.equal(salt.length, 4);
+ salt = cryptoSvc.generateRandomBytes(3);
+ Assert.equal(salt.length, 4);
+ salt = cryptoSvc.generateRandomBytes(4);
+ Assert.equal(salt.length, 8);
+ salt = cryptoSvc.generateRandomBytes(8);
+ Assert.equal(salt.length, 12);
+
+ // sanity check to make sure salts seem random
+ var salt2 = cryptoSvc.generateRandomBytes(8);
+ Assert.equal(salt2.length, 12);
+ Assert.notEqual(salt, salt2);
+
+ salt = cryptoSvc.generateRandomBytes(1024);
+ Assert.equal(salt.length, 1368);
+ salt = cryptoSvc.generateRandomBytes(16);
+ Assert.equal(salt.length, 24);
+
+ // Test random key generation
+ var keydata, keydata2, iv;
+
+ keydata = await cryptoSvc.generateRandomKey();
+ Assert.equal(keydata.length, 44);
+ keydata2 = await cryptoSvc.generateRandomKey();
+ Assert.notEqual(keydata, keydata2); // sanity check for randomness
+ iv = cryptoSvc.generateRandomIV();
+ Assert.equal(iv.length, 24);
+
+ if (this.gczeal) {
+ gczeal(0);
+ }
+});
diff --git a/services/crypto/tests/unit/test_jwcrypto.js b/services/crypto/tests/unit/test_jwcrypto.js
new file mode 100644
index 0000000000..02f064d431
--- /dev/null
+++ b/services/crypto/tests/unit/test_jwcrypto.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs",
+});
+
+// Enable logging from jwcrypto.jsm.
+Services.prefs.setStringPref("services.crypto.jwcrypto.log.level", "Debug");
+
+add_task(async function test_jwe_roundtrip_ecdh_es_encryption() {
+ const plaintext = crypto.getRandomValues(new Uint8Array(123));
+ const remoteKey = await crypto.subtle.generateKey(
+ {
+ name: "ECDH",
+ namedCurve: "P-256",
+ },
+ true,
+ ["deriveKey"]
+ );
+ const remoteJWK = await crypto.subtle.exportKey("jwk", remoteKey.publicKey);
+ delete remoteJWK.key_ops;
+ const jwe = await jwcrypto.generateJWE(remoteJWK, plaintext);
+ const decrypted = await jwcrypto.decryptJWE(jwe, remoteKey.privateKey);
+ Assert.deepEqual(plaintext, decrypted);
+});
+
+add_task(async function test_jwe_header_includes_key_id() {
+ const plaintext = crypto.getRandomValues(new Uint8Array(123));
+ const remoteKey = await crypto.subtle.generateKey(
+ {
+ name: "ECDH",
+ namedCurve: "P-256",
+ },
+ true,
+ ["deriveKey"]
+ );
+ const remoteJWK = await crypto.subtle.exportKey("jwk", remoteKey.publicKey);
+ delete remoteJWK.key_ops;
+ remoteJWK.kid = "key identifier";
+ const jwe = await jwcrypto.generateJWE(remoteJWK, plaintext);
+ let [header /* other items deliberately ignored */] = jwe.split(".");
+ header = JSON.parse(
+ new TextDecoder().decode(
+ ChromeUtils.base64URLDecode(header, { padding: "reject" })
+ )
+ );
+ Assert.equal(header.kid, "key identifier");
+});
diff --git a/services/crypto/tests/unit/test_load_modules.js b/services/crypto/tests/unit/test_load_modules.js
new file mode 100644
index 0000000000..2c850d3dab
--- /dev/null
+++ b/services/crypto/tests/unit/test_load_modules.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const modules = ["utils.sys.mjs", "WeaveCrypto.sys.mjs"];
+
+function run_test() {
+ for (let m of modules) {
+ let resource = "resource://services-crypto/" + m;
+ _("Attempting to import: " + resource);
+ ChromeUtils.importESModule(resource);
+ }
+}
diff --git a/services/crypto/tests/unit/test_utils_hawk.js b/services/crypto/tests/unit/test_utils_hawk.js
new file mode 100644
index 0000000000..71702b7349
--- /dev/null
+++ b/services/crypto/tests/unit/test_utils_hawk.js
@@ -0,0 +1,346 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { CryptoUtils } = ChromeUtils.importESModule(
+ "resource://services-crypto/utils.sys.mjs"
+);
+
+function run_test() {
+ initTestLogging();
+
+ run_next_test();
+}
+
+add_task(async function test_hawk() {
+ let compute = CryptoUtils.computeHAWK;
+
+ let method = "POST";
+ let ts = 1353809207;
+ let nonce = "Ygvqdz";
+
+ let credentials = {
+ id: "123456",
+ key: "2983d45yun89q",
+ };
+
+ let uri_https = CommonUtils.makeURI(
+ "https://example.net/somewhere/over/the/rainbow"
+ );
+ let opts = {
+ credentials,
+ ext: "Bazinga!",
+ ts,
+ nonce,
+ payload: "something to write about",
+ contentType: "text/plain",
+ };
+
+ let result = await compute(uri_https, method, opts);
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
+ 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
+ 'ext="Bazinga!", ' +
+ 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'
+ );
+ Assert.equal(result.artifacts.ts, ts);
+ Assert.equal(result.artifacts.nonce, nonce);
+ Assert.equal(result.artifacts.method, method);
+ Assert.equal(result.artifacts.resource, "/somewhere/over/the/rainbow");
+ Assert.equal(result.artifacts.host, "example.net");
+ Assert.equal(result.artifacts.port, 443);
+ Assert.equal(
+ result.artifacts.hash,
+ "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY="
+ );
+ Assert.equal(result.artifacts.ext, "Bazinga!");
+
+ let opts_noext = {
+ credentials,
+ ts,
+ nonce,
+ payload: "something to write about",
+ contentType: "text/plain",
+ };
+ result = await compute(uri_https, method, opts_noext);
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
+ 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
+ 'mac="HTgtd0jPI6E4izx8e4OHdO36q00xFCU0FolNq3RiCYs="'
+ );
+ Assert.equal(result.artifacts.ts, ts);
+ Assert.equal(result.artifacts.nonce, nonce);
+ Assert.equal(result.artifacts.method, method);
+ Assert.equal(result.artifacts.resource, "/somewhere/over/the/rainbow");
+ Assert.equal(result.artifacts.host, "example.net");
+ Assert.equal(result.artifacts.port, 443);
+ Assert.equal(
+ result.artifacts.hash,
+ "2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY="
+ );
+
+ /* Leaving optional fields out should work, although of course then we can't
+ * assert much about the resulting hashes. The resulting header should look
+ * roughly like:
+ * Hawk id="123456", ts="1378764955", nonce="QkynqsrS44M=", mac="/C5NsoAs2fVn+d/I5wMfwe2Gr1MZyAJ6pFyDHG4Gf9U="
+ */
+
+ result = await compute(uri_https, method, { credentials });
+ let fields = result.field.split(" ");
+ Assert.equal(fields[0], "Hawk");
+ Assert.equal(fields[1], 'id="123456",'); // from creds.id
+ Assert.ok(fields[2].startsWith('ts="'));
+ /* The HAWK spec calls for seconds-since-epoch, not ms-since-epoch.
+ * Warning: this test will fail in the year 33658, and for time travellers
+ * who journey earlier than 2001. Please plan accordingly. */
+ Assert.ok(result.artifacts.ts > 1000 * 1000 * 1000);
+ Assert.ok(result.artifacts.ts < 1000 * 1000 * 1000 * 1000);
+ Assert.ok(fields[3].startsWith('nonce="'));
+ Assert.equal(fields[3].length, 'nonce="12345678901=",'.length);
+ Assert.equal(result.artifacts.nonce.length, "12345678901=".length);
+
+ let result2 = await compute(uri_https, method, { credentials });
+ Assert.notEqual(result.artifacts.nonce, result2.artifacts.nonce);
+
+ /* Using an upper-case URI hostname shouldn't affect the hash. */
+
+ let uri_https_upper = CommonUtils.makeURI(
+ "https://EXAMPLE.NET/somewhere/over/the/rainbow"
+ );
+ result = await compute(uri_https_upper, method, opts);
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
+ 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
+ 'ext="Bazinga!", ' +
+ 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'
+ );
+
+ /* Using a lower-case method name shouldn't affect the hash. */
+ result = await compute(uri_https_upper, method.toLowerCase(), opts);
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ' +
+ 'hash="2QfCt3GuY9HQnHWyWD3wX68ZOKbynqlfYmuO2ZBRqtY=", ' +
+ 'ext="Bazinga!", ' +
+ 'mac="q1CwFoSHzPZSkbIvl0oYlD+91rBUEvFk763nMjMndj8="'
+ );
+
+ /* The localtimeOffsetMsec field should be honored. HAWK uses this to
+ * compensate for clock skew between client and server: if the request is
+ * rejected with a timestamp out-of-range error, the error includes the
+ * server's time, and the client computes its clock offset and tries again.
+ * Clients can remember this offset for a while.
+ */
+
+ result = await compute(uri_https, method, {
+ credentials,
+ now: 1378848968650,
+ });
+ Assert.equal(result.artifacts.ts, 1378848968);
+
+ result = await compute(uri_https, method, {
+ credentials,
+ now: 1378848968650,
+ localtimeOffsetMsec: 1000 * 1000,
+ });
+ Assert.equal(result.artifacts.ts, 1378848968 + 1000);
+
+ /* Search/query-args in URIs should be included in the hash. */
+ let makeURI = CommonUtils.makeURI;
+ result = await compute(makeURI("http://example.net/path"), method, opts);
+ Assert.equal(result.artifacts.resource, "/path");
+ Assert.equal(
+ result.artifacts.mac,
+ "WyKHJjWaeYt8aJD+H9UeCWc0Y9C+07ooTmrcrOW4MPI="
+ );
+
+ result = await compute(makeURI("http://example.net/path/"), method, opts);
+ Assert.equal(result.artifacts.resource, "/path/");
+ Assert.equal(
+ result.artifacts.mac,
+ "xAYp2MgZQFvTKJT9u8nsvMjshCRRkuaeYqQbYSFp9Qw="
+ );
+
+ result = await compute(
+ makeURI("http://example.net/path?query=search"),
+ method,
+ opts
+ );
+ Assert.equal(result.artifacts.resource, "/path?query=search");
+ Assert.equal(
+ result.artifacts.mac,
+ "C06a8pip2rA4QkBiosEmC32WcgFcW/R5SQC6kUWyqho="
+ );
+
+ /* Test handling of the payload, which is supposed to be a bytestring
+ (String with codepoints from U+0000 to U+00FF, pre-encoded). */
+
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(result.artifacts.hash, undefined);
+ Assert.equal(
+ result.artifacts.mac,
+ "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY="
+ );
+
+ // Empty payload changes nothing.
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: null,
+ });
+ Assert.equal(result.artifacts.hash, undefined);
+ Assert.equal(
+ result.artifacts.mac,
+ "S3f8E4hAURAqJxOlsYugkPZxLoRYrClgbSQ/3FmKMbY="
+ );
+
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: "hello",
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "uZJnFj0XVBA6Rs1hEvdIDf8NraM0qRNXdFbR3NEQbVA="
+ );
+ Assert.equal(
+ result.artifacts.mac,
+ "pLsHHzngIn5CTJhWBtBr+BezUFvdd/IadpTp/FYVIRM="
+ );
+
+ // update, utf-8 payload
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: "andré@example.org", // non-ASCII
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k="
+ );
+ Assert.equal(
+ result.artifacts.mac,
+ "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk="
+ );
+
+ /* If "hash" is provided, "payload" is ignored. */
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ hash: "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k=",
+ payload: "something else",
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "66DiyapJ0oGgj09IXWdMv8VCg9xk0PL5RqX7bNnQW2k="
+ );
+ Assert.equal(
+ result.artifacts.mac,
+ "2B++3x5xfHEZbPZGDiK3IwfPZctkV4DUr2ORg1vIHvk="
+ );
+
+ // the payload "hash" is also non-urlsafe base64 (+/)
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ payload: "something else",
+ });
+ Assert.equal(
+ result.artifacts.hash,
+ "lERFXr/IKOaAoYw+eBseDUSwmqZTX0uKZpcWLxsdzt8="
+ );
+ Assert.equal(
+ result.artifacts.mac,
+ "jiZuhsac35oD7IdcblhFncBr8tJFHcwWLr8NIYWr9PQ="
+ );
+
+ /* Test non-ascii hostname. HAWK (via the node.js "url" module) punycodes
+ * "ëxample.net" into "xn--xample-ova.net" before hashing. I still think
+ * punycode was a bad joke that got out of the lab and into a spec.
+ */
+
+ result = await compute(makeURI("http://ëxample.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "pILiHl1q8bbNQIdaaLwAFyaFmDU70MGehFuCs3AA5M0="
+ );
+ Assert.equal(result.artifacts.host, "xn--xample-ova.net");
+
+ result = await compute(makeURI("http://example.net/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ ext: 'backslash=\\ quote=" EOF',
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="
+ );
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", ext="backslash=\\\\ quote=\\" EOF", mac="BEMW76lwaJlPX4E/dajF970T6+GzWvaeyLzUt8eOTOc="'
+ );
+
+ result = await compute(makeURI("http://example.net:1234/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="
+ );
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="'
+ );
+
+ /* HAWK (the node.js library) uses a URL parser which stores the "port"
+ * field as a string, but makeURI() gives us an integer. So we'll diverge
+ * on ports with a leading zero. This test vector would fail on the node.js
+ * library (HAWK-1.1.1), where they get a MAC of
+ * "T+GcAsDO8GRHIvZLeepSvXLwDlFJugcZroAy9+uAtcw=". I think HAWK should be
+ * updated to do what we do here, so port="01234" should get the same hash
+ * as port="1234".
+ */
+ result = await compute(makeURI("http://example.net:01234/path"), method, {
+ credentials,
+ ts: 1353809207,
+ nonce: "Ygvqdz",
+ });
+ Assert.equal(
+ result.artifacts.mac,
+ "6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="
+ );
+ Assert.equal(
+ result.field,
+ 'Hawk id="123456", ts="1353809207", nonce="Ygvqdz", mac="6D3JSFDtozuq8QvJTNUc1JzeCfy6h5oRvlhmSTPv6LE="'
+ );
+});
+
+add_test(function test_strip_header_attributes() {
+ let strip = CryptoUtils.stripHeaderAttributes;
+
+ Assert.equal(strip(undefined), "");
+ Assert.equal(strip("text/plain"), "text/plain");
+ Assert.equal(strip("TEXT/PLAIN"), "text/plain");
+ Assert.equal(strip(" text/plain "), "text/plain");
+ Assert.equal(strip("text/plain ; charset=utf-8 "), "text/plain");
+
+ run_next_test();
+});
diff --git a/services/crypto/tests/unit/test_utils_httpmac.js b/services/crypto/tests/unit/test_utils_httpmac.js
new file mode 100644
index 0000000000..4831683e70
--- /dev/null
+++ b/services/crypto/tests/unit/test_utils_httpmac.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { CryptoUtils } = ChromeUtils.importESModule(
+ "resource://services-crypto/utils.sys.mjs"
+);
+
+add_test(function setup() {
+ initTestLogging();
+ run_next_test();
+});
+
+add_task(async function test_sha1() {
+ _("Ensure HTTP MAC SHA1 generation works as expected.");
+
+ let id = "vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7";
+ let key = "b8u1cc5iiio5o319og7hh8faf2gi5ym4aq0zwf112cv1287an65fudu5zj7zo7dz";
+ let ts = 1329181221;
+ let method = "GET";
+ let nonce = "wGX71";
+ let uri = CommonUtils.makeURI("http://10.250.2.176/alias/");
+
+ let result = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, {
+ ts,
+ nonce,
+ });
+
+ Assert.equal(btoa(result.mac), "jzh5chjQc2zFEvLbyHnPdX11Yck=");
+
+ Assert.equal(
+ result.getHeader(),
+ 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' +
+ 'ts="1329181221", nonce="wGX71", mac="jzh5chjQc2zFEvLbyHnPdX11Yck="'
+ );
+
+ let ext = "EXTRA DATA; foo,bar=1";
+
+ result = await CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, {
+ ts,
+ nonce,
+ ext,
+ });
+ Assert.equal(btoa(result.mac), "bNf4Fnt5k6DnhmyipLPkuZroH68=");
+ Assert.equal(
+ result.getHeader(),
+ 'MAC id="vmo1txkttblmn51u2p3zk2xiy16hgvm5ok8qiv1yyi86ffjzy9zj0ez9x6wnvbx7", ' +
+ 'ts="1329181221", nonce="wGX71", mac="bNf4Fnt5k6DnhmyipLPkuZroH68=", ' +
+ 'ext="EXTRA DATA; foo,bar=1"'
+ );
+});
+
+add_task(async function test_nonce_length() {
+ _("Ensure custom nonce lengths are honoured.");
+
+ function get_mac(length) {
+ let uri = CommonUtils.makeURI("http://example.com/");
+ return CryptoUtils.computeHTTPMACSHA1("foo", "bar", "GET", uri, {
+ nonce_bytes: length,
+ });
+ }
+
+ let result = await get_mac(12);
+ Assert.equal(12, atob(result.nonce).length);
+
+ result = await get_mac(2);
+ Assert.equal(2, atob(result.nonce).length);
+
+ result = await get_mac(0);
+ Assert.equal(8, atob(result.nonce).length);
+
+ result = await get_mac(-1);
+ Assert.equal(8, atob(result.nonce).length);
+});
diff --git a/services/crypto/tests/unit/xpcshell.toml b/services/crypto/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..9ce7748357
--- /dev/null
+++ b/services/crypto/tests/unit/xpcshell.toml
@@ -0,0 +1,18 @@
+[DEFAULT]
+head = "head_helpers.js ../../../common/tests/unit/head_helpers.js"
+firefox-appdir = "browser"
+support-files = ["!/services/common/tests/unit/head_helpers.js"]
+
+["test_crypto_crypt.js"]
+
+["test_crypto_random.js"]
+skip-if = ["appname == 'thunderbird'"]
+
+["test_jwcrypto.js"]
+skip-if = ["appname == 'thunderbird'"]
+
+["test_load_modules.js"]
+
+["test_utils_hawk.js"]
+
+["test_utils_httpmac.js"]