Bug 760307 - Preloaded strict-transport-security site list. r=mayhemer, bsmith
authorDavid Keeler <dkeeler@mozilla.com>
Fri, 24 Aug 2012 14:17:27 -0700
changeset 103413 ce222ba667f21636ef3ad54e0c1ad60816bba468
parent 103412 36f85f213ee28ce22a753c963d971408674cbfb1
child 103414 35fddfaef1b6d52763972f51fd79e674400f8947
push id13981
push userryanvm@gmail.com
push dateSat, 25 Aug 2012 19:48:18 +0000
treeherdermozilla-inbound@779bdf71cde5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmayhemer, bsmith
bugs760307
milestone17.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 760307 - Preloaded strict-transport-security site list. r=mayhemer, bsmith
security/manager/boot/src/nsSTSPreloadList.inc
security/manager/boot/src/nsStrictTransportSecurityService.cpp
security/manager/boot/src/nsStrictTransportSecurityService.h
security/manager/ssl/tests/unit/test_sts_preloadlist.js
security/manager/ssl/tests/unit/xpcshell.ini
security/manager/tools/getHSTSPreloadList.py
new file mode 100644
--- /dev/null
+++ b/security/manager/boot/src/nsSTSPreloadList.inc
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*****************************************************************************/
+/* This is an automatically generated file. If you're not                    */
+/* nsStrictTransportSecurityService.cpp, you shouldn't be #including it.     */
+/*****************************************************************************/
+
+#include <prtypes.h>
+
+class nsSTSPreload
+{
+  public:
+    const char *mHost;
+    const bool mIncludeSubdomains;
+};
+
+static const nsSTSPreload kSTSPreloadList[] = {
+  { "accounts.google.com", true },
+  { "aladdinschools.appspot.com", false },
+  { "alpha.irccloud.com", false },
+  { "api.recurly.com", true },
+  { "apis.google.com", true },
+  { "app.recurly.com", true },
+  { "appengine.google.com", false },
+  { "arivo.com.br", true },
+  { "betnet.fr", true },
+  { "bigshinylock.minazo.net", true },
+  { "blog.torproject.org", true },
+  { "braintreegateway.com", true },
+  { "braintreepayments.com", false },
+  { "browserid.org", true },
+  { "business.medbank.com.mt", true },
+  { "cert.se", true },
+  { "check.torproject.org", true },
+  { "checkout.google.com", true },
+  { "chrome.google.com", true },
+  { "chromiumcodereview.appspot.com", true },
+  { "cloudsecurityalliance.org", true },
+  { "codereview.appspot.com", true },
+  { "crate.io", true },
+  { "crypto.cat", true },
+  { "crypto.is", true },
+  { "developer.mydigipass.com", false },
+  { "docs.google.com", true },
+  { "download.jitsi.org", false },
+  { "drive.google.com", true },
+  { "dropcam.com", false },
+  { "ebanking.indovinabank.com.vn", true },
+  { "emailprivacytester.com", false },
+  { "encrypted.google.com", true },
+  { "entropia.de", false },
+  { "epoxate.com", false },
+  { "factor.cc", false },
+  { "gmail.com", false },
+  { "googlemail.com", false },
+  { "googleplex.com", true },
+  { "greplin.com", false },
+  { "grepular.com", true },
+  { "groups.google.com", true },
+  { "health.google.com", true },
+  { "hostedtalkgadget.google.com", true },
+  { "howrandom.org", true },
+  { "id.mayfirst.org", false },
+  { "irccloud.com", false },
+  { "jitsi.org", false },
+  { "jottit.com", true },
+  { "keyerror.com", true },
+  { "kyps.net", false },
+  { "lastpass.com", false },
+  { "ledgerscope.net", false },
+  { "linx.net", true },
+  { "lists.mayfirst.org", false },
+  { "logentries.com", false },
+  { "login.persona.org", true },
+  { "login.sapo.pt", true },
+  { "luneta.nearbuysystems.com", true },
+  { "mail.google.com", true },
+  { "market.android.com", true },
+  { "mattmccutchen.net", true },
+  { "members.mayfirst.org", false },
+  { "mydigipass.com", false },
+  { "neg9.org", false },
+  { "neonisi.com", false },
+  { "ottospora.nl", true },
+  { "passwd.io", true },
+  { "piratenlogin.de", true },
+  { "pixi.me", true },
+  { "plus.google.com", true },
+  { "profiles.google.com", true },
+  { "riseup.net", true },
+  { "romab.com", true },
+  { "sandbox.mydigipass.com", false },
+  { "script.google.com", true },
+  { "shops.neonisi.com", true },
+  { "simon.butcher.name", true },
+  { "sites.google.com", true },
+  { "sol.io", true },
+  { "spreadsheets.google.com", true },
+  { "squareup.com", false },
+  { "ssl.google-analytics.com", true },
+  { "stripe.com", true },
+  { "sunshinepress.org", true },
+  { "support.mayfirst.org", false },
+  { "talk.google.com", true },
+  { "talkgadget.google.com", true },
+  { "torproject.org", false },
+  { "ubertt.org", true },
+  { "uprotect.it", true },
+  { "www.apollo-auto.com", true },
+  { "www.braintreepayments.com", false },
+  { "www.cueup.com", true },
+  { "www.developer.mydigipass.com", false },
+  { "www.dropcam.com", false },
+  { "www.elanex.biz", false },
+  { "www.entropia.de", false },
+  { "www.gmail.com", false },
+  { "www.googlemail.com", false },
+  { "www.greplin.com", false },
+  { "www.irccloud.com", false },
+  { "www.jitsi.org", false },
+  { "www.kyps.net", false },
+  { "www.lastpass.com", false },
+  { "www.ledgerscope.net", false },
+  { "www.logentries.com", false },
+  { "www.moneybookers.com", true },
+  { "www.mydigipass.com", false },
+  { "www.neonisi.com", true },
+  { "www.noisebridge.net", false },
+  { "www.paycheckrecords.com", false },
+  { "www.paypal.com", false },
+  { "www.sandbox.mydigipass.com", false },
+  { "www.torproject.org", true },
+};
--- a/security/manager/boot/src/nsStrictTransportSecurityService.cpp
+++ b/security/manager/boot/src/nsStrictTransportSecurityService.cpp
@@ -12,64 +12,58 @@
 #include "nsISSLStatusProvider.h"
 #include "nsStrictTransportSecurityService.h"
 #include "nsIURI.h"
 #include "nsNetUtil.h"
 #include "nsThreadUtils.h"
 #include "nsStringGlue.h"
 #include "nsIScriptSecurityManager.h"
 
+// A note about the preload list:
+// When a site specifically disables sts by sending a header with
+// 'max-age: 0', we keep a "knockout" value that means "we have no information
+// regarding the sts state of this host" (any ancestor of "this host" can still
+// influence its sts status via include subdomains, however).
+// This prevents the preload list from overriding the site's current
+// desired sts status. Knockout values are indicated by permission values of
+// STS_KNOCKOUT.
+#include "nsSTSPreloadList.inc"
+
+#define STS_SET (nsIPermissionManager::ALLOW_ACTION)
+#define STS_UNSET (nsIPermissionManager::UNKNOWN_ACTION)
+#define STS_KNOCKOUT (nsIPermissionManager::DENY_ACTION)
+
 #if defined(PR_LOGGING)
 PRLogModuleInfo *gSTSLog = PR_NewLogModule("nsSTSService");
 #endif
 
 #define STSLOG(args) PR_LOG(gSTSLog, 4, args)
 
 #define STS_PARSER_FAIL_IF(test,args) \
   if (test) { \
     STSLOG(args); \
     return NS_ERROR_FAILURE; \
   }
 
-namespace {
-
-/**
- * Returns a principal (aPrincipal) corresponding to aURI.
- * This is used to interact with the permission manager.
- */
-nsresult
-GetPrincipalForURI(nsIURI* aURI, nsIPrincipal** aPrincipal)
-{
-   // The permission manager wants a principal but don't actually check a
-   // permission but a data we saved in the permission manager so we are good by
-   // creating a no-app codebase principal and send it to the permission manager.
-   nsresult rv;
-   nsCOMPtr<nsIScriptSecurityManager> securityManager =
-      do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv);
-   NS_ENSURE_SUCCESS(rv, rv);
-
-   return securityManager->GetNoAppCodebasePrincipal(aURI, aPrincipal);
-}
-
-} // anonymous namespace
-
 ////////////////////////////////////////////////////////////////////////////////
 
 nsSTSHostEntry::nsSTSHostEntry(const char* aHost)
   : mHost(aHost)
   , mExpireTime(0)
-  , mDeleted(false)
+  , mExpired(false)
+  , mStsPermission(STS_UNSET)
   , mIncludeSubdomains(false)
 {
 }
 
 nsSTSHostEntry::nsSTSHostEntry(const nsSTSHostEntry& toCopy)
   : mHost(toCopy.mHost)
   , mExpireTime(toCopy.mExpireTime)
-  , mDeleted(toCopy.mDeleted)
+  , mExpired(toCopy.mExpired)
+  , mStsPermission(toCopy.mStsPermission)
   , mIncludeSubdomains(toCopy.mIncludeSubdomains)
 {
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 
 
 nsStrictTransportSecurityService::nsStrictTransportSecurityService()
@@ -119,44 +113,68 @@ nsStrictTransportSecurityService::GetHos
 
   if (NS_FAILED(rv) || aResult.IsEmpty())
     return NS_ERROR_UNEXPECTED;
 
   return NS_OK;
 }
 
 nsresult
+nsStrictTransportSecurityService::GetPrincipalForURI(nsIURI* aURI,
+                                                     nsIPrincipal** aPrincipal)
+{
+  nsresult rv;
+  nsCOMPtr<nsIScriptSecurityManager> securityManager =
+     do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // We have to normalize the scheme of the URIs we're using, so just use https.
+  // HSTS information is shared across all ports for a given host.
+  nsCAutoString host;
+  rv = GetHost(aURI, host);
+  NS_ENSURE_SUCCESS(rv, rv);
+  nsCOMPtr<nsIURI> uri;
+  rv = NS_NewURI(getter_AddRefs(uri), NS_LITERAL_CSTRING("https://") + host);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // We want all apps to share HSTS state, so this is one of the few places
+  // where we do not silo persistent state by extended origin.
+  return securityManager->GetNoAppCodebasePrincipal(uri, aPrincipal);
+}
+
+nsresult
 nsStrictTransportSecurityService::SetStsState(nsIURI* aSourceURI,
                                               int64_t maxage,
                                               bool includeSubdomains)
 {
   // If max-age is zero, that's an indication to immediately remove the
   // permissions, so here's a shortcut.
   if (!maxage)
     return RemoveStsState(aSourceURI);
 
   // Expire time is millis from now.  Since STS max-age is in seconds, and
   // PR_Now() is in micros, must equalize the units at milliseconds.
-  int64_t expiretime = (PR_Now() / 1000) + (maxage * 1000);
+  int64_t expiretime = (PR_Now() / PR_USEC_PER_MSEC) +
+                       (maxage * PR_MSEC_PER_SEC);
 
   // record entry for this host with max-age in the permissions manager
   STSLOG(("STS: maxage permission SET, adding permission\n"));
   nsresult rv = AddPermission(aSourceURI,
                               STS_PERMISSION,
-                              (uint32_t) nsIPermissionManager::ALLOW_ACTION,
+                              (uint32_t) STS_SET,
                               (uint32_t) nsIPermissionManager::EXPIRE_TIME,
                               expiretime);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (includeSubdomains) {
     // record entry for this host with include subdomains in the permissions manager
     STSLOG(("STS: subdomains permission SET, adding permission\n"));
     rv = AddPermission(aSourceURI,
                        STS_SUBDOMAIN_PERMISSION,
-                       (uint32_t) nsIPermissionManager::ALLOW_ACTION,
+                       (uint32_t) STS_SET,
                        (uint32_t) nsIPermissionManager::EXPIRE_TIME,
                        expiretime);
     NS_ENSURE_SUCCESS(rv, rv);
   } else { // !includeSubdomains
     nsCAutoString hostname;
     rv = GetHost(aSourceURI, hostname);
     NS_ENSURE_SUCCESS(rv, rv);
 
@@ -314,36 +332,171 @@ nsStrictTransportSecurityService::IsStsH
   nsCOMPtr<nsIURI> uri;
   nsDependentCString hostString(aHost);
   nsresult rv = NS_NewURI(getter_AddRefs(uri),
                           NS_LITERAL_CSTRING("https://") + hostString);
   NS_ENSURE_SUCCESS(rv, rv);
   return IsStsURI(uri, aResult);
 }
 
+int STSPreloadCompare(const void *key, const void *entry)
+{
+  const char *keyStr = (const char *)key;
+  const nsSTSPreload *preloadEntry = (const nsSTSPreload *)entry;
+  return strcmp(keyStr, preloadEntry->mHost);
+}
+
+// Returns the preload list entry for the given host, if it exists.
+// Only does exact host matching - the user must decide how to use the returned
+// data. May return null.
+const nsSTSPreload *
+nsStrictTransportSecurityService::GetPreloadListEntry(const char *aHost)
+{
+  return (const nsSTSPreload *) bsearch(aHost,
+                                        kSTSPreloadList,
+                                        PR_ARRAY_SIZE(kSTSPreloadList),
+                                        sizeof(nsSTSPreload),
+                                        STSPreloadCompare);
+}
+
 NS_IMETHODIMP
 nsStrictTransportSecurityService::IsStsURI(nsIURI* aURI, bool* aResult)
 {
   // Should be called on the main thread (or via proxy) since the permission
   // manager is used and it's not threadsafe.
   NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED);
 
-  nsresult rv;
-  uint32_t permExact, permGeneral;
-  // If this domain has the forcehttps permission, this is an STS host.
-  rv = TestPermission(aURI, STS_PERMISSION, &permExact, true);
+  // set default in case if we can't find any STS information
+  *aResult = false;
+
+  nsCAutoString host;
+  nsresult rv = GetHost(aURI, host);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  const nsSTSPreload *preload = nullptr;
+  nsSTSHostEntry *pbEntry = nullptr;
+
+  if (mInPrivateMode) {
+    pbEntry = mPrivateModeHostTable.GetEntry(host.get());
+  }
+
+  nsCOMPtr<nsIPrincipal> principal;
+  rv = GetPrincipalForURI(aURI, getter_AddRefs(principal));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  PRUint32 permMgrPermission;
+  rv = mPermMgr->TestExactPermissionFromPrincipal(principal, STS_PERMISSION,
+                                                  &permMgrPermission);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // If any super-domain has the includeSubdomains permission, this is an
-  // STS host.
-  rv = TestPermission(aURI, STS_SUBDOMAIN_PERMISSION, &permGeneral, false);
-  NS_ENSURE_SUCCESS(rv, rv);
+  // First check the exact host. This involves first checking for an entry in
+  // the private browsing table. If that entry exists, we don't want to check
+  // in either the permission manager or the preload list. We only want to use
+  // the stored permission if it is not a knockout entry, however.
+  // Additionally, if it is a knockout entry, we want to stop looking for data
+  // on the host, because the knockout entry indicates "we have no information
+  // regarding the sts status of this host".
+  if (pbEntry && pbEntry->mStsPermission != STS_UNSET) {
+    STSLOG(("Found private browsing table entry for %s", host.get()));
+    if (!pbEntry->IsExpired() && pbEntry->mStsPermission == STS_SET) {
+      *aResult = true;
+      return NS_OK;
+    }
+  }
+  // Next we look in the permission manager. Same story here regarding
+  // knockout entries.
+  else if (permMgrPermission != STS_UNSET) {
+    STSLOG(("Found permission manager entry for %s", host.get()));
+    if (permMgrPermission == STS_SET) {
+      *aResult = true;
+      return NS_OK;
+    }
+  }
+  // Finally look in the preloaded list. This is the exact host,
+  // so if an entry exists at all, this host is sts.
+  else if (GetPreloadListEntry(host.get())) {
+    STSLOG(("%s is a preloaded STS host", host.get()));
+    *aResult = true;
+    return NS_OK;
+  }
+
+  // Used for testing permissions as we walk up the domain tree.
+  nsCOMPtr<nsIURI> domainWalkURI;
+  nsCOMPtr<nsIPrincipal> domainWalkPrincipal;
+  const char *subdomain;
+
+  STSLOG(("no HSTS data for %s found, walking up domain", host.get()));
+  PRUint32 offset = 0;
+  for (offset = host.FindChar('.', offset) + 1;
+       offset > 0;
+       offset = host.FindChar('.', offset) + 1) {
+
+    subdomain = host.get() + offset;
+
+    // If we get an empty string, don't continue.
+    if (strlen(subdomain) < 1) {
+      break;
+    }
+
+    if (mInPrivateMode) {
+      pbEntry = mPrivateModeHostTable.GetEntry(subdomain);
+    }
 
-  *aResult = ((permExact   == nsIPermissionManager::ALLOW_ACTION) ||
-              (permGeneral == nsIPermissionManager::ALLOW_ACTION));
+    // normalize all URIs with https://
+    rv = NS_NewURI(getter_AddRefs(domainWalkURI),
+                   NS_LITERAL_CSTRING("https://") + Substring(host, offset));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = GetPrincipalForURI(domainWalkURI, getter_AddRefs(domainWalkPrincipal));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = mPermMgr->TestExactPermissionFromPrincipal(domainWalkPrincipal,
+                                                    STS_PERMISSION,
+                                                    &permMgrPermission);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Do the same thing as with the exact host, except now we're looking at
+    // ancestor domains of the original host. So, we have to look at the
+    // include subdomains permissions (although we still have to check for the
+    // STS_PERMISSION first to check that this is an sts host and not a
+    // knockout entry - and again, if it is a knockout entry, we stop looking
+    // for data on it and skip to the next higher up ancestor domain).
+    if (pbEntry && pbEntry->mStsPermission != STS_UNSET) {
+      STSLOG(("Found private browsing table entry for %s", subdomain));
+      if (!pbEntry->IsExpired() && pbEntry->mStsPermission == STS_SET) {
+        *aResult = pbEntry->mIncludeSubdomains;
+        break;
+      }
+    }
+    else if (permMgrPermission != STS_UNSET) {
+      STSLOG(("Found permission manager entry for %s", subdomain));
+      if (permMgrPermission == STS_SET) {
+        PRUint32 subdomainPermission;
+        rv = mPermMgr->TestExactPermissionFromPrincipal(domainWalkPrincipal,
+                                                        STS_SUBDOMAIN_PERMISSION,
+                                                        &subdomainPermission);
+        NS_ENSURE_SUCCESS(rv, rv);
+        *aResult = (subdomainPermission == STS_SET);
+        break;
+      }
+    }
+    // This is an ancestor, so if we get a match, we have to check if the
+    // preloaded entry includes subdomains.
+    else if ((preload = GetPreloadListEntry(subdomain)) != nullptr) {
+      if (preload->mIncludeSubdomains) {
+        STSLOG(("%s is a preloaded STS host", subdomain));
+        *aResult = true;
+        break;
+      }
+    }
+
+    STSLOG(("no HSTS data for %s found, walking up domain", subdomain));
+  }
+
+  // Use whatever we ended up with, which defaults to false.
   return NS_OK;
 }
 
 
 // Verify the trustworthiness of the security info (are there any cert errors?)
 NS_IMETHODIMP
 nsStrictTransportSecurityService::ShouldIgnoreStsHeader(nsISupports* aSecurityInfo,
                                                         bool* aResult)
@@ -424,205 +577,90 @@ nsStrictTransportSecurityService::AddPer
 
       return mPermMgr->AddFromPrincipal(principal, aType, aPermission,
                                         aExpireType, aExpireTime);
     }
 
     nsCAutoString host;
     nsresult rv = GetHost(aURI, host);
     NS_ENSURE_SUCCESS(rv, rv);
-    STSLOG(("AddPermission for entry for for %s", host.get()));
+    STSLOG(("AddPermission for entry for %s", host.get()));
 
     // Update in mPrivateModeHostTable only, so any changes will be rolled
     // back when exiting private mode.
 
     // Note: EXPIRE_NEVER permissions should trump anything that shows up in
     // the HTTP header, so if there's an EXPIRE_NEVER permission already
     // don't store anything new.
     // Currently there's no way to get the type of expiry out of the
     // permission manager, but that's okay since there's nothing that stores
     // EXPIRE_NEVER permissions.
 
     // PutEntry returns an existing entry if there already is one, or it
     // creates a new one if there isn't.
     nsSTSHostEntry* entry = mPrivateModeHostTable.PutEntry(host.get());
-    STSLOG(("Created private mode entry for for %s", host.get()));
+    if (!entry) {
+      return NS_ERROR_OUT_OF_MEMORY;
+    }
+    STSLOG(("Created private mode entry for %s", host.get()));
 
     // AddPermission() will be called twice if the STS header encountered has
     // includeSubdomains (first for the main permission and second for the
     // subdomains permission). If AddPermission() gets called a second time
     // with the STS_SUBDOMAIN_PERMISSION, we just have to flip that bit in
     // the nsSTSHostEntry.
     if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) {
       entry->mIncludeSubdomains = true;
     }
-    // for the case where PutEntry() returned an existing host entry, make
-    // sure it's not set as deleted (which might have happened in the past).
-    entry->mDeleted = false;
+    else if (strcmp(aType, STS_PERMISSION) == 0) {
+      entry->mStsPermission = aPermission;
+    }
 
     // Also refresh the expiration time.
-    entry->mExpireTime = aExpireTime;
+    entry->SetExpireTime(aExpireTime);
     return NS_OK;
-
 }
 
 nsresult
 nsStrictTransportSecurityService::RemovePermission(const nsCString  &aHost,
                                                    const char       *aType)
 {
     // Build up a principal for use with the permission manager.
+    // normalize all URIs with https://
     nsCOMPtr<nsIURI> uri;
     nsresult rv = NS_NewURI(getter_AddRefs(uri),
-                            NS_LITERAL_CSTRING("http://") + aHost);
+                            NS_LITERAL_CSTRING("https://") + aHost);
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsCOMPtr<nsIPrincipal> principal;
     rv = GetPrincipalForURI(uri, getter_AddRefs(principal));
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (!mInPrivateMode) {
       // Not in private mode: remove permissions persistently.
-      return mPermMgr->RemoveFromPrincipal(principal, aType);
+      // This means setting the permission to STS_KNOCKOUT in case
+      // this host is on the preload list (so we can override it).
+      return mPermMgr->AddFromPrincipal(principal, aType,
+                                        STS_KNOCKOUT,
+                                        nsIPermissionManager::EXPIRE_NEVER, 0);
     }
 
     // Make changes in mPrivateModeHostTable only, so any changes will be
     // rolled back when exiting private mode.
     nsSTSHostEntry* entry = mPrivateModeHostTable.GetEntry(aHost.get());
 
-    // Check to see if there's STS data stored for this host in the
-    // permission manager (probably set outside private mode).
-    uint32_t permmgrValue;
-    rv = mPermMgr->TestExactPermissionFromPrincipal(principal, aType,
-                                                    &permmgrValue);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    // If there is STS data in the permission manager, store a "deleted" mask
-    // for the permission in mPrivateModeHostTable (either update
-    // mPrivateModeHostTable to have the deleted mask, or add one).
-    // This is because we don't want removals that happen in private mode to
-    // be reflected when private mode is exited -- but while in private mode
-    // we still want the effect of the removal.
-    if (permmgrValue != nsIPermissionManager::UNKNOWN_ACTION) {
-      // if there's no entry in mPrivateModeHostTable, we have to make one.
+    if (!entry) {
+      entry = mPrivateModeHostTable.PutEntry(aHost.get());
       if (!entry) {
-        entry = mPrivateModeHostTable.PutEntry(aHost.get());
-        STSLOG(("Created private mode deleted mask for for %s", aHost.get()));
+        return NS_ERROR_OUT_OF_MEMORY;
       }
-      entry->mDeleted = true;
-      entry->mIncludeSubdomains = false;
-      return NS_OK;
-    }
-
-    // Otherwise, permission doesn't exist in the real permission manager, so
-    // there's nothing to "pretend" to delete.  I'ts ok to delete any copy in
-    // mPrivateModeHostTable.
-    if (entry) mPrivateModeHostTable.RawRemoveEntry(entry);
-    return NS_OK;
-}
-
-nsresult
-nsStrictTransportSecurityService::TestPermission(nsIURI     *aURI,
-                                                 const char *aType,
-                                                 uint32_t   *aPermission,
-                                                 bool       testExact)
-{
-    // set default for if we can't find any STS information
-    *aPermission = nsIPermissionManager::UNKNOWN_ACTION;
-
-    if (!mInPrivateMode) {
-      // if not in private mode, just delegate to the permission manager.
-      nsCOMPtr<nsIPrincipal> principal;
-      nsresult rv = GetPrincipalForURI(aURI, getter_AddRefs(principal));
-      NS_ENSURE_SUCCESS(rv, rv);
-
-      if (testExact)
-        return mPermMgr->TestExactPermissionFromPrincipal(principal, aType, aPermission);
-      else
-        return mPermMgr->TestPermissionFromPrincipal(principal, aType, aPermission);
+      STSLOG(("Created private mode deleted mask for %s", aHost.get()));
     }
 
-    nsCAutoString host;
-    nsresult rv = GetHost(aURI, host);
-    if (NS_FAILED(rv)) return NS_OK;
-
-    nsSTSHostEntry *entry;
-    uint32_t actualExactPermission;
-    uint32_t offset = 0;
-    int64_t now = PR_Now() / 1000;
-
-    // Used for testing permissions as we walk up the domain tree.
-    nsCOMPtr<nsIURI> domainWalkURI;
-
-    // In parallel, loop over private mode cache and also the real permission
-    // manager--ignoring any masked as "deleted" in the local cache. We have
-    // to do this here since the most specific permission in *either* the
-    // permission manager or mPrivateModeHostTable should be used.
-    do {
-      entry = mPrivateModeHostTable.GetEntry(host.get() + offset);
-      STSLOG(("Checking PM Table entry and permmgr for %s", host.get()+offset));
-
-      // flag as deleted any entries encountered that have expired.  We only
-      // flag the nsSTSHostEntry because there could be some data in the
-      // permission manager that -- if not in private mode -- would have been
-      // overwritten by newly encountered STS data.
-      if (entry && (now > entry->mExpireTime)) {
-        STSLOG(("Deleting expired PM Table entry for %s", host.get()+offset));
-        entry->mDeleted = true;
-        entry->mIncludeSubdomains = false;
-      }
-
-      rv = NS_NewURI(getter_AddRefs(domainWalkURI),
-                      NS_LITERAL_CSTRING("http://") + Substring(host, offset));
-      NS_ENSURE_SUCCESS(rv, rv);
-
-      nsCOMPtr<nsIPrincipal> principal;
-      nsresult rv = GetPrincipalForURI(domainWalkURI, getter_AddRefs(principal));
-      NS_ENSURE_SUCCESS(rv, rv);
-
-      rv = mPermMgr->TestExactPermissionFromPrincipal(principal, aType,
-                                                      &actualExactPermission);
-      NS_ENSURE_SUCCESS(rv, rv);
+    if (strcmp(aType, STS_PERMISSION) == 0) {
+      entry->mStsPermission = STS_KNOCKOUT;
+    }
+    else if (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0) {
+      entry->mIncludeSubdomains = false;
+    }
 
-      // There are three cases as we walk up the hostname testing
-      // permissions:
-      // 1. There's no entry in mPrivateModeHostTable for this host; rely
-      // on data in the permission manager
-      if (!entry) {
-        if (actualExactPermission != nsIPermissionManager::UNKNOWN_ACTION) {
-          // no cached data but a permission in the permission manager so use
-          // it and stop looking.
-          *aPermission = actualExactPermission;
-          STSLOG(("no PM Table entry for %s, using permmgr", host.get()+offset));
-          break;
-        }
-      }
-      // 2. There's a "deleted" mask in mPrivateModeHostTable for this host
-      // or we're looking for includeSubdomain information and it's not set:
-      // any data in the permission manager must be ignored, since the
-      // permission would have been deleted if not in private mode.
-      else if (entry->mDeleted || (strcmp(aType, STS_SUBDOMAIN_PERMISSION) == 0
-                                  && !entry->mIncludeSubdomains)) {
-        STSLOG(("no entry at all for %s, walking up", host.get()+offset));
-        // keep looking
-      }
-      // 3. There's a non-deleted entry in mPrivateModeHostTable for this
-      // host, so it should be used.
-      else {
-        // All STS permissions' values are ALLOW_ACTION or they are not
-        // known (as in, not set or turned off).
-        *aPermission = nsIPermissionManager::ALLOW_ACTION;
-        STSLOG(("PM Table entry for %s: forcing", host.get()+offset));
-        break;
-      }
-
-      // Don't continue walking up the host segments if the test was for an
-      // exact match only.
-      if (testExact) break;
-
-      STSLOG(("no PM Table entry or permmgr data for %s, walking up domain",
-              host.get()+offset));
-      // walk up the host segments
-      offset = host.FindChar('.', offset) + 1;
-    } while (offset > 0);
-
-    // Use whatever we ended up with, which defaults to UNKNOWN_ACTION.
     return NS_OK;
 }
--- a/security/manager/boot/src/nsStrictTransportSecurityService.h
+++ b/security/manager/boot/src/nsStrictTransportSecurityService.h
@@ -24,22 +24,25 @@
     {0x93, 0x09, 0xc4, 0x2a, 0x46, 0x51, 0x38, 0xa1} }
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsSTSHostEntry - similar to the nsHostEntry class in
 // nsPermissionManager.cpp, but specific to private-mode caching of STS
 // permissions.
 //
 // Each nsSTSHostEntry contains:
-//  - Expiry time
-//  - Deleted flag (boolean, default false)
-//  - Subdomains flag (boolean, default false)
+//  - Expiry time (PRTime, milliseconds)
+//  - Expired flag (bool, default false)
+//  - STS permission (uint32_t, default STS_UNSET)
+//  - Include subdomains flag (bool, default false)
+//
+// Note: the subdomains flag has no meaning if the STS permission is STS_UNSET.
 //
 // The existence of the nsSTSHostEntry implies STS state is set for the given
-// host -- unless the deleted flag is set, in which case not only is the STS
+// host -- unless the expired flag is set, in which case not only is the STS
 // state not set for the host, but any permission actually present in the
 // permission manager should be ignored.
 //
 // Note: Only one expiry time is stored since the subdomains and STS
 // permissions are both encountered at the same time in the HTTP header; if the
 // includeSubdomains directive isn't present in the header, it means to delete
 // the permission, so the subdomains flag in the nsSTSHostEntry means both that
 // the permission doesn't exist and any permission in the real permission
@@ -53,19 +56,20 @@
 
 class nsSTSHostEntry : public PLDHashEntryHdr
 {
   public:
     explicit nsSTSHostEntry(const char* aHost);
     explicit nsSTSHostEntry(const nsSTSHostEntry& toCopy);
 
     nsCString    mHost;
-    int64_t      mExpireTime;
-    bool mDeleted;
-    bool mIncludeSubdomains;
+    PRTime       mExpireTime;
+    uint32_t     mStsPermission;
+    bool         mExpired;
+    bool         mIncludeSubdomains;
 
     // Hash methods
     typedef const char* KeyType;
     typedef const char* KeyTypePointer;
 
     KeyType GetKey() const
     {
       return mHost.get();
@@ -81,50 +85,73 @@ class nsSTSHostEntry : public PLDHashEnt
       return aKey;
     }
 
     static PLDHashNumber HashKey(KeyTypePointer aKey)
     {
       return PL_DHashStringKey(nullptr, aKey);
     }
 
+    void SetExpireTime(PRTime aExpireTime)
+    {
+      mExpireTime = aExpireTime;
+      mExpired = false;
+    }
+
+    bool IsExpired()
+    {
+      // If mExpireTime is 0, this entry never expires (this is the case for
+      // knockout entries).
+      // If we've already expired or we never expire, return early.
+      if (mExpired || mExpireTime == 0) {
+        return mExpired;
+      }
+
+      PRTime now = PR_Now() / PR_USEC_PER_MSEC;
+      if (now > mExpireTime) {
+        mExpired = true;
+      }
+
+      return mExpired;
+    }
+
     // force the hashtable to use the copy constructor.
     enum { ALLOW_MEMMOVE = false };
 };
 ////////////////////////////////////////////////////////////////////////////////
 
+class nsSTSPreload;
+
 class nsStrictTransportSecurityService : public nsIStrictTransportSecurityService
                                        , public nsIObserver
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
   NS_DECL_NSISTRICTTRANSPORTSECURITYSERVICE
 
   nsStrictTransportSecurityService();
   nsresult Init();
   virtual ~nsStrictTransportSecurityService();
 
 private:
   nsresult GetHost(nsIURI *aURI, nsACString &aResult);
+  nsresult GetPrincipalForURI(nsIURI *aURI, nsIPrincipal **aPrincipal);
   nsresult SetStsState(nsIURI* aSourceURI, int64_t maxage, bool includeSubdomains);
   nsresult ProcessStsHeaderMutating(nsIURI* aSourceURI, char* aHeader);
+  const nsSTSPreload *GetPreloadListEntry(const char *aHost);
 
   // private-mode-preserving permission manager overlay functions
   nsresult AddPermission(nsIURI     *aURI,
                          const char *aType,
                          uint32_t   aPermission,
                          uint32_t   aExpireType,
                          int64_t    aExpireTime);
   nsresult RemovePermission(const nsCString  &aHost,
                             const char       *aType);
-  nsresult TestPermission(nsIURI     *aURI,
-                          const char *aType,
-                          uint32_t   *aPermission,
-                          bool       testExact);
 
   // cached services
   nsCOMPtr<nsIPermissionManager> mPermMgr;
   nsCOMPtr<nsIObserverService> mObserverService;
 
   bool mInPrivateMode;
   nsTHashtable<nsSTSHostEntry> mPrivateModeHostTable;
 };
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_sts_preloadlist.js
@@ -0,0 +1,180 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var gPBService = Cc["@mozilla.org/privatebrowsing;1"]
+                 .getService(Ci.nsIPrivateBrowsingService);
+var gSTSService = Cc["@mozilla.org/stsservice;1"]
+                  .getService(Ci.nsIStrictTransportSecurityService);
+
+function Observer() {}
+Observer.prototype = {
+  observe: function(subject, topic, data) {
+    run_next_test();
+  }
+};
+
+var gObserver = new Observer();
+
+// This is a list of every host we call processStsHeader with
+// (we have to remove any state added to the sts service so as to not muck
+// with other tests).
+var hosts = ["http://keyerror.com", "http://subdomain.kyps.net",
+             "http://subdomain.cert.se", "http://crypto.cat",
+             "http://www.logentries.com"];
+
+function cleanup() {
+  Services.obs.removeObserver(gObserver, "private-browsing-transition-complete");
+  gPBService.privateBrowsingEnabled = false;
+  for (var host of hosts) {
+    var uri = Services.io.newURI(host, null, null);
+    gSTSService.removeStsState(uri);
+  }
+}
+
+function run_test() {
+  do_register_cleanup(cleanup);
+  Services.obs.addObserver(gObserver, "private-browsing-transition-complete", false);
+  Services.prefs.setBoolPref("browser.privatebrowsing.keep_current_session", true);
+
+  add_test(test_part1);
+  add_test(test_private_browsing1);
+  add_test(test_private_browsing2);
+
+  run_next_test();
+}
+
+function test_part1() {
+  // check that a host not in the list is not identified as an sts host
+  do_check_false(gSTSService.isStsHost("nonexistent.mozilla.com"));
+
+  // check that an ancestor domain is not identified as an sts host
+  do_check_false(gSTSService.isStsHost("com"));
+
+  // Note: the following were taken from the STS preload list
+  // as of June 2012. If the list changes, this test will need to be modified.
+  // check that an entry at the beginning of the list is an sts host
+  do_check_true(gSTSService.isStsHost("health.google.com"));
+
+  // check that a subdomain is an sts host (includeSubdomains is set)
+  do_check_true(gSTSService.isStsHost("subdomain.health.google.com"));
+
+  // check that another subdomain is an sts host (includeSubdomains is set)
+  do_check_true(gSTSService.isStsHost("a.b.c.subdomain.health.google.com"));
+
+  // check that an entry in the middle of the list is an sts host
+  do_check_true(gSTSService.isStsHost("epoxate.com"));
+
+  // check that a subdomain is not an sts host (includeSubdomains is not set)
+  do_check_false(gSTSService.isStsHost("subdomain.epoxate.com"));
+
+  // check that an entry at the end of the list is an sts host
+  do_check_true(gSTSService.isStsHost("www.googlemail.com"));
+
+  // check that a subdomain is not an sts host (includeSubdomains is not set)
+  do_check_false(gSTSService.isStsHost("a.subdomain.www.googlemail.com"));
+
+  // check that a host with a dot on the end won't break anything
+  do_check_false(gSTSService.isStsHost("notsts.nonexistent.mozilla.com."));
+
+  // check that processing a header with max-age: 0 will remove a preloaded
+  // site from the list
+  var uri = Services.io.newURI("http://keyerror.com", null, null);
+  gSTSService.processStsHeader(uri, "max-age=0");
+  do_check_false(gSTSService.isStsHost("keyerror.com"));
+  do_check_false(gSTSService.isStsHost("subdomain.keyerror.com"));
+  // check that processing another header (with max-age non-zero) will
+  // re-enable a site's sts status
+  gSTSService.processStsHeader(uri, "max-age=1000");
+  do_check_true(gSTSService.isStsHost("keyerror.com"));
+  // but this time include subdomains was not set, so test for that
+  do_check_false(gSTSService.isStsHost("subdomain.keyerror.com"));
+
+  // check that processing a header with max-age: 0 from a subdomain of a site
+  // will not remove that (ancestor) site from the list
+  var uri = Services.io.newURI("http://subdomain.kyps.net", null, null);
+  gSTSService.processStsHeader(uri, "max-age=0");
+  do_check_true(gSTSService.isStsHost("kyps.net"));
+  do_check_false(gSTSService.isStsHost("subdomain.kyps.net"));
+
+  var uri = Services.io.newURI("http://subdomain.cert.se", null, null);
+  gSTSService.processStsHeader(uri, "max-age=0");
+  // we received a header with "max-age=0", so we have "no information"
+  // regarding the sts state of subdomain.cert.se specifically, but
+  // it is actually still an STS host, because of the preloaded cert.se
+  // including subdomains.
+  // Here's a drawing:
+  // |-- cert.se (in preload list, includes subdomains)      IS sts host
+  //     |-- subdomain.cert.se                               IS sts host
+  //     |   `-- another.subdomain.cert.se                   IS sts host
+  //     `-- sibling.cert.se                                 IS sts host
+  do_check_true(gSTSService.isStsHost("subdomain.cert.se"));
+  do_check_true(gSTSService.isStsHost("sibling.cert.se"));
+  do_check_true(gSTSService.isStsHost("another.subdomain.cert.se"));
+
+  gSTSService.processStsHeader(uri, "max-age=1000");
+  // Here's what we have now:
+  // |-- cert.se (in preload list, includes subdomains)      IS sts host
+  //     |-- subdomain.cert.se (include subdomains is false) IS sts host
+  //     |   `-- another.subdomain.cert.se                   IS NOT sts host
+  //     `-- sibling.cert.se                                 IS sts host
+  do_check_true(gSTSService.isStsHost("subdomain.cert.se"));
+  do_check_true(gSTSService.isStsHost("sibling.cert.se"));
+  do_check_false(gSTSService.isStsHost("another.subdomain.cert.se"));
+
+  // test private browsing correctly interacts with removing preloaded sites
+  gPBService.privateBrowsingEnabled = true;
+}
+
+function test_private_browsing1() {
+  // sanity - crypto.cat is preloaded, includeSubdomains set
+  do_check_true(gSTSService.isStsHost("crypto.cat"));
+  do_check_true(gSTSService.isStsHost("a.b.c.subdomain.crypto.cat"));
+
+  var uri = Services.io.newURI("http://crypto.cat", null, null);
+  gSTSService.processStsHeader(uri, "max-age=0");
+  do_check_false(gSTSService.isStsHost("crypto.cat"));
+  do_check_false(gSTSService.isStsHost("a.b.subdomain.crypto.cat"));
+
+  // check adding it back in
+  gSTSService.processStsHeader(uri, "max-age=1000");
+  do_check_true(gSTSService.isStsHost("crypto.cat"));
+  // but no includeSubdomains this time
+  do_check_false(gSTSService.isStsHost("b.subdomain.crypto.cat"));
+
+  // do the hokey-pokey...
+  gSTSService.processStsHeader(uri, "max-age=0");
+  do_check_false(gSTSService.isStsHost("crypto.cat"));
+  do_check_false(gSTSService.isStsHost("subdomain.crypto.cat"));
+
+  // TODO unfortunately we don't have a good way to know when an entry
+  // has expired in the permission manager, so we can't yet extend this test
+  // to that case.
+  // Test that an expired private browsing entry results in correctly
+  // identifying a host that is on the preload list as no longer sts.
+  // (This happens when we're in private browsing mode, we get a header from
+  // a site on the preload list, and that header later expires. We need to
+  // then treat that host as no longer an sts host.)
+  // (sanity check first - this should be in the preload list)
+  do_check_true(gSTSService.isStsHost("www.logentries.com"));
+  var uri = Services.io.newURI("http://www.logentries.com", null, null);
+  // according to the rfc, max-age can't be negative, but this is a great
+  // way to test an expired entry
+  gSTSService.processStsHeader(uri, "max-age=-1000");
+  do_check_false(gSTSService.isStsHost("www.logentries.com"));
+
+  gPBService.privateBrowsingEnabled = false;
+}
+
+function test_private_browsing2() {
+  do_check_true(gSTSService.isStsHost("crypto.cat"));
+  // the crypto.cat entry has includeSubdomains set
+  do_check_true(gSTSService.isStsHost("subdomain.crypto.cat"));
+
+  // Now that we're out of private browsing mode, we need to make sure
+  // we've "forgotten" that we "forgot" this site's sts status.
+  do_check_true(gSTSService.isStsHost("www.logentries.com"));
+
+  run_next_test();
+}
--- a/security/manager/ssl/tests/unit/xpcshell.ini
+++ b/security/manager/ssl/tests/unit/xpcshell.ini
@@ -6,8 +6,9 @@ tail =
 # Bug 676972: test hangs consistently on Android
 skip-if = os == "android"
 [test_hash_algorithms.js]
 # Bug 676972: test hangs consistently on Android
 skip-if = os == "android"
 [test_hmac.js]
 # Bug 676972: test hangs consistently on Android
 skip-if = os == "android"
+[test_sts_preloadlist.js]
new file mode 100644
--- /dev/null
+++ b/security/manager/tools/getHSTSPreloadList.py
@@ -0,0 +1,115 @@
+#!/usr/bin/python
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys, subprocess, json, argparse
+
+SOURCE = "https://src.chromium.org/viewvc/chrome/trunk/src/net/base/transport_security_state_static.json"
+OUTPUT = "nsSTSPreloadList.inc"
+PREFIX = """/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*****************************************************************************/
+/* This is an automatically generated file. If you're not                    */
+/* nsStrictTransportSecurityService.cpp, you shouldn't be #including it.     */
+/*****************************************************************************/
+
+#include <prtypes.h>
+
+class nsSTSPreload
+{
+  public:
+    const char *mHost;
+    const bool mIncludeSubdomains;
+};
+
+static const nsSTSPreload kSTSPreloadList[] = {
+"""
+POSTFIX = """};
+"""
+
+def filterComments(stream):
+  lines = []
+  for line in stream:
+    # each line still has '\n' at the end, so if find returns -1,
+    # the newline gets chopped off like we want
+    # (and otherwise, comments are filtered out like we want)
+    lines.append(line[0:line.find("//")])
+  return "".join(lines)
+
+def readFile(source):
+  if source != "-":
+    f = open(source, 'r')
+  else:
+    f = sys.stdin
+  return filterComments(f)
+
+def download(source):
+  download = subprocess.Popen(["wget", "-O", "-", source], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
+  contents = filterComments(download.stdout)
+  download.wait()
+  if download.returncode != 0:
+    raise Exception()
+  return contents
+
+def output(filename, jsonblob):
+  if filename != "-":
+    outstream = open(filename, 'w')
+  else:
+    outstream = sys.stdout
+  if not 'entries' in jsonblob:
+    raise Exception()
+  else:
+    outstream.write(PREFIX)
+    # use a dictionary to prevent duplicates
+    lines = {}
+    for entry in jsonblob['entries']:
+      if 'name' in entry and 'mode' in entry and entry['mode'] == "force-https":
+        line = "  { \"" + entry['name'] + "\", "
+        if 'include_subdomains' in entry and entry['include_subdomains']:
+          line = line + "true },\n"
+        else:
+          line = line + "false },\n"
+        lines[line] = True
+    # The data must be sorted by domain name because we do a binary search to
+    # determine if a host is in the preload list.
+    keys = lines.keys()
+    keys.sort()
+    for line in keys:
+      outstream.write(line)
+    outstream.write(POSTFIX);
+  outstream.close()
+
+def main():
+  parser = argparse.ArgumentParser(description="Download Chrome's STS preload list and format it for Firefox")
+  parser.add_argument("-s", "--source", default=SOURCE, help="Specify source for input list (can be a file, url, or '-' for stdin)")
+  parser.add_argument("-o", "--output", default=OUTPUT, help="Specify output file ('-' for stdout)")
+  args = parser.parse_args()
+  contents = None
+  try:
+    contents = readFile(args.source)
+  except:
+    pass
+  if not contents:
+    try:
+      contents = download(args.source)
+    except:
+      print >> sys.stderr, "Could not read source '%s'" % args.source
+      return 1
+  try:
+    jsonblob = json.loads(contents)
+  except:
+    print >> sys.stderr, "Could not parse contents of file '%s'" % args.source
+    return 1
+  try:
+    output(args.output, jsonblob)
+  except:
+    print >> sys.stderr, "Could not write to '%s'" % args.output
+    return 1
+  return 0
+
+if __name__ == "__main__":
+  sys.exit(main())