michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: * http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: /** michael@0: * This file tests signature extraction using Windows Authenticode APIs of michael@0: * downloaded files. michael@0: */ michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Globals michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", michael@0: "resource://gre/modules/FileUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: michael@0: const BackgroundFileSaverOutputStream = Components.Constructor( michael@0: "@mozilla.org/network/background-file-saver;1?mode=outputstream", michael@0: "nsIBackgroundFileSaver"); michael@0: michael@0: const StringInputStream = Components.Constructor( michael@0: "@mozilla.org/io/string-input-stream;1", michael@0: "nsIStringInputStream", michael@0: "setData"); michael@0: michael@0: const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt"; michael@0: michael@0: const gAppRep = Cc["@mozilla.org/downloads/application-reputation-service;1"]. michael@0: getService(Ci.nsIApplicationReputationService); michael@0: let gStillRunning = true; michael@0: let gTables = {}; michael@0: let gHttpServer = null; michael@0: michael@0: /** michael@0: * Returns a reference to a temporary file. If the file is then created, it michael@0: * will be removed when tests in this file finish. michael@0: */ michael@0: function getTempFile(aLeafName) { michael@0: let file = FileUtils.getFile("TmpD", [aLeafName]); michael@0: do_register_cleanup(function GTF_cleanup() { michael@0: if (file.exists()) { michael@0: file.remove(false); michael@0: } michael@0: }); michael@0: return file; michael@0: } michael@0: michael@0: function readFileToString(aFilename) { michael@0: let f = do_get_file(aFilename); michael@0: let stream = Cc["@mozilla.org/network/file-input-stream;1"] michael@0: .createInstance(Ci.nsIFileInputStream); michael@0: stream.init(f, -1, 0, 0); michael@0: let buf = NetUtil.readInputStreamToString(stream, stream.available()); michael@0: return buf; michael@0: } michael@0: michael@0: /** michael@0: * Waits for the given saver object to complete. michael@0: * michael@0: * @param aSaver michael@0: * The saver, with the output stream or a stream listener implementation. michael@0: * @param aOnTargetChangeFn michael@0: * Optional callback invoked with the target file name when it changes. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When onSaveComplete is called with a success code. michael@0: * @rejects With an exception, if onSaveComplete is called with a failure code. michael@0: */ michael@0: function promiseSaverComplete(aSaver, aOnTargetChangeFn) { michael@0: let deferred = Promise.defer(); michael@0: aSaver.observer = { michael@0: onTargetChange: function BFSO_onSaveComplete(aSaver, aTarget) michael@0: { michael@0: if (aOnTargetChangeFn) { michael@0: aOnTargetChangeFn(aTarget); michael@0: } michael@0: }, michael@0: onSaveComplete: function BFSO_onSaveComplete(aSaver, aStatus) michael@0: { michael@0: if (Components.isSuccessCode(aStatus)) { michael@0: deferred.resolve(); michael@0: } else { michael@0: deferred.reject(new Components.Exception("Saver failed.", aStatus)); michael@0: } michael@0: }, michael@0: }; michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Feeds a string to a BackgroundFileSaverOutputStream. michael@0: * michael@0: * @param aSourceString michael@0: * The source data to copy. michael@0: * @param aSaverOutputStream michael@0: * The BackgroundFileSaverOutputStream to feed. michael@0: * @param aCloseWhenDone michael@0: * If true, the output stream will be closed when the copy finishes. michael@0: * michael@0: * @return {Promise} michael@0: * @resolves When the copy completes with a success code. michael@0: * @rejects With an exception, if the copy fails. michael@0: */ michael@0: function promiseCopyToSaver(aSourceString, aSaverOutputStream, aCloseWhenDone) { michael@0: let deferred = Promise.defer(); michael@0: let inputStream = new StringInputStream(aSourceString, aSourceString.length); michael@0: let copier = Cc["@mozilla.org/network/async-stream-copier;1"] michael@0: .createInstance(Ci.nsIAsyncStreamCopier); michael@0: copier.init(inputStream, aSaverOutputStream, null, false, true, 0x8000, true, michael@0: aCloseWhenDone); michael@0: copier.asyncCopy({ michael@0: onStartRequest: function () { }, michael@0: onStopRequest: function (aRequest, aContext, aStatusCode) michael@0: { michael@0: if (Components.isSuccessCode(aStatusCode)) { michael@0: deferred.resolve(); michael@0: } else { michael@0: deferred.reject(new Components.Exception(aResult)); michael@0: } michael@0: }, michael@0: }, null); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: // Registers a table for which to serve update chunks. michael@0: function registerTableUpdate(aTable, aFilename) { michael@0: // If we haven't been given an update for this table yet, add it to the map michael@0: if (!(aTable in gTables)) { michael@0: gTables[aTable] = []; michael@0: } michael@0: michael@0: // The number of chunks associated with this table. michael@0: let numChunks = gTables[aTable].length + 1; michael@0: let redirectPath = "/" + aTable + "-" + numChunks; michael@0: let redirectUrl = "localhost:4444" + redirectPath; michael@0: michael@0: // Store redirect url for that table so we can return it later when we michael@0: // process an update request. michael@0: gTables[aTable].push(redirectUrl); michael@0: michael@0: gHttpServer.registerPathHandler(redirectPath, function(request, response) { michael@0: do_print("Mock safebrowsing server handling request for " + redirectPath); michael@0: let contents = readFileToString(aFilename); michael@0: do_print("Length of " + aFilename + ": " + contents.length); michael@0: response.setHeader("Content-Type", michael@0: "application/vnd.google.safebrowsing-update", false); michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: response.bodyOutputStream.write(contents, contents.length); michael@0: }); michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Tests michael@0: michael@0: function run_test() michael@0: { michael@0: run_next_test(); michael@0: } michael@0: michael@0: add_task(function test_setup() michael@0: { michael@0: // Wait 10 minutes, that is half of the external xpcshell timeout. michael@0: do_timeout(10 * 60 * 1000, function() { michael@0: if (gStillRunning) { michael@0: do_throw("Test timed out."); michael@0: } michael@0: }); michael@0: // Set up a local HTTP server to return bad verdicts. michael@0: Services.prefs.setCharPref("browser.safebrowsing.appRepURL", michael@0: "http://localhost:4444/download"); michael@0: // Ensure safebrowsing is enabled for this test, even if the app michael@0: // doesn't have it enabled. michael@0: Services.prefs.setBoolPref("browser.safebrowsing.malware.enabled", true); michael@0: do_register_cleanup(function() { michael@0: Services.prefs.clearUserPref("browser.safebrowsing.malware.enabled"); michael@0: }); michael@0: michael@0: gHttpServer = new HttpServer(); michael@0: gHttpServer.registerDirectory("/", do_get_cwd()); michael@0: michael@0: function createVerdict(aShouldBlock) { michael@0: // We can't programmatically create a protocol buffer here, so just michael@0: // hardcode some already serialized ones. michael@0: blob = String.fromCharCode(parseInt(0x08, 16)); michael@0: if (aShouldBlock) { michael@0: // A safe_browsing::ClientDownloadRequest with a DANGEROUS verdict michael@0: blob += String.fromCharCode(parseInt(0x01, 16)); michael@0: } else { michael@0: // A safe_browsing::ClientDownloadRequest with a SAFE verdict michael@0: blob += String.fromCharCode(parseInt(0x00, 16)); michael@0: } michael@0: return blob; michael@0: } michael@0: michael@0: gHttpServer.registerPathHandler("/throw", function(request, response) { michael@0: do_throw("We shouldn't be getting here"); michael@0: }); michael@0: michael@0: gHttpServer.registerPathHandler("/download", function(request, response) { michael@0: do_print("Querying remote server for verdict"); michael@0: response.setHeader("Content-Type", "application/octet-stream", false); michael@0: let buf = NetUtil.readInputStreamToString( michael@0: request.bodyInputStream, michael@0: request.bodyInputStream.available()); michael@0: do_print("Request length: " + buf.length); michael@0: // A garbage response. By default this produces NS_CANNOT_CONVERT_DATA as michael@0: // the callback status. michael@0: let blob = "this is not a serialized protocol buffer"; michael@0: // We can't actually parse the protocol buffer here, so just switch on the michael@0: // length instead of inspecting the contents. michael@0: if (buf.length == 45) { michael@0: // evil.com michael@0: blob = createVerdict(true); michael@0: } else if (buf.length == 48) { michael@0: // mozilla.com michael@0: blob = createVerdict(false); michael@0: } michael@0: response.bodyOutputStream.write(blob, blob.length); michael@0: }); michael@0: michael@0: gHttpServer.start(4444); michael@0: }); michael@0: michael@0: // Construct a response with redirect urls. michael@0: function processUpdateRequest() { michael@0: let response = "n:1000\n"; michael@0: for (let table in gTables) { michael@0: response += "i:" + table + "\n"; michael@0: for (let i = 0; i < gTables[table].length; ++i) { michael@0: response += "u:" + gTables[table][i] + "\n"; michael@0: } michael@0: } michael@0: do_print("Returning update response: " + response); michael@0: return response; michael@0: } michael@0: michael@0: // Set up the local whitelist. michael@0: function waitForUpdates() { michael@0: let deferred = Promise.defer(); michael@0: gHttpServer.registerPathHandler("/downloads", function(request, response) { michael@0: let buf = NetUtil.readInputStreamToString(request.bodyInputStream, michael@0: request.bodyInputStream.available()); michael@0: let blob = processUpdateRequest(); michael@0: response.setHeader("Content-Type", michael@0: "application/vnd.google.safebrowsing-update", false); michael@0: response.setStatusLine(request.httpVersion, 200, "OK"); michael@0: response.bodyOutputStream.write(blob, blob.length); michael@0: }); michael@0: michael@0: let streamUpdater = Cc["@mozilla.org/url-classifier/streamupdater;1"] michael@0: .getService(Ci.nsIUrlClassifierStreamUpdater); michael@0: streamUpdater.updateUrl = "http://localhost:4444/downloads"; michael@0: michael@0: // Load up some update chunks for the safebrowsing server to serve. This michael@0: // particular chunk contains the hash of whitelisted.com/ and michael@0: // sb-ssl.google.com/safebrowsing/csd/certificate/. michael@0: registerTableUpdate("goog-downloadwhite-digest256", "data/digest.chunk"); michael@0: michael@0: // Resolve the promise once processing the updates is complete. michael@0: function updateSuccess(aEvent) { michael@0: // Timeout of n:1000 is constructed in processUpdateRequest above and michael@0: // passed back in the callback in nsIUrlClassifierStreamUpdater on success. michael@0: do_check_eq("1000", aEvent); michael@0: do_print("All data processed"); michael@0: deferred.resolve(true); michael@0: } michael@0: // Just throw if we ever get an update or download error. michael@0: function handleError(aEvent) { michael@0: do_throw("We didn't download or update correctly: " + aEvent); michael@0: deferred.reject(); michael@0: } michael@0: streamUpdater.downloadUpdates( michael@0: "goog-downloadwhite-digest256", michael@0: "goog-downloadwhite-digest256;\n", michael@0: updateSuccess, handleError, handleError); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function promiseQueryReputation(query, expectedShouldBlock) { michael@0: let deferred = Promise.defer(); michael@0: function onComplete(aShouldBlock, aStatus) { michael@0: do_check_eq(Cr.NS_OK, aStatus); michael@0: do_check_eq(aShouldBlock, expectedShouldBlock); michael@0: deferred.resolve(true); michael@0: } michael@0: gAppRep.queryReputation(query, onComplete); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: add_task(function() michael@0: { michael@0: // Wait for Safebrowsing local list updates to complete. michael@0: yield waitForUpdates(); michael@0: }); michael@0: michael@0: add_task(function test_signature_whitelists() michael@0: { michael@0: // We should never get to the remote server. michael@0: Services.prefs.setCharPref("browser.safebrowsing.appRepURL", michael@0: "http://localhost:4444/throw"); michael@0: michael@0: // Use BackgroundFileSaver to extract the signature on Windows. michael@0: let destFile = getTempFile(TEST_FILE_NAME_1); michael@0: michael@0: let data = readFileToString("data/signed_win.exe"); michael@0: let saver = new BackgroundFileSaverOutputStream(); michael@0: let completionPromise = promiseSaverComplete(saver); michael@0: saver.enableSignatureInfo(); michael@0: saver.setTarget(destFile, false); michael@0: yield promiseCopyToSaver(data, saver, true); michael@0: michael@0: saver.finish(Cr.NS_OK); michael@0: yield completionPromise; michael@0: michael@0: // Clean up. michael@0: destFile.remove(false); michael@0: michael@0: // evil.com is not on the allowlist, but this binary is signed by an entity michael@0: // whose certificate information is on the allowlist. michael@0: yield promiseQueryReputation({sourceURI: createURI("http://evil.com"), michael@0: signatureInfo: saver.signatureInfo, michael@0: fileSize: 12}, false); michael@0: }); michael@0: michael@0: add_task(function test_blocked_binary() michael@0: { michael@0: // We should reach the remote server for a verdict. michael@0: Services.prefs.setCharPref("browser.safebrowsing.appRepURL", michael@0: "http://localhost:4444/download"); michael@0: // evil.com should return a malware verdict from the remote server. michael@0: yield promiseQueryReputation({sourceURI: createURI("http://evil.com"), michael@0: suggestedFileName: "noop.bat", michael@0: fileSize: 12}, true); michael@0: }); michael@0: michael@0: add_task(function test_non_binary() michael@0: { michael@0: // We should not reach the remote server for a verdict for non-binary files. michael@0: Services.prefs.setCharPref("browser.safebrowsing.appRepURL", michael@0: "http://localhost:4444/throw"); michael@0: yield promiseQueryReputation({sourceURI: createURI("http://evil.com"), michael@0: suggestedFileName: "noop.txt", michael@0: fileSize: 12}, false); michael@0: }); michael@0: michael@0: add_task(function test_good_binary() michael@0: { michael@0: // We should reach the remote server for a verdict. michael@0: Services.prefs.setCharPref("browser.safebrowsing.appRepURL", michael@0: "http://localhost:4444/download"); michael@0: // mozilla.com should return a not-guilty verdict from the remote server. michael@0: yield promiseQueryReputation({sourceURI: createURI("http://mozilla.com"), michael@0: suggestedFileName: "noop.bat", michael@0: fileSize: 12}, false); michael@0: }); michael@0: michael@0: add_task(function test_teardown() michael@0: { michael@0: gStillRunning = false; michael@0: });