Bug 1226928 - content-signature verification tests for about:newtab, r=mconley draft
authorFranziskus Kiefer <franziskuskiefer@gmail.com>
Sat, 21 Nov 2015 23:25:29 -0800
changeset 340473 24208e26acb43c571f30064aec6e63574044c004
parent 340472 8bc3e4e14428f7502c19409749e2f7470a073d23
child 340474 446b32c11e3d2363ff9fec9f275f375bff261437
push id12972
push userjhao@mozilla.com
push dateTue, 15 Mar 2016 11:01:23 +0000
reviewersmconley
bugs1226928
milestone48.0a1
Bug 1226928 - content-signature verification tests for about:newtab, r=mconley MozReview-Commit-ID: 2lBiVezXQXl
dom/security/test/contentverifier/browser.ini
dom/security/test/contentverifier/browser_verify_content_about_newtab.js
dom/security/test/contentverifier/file_about_newtab.html
dom/security/test/contentverifier/file_about_newtab_bad.html
dom/security/test/contentverifier/file_about_newtab_bad_signature
dom/security/test/contentverifier/file_about_newtab_broken_signature
dom/security/test/contentverifier/file_about_newtab_good_signature
dom/security/test/contentverifier/file_contentserver.sjs
dom/security/test/contentverifier/signature.der
dom/security/test/contentverifier/sk.pem
dom/security/test/moz.build
new file mode 100644
--- /dev/null
+++ b/dom/security/test/contentverifier/browser.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+support-files =
+  file_contentserver.sjs
+  file_about_newtab.html
+  file_about_newtab_bad.html
+  file_about_newtab_good_signature
+  file_about_newtab_bad_signature
+  file_about_newtab_broken_signature
+
+[browser_verify_content_about_newtab.js]
new file mode 100644
--- /dev/null
+++ b/dom/security/test/contentverifier/browser_verify_content_about_newtab.js
@@ -0,0 +1,188 @@
+/*
+ * Test Content-Signature for remote about:newtab
+ *  - Bug 1226928 - allow about:newtab to load remote content
+ *
+ * This tests content-signature verification on remote about:newtab in the
+ * following cases (see TESTS, all failed loads display about:blank fallback):
+ * - good case (signature should verify and correct page is displayed)
+ * - reload of newtab when the siganture was invalidated after the last correct
+ *   load
+ * - malformed content-signature header
+ * - malformed keyid directive
+ * - malformed p384ecdsa directive
+ * - wrong signature (this is not a siganture for the delivered document)
+ * - invalid signature (this is not even a signature)
+ * - loading a file that doesn't fit the key or signature
+ * - cache poisoning (load a malicious remote page not in newtab, subsequent
+ *   newtab load has to load the fallback)
+ */
+
+const ABOUT_NEWTAB_URI = "about:newtab";
+
+const BASE = "https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?";
+const URI_GOOD = BASE + "sig=good&key=good&file=good&header=good";
+
+const INVALIDATE_FILE = BASE + "invalidateFile=yep";
+const VALIDATE_FILE = BASE + "validateFile=yep";
+
+const URI_HEADER_BASE = BASE + "sig=good&key=good&file=good&header=";
+const URI_ERROR_HEADER = URI_HEADER_BASE + "error";
+const URI_KEYERROR_HEADER = URI_HEADER_BASE + "errorInKeyid";
+const URI_SIGERROR_HEADER = URI_HEADER_BASE + "errorInSignature";
+const URI_NO_HEADER = URI_HEADER_BASE + "noHeader";
+
+const URI_BAD_SIG = BASE + "sig=bad&key=good&file=good&header=good";
+const URI_BROKEN_SIG = BASE + "sig=broken&key=good&file=good&header=good";
+const URI_BAD_KEY = BASE + "sig=good&key=bad&file=good&header=good";
+const URI_BAD_FILE = BASE + "sig=good&key=good&file=bad&header=good";
+const URI_BAD_ALL = BASE + "sig=bad&key=bad&file=bad&header=bad";
+
+const URI_BAD_FILE_CACHED = BASE + "sig=good&key=good&file=bad&header=good&cached=true";
+
+const GOOD_ABOUT_STRING = "Just a fully good testpage for Bug 1226928";
+const BAD_ABOUT_STRING = "Just a bad testpage for Bug 1226928";
+const ABOUT_BLANK = "<head></head><body></body>";
+
+const TESTS = [
+  // { newtab (aboutURI) or regular load (url) : url,
+  //   testString : expected string in the loaded page }
+  { "aboutURI" : URI_GOOD, "testString" : GOOD_ABOUT_STRING },
+  { "aboutURI" : URI_ERROR_HEADER, "testString" : ABOUT_BLANK },
+  { "aboutURI" : URI_KEYERROR_HEADER, "testString" : ABOUT_BLANK },
+  { "aboutURI" : URI_SIGERROR_HEADER, "testString" : ABOUT_BLANK },
+  { "aboutURI" : URI_NO_HEADER, "testString" : ABOUT_BLANK },
+  { "aboutURI" : URI_BAD_SIG, "testString" : ABOUT_BLANK },
+  { "aboutURI" : URI_BROKEN_SIG, "testString" : ABOUT_BLANK },
+  { "aboutURI" : URI_BAD_KEY, "testString" : ABOUT_BLANK },
+  { "aboutURI" : URI_BAD_FILE, "testString" : ABOUT_BLANK },
+  { "aboutURI" : URI_BAD_ALL, "testString" : ABOUT_BLANK },
+  { "url" : URI_BAD_FILE_CACHED, "testString" : BAD_ABOUT_STRING },
+  { "aboutURI" : URI_BAD_FILE_CACHED, "testString" : ABOUT_BLANK },
+  { "aboutURI" : URI_GOOD, "testString" : GOOD_ABOUT_STRING }
+];
+
+var browser = null;
+var aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"]
+                           .getService(Ci.nsIAboutNewTabService);
+
+function pushPrefs(...aPrefs) {
+  return new Promise((resolve) => {
+    SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve);
+  });
+}
+
+/*
+ * run tests with input from TESTS
+ */
+function doTest(aExpectedString, reload, aUrl, aNewTabPref) {
+  // set about:newtab location for this test if it's a newtab test
+  if (aNewTabPref) {
+    aboutNewTabService.newTabURL = aNewTabPref;
+  }
+
+  // set prefs
+  yield pushPrefs(
+      ["browser.newtabpage.remote.content-signing-test", true],
+      ["browser.newtabpage.remote", true], [
+        "browser.newtabpage.remote.keys",
+        "RemoteNewTabNightlyv0=BO9QHuP6E2eLKybql8iuD4o4Np9YFDfW3D+k" +
+        "a70EcXXTqZcikc7Am1CwyP1xBDTpEoe6gb9SWzJmaDW3dNh1av2u90VkUM" +
+        "B7aHIrImjTjLNg/1oC8GRcTKM4+WzbKF00iA==;OtherKey=eKQJ2fNSId" +
+        "CFzL6N326EzZ/5LCeFU5eyq3enwZ5MLmvOw+3gycr4ZVRc36/EiSPsQYHE" +
+        "3JvJs1EKs0QCaguHFOZsHwqXMPicwp/gLdeYbuOmN2s1SEf/cxw8GtcxSA" +
+        "kG;RemoteNewTab=MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4k3FmG7dFo" +
+        "Ot3Tuzl76abTRtK8sb/r/ibCSeVKa96RbrOX2ciscz/TT8wfqBYS/8cN4z" +
+        "Me1+f7wRmkNrCUojZR1ZKmYM2BeiUOMlMoqk2O7+uwsn1DwNQSYP58TkvZt6"
+      ]);
+
+  // start the test
+  yield BrowserTestUtils.withNewTab({
+      gBrowser,
+      url: aUrl,
+    },
+    function * (browser) {
+      // check if everything's set correct for testing
+      ok(Services.prefs.getBoolPref(
+          "browser.newtabpage.remote.content-signing-test"),
+          "sanity check: remote newtab signing test should be used");
+      ok(Services.prefs.getBoolPref("browser.newtabpage.remote"),
+          "sanity check: remote newtab should be used");
+      // we only check this if we really do a newtab test
+      if (aNewTabPref) {
+        ok(aboutNewTabService.overridden,
+            "sanity check: default URL for about:newtab should be overriden");
+        is(aboutNewTabService.newTabURL, aNewTabPref,
+            "sanity check: default URL for about:newtab should return the new URL");
+      }
+      yield ContentTask.spawn(
+          browser, aExpectedString, function * (aExpectedString) {
+            ok(content.document.documentElement.innerHTML.includes(aExpectedString),
+               "Expect the following value in the result\n" + aExpectedString +
+               "\nand got " + content.document.documentElement.innerHTML);
+          });
+
+      // for good test cases we check if a reload fails if the remote page
+      // changed from valid to invalid in the meantime
+      if (reload) {
+        yield BrowserTestUtils.withNewTab({
+            gBrowser,
+            url: INVALIDATE_FILE,
+          },
+          function * (browser2) {
+            yield ContentTask.spawn(browser2, null, function * () {
+              ok(content.document.documentElement.innerHTML.includes("Done"),
+                 "Expect the following value in the result\n" + "Done" +
+                 "\nand got " + content.document.documentElement.innerHTML);
+            });
+          }
+        );
+
+        browser.reload();
+        yield BrowserTestUtils.browserLoaded(browser);
+
+        aExpectedString = ABOUT_BLANK;
+        yield ContentTask.spawn(browser, aExpectedString,
+          function * (aExpectedString) {
+            ok(content.document.documentElement.innerHTML.includes(aExpectedString),
+               "Expect the following value in the result\n" + aExpectedString +
+               "\nand got " + content.document.documentElement.innerHTML);
+          }
+        );
+
+        yield BrowserTestUtils.withNewTab({
+            gBrowser,
+            url: VALIDATE_FILE,
+          },
+          function * (browser2) {
+            yield ContentTask.spawn(browser2, null, function * () {
+              ok(content.document.documentElement.innerHTML.includes("Done"),
+                 "Expect the following value in the result\n" + "Done" +
+                 "\nand got " + content.document.documentElement.innerHTML);
+              });
+          }
+        );
+      }
+    }
+  );
+}
+
+add_task(function * test() {
+  // run tests from TESTS
+  for (let i = 0; i < TESTS.length; i++) {
+    let testCase = TESTS[i];
+    let url = "", aNewTabPref = "";
+    let reload = false;
+    let aExpectedString = testCase.testString;
+    if (testCase.aboutURI) {
+      url = ABOUT_NEWTAB_URI;
+      aNewTabPref = testCase.aboutURI;
+      if (aExpectedString == GOOD_ABOUT_STRING) {
+        reload = true;
+      }
+    } else {
+      url = testCase.url;
+    }
+
+    yield doTest(aExpectedString, reload, url, aNewTabPref);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/dom/security/test/contentverifier/file_about_newtab.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1226928 -->
+<head>
+  <meta charset="utf-8">
+  <title>Testpage for bug 1226928</title>
+</head>
+<body>
+  Just a fully good testpage for Bug 1226928<br/>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/security/test/contentverifier/file_about_newtab_bad.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1226928 -->
+<head>
+  <meta charset="utf-8">
+  <title>Testpage for bug 1226928</title>
+</head>
+<body>
+  Just a bad testpage for Bug 1226928<br/>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/security/test/contentverifier/file_about_newtab_bad_signature
@@ -0,0 +1,1 @@
+KirX94omQL7lKfWGhc777t8U29enDg0O0UcJLH3PRXcvWGO8KA6mmLS3yNCFnGiTjP3vNnVtm-sUkXr4ix8WTkKABkU4fEAi77sNOkLCKw40M9sDJOesmYInS_J2AuXX
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/security/test/contentverifier/file_about_newtab_broken_signature
@@ -0,0 +1,1 @@
+MGUCMFwSs3o95ukwBWXN1WbLgnpJ_uHWFiQROPm9zjrSqzlfiSMyLwJwIZzldWo_pBJtOwIxAJIfhXIiMVfl5NkFEJUUMxzu6FuxOJl5DCpG2wHLy9AhayLUzm4X4SpwZ6QBPapdTg
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/security/test/contentverifier/file_about_newtab_good_signature
@@ -0,0 +1,1 @@
+XBKzej3i6TAFZc3VZsuCekn-4dYWJBE4-b3OOtKrOV-JIzIvAnAhnOV1aj-kEm07kh-FciIxV-Xk2QUQlRQzHO7oW7E4mXkMKkbbAcvL0CFrItTObhfhKnBnpAE9ql1O
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/security/test/contentverifier/file_contentserver.sjs
@@ -0,0 +1,179 @@
+// sjs for remote about:newtab (bug 1226928)
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.importGlobalProperties(["URLSearchParams"]);
+
+const path = "browser/dom/security/test/contentverifier/";
+
+const goodFileName = "file_about_newtab.html";
+const goodFileBase = path + goodFileName;
+const goodFile = FileUtils.getDir("TmpD", [], true);
+goodFile.append(goodFileName);
+const goodSignature = path + "file_about_newtab_good_signature";
+const goodKeyId = "RemoteNewTab";
+
+const badFile = path + "file_about_newtab_bad.html";
+const brokenSignature = path + "file_about_newtab_broken_signature";
+const badSignature = path + "file_about_newtab_bad_signature";
+const badKeyId = "OldRemoteNewTabKey";
+
+// we copy the file to serve as newtab to a temp directory because
+// we modify it during tests.
+setupTestFile();
+
+function setupTestFile() {
+  let tempFile = FileUtils.getDir("TmpD", [], true);
+  tempFile.append(goodFileName);
+  if (!tempFile.exists()) {
+    let fileIn = getFileName(goodFileBase, "CurWorkD");
+    fileIn.copyTo(FileUtils.getDir("TmpD", [], true), "");
+  }
+}
+
+function getFileName(filePath, dir) {
+  // Since it's relative to the cwd of the test runner, we start there and
+  // append to get to the actual path of the file.
+  let testFile =
+    Cc["@mozilla.org/file/directory_service;1"].
+      getService(Components.interfaces.nsIProperties).
+      get(dir, Components.interfaces.nsILocalFile);
+  let dirs = filePath.split("/");
+  for (let i = 0; i < dirs.length; i++) {
+    testFile.append(dirs[i]);
+  }
+  return testFile;
+}
+
+function loadFile(file) {
+  // Load a file to return it.
+  let testFileStream =
+    Cc["@mozilla.org/network/file-input-stream;1"]
+      .createInstance(Components.interfaces.nsIFileInputStream);
+  testFileStream.init(file, -1, 0, 0);
+  return NetUtil.readInputStreamToString(testFileStream,
+                                         testFileStream.available());
+}
+
+function appendToFile(aFile, content) {
+  try {
+    let file = FileUtils.openFileOutputStream(aFile, FileUtils.MODE_APPEND |
+                                                     FileUtils.MODE_WRONLY);
+    file.write(content, content.length);
+    file.close();
+  } catch (e) {
+    dump(">>> Error in appendToFile "+e);
+    return "Error";
+  }
+  return "Done";
+}
+
+function truncateFile(aFile, length) {
+  let fileIn = loadFile(aFile);
+  fileIn = fileIn.slice(0, -length);
+
+  try {
+    let file = FileUtils.openFileOutputStream(aFile, FileUtils.MODE_WRONLY |
+                                                     FileUtils.MODE_TRUNCATE);
+    file.write(fileIn, fileIn.length);
+    file.close();
+  } catch (e) {
+    dump(">>> Error in truncateFile "+e);
+    return "Error";
+  }
+  return "Done";
+}
+
+/*
+ * handle requests of the following form:
+ * sig=good&key=good&file=good&header=good&cached=no to serve pages with
+ * content signatures
+ *
+ * it further handles invalidateFile=yep and validateFile=yep to change the
+ * served file
+ */
+function handleRequest(request, response) {
+  let params = new URLSearchParams(request.queryString);
+  let keyType = params.get("key");
+  let signatureType = params.get("sig");
+  let fileType = params.get("file");
+  let headerType = params.get("header");
+  let cached = params.get("cached");
+  let invalidateFile = params.get("invalidateFile");
+  let validateFile = params.get("validateFile");
+
+  // if invalidateFile is set, this doesn't actually return a newtab page
+  // but changes the served file to invalidate the signature
+  // NOTE: make sure to make the file valid again afterwards!
+  if (invalidateFile) {
+    response.setHeader("Content-Type", "text/html", false);
+    let r = appendToFile(goodFile, "!");
+    response.write(r);
+    return;
+  }
+
+  // if validateFile is set, this doesn't actually return a newtab page
+  // but changes the served file to make the signature valid again
+  if (validateFile) {
+    response.setHeader("Content-Type", "text/html", false);
+    let r = truncateFile(goodFile, 1);
+    response.write(r);
+    return;
+  }
+
+  // avoid confusing cache behaviours
+  if (!cached) {
+    response.setHeader("Cache-Control", "no-cache", false);
+  } else {
+    response.setHeader("Cache-Control", "max-age=3600", false);
+  }
+
+  // send HTML to test allowed/blocked behaviours
+  response.setHeader("Content-Type", "text/html", false);
+
+  // set signature header and key for Content-Signature header
+  /* By default a good content-signature header is returned. Any broken return
+   * value has to be indicated in the url.
+   */
+  let csHeader = "";
+  let keyId = goodKeyId;
+  let signature = goodSignature;
+  let file = goodFile;
+  if (keyType == "bad") {
+    keyId = badKeyId;
+  }
+  if (signatureType == "bad") {
+    signature = badSignature;
+  } else if (signatureType == "broken") {
+    signature = brokenSignature;
+  }
+  if (fileType == "bad") {
+    file = getFileName(badFile, "CurWorkD");
+  }
+
+  if (headerType == "good") {
+    // a valid content-signature header
+    csHeader = "keyid=" + keyId + ";p384ecdsa=" +
+               loadFile(getFileName(signature, "CurWorkD"));
+  } else if (headerType == "error") {
+    // this content-signature header is missing ; before p384ecdsa
+    csHeader = "keyid=" + keyId + "p384ecdsa=" +
+               loadFile(getFileName(signature, "CurWorkD"));
+  } else if (headerType == "errorInKeyid") {
+    // this content-signature header is missing the keyid directive
+    csHeader = "keid=" + keyId + ";p384ecdsa=" +
+               loadFile(getFileName(signature, "CurWorkD"));
+  } else if (headerType == "errorInSignature") {
+    // this content-signature header is missing the p384ecdsa directive
+    csHeader = "keyid=" + keyId + ";p385ecdsa=" +
+               loadFile(getFileName(signature, "CurWorkD"));
+  }
+
+  if (csHeader) {
+    response.setHeader("Content-Signature", csHeader, false);
+  }
+  let result = loadFile(file);
+
+  response.write(result);
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..011b94142c3d79406bbd812a154ade1531a5a002
GIT binary patch
literal 103
zc$@)e0GR(UWdbl<60>?e;^{C2WzE%Q%Yu4I{^8aZBoR3Iz0Nw)t2tkZBQh@na3P%K
zb!tDP5^XyIF#wVug>oV>SLNi{1rU`KGaT;dTd_Eqc?>E>+X2hV&>?Fg)Xr`f;VN)v
JqyasuT~785E71S|
new file mode 100644
--- /dev/null
+++ b/dom/security/test/contentverifier/sk.pem
@@ -0,0 +1,9 @@
+-----BEGIN EC PARAMETERS-----
+BgUrgQQAIg==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MIGkAgEBBDAzX2TrGOr0WE92AbAl+nqnpqh25pKCLYNMTV2hJHztrkVPWOp8w0mh
+scIodK8RMpagBwYFK4EEACKhZANiAATiTcWYbt0Wg63dO7OXvpptNG0ryxv+v+Js
+JJ5Upr3pFus5fZyKxzP9NPzB+oFhL/xw3jMx7X5/vBGaQ2sJSiNlHVkqZgzYF6JQ
+4yUyiqTY7v67CyfUPA1BJg/nxOS9m3o=
+-----END EC PRIVATE KEY-----
--- a/dom/security/test/moz.build
+++ b/dom/security/test/moz.build
@@ -19,10 +19,11 @@ MOCHITEST_MANIFESTS += [
     'sri/mochitest.ini',
 ]
 
 MOCHITEST_CHROME_MANIFESTS += [
     'csp/chrome.ini',
 ]
 
 BROWSER_CHROME_MANIFESTS += [
+    'contentverifier/browser.ini',
     'csp/browser.ini',
 ]