1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/downloads/test/unit/test_app_rep_windows.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,368 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80: */ 1.6 +/* Any copyright is dedicated to the Public Domain. 1.7 + * http://creativecommons.org/publicdomain/zero/1.0/ */ 1.8 + 1.9 +/** 1.10 + * This file tests signature extraction using Windows Authenticode APIs of 1.11 + * downloaded files. 1.12 + */ 1.13 + 1.14 +//////////////////////////////////////////////////////////////////////////////// 1.15 +//// Globals 1.16 + 1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.18 + 1.19 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", 1.20 + "resource://gre/modules/FileUtils.jsm"); 1.21 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 1.22 + "resource://gre/modules/NetUtil.jsm"); 1.23 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.24 + "resource://gre/modules/Promise.jsm"); 1.25 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.26 + "resource://gre/modules/Task.jsm"); 1.27 + 1.28 +const BackgroundFileSaverOutputStream = Components.Constructor( 1.29 + "@mozilla.org/network/background-file-saver;1?mode=outputstream", 1.30 + "nsIBackgroundFileSaver"); 1.31 + 1.32 +const StringInputStream = Components.Constructor( 1.33 + "@mozilla.org/io/string-input-stream;1", 1.34 + "nsIStringInputStream", 1.35 + "setData"); 1.36 + 1.37 +const TEST_FILE_NAME_1 = "test-backgroundfilesaver-1.txt"; 1.38 + 1.39 +const gAppRep = Cc["@mozilla.org/downloads/application-reputation-service;1"]. 1.40 + getService(Ci.nsIApplicationReputationService); 1.41 +let gStillRunning = true; 1.42 +let gTables = {}; 1.43 +let gHttpServer = null; 1.44 + 1.45 +/** 1.46 + * Returns a reference to a temporary file. If the file is then created, it 1.47 + * will be removed when tests in this file finish. 1.48 + */ 1.49 +function getTempFile(aLeafName) { 1.50 + let file = FileUtils.getFile("TmpD", [aLeafName]); 1.51 + do_register_cleanup(function GTF_cleanup() { 1.52 + if (file.exists()) { 1.53 + file.remove(false); 1.54 + } 1.55 + }); 1.56 + return file; 1.57 +} 1.58 + 1.59 +function readFileToString(aFilename) { 1.60 + let f = do_get_file(aFilename); 1.61 + let stream = Cc["@mozilla.org/network/file-input-stream;1"] 1.62 + .createInstance(Ci.nsIFileInputStream); 1.63 + stream.init(f, -1, 0, 0); 1.64 + let buf = NetUtil.readInputStreamToString(stream, stream.available()); 1.65 + return buf; 1.66 +} 1.67 + 1.68 +/** 1.69 + * Waits for the given saver object to complete. 1.70 + * 1.71 + * @param aSaver 1.72 + * The saver, with the output stream or a stream listener implementation. 1.73 + * @param aOnTargetChangeFn 1.74 + * Optional callback invoked with the target file name when it changes. 1.75 + * 1.76 + * @return {Promise} 1.77 + * @resolves When onSaveComplete is called with a success code. 1.78 + * @rejects With an exception, if onSaveComplete is called with a failure code. 1.79 + */ 1.80 +function promiseSaverComplete(aSaver, aOnTargetChangeFn) { 1.81 + let deferred = Promise.defer(); 1.82 + aSaver.observer = { 1.83 + onTargetChange: function BFSO_onSaveComplete(aSaver, aTarget) 1.84 + { 1.85 + if (aOnTargetChangeFn) { 1.86 + aOnTargetChangeFn(aTarget); 1.87 + } 1.88 + }, 1.89 + onSaveComplete: function BFSO_onSaveComplete(aSaver, aStatus) 1.90 + { 1.91 + if (Components.isSuccessCode(aStatus)) { 1.92 + deferred.resolve(); 1.93 + } else { 1.94 + deferred.reject(new Components.Exception("Saver failed.", aStatus)); 1.95 + } 1.96 + }, 1.97 + }; 1.98 + return deferred.promise; 1.99 +} 1.100 + 1.101 +/** 1.102 + * Feeds a string to a BackgroundFileSaverOutputStream. 1.103 + * 1.104 + * @param aSourceString 1.105 + * The source data to copy. 1.106 + * @param aSaverOutputStream 1.107 + * The BackgroundFileSaverOutputStream to feed. 1.108 + * @param aCloseWhenDone 1.109 + * If true, the output stream will be closed when the copy finishes. 1.110 + * 1.111 + * @return {Promise} 1.112 + * @resolves When the copy completes with a success code. 1.113 + * @rejects With an exception, if the copy fails. 1.114 + */ 1.115 +function promiseCopyToSaver(aSourceString, aSaverOutputStream, aCloseWhenDone) { 1.116 + let deferred = Promise.defer(); 1.117 + let inputStream = new StringInputStream(aSourceString, aSourceString.length); 1.118 + let copier = Cc["@mozilla.org/network/async-stream-copier;1"] 1.119 + .createInstance(Ci.nsIAsyncStreamCopier); 1.120 + copier.init(inputStream, aSaverOutputStream, null, false, true, 0x8000, true, 1.121 + aCloseWhenDone); 1.122 + copier.asyncCopy({ 1.123 + onStartRequest: function () { }, 1.124 + onStopRequest: function (aRequest, aContext, aStatusCode) 1.125 + { 1.126 + if (Components.isSuccessCode(aStatusCode)) { 1.127 + deferred.resolve(); 1.128 + } else { 1.129 + deferred.reject(new Components.Exception(aResult)); 1.130 + } 1.131 + }, 1.132 + }, null); 1.133 + return deferred.promise; 1.134 +} 1.135 + 1.136 +// Registers a table for which to serve update chunks. 1.137 +function registerTableUpdate(aTable, aFilename) { 1.138 + // If we haven't been given an update for this table yet, add it to the map 1.139 + if (!(aTable in gTables)) { 1.140 + gTables[aTable] = []; 1.141 + } 1.142 + 1.143 + // The number of chunks associated with this table. 1.144 + let numChunks = gTables[aTable].length + 1; 1.145 + let redirectPath = "/" + aTable + "-" + numChunks; 1.146 + let redirectUrl = "localhost:4444" + redirectPath; 1.147 + 1.148 + // Store redirect url for that table so we can return it later when we 1.149 + // process an update request. 1.150 + gTables[aTable].push(redirectUrl); 1.151 + 1.152 + gHttpServer.registerPathHandler(redirectPath, function(request, response) { 1.153 + do_print("Mock safebrowsing server handling request for " + redirectPath); 1.154 + let contents = readFileToString(aFilename); 1.155 + do_print("Length of " + aFilename + ": " + contents.length); 1.156 + response.setHeader("Content-Type", 1.157 + "application/vnd.google.safebrowsing-update", false); 1.158 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.159 + response.bodyOutputStream.write(contents, contents.length); 1.160 + }); 1.161 +} 1.162 + 1.163 +//////////////////////////////////////////////////////////////////////////////// 1.164 +//// Tests 1.165 + 1.166 +function run_test() 1.167 +{ 1.168 + run_next_test(); 1.169 +} 1.170 + 1.171 +add_task(function test_setup() 1.172 +{ 1.173 + // Wait 10 minutes, that is half of the external xpcshell timeout. 1.174 + do_timeout(10 * 60 * 1000, function() { 1.175 + if (gStillRunning) { 1.176 + do_throw("Test timed out."); 1.177 + } 1.178 + }); 1.179 + // Set up a local HTTP server to return bad verdicts. 1.180 + Services.prefs.setCharPref("browser.safebrowsing.appRepURL", 1.181 + "http://localhost:4444/download"); 1.182 + // Ensure safebrowsing is enabled for this test, even if the app 1.183 + // doesn't have it enabled. 1.184 + Services.prefs.setBoolPref("browser.safebrowsing.malware.enabled", true); 1.185 + do_register_cleanup(function() { 1.186 + Services.prefs.clearUserPref("browser.safebrowsing.malware.enabled"); 1.187 + }); 1.188 + 1.189 + gHttpServer = new HttpServer(); 1.190 + gHttpServer.registerDirectory("/", do_get_cwd()); 1.191 + 1.192 + function createVerdict(aShouldBlock) { 1.193 + // We can't programmatically create a protocol buffer here, so just 1.194 + // hardcode some already serialized ones. 1.195 + blob = String.fromCharCode(parseInt(0x08, 16)); 1.196 + if (aShouldBlock) { 1.197 + // A safe_browsing::ClientDownloadRequest with a DANGEROUS verdict 1.198 + blob += String.fromCharCode(parseInt(0x01, 16)); 1.199 + } else { 1.200 + // A safe_browsing::ClientDownloadRequest with a SAFE verdict 1.201 + blob += String.fromCharCode(parseInt(0x00, 16)); 1.202 + } 1.203 + return blob; 1.204 + } 1.205 + 1.206 + gHttpServer.registerPathHandler("/throw", function(request, response) { 1.207 + do_throw("We shouldn't be getting here"); 1.208 + }); 1.209 + 1.210 + gHttpServer.registerPathHandler("/download", function(request, response) { 1.211 + do_print("Querying remote server for verdict"); 1.212 + response.setHeader("Content-Type", "application/octet-stream", false); 1.213 + let buf = NetUtil.readInputStreamToString( 1.214 + request.bodyInputStream, 1.215 + request.bodyInputStream.available()); 1.216 + do_print("Request length: " + buf.length); 1.217 + // A garbage response. By default this produces NS_CANNOT_CONVERT_DATA as 1.218 + // the callback status. 1.219 + let blob = "this is not a serialized protocol buffer"; 1.220 + // We can't actually parse the protocol buffer here, so just switch on the 1.221 + // length instead of inspecting the contents. 1.222 + if (buf.length == 45) { 1.223 + // evil.com 1.224 + blob = createVerdict(true); 1.225 + } else if (buf.length == 48) { 1.226 + // mozilla.com 1.227 + blob = createVerdict(false); 1.228 + } 1.229 + response.bodyOutputStream.write(blob, blob.length); 1.230 + }); 1.231 + 1.232 + gHttpServer.start(4444); 1.233 +}); 1.234 + 1.235 +// Construct a response with redirect urls. 1.236 +function processUpdateRequest() { 1.237 + let response = "n:1000\n"; 1.238 + for (let table in gTables) { 1.239 + response += "i:" + table + "\n"; 1.240 + for (let i = 0; i < gTables[table].length; ++i) { 1.241 + response += "u:" + gTables[table][i] + "\n"; 1.242 + } 1.243 + } 1.244 + do_print("Returning update response: " + response); 1.245 + return response; 1.246 +} 1.247 + 1.248 +// Set up the local whitelist. 1.249 +function waitForUpdates() { 1.250 + let deferred = Promise.defer(); 1.251 + gHttpServer.registerPathHandler("/downloads", function(request, response) { 1.252 + let buf = NetUtil.readInputStreamToString(request.bodyInputStream, 1.253 + request.bodyInputStream.available()); 1.254 + let blob = processUpdateRequest(); 1.255 + response.setHeader("Content-Type", 1.256 + "application/vnd.google.safebrowsing-update", false); 1.257 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.258 + response.bodyOutputStream.write(blob, blob.length); 1.259 + }); 1.260 + 1.261 + let streamUpdater = Cc["@mozilla.org/url-classifier/streamupdater;1"] 1.262 + .getService(Ci.nsIUrlClassifierStreamUpdater); 1.263 + streamUpdater.updateUrl = "http://localhost:4444/downloads"; 1.264 + 1.265 + // Load up some update chunks for the safebrowsing server to serve. This 1.266 + // particular chunk contains the hash of whitelisted.com/ and 1.267 + // sb-ssl.google.com/safebrowsing/csd/certificate/. 1.268 + registerTableUpdate("goog-downloadwhite-digest256", "data/digest.chunk"); 1.269 + 1.270 + // Resolve the promise once processing the updates is complete. 1.271 + function updateSuccess(aEvent) { 1.272 + // Timeout of n:1000 is constructed in processUpdateRequest above and 1.273 + // passed back in the callback in nsIUrlClassifierStreamUpdater on success. 1.274 + do_check_eq("1000", aEvent); 1.275 + do_print("All data processed"); 1.276 + deferred.resolve(true); 1.277 + } 1.278 + // Just throw if we ever get an update or download error. 1.279 + function handleError(aEvent) { 1.280 + do_throw("We didn't download or update correctly: " + aEvent); 1.281 + deferred.reject(); 1.282 + } 1.283 + streamUpdater.downloadUpdates( 1.284 + "goog-downloadwhite-digest256", 1.285 + "goog-downloadwhite-digest256;\n", 1.286 + updateSuccess, handleError, handleError); 1.287 + return deferred.promise; 1.288 +} 1.289 + 1.290 +function promiseQueryReputation(query, expectedShouldBlock) { 1.291 + let deferred = Promise.defer(); 1.292 + function onComplete(aShouldBlock, aStatus) { 1.293 + do_check_eq(Cr.NS_OK, aStatus); 1.294 + do_check_eq(aShouldBlock, expectedShouldBlock); 1.295 + deferred.resolve(true); 1.296 + } 1.297 + gAppRep.queryReputation(query, onComplete); 1.298 + return deferred.promise; 1.299 +} 1.300 + 1.301 +add_task(function() 1.302 +{ 1.303 + // Wait for Safebrowsing local list updates to complete. 1.304 + yield waitForUpdates(); 1.305 +}); 1.306 + 1.307 +add_task(function test_signature_whitelists() 1.308 +{ 1.309 + // We should never get to the remote server. 1.310 + Services.prefs.setCharPref("browser.safebrowsing.appRepURL", 1.311 + "http://localhost:4444/throw"); 1.312 + 1.313 + // Use BackgroundFileSaver to extract the signature on Windows. 1.314 + let destFile = getTempFile(TEST_FILE_NAME_1); 1.315 + 1.316 + let data = readFileToString("data/signed_win.exe"); 1.317 + let saver = new BackgroundFileSaverOutputStream(); 1.318 + let completionPromise = promiseSaverComplete(saver); 1.319 + saver.enableSignatureInfo(); 1.320 + saver.setTarget(destFile, false); 1.321 + yield promiseCopyToSaver(data, saver, true); 1.322 + 1.323 + saver.finish(Cr.NS_OK); 1.324 + yield completionPromise; 1.325 + 1.326 + // Clean up. 1.327 + destFile.remove(false); 1.328 + 1.329 + // evil.com is not on the allowlist, but this binary is signed by an entity 1.330 + // whose certificate information is on the allowlist. 1.331 + yield promiseQueryReputation({sourceURI: createURI("http://evil.com"), 1.332 + signatureInfo: saver.signatureInfo, 1.333 + fileSize: 12}, false); 1.334 +}); 1.335 + 1.336 +add_task(function test_blocked_binary() 1.337 +{ 1.338 + // We should reach the remote server for a verdict. 1.339 + Services.prefs.setCharPref("browser.safebrowsing.appRepURL", 1.340 + "http://localhost:4444/download"); 1.341 + // evil.com should return a malware verdict from the remote server. 1.342 + yield promiseQueryReputation({sourceURI: createURI("http://evil.com"), 1.343 + suggestedFileName: "noop.bat", 1.344 + fileSize: 12}, true); 1.345 +}); 1.346 + 1.347 +add_task(function test_non_binary() 1.348 +{ 1.349 + // We should not reach the remote server for a verdict for non-binary files. 1.350 + Services.prefs.setCharPref("browser.safebrowsing.appRepURL", 1.351 + "http://localhost:4444/throw"); 1.352 + yield promiseQueryReputation({sourceURI: createURI("http://evil.com"), 1.353 + suggestedFileName: "noop.txt", 1.354 + fileSize: 12}, false); 1.355 +}); 1.356 + 1.357 +add_task(function test_good_binary() 1.358 +{ 1.359 + // We should reach the remote server for a verdict. 1.360 + Services.prefs.setCharPref("browser.safebrowsing.appRepURL", 1.361 + "http://localhost:4444/download"); 1.362 + // mozilla.com should return a not-guilty verdict from the remote server. 1.363 + yield promiseQueryReputation({sourceURI: createURI("http://mozilla.com"), 1.364 + suggestedFileName: "noop.bat", 1.365 + fileSize: 12}, false); 1.366 +}); 1.367 + 1.368 +add_task(function test_teardown() 1.369 +{ 1.370 + gStillRunning = false; 1.371 +});