// Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ "use strict"; // Tests that signed extensions extracted/unpacked into a directory work pass // verification when non-tampered, and fail verification when tampered via // various means. const { ZipUtils } = Cu.import("resource://gre/modules/ZipUtils.jsm", {}); do_get_profile(); // must be called before getting nsIX509CertDB const certdb = Cc["@mozilla.org/security/x509certdb;1"] .getService(Ci.nsIX509CertDB); /** * Signed test extension. This is any arbitrary Mozilla signed XPI that * preferably has recently been signed (but note that it actually doesn't * matter, since we ignore expired certificates when checking signing). * @type nsIFile */ var gSignedXPI = do_get_file("test_signed_dir/lightbeam_for_firefox-1.3.1-fx.xpi", false); /** * The directory that the test extension will be extracted to. * @type nsIFile */ var gTarget = FileUtils.getDir("TmpD", ["test_signed_dir"]); gTarget.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); /** * Each property below is optional. Defining none of them means "don't tamper". * * @typedef {TamperInstructions} * @type Object * @property {String[][]} copy * Format: [[path,newname], [path2,newname2], ...] * Copy the file located at |path| and name it |newname|. * @property {String[]} delete * List of paths to files to delete. * @property {String[]} corrupt * List of paths to files to corrupt. */ /** * Extracts the signed XPI into a directory, and tampers the files in that * directory if instructed. * * @param {TamperInstructions} tamper * Instructions on whether to tamper any files, and if so, how. * @returns {nsIFile} * The directory where the XPI was extracted to. */ function prepare(tamper) { ZipUtils.extractFiles(gSignedXPI, gTarget); // copy files if (tamper.copy) { tamper.copy.forEach(i => { let f = gTarget.clone(); i[0].split("/").forEach(seg => { f.append(seg); }); f.copyTo(null, i[1]); }); } // delete files if (tamper.delete) { tamper.delete.forEach(i => { let f = gTarget.clone(); i.split("/").forEach(seg => { f.append(seg); }); f.remove(true); }); } // corrupt files if (tamper.corrupt) { tamper.corrupt.forEach(i => { let f = gTarget.clone(); i.split("/").forEach(seg => { f.append(seg); }); let s = FileUtils.openFileOutputStream(f, FileUtils.MODE_WRONLY); const str = "Kilroy was here"; s.write(str, str.length); s.close(); }); } return gTarget; } function checkResult(expectedRv, dir, resolve) { return function verifySignedDirCallback(rv, aSignerCert) { equal(rv, expectedRv, "Actual and expected return value should match"); equal(aSignerCert != null, Components.isSuccessCode(expectedRv), "expecting certificate:"); dir.remove(true); resolve(); }; } function verifyDirAsync(expectedRv, tamper) { let targetDir = prepare(tamper); return new Promise((resolve, reject) => { certdb.verifySignedDirectoryAsync( Ci.nsIX509CertDB.AddonsPublicRoot, targetDir, checkResult(expectedRv, targetDir, resolve)); }); } // // the tests // add_task(function* testValid() { yield verifyDirAsync(Cr.NS_OK, {} /* no tampering */); }); add_task(function* testNoMetaDir() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED, {delete: ["META-INF"]}); }); add_task(function* testEmptyMetaDir() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED, {delete: ["META-INF/mozilla.rsa", "META-INF/mozilla.sf", "META-INF/manifest.mf"]}); }); add_task(function* testTwoRSAFiles() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID, {copy: [["META-INF/mozilla.rsa", "extra.rsa"]]}); }); add_task(function* testCorruptRSAFile() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID, {corrupt: ["META-INF/mozilla.rsa"]}); }); add_task(function* testMissingSFFile() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID, {delete: ["META-INF/mozilla.sf"]}); }); add_task(function* testCorruptSFFile() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID, {corrupt: ["META-INF/mozilla.sf"]}); }); add_task(function* testExtraInvalidSFFile() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY, {copy: [["META-INF/mozilla.rsa", "extra.sf"]]}); }); add_task(function* testExtraValidSFFile() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY, {copy: [["META-INF/mozilla.sf", "extra.sf"]]}); }); add_task(function* testMissingManifest() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID, {delete: ["META-INF/manifest.mf"]}); }); add_task(function* testCorruptManifest() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_MANIFEST_INVALID, {corrupt: ["META-INF/manifest.mf"]}); }); add_task(function* testMissingFile() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING, {delete: ["bootstrap.js"]}); }); add_task(function* testCorruptFile() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY, {corrupt: ["bootstrap.js"]}); }); add_task(function* testExtraFile() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY, {copy: [["bootstrap.js", "extra"]]}); }); add_task(function* testMissingFileInDir() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_ENTRY_MISSING, {delete: ["lib/ui.js"]}); }); add_task(function* testCorruptFileInDir() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_MODIFIED_ENTRY, {corrupt: ["lib/ui.js"]}); }); add_task(function* testExtraFileInDir() { yield verifyDirAsync(Cr.NS_ERROR_SIGNED_JAR_UNSIGNED_ENTRY, {copy: [["lib/ui.js", "extra"]]}); }); do_register_cleanup(function() { if (gTarget.exists()) { gTarget.remove(true); } });