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 105477 ce222ba667f21636ef3ad54e0c1ad60816bba468
parent 105476 36f85f213ee28ce22a753c963d971408674cbfb1
child 105478 35fddfaef1b6d52763972f51fd79e674400f8947
push id55
push usershu@rfrn.org
push dateThu, 30 Aug 2012 01:33:09 +0000
reviewersmayhemer, bsmith
bugs760307
milestone17.0a1
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())