summaryrefslogtreecommitdiffstats
path: root/services/crypto/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--services/crypto/tests/unit/head_helpers.js79
-rw-r--r--services/crypto/tests/unit/test_crypto_crypt.js227
-rw-r--r--services/crypto/tests/unit/test_crypto_random.js50
-rw-r--r--services/crypto/tests/unit/test_crypto_service.js133
-rw-r--r--services/crypto/tests/unit/test_jwcrypto.js240
-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.ini17
9 files changed, 1177 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..8849eff262
--- /dev/null
+++ b/services/crypto/tests/unit/head_helpers.js
@@ -0,0 +1,79 @@
+/* import-globals-from ../../../common/tests/unit/head_helpers.js */
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+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";
+ }
+
+ ChromeUtils.import("resource://testing-common/AppInfo.jsm", this);
+ 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 { Services } = ChromeUtils.import(
+ "resource://gre/modules/Services.jsm"
+ );
+ 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..b79f2d8daa
--- /dev/null
+++ b/services/crypto/tests/unit/test_crypto_crypt.js
@@ -0,0 +1,227 @@
+const { WeaveCrypto } = ChromeUtils.import(
+ "resource://services-crypto/WeaveCrypto.js"
+);
+Cu.importGlobalProperties(["crypto"]);
+
+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.import("resource://gre/modules/ctypes.jsm");
+
+ 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..5162b5f3fd
--- /dev/null
+++ b/services/crypto/tests/unit/test_crypto_random.js
@@ -0,0 +1,50 @@
+ChromeUtils.import("resource://services-crypto/WeaveCrypto.js", this);
+
+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_crypto_service.js b/services/crypto/tests/unit/test_crypto_service.js
new file mode 100644
index 0000000000..8488924113
--- /dev/null
+++ b/services/crypto/tests/unit/test_crypto_service.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const idService = Cc["@mozilla.org/identity/crypto-service;1"].getService(
+ Ci.nsIIdentityCryptoService
+);
+
+const ALG_DSA = "DS160";
+const ALG_RSA = "RS256";
+
+const BASE64_URL_ENCODINGS = [
+ // The vectors from RFC 4648 are very silly, but we may as well include them.
+ ["", ""],
+ ["f", "Zg=="],
+ ["fo", "Zm8="],
+ ["foo", "Zm9v"],
+ ["foob", "Zm9vYg=="],
+ ["fooba", "Zm9vYmE="],
+ ["foobar", "Zm9vYmFy"],
+
+ // It's quite likely you could get a string like this in an assertion audience
+ ["i-like-pie.com", "aS1saWtlLXBpZS5jb20="],
+
+ // A few extra to be really sure
+ ["andré@example.com", "YW5kcsOpQGV4YW1wbGUuY29t"],
+ [
+ "πόλλ' οἶδ' ἀλώπηξ, ἀλλ' ἐχῖνος ἓν μέγα",
+ "z4DPjM67zrsnIM6_4by2zrQnIOG8gM67z47PgM63zr4sIOG8gM67zrsnIOG8kM-H4b-Wzr3Ov8-CIOG8k869IM68zq3Os86x",
+ ],
+];
+
+let log = Log.repository.getLogger("crypto.service.test");
+(function() {
+ let appender = new Log.DumpAppender();
+ log.level = Log.Level.Debug;
+ log.addAppender(appender);
+})();
+
+// When the output of an operation is a
+function do_check_eq_or_slightly_less(x, y) {
+ Assert.ok(x >= y - 3 * 8);
+}
+
+function test_base64_roundtrip() {
+ let message = "Attack at dawn!";
+ let encoded = idService.base64UrlEncode(message);
+ let decoded = base64UrlDecode(encoded);
+ Assert.notEqual(message, encoded);
+ Assert.equal(decoded, message);
+ run_next_test();
+}
+
+function test_dsa() {
+ idService.generateKeyPair(ALG_DSA, function(rv, keyPair) {
+ log.debug("DSA generateKeyPair finished " + rv);
+ Assert.ok(Components.isSuccessCode(rv));
+ Assert.equal(typeof keyPair.sign, "function");
+ Assert.equal(keyPair.keyType, ALG_DSA);
+ do_check_eq_or_slightly_less(
+ keyPair.hexDSAGenerator.length,
+ (1024 / 8) * 2
+ );
+ do_check_eq_or_slightly_less(keyPair.hexDSAPrime.length, (1024 / 8) * 2);
+ do_check_eq_or_slightly_less(keyPair.hexDSASubPrime.length, (160 / 8) * 2);
+ do_check_eq_or_slightly_less(
+ keyPair.hexDSAPublicValue.length,
+ (1024 / 8) * 2
+ );
+ // XXX: test that RSA parameters throw the correct error
+
+ log.debug("about to sign with DSA key");
+ keyPair.sign("foo", function(rv2, signature) {
+ log.debug("DSA sign finished " + rv2 + " " + signature);
+ Assert.ok(Components.isSuccessCode(rv2));
+ Assert.ok(signature.length > 1);
+ // TODO: verify the signature with the public key
+ run_next_test();
+ });
+ });
+}
+
+function test_rsa() {
+ idService.generateKeyPair(ALG_RSA, function(rv, keyPair) {
+ log.debug("RSA generateKeyPair finished " + rv);
+ Assert.ok(Components.isSuccessCode(rv));
+ Assert.equal(typeof keyPair.sign, "function");
+ Assert.equal(keyPair.keyType, ALG_RSA);
+ do_check_eq_or_slightly_less(
+ keyPair.hexRSAPublicKeyModulus.length,
+ 2048 / 8
+ );
+ Assert.ok(keyPair.hexRSAPublicKeyExponent.length > 1);
+
+ log.debug("about to sign with RSA key");
+ keyPair.sign("foo", function(rv2, signature) {
+ log.debug("RSA sign finished " + rv2 + " " + signature);
+ Assert.ok(Components.isSuccessCode(rv2));
+ Assert.ok(signature.length > 1);
+ run_next_test();
+ });
+ });
+}
+
+function test_base64UrlEncode() {
+ for (let [source, target] of BASE64_URL_ENCODINGS) {
+ Assert.equal(target, idService.base64UrlEncode(source));
+ }
+ run_next_test();
+}
+
+function test_base64UrlDecode() {
+ let utf8Converter = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ utf8Converter.charset = "UTF-8";
+
+ // We know the encoding of our inputs - on conversion back out again, make
+ // sure they're the same.
+ for (let [source, target] of BASE64_URL_ENCODINGS) {
+ let result = utf8Converter.ConvertToUnicode(base64UrlDecode(target));
+ result += utf8Converter.Finish();
+ Assert.equal(source, result);
+ }
+ run_next_test();
+}
+
+add_test(test_base64_roundtrip);
+add_test(test_dsa);
+add_test(test_rsa);
+add_test(test_base64UrlEncode);
+add_test(test_base64UrlDecode);
diff --git a/services/crypto/tests/unit/test_jwcrypto.js b/services/crypto/tests/unit/test_jwcrypto.js
new file mode 100644
index 0000000000..2c51395bb3
--- /dev/null
+++ b/services/crypto/tests/unit/test_jwcrypto.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "jwcrypto",
+ "resource://services-crypto/jwcrypto.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "CryptoService",
+ "@mozilla.org/identity/crypto-service;1",
+ "nsIIdentityCryptoService"
+);
+
+Cu.importGlobalProperties(["crypto"]);
+
+const RP_ORIGIN = "http://123done.org";
+const INTERNAL_ORIGIN = "browserid://";
+
+const SECOND_MS = 1000;
+const MINUTE_MS = SECOND_MS * 60;
+const HOUR_MS = MINUTE_MS * 60;
+
+// Enable logging from jwcrypto.jsm.
+Services.prefs.setCharPref("services.crypto.jwcrypto.log.level", "Debug");
+
+function promisify(fn) {
+ return (...args) => {
+ return new Promise((res, rej) => {
+ fn(...args, (err, result) => {
+ err ? rej(err) : res(result);
+ });
+ });
+ };
+}
+const generateKeyPair = promisify(jwcrypto.generateKeyPair);
+const generateAssertion = promisify(jwcrypto.generateAssertion);
+
+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");
+});
+
+add_task(async function test_sanity() {
+ // Shouldn't reject.
+ await generateKeyPair("DS160");
+});
+
+add_task(async function test_generate() {
+ let kp = await generateKeyPair("DS160");
+ Assert.notEqual(kp, null);
+});
+
+add_task(async function test_get_assertion() {
+ let kp = await generateKeyPair("DS160");
+ let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN);
+ Assert.equal(backedAssertion.split("~").length, 2);
+ Assert.equal(backedAssertion.split(".").length, 3);
+});
+
+add_task(async function test_rsa() {
+ let kpo = await generateKeyPair("RS256");
+ Assert.notEqual(kpo, undefined);
+ info(kpo.serializedPublicKey);
+ let pk = JSON.parse(kpo.serializedPublicKey);
+ Assert.equal(pk.algorithm, "RS");
+ /* TODO
+ do_check_neq(kpo.sign, null);
+ do_check_eq(typeof kpo.sign, "function");
+ do_check_neq(kpo.userID, null);
+ do_check_neq(kpo.url, null);
+ do_check_eq(kpo.url, INTERNAL_ORIGIN);
+ do_check_neq(kpo.exponent, null);
+ do_check_neq(kpo.modulus, null);
+
+ // TODO: should sign be async?
+ let sig = kpo.sign("This is a message to sign");
+
+ do_check_neq(sig, null);
+ do_check_eq(typeof sig, "string");
+ do_check_true(sig.length > 1);
+ */
+});
+
+add_task(async function test_dsa() {
+ let kpo = await generateKeyPair("DS160");
+ info(kpo.serializedPublicKey);
+ let pk = JSON.parse(kpo.serializedPublicKey);
+ Assert.equal(pk.algorithm, "DS");
+ /* TODO
+ do_check_neq(kpo.sign, null);
+ do_check_eq(typeof kpo.sign, "function");
+ do_check_neq(kpo.userID, null);
+ do_check_neq(kpo.url, null);
+ do_check_eq(kpo.url, INTERNAL_ORIGIN);
+ do_check_neq(kpo.generator, null);
+ do_check_neq(kpo.prime, null);
+ do_check_neq(kpo.subPrime, null);
+ do_check_neq(kpo.publicValue, null);
+
+ let sig = kpo.sign("This is a message to sign");
+
+ do_check_neq(sig, null);
+ do_check_eq(typeof sig, "string");
+ do_check_true(sig.length > 1);
+ */
+});
+
+add_task(async function test_get_assertion_with_offset() {
+ // Use an arbitrary date in the past to ensure we don't accidentally pass
+ // this test with current dates, missing offsets, etc.
+ let serverMsec = Date.parse("Tue Oct 31 2000 00:00:00 GMT-0800");
+
+ // local clock skew
+ // clock is 12 hours fast; -12 hours offset must be applied
+ let localtimeOffsetMsec = -1 * 12 * HOUR_MS;
+ let localMsec = serverMsec - localtimeOffsetMsec;
+
+ let kp = await generateKeyPair("DS160");
+ let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN, {
+ duration: MINUTE_MS,
+ localtimeOffsetMsec,
+ now: localMsec,
+ });
+ // properly formed
+ let cert;
+ let assertion;
+ [cert, assertion] = backedAssertion.split("~");
+
+ Assert.equal(cert, "fake-cert");
+ Assert.equal(assertion.split(".").length, 3);
+
+ let components = extractComponents(assertion);
+
+ // Expiry is within two minutes, corrected for skew
+ let exp = parseInt(components.payload.exp, 10);
+ Assert.ok(exp - serverMsec === MINUTE_MS);
+});
+
+add_task(async function test_assertion_lifetime() {
+ let kp = await generateKeyPair("DS160");
+ let backedAssertion = await generateAssertion("fake-cert", kp, RP_ORIGIN, {
+ duration: MINUTE_MS,
+ });
+ // properly formed
+ let cert;
+ let assertion;
+ [cert, assertion] = backedAssertion.split("~");
+
+ Assert.equal(cert, "fake-cert");
+ Assert.equal(assertion.split(".").length, 3);
+
+ let components = extractComponents(assertion);
+
+ // Expiry is within one minute, as we specified above
+ let exp = parseInt(components.payload.exp, 10);
+ Assert.ok(Math.abs(Date.now() - exp) > 50 * SECOND_MS);
+ Assert.ok(Math.abs(Date.now() - exp) <= MINUTE_MS);
+});
+
+add_task(async function test_audience_encoding_bug972582() {
+ let audience = "i-like-pie.com";
+ let kp = await generateKeyPair("DS160");
+ let backedAssertion = await generateAssertion("fake-cert", kp, audience);
+ let [, /* cert */ assertion] = backedAssertion.split("~");
+ let components = extractComponents(assertion);
+ Assert.equal(components.payload.aud, audience);
+});
+
+function extractComponents(signedObject) {
+ if (typeof signedObject != "string") {
+ throw new Error("malformed signature " + typeof signedObject);
+ }
+
+ let parts = signedObject.split(".");
+ if (parts.length != 3) {
+ throw new Error(
+ "signed object must have three parts, this one has " + parts.length
+ );
+ }
+
+ let headerSegment = parts[0];
+ let payloadSegment = parts[1];
+ let cryptoSegment = parts[2];
+
+ let header = JSON.parse(base64UrlDecode(headerSegment));
+ let payload = JSON.parse(base64UrlDecode(payloadSegment));
+
+ // Ensure well-formed header
+ Assert.equal(Object.keys(header).length, 1);
+ Assert.ok(!!header.alg);
+
+ // Ensure well-formed payload
+ for (let field of ["exp", "aud"]) {
+ Assert.ok(!!payload[field]);
+ }
+
+ return { header, payload, headerSegment, payloadSegment, cryptoSegment };
+}
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..1ab3a888d9
--- /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.js", "WeaveCrypto.js"];
+
+function run_test() {
+ for (let m of modules) {
+ let resource = "resource://services-crypto/" + m;
+ _("Attempting to import: " + resource);
+ ChromeUtils.import(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..bfeb5e859f
--- /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.import(
+ "resource://services-crypto/utils.js"
+);
+
+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..e0936b9142
--- /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.import(
+ "resource://services-crypto/utils.js"
+);
+
+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.ini b/services/crypto/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..ec5f9a4211
--- /dev/null
+++ b/services/crypto/tests/unit/xpcshell.ini
@@ -0,0 +1,17 @@
+[DEFAULT]
+head = head_helpers.js ../../../common/tests/unit/head_helpers.js
+firefox-appdir = browser
+support-files =
+ !/services/common/tests/unit/head_helpers.js
+
+[test_load_modules.js]
+
+[test_crypto_crypt.js]
+[test_crypto_random.js]
+[test_crypto_service.js]
+skip-if = appname == 'thunderbird'
+[test_jwcrypto.js]
+skip-if = appname == 'thunderbird'
+
+[test_utils_hawk.js]
+[test_utils_httpmac.js]