Bug 1306471 - Modify the SiteSecurityService to allow dynamic pin preloads r=keeler draft
authorMark Goodwin <mgoodwin@mozilla.com>
Thu, 06 Oct 2016 11:00:43 +0100
changeset 421488 fec1e75d3ca35d17d71d797e46c45983aaeb0c12
parent 421441 c7d62e6d052c5d2638b08d480a720254ea09ff2d
child 533110 49c40d4d90657743174bbaa5f51616d3f866c55e
push id31534
push usermgoodwin@mozilla.com
push dateThu, 06 Oct 2016 10:02:04 +0000
reviewerskeeler
bugs1306471
milestone52.0a1
Bug 1306471 - Modify the SiteSecurityService to allow dynamic pin preloads r=keeler MozReview-Commit-ID: JLbJcMuvcyI
security/manager/ssl/nsISiteSecurityService.idl
security/manager/ssl/nsSiteSecurityService.cpp
security/manager/ssl/nsSiteSecurityService.h
security/manager/ssl/tests/unit/head_psm.js
security/manager/ssl/tests/unit/test_pinning_dynamic.js
security/manager/ssl/tests/unit/test_pinning_dynamic/a.preload.example.com-badca.pem
security/manager/ssl/tests/unit/test_pinning_dynamic/a.preload.example.com-badca.pem.certspec
security/manager/ssl/tests/unit/test_pinning_dynamic/a.preload.example.com-pinningroot.pem
security/manager/ssl/tests/unit/test_pinning_dynamic/a.preload.example.com-pinningroot.pem.certspec
security/manager/ssl/tests/unit/test_pinning_dynamic/b.preload.example.com-badca.pem
security/manager/ssl/tests/unit/test_pinning_dynamic/b.preload.example.com-badca.pem.certspec
security/manager/ssl/tests/unit/test_pinning_dynamic/b.preload.example.com-pinningroot.pem
security/manager/ssl/tests/unit/test_pinning_dynamic/b.preload.example.com-pinningroot.pem.certspec
security/manager/ssl/tests/unit/test_pinning_dynamic/moz.build
security/manager/ssl/tests/unit/test_sss_eviction.js
security/manager/ssl/tests/unit/test_sss_readstate.js
security/manager/ssl/tests/unit/test_sss_readstate_garbage.js
security/manager/ssl/tests/unit/test_sss_readstate_huge.js
security/manager/ssl/tests/unit/test_sss_savestate.js
--- a/security/manager/ssl/nsISiteSecurityService.idl
+++ b/security/manager/ssl/nsISiteSecurityService.idl
@@ -138,21 +138,27 @@ interface nsISiteSecurityService : nsISu
      *                  NO_PERMANENT_STORAGE
      * @param aCached true if we have cached information regarding whether or not
      *                  the host is HSTS, false otherwise.
      */
     boolean isSecureURI(in uint32_t aType, in nsIURI aURI, in uint32_t aFlags,
                         [optional] out boolean aCached);
 
     /**
-     * Removes all security state by resetting to factory-original settings.
+     * Removes all non-preloaded security state by resetting to factory-original
+     * settings.
      */
     void clearAll();
 
     /**
+     * Removes all preloaded security state.
+     */
+    void clearPreloads();
+
+    /**
      * Returns an array of sha256-hashed key pins for the given domain, if any.
      * If these pins also apply to subdomains of the given domain,
      * aIncludeSubdomains will be true. Pins returned are only for non-built-in
      * pin entries.
      *
      * @param aHostname the hosname (punycode) to be queried about
      * @param the time at which the pins should be valid. This is in
               mozilla::pkix::Time which uses internally seconds since 0 AD.
@@ -167,23 +173,26 @@ interface nsISiteSecurityService : nsISu
 
     /**
      * Set public-key pins for a host. The resulting pins will be permanent
      * and visible from private and non-private contexts. These pins replace
      * any already set by this mechanism or those built-in to Gecko.
      *
      * @param aHost the hostname (punycode) that pins will apply to
      * @param aIncludeSubdomains whether these pins also apply to subdomains
-     * @param aMaxAge lifetime (in seconds) of this pin set
+     * @param aExpires the time this pin should expire (millis since epoch)
      * @param aPinCount number of keys being pinnned
      * @param aSha256Pins array of hashed key fingerprints (SHA-256, base64)
+     * @param aIsPreload are these key pins for a preload entry? (false by
+     *        default)
      */
      boolean setKeyPins(in string aHost, in boolean aIncludeSubdomains,
-                        in unsigned long aMaxAge, in unsigned long aPinCount,
-                        [array, size_is(aPinCount)] in string aSha256Pins);
+                        in int64_t aExpires, in unsigned long aPinCount,
+                        [array, size_is(aPinCount)] in string aSha256Pins,
+                        [optional] in boolean aIsPreload);
 
     /**
      * Mark a host as declining to provide a given security state so that features
      * such as HSTS priming will not flood a server with requests.
      *
      * @param aURI the nsIURI that this applies to
      * @param aMaxAge lifetime (in seconds) of this negative cache
      */
--- a/security/manager/ssl/nsSiteSecurityService.cpp
+++ b/security/manager/ssl/nsSiteSecurityService.cpp
@@ -243,25 +243,32 @@ nsSiteSecurityService::Init()
   mozilla::Preferences::AddStrongObserver(this,
     "security.cert_pinning.process_headers_from_non_builtin_roots");
   mPreloadListTimeOffset = mozilla::Preferences::GetInt(
     "test.currentTimeOffsetSeconds", 0);
   mozilla::Preferences::AddStrongObserver(this,
     "test.currentTimeOffsetSeconds");
   mSiteStateStorage =
     mozilla::DataStorage::Get(NS_LITERAL_STRING("SiteSecurityServiceState.txt"));
+  mPreloadStateStorage =
+    mozilla::DataStorage::Get(NS_LITERAL_STRING("SecurityPreloadState.txt"));
   bool storageWillPersist = false;
+  bool preloadStorageWillPersist = false;
   nsresult rv = mSiteStateStorage->Init(storageWillPersist);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
+  rv = mPreloadStateStorage->Init(preloadStorageWillPersist);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
   // This is not fatal. There are some cases where there won't be a
   // profile directory (e.g. running xpcshell). There isn't the
   // expectation that site information will be presisted in those cases.
-  if (!storageWillPersist) {
+  if (!storageWillPersist || !preloadStorageWillPersist) {
     NS_WARNING("site security information will not be persisted");
   }
 
   return NS_OK;
 }
 
 nsresult
 nsSiteSecurityService::GetHost(nsIURI* aURI, nsACString& aResult)
@@ -810,17 +817,17 @@ nsSiteSecurityService::ProcessPKPHeader(
   }
 
   int64_t expireTime = ExpireTimeFromMaxAge(maxAge);
   SiteHPKPState dynamicEntry(expireTime, SecurityPropertySet,
                              foundIncludeSubdomains, sha256keys);
   SSSLOG(("SSS: about to set pins for  %s, expires=%ld now=%ld maxAge=%lu\n",
            host.get(), expireTime, PR_Now() / PR_USEC_PER_MSEC, maxAge));
 
-  rv = SetHPKPState(host.get(), dynamicEntry, aFlags);
+  rv = SetHPKPState(host.get(), dynamicEntry, aFlags, false);
   if (NS_FAILED(rv)) {
     SSSLOG(("SSS: failed to set pins for %s\n", host.get()));
     if (aFailureResult) {
       *aFailureResult = nsISiteSecurityService::ERROR_COULD_NOT_SAVE_STATE;
     }
     return rv;
   }
 
@@ -1133,111 +1140,138 @@ nsSiteSecurityService::ClearAll()
    if (!XRE_IsParentProcess()) {
      MOZ_CRASH("Child process: no direct access to nsISiteSecurityService::ClearAll");
    }
 
   return mSiteStateStorage->Clear();
 }
 
 NS_IMETHODIMP
+nsSiteSecurityService::ClearPreloads()
+{
+  // Child processes are not allowed direct access to this.
+  if (!XRE_IsParentProcess()) {
+    MOZ_CRASH("Child process: no direct access to nsISiteSecurityService::ClearPreloads");
+  }
+
+  return mPreloadStateStorage->Clear();
+}
+
+bool entryStateNotOK(SiteHPKPState& state, mozilla::pkix::Time& aEvalTime) {
+  return state.mState != SecurityPropertySet || state.IsExpired(aEvalTime) ||
+         state.mSHA256keys.Length() < 1;
+}
+
+NS_IMETHODIMP
 nsSiteSecurityService::GetKeyPinsForHostname(const char* aHostname,
                                              mozilla::pkix::Time& aEvalTime,
                                              /*out*/ nsTArray<nsCString>& pinArray,
                                              /*out*/ bool* aIncludeSubdomains,
                                              /*out*/ bool* afound) {
    // Child processes are not allowed direct access to this.
    if (!XRE_IsParentProcess()) {
      MOZ_CRASH("Child process: no direct access to nsISiteSecurityService::GetKeyPinsForHostname");
    }
 
   NS_ENSURE_ARG(afound);
   NS_ENSURE_ARG(aHostname);
 
-  SSSLOG(("Top of GetKeyPinsForHostname"));
+  SSSLOG(("Top of GetKeyPinsForHostname for %s", aHostname));
   *afound = false;
   *aIncludeSubdomains = false;
   pinArray.Clear();
 
   nsAutoCString host(PublicKeyPinningService::CanonicalizeHostname(aHostname));
   nsAutoCString storageKey;
   SetStorageKey(storageKey, host, nsISiteSecurityService::HEADER_HPKP);
 
   SSSLOG(("storagekey '%s'\n", storageKey.get()));
   mozilla::DataStorageType storageType = mozilla::DataStorage_Persistent;
   nsCString value = mSiteStateStorage->Get(storageKey, storageType);
+
   // decode now
   SiteHPKPState foundEntry(value);
-  if (foundEntry.mState != SecurityPropertySet ||
-      foundEntry.IsExpired(aEvalTime) ||
-      foundEntry.mSHA256keys.Length() < 1 ) {
+  if (entryStateNotOK(foundEntry, aEvalTime)) {
     // not in permanent storage, try now private
     value = mSiteStateStorage->Get(storageKey, mozilla::DataStorage_Private);
     SiteHPKPState privateEntry(value);
-    if (privateEntry.mState != SecurityPropertySet ||
-        privateEntry.IsExpired(aEvalTime) ||
-        privateEntry.mSHA256keys.Length() < 1 ) {
-      return NS_OK;
+    if (entryStateNotOK(privateEntry, aEvalTime)) {
+      // not in private storage, try dynamic preload
+      value = mPreloadStateStorage->Get(storageKey,
+                                        mozilla::DataStorage_Persistent);
+      SiteHPKPState preloadEntry(value);
+      if (entryStateNotOK(preloadEntry, aEvalTime)) {
+        return NS_OK;
+      }
+      foundEntry = preloadEntry;
+    } else {
+      foundEntry = privateEntry;
     }
-    foundEntry = privateEntry;
   }
   pinArray = foundEntry.mSHA256keys;
   *aIncludeSubdomains = foundEntry.mIncludeSubdomains;
   *afound = true;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSiteSecurityService::SetKeyPins(const char* aHost, bool aIncludeSubdomains,
-                                  uint32_t aMaxAge, uint32_t aPinCount,
+                                  int64_t aExpires, uint32_t aPinCount,
                                   const char** aSha256Pins,
+                                  bool aIsPreload,
                                   /*out*/ bool* aResult)
 {
    // Child processes are not allowed direct access to this.
    if (!XRE_IsParentProcess()) {
      MOZ_CRASH("Child process: no direct access to nsISiteSecurityService::SetKeyPins");
    }
 
   NS_ENSURE_ARG_POINTER(aHost);
   NS_ENSURE_ARG_POINTER(aResult);
   NS_ENSURE_ARG_POINTER(aSha256Pins);
 
   SSSLOG(("Top of SetPins"));
 
-  int64_t expireTime = ExpireTimeFromMaxAge(aMaxAge);
   nsTArray<nsCString> sha256keys;
   for (unsigned int i = 0; i < aPinCount; i++) {
     nsAutoCString pin(aSha256Pins[i]);
     SSSLOG(("SetPins pin=%s\n", pin.get()));
     if (!stringIsBase64EncodingOf256bitValue(pin)) {
       return NS_ERROR_INVALID_ARG;
     }
     sha256keys.AppendElement(pin);
   }
-  SiteHPKPState dynamicEntry(expireTime, SecurityPropertySet,
+  SiteHPKPState dynamicEntry(aExpires, SecurityPropertySet,
                              aIncludeSubdomains, sha256keys);
   // we always store data in permanent storage (ie no flags)
   nsAutoCString host(PublicKeyPinningService::CanonicalizeHostname(aHost));
-  return SetHPKPState(host.get(), dynamicEntry, 0);
+  return SetHPKPState(host.get(), dynamicEntry, 0, aIsPreload);
 }
 
 nsresult
 nsSiteSecurityService::SetHPKPState(const char* aHost, SiteHPKPState& entry,
-                                    uint32_t aFlags)
+                                    uint32_t aFlags, bool aIsPreload)
 {
   SSSLOG(("Top of SetPKPState"));
   nsAutoCString host(aHost);
   nsAutoCString storageKey;
   SetStorageKey(storageKey, host, nsISiteSecurityService::HEADER_HPKP);
   bool isPrivate = aFlags & nsISocketProvider::NO_PERMANENT_STORAGE;
   mozilla::DataStorageType storageType = isPrivate
                                          ? mozilla::DataStorage_Private
                                          : mozilla::DataStorage_Persistent;
   nsAutoCString stateString;
   entry.ToString(stateString);
-  nsresult rv = mSiteStateStorage->Put(storageKey, stateString, storageType);
+
+  nsresult rv;
+  if (aIsPreload) {
+    rv = mPreloadStateStorage->Put(storageKey, stateString, storageType);
+  } else {
+    rv = mSiteStateStorage->Put(storageKey, stateString, storageType);
+  }
   NS_ENSURE_SUCCESS(rv, rv);
   return NS_OK;
 }
 
 //------------------------------------------------------------
 // nsSiteSecurityService::nsIObserver
 //------------------------------------------------------------
 
--- a/security/manager/ssl/nsSiteSecurityService.h
+++ b/security/manager/ssl/nsSiteSecurityService.h
@@ -138,20 +138,22 @@ private:
                                  uint32_t* aFailureResult);
   nsresult ProcessSTSHeader(nsIURI* aSourceURI, const char* aHeader,
                             uint32_t flags, uint64_t* aMaxAge,
                             bool* aIncludeSubdomains, uint32_t* aFailureResult);
   nsresult ProcessPKPHeader(nsIURI* aSourceURI, const char* aHeader,
                             nsISSLStatus* aSSLStatus, uint32_t flags,
                             uint64_t* aMaxAge, bool* aIncludeSubdomains,
                             uint32_t* aFailureResult);
-  nsresult SetHPKPState(const char* aHost, SiteHPKPState& entry, uint32_t flags);
+  nsresult SetHPKPState(const char* aHost, SiteHPKPState& entry, uint32_t flags,
+                        bool aIsPreload);
 
   const nsSTSPreload *GetPreloadListEntry(const char *aHost);
 
   uint64_t mMaxMaxAge;
   bool mUsePreloadList;
   int64_t mPreloadListTimeOffset;
   bool mProcessPKPHeadersFromNonBuiltInRoots;
   RefPtr<mozilla::DataStorage> mSiteStateStorage;
+  RefPtr<mozilla::DataStorage> mPreloadStateStorage;
 };
 
 #endif // __nsSiteSecurityService_h__
--- a/security/manager/ssl/tests/unit/head_psm.js
+++ b/security/manager/ssl/tests/unit/head_psm.js
@@ -23,16 +23,17 @@ const isDebugBuild = Cc["@mozilla.org/xp
 
 // The test EV roots are only enabled in debug builds as a security measure.
 //
 // Bug 1008316: B2G doesn't have EV enabled, so EV is not expected even in debug
 // builds.
 const gEVExpected = isDebugBuild && !("@mozilla.org/b2g-process-global;1" in Cc);
 
 const SSS_STATE_FILE_NAME = "SiteSecurityServiceState.txt";
+const PRELOAD_STATE_FILE_NAME = "SecurityPreloadState.txt";
 
 const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE;
 const SSL_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE;
 const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE;
 
 // This isn't really a valid PRErrorCode, but is useful for signalling that
 // a test is expected to succeed.
 const PRErrorCodeSuccess = 0;
--- a/security/manager/ssl/tests/unit/test_pinning_dynamic.js
+++ b/security/manager/ssl/tests/unit/test_pinning_dynamic.js
@@ -6,16 +6,18 @@
 // The purpose of this test is to create a site security service state file
 // and see that the site security service reads it properly.
 
 function writeLine(aLine, aOutputStream) {
   aOutputStream.write(aLine, aLine.length);
 }
 
 var gSSService = null;
+var gSSSStateSeen = false;
+var gPreloadStateSeen = false;
 
 var profileDir = do_get_profile();
 var certdb;
 
 function certFromFile(cert_name) {
   return constructCertFromFile("test_pinning_dynamic/" + cert_name + ".pem");
 }
 
@@ -38,26 +40,36 @@ function checkFail(cert, hostname) {
 const NON_ISSUED_KEY_HASH = "KHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN=";
 const PINNING_ROOT_KEY_HASH = "VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=";
 
 function run_test() {
   Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 2);
 
   let stateFile = profileDir.clone();
   stateFile.append(SSS_STATE_FILE_NAME);
-  // Assuming we're working with a clean slate, the file shouldn't exist
-  // until we create it.
+  // Assuming we're working with a clean slate, the SSS_STATE file shouldn't
+  // exist until we create it.
   ok(!stateFile.exists(),
      "State file should not exist when working with a clean slate");
   let outputStream = FileUtils.openFileOutputStream(stateFile);
   let now = (new Date()).getTime();
   writeLine(`a.pinning2.example.com:HPKP\t0\t0\t${now + 100000},1,0,${PINNING_ROOT_KEY_HASH}\n`, outputStream);
   writeLine(`b.pinning2.example.com:HPKP\t0\t0\t${now + 100000},1,1,${PINNING_ROOT_KEY_HASH}\n`, outputStream);
 
   outputStream.close();
+
+  let preloadFile = profileDir.clone();
+  preloadFile.append(PRELOAD_STATE_FILE_NAME);
+  ok(!preloadFile.exists(),
+     "Preload file should not exist when working with a clean slate");
+
+  outputStream = FileUtils.openFileOutputStream(preloadFile);
+  writeLine(`a.preload.example.com:HPKP\t0\t0\t${now + 100000},1,1,${PINNING_ROOT_KEY_HASH}\n`, outputStream);
+  outputStream.close();
+
   Services.obs.addObserver(checkStateRead, "data-storage-ready", false);
   do_test_pending();
   gSSService = Cc["@mozilla.org/ssservice;1"]
                  .getService(Ci.nsISiteSecurityService);
   notEqual(gSSService, null,
            "SiteSecurityService should have initialized successfully using" +
            " the generated state file");
 }
@@ -73,18 +85,30 @@ function checkDefaultSiteHPKPStatus() {
                              "b.pinning2.example.com", 0),
      "b.pinning2.example.com should have HPKP status");
   ok(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP,
                              "x.b.pinning2.example.com", 0),
      "x.b.pinning2.example.com should have HPKP status");
 }
 
 function checkStateRead(aSubject, aTopic, aData) {
-  equal(aData, SSS_STATE_FILE_NAME,
-        "Observed data should be the Site Security Service state file name");
+  if (aData == SSS_STATE_FILE_NAME) {
+    gSSSStateSeen = true;
+  } else if (aData == PRELOAD_STATE_FILE_NAME) {
+    gPreloadStateSeen = true;
+  } else {
+    throw new Error("Observed data should either be the Site Security " +
+                    "Service state file name or the preload file name");
+    return;
+  }
+
+  if (!gSSSStateSeen || !gPreloadStateSeen) {
+    return;
+  }
+
   notEqual(gSSService, null, "SiteSecurityService should be initialized");
 
   // Initializing the certificate DB will cause NSS-initialization, which in
   // turn initializes the site security service. Since we're in part testing
   // that the site security service correctly reads its state file, we have to
   // make sure it doesn't start up before we've populated the file
   certdb = Cc["@mozilla.org/security/x509certdb;1"]
              .getService(Ci.nsIX509CertDB);
@@ -103,17 +127,18 @@ function checkStateRead(aSubject, aTopic
   checkOK(certFromFile('b.pinning2.example.com-pinningroot'), "b.pinning2.example.com");
   checkFail(certFromFile('x.b.pinning2.example.com-badca'), "x.b.pinning2.example.com");
   checkOK(certFromFile('x.b.pinning2.example.com-pinningroot'), "x.b.pinning2.example.com");
 
   checkDefaultSiteHPKPStatus();
 
 
   // add includeSubdomains to a.pinning2.example.com
-  gSSService.setKeyPins("a.pinning2.example.com", true, 1000, 2,
+  gSSService.setKeyPins("a.pinning2.example.com", true,
+                        new Date().getTime() + 1000000, 2,
                         [NON_ISSUED_KEY_HASH, PINNING_ROOT_KEY_HASH]);
   checkFail(certFromFile('a.pinning2.example.com-badca'), "a.pinning2.example.com");
   checkOK(certFromFile('a.pinning2.example.com-pinningroot'), "a.pinning2.example.com");
   checkFail(certFromFile('x.a.pinning2.example.com-badca'), "x.a.pinning2.example.com");
   checkOK(certFromFile('x.a.pinning2.example.com-pinningroot'), "x.a.pinning2.example.com");
   checkFail(certFromFile('b.pinning2.example.com-badca'), "b.pinning2.example.com");
   checkOK(certFromFile('b.pinning2.example.com-pinningroot'), "b.pinning2.example.com");
   checkFail(certFromFile('x.b.pinning2.example.com-badca'), "x.b.pinning2.example.com");
@@ -124,73 +149,99 @@ function checkStateRead(aSubject, aTopic
      "a.pinning2.example.com should still have HPKP status after adding" +
      " includeSubdomains to a.pinning2.example.com");
   ok(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP,
                              "x.a.pinning2.example.com", 0),
      "x.a.pinning2.example.com should now have HPKP status after adding" +
      " includeSubdomains to a.pinning2.example.com");
 
   // Now setpins without subdomains
-  gSSService.setKeyPins("a.pinning2.example.com", false, 1000, 2,
+  gSSService.setKeyPins("a.pinning2.example.com", false,
+                        new Date().getTime() + 1000000, 2,
                         [NON_ISSUED_KEY_HASH, PINNING_ROOT_KEY_HASH]);
   checkFail(certFromFile('a.pinning2.example.com-badca'), "a.pinning2.example.com");
   checkOK(certFromFile('a.pinning2.example.com-pinningroot'), "a.pinning2.example.com");
   checkOK(certFromFile('x.a.pinning2.example.com-badca'), "x.a.pinning2.example.com");
   checkOK(certFromFile('x.a.pinning2.example.com-pinningroot'), "x.a.pinning2.example.com");
 
   checkFail(certFromFile('b.pinning2.example.com-badca'), "b.pinning2.example.com");
   checkOK(certFromFile('b.pinning2.example.com-pinningroot'), "b.pinning2.example.com");
   checkFail(certFromFile('x.b.pinning2.example.com-badca'), "x.b.pinning2.example.com");
   checkOK(certFromFile('x.b.pinning2.example.com-pinningroot'), "x.b.pinning2.example.com");
 
   checkDefaultSiteHPKPStatus();
 
   // failure to insert new pin entry leaves previous pin behavior
   throws(() => {
-    gSSService.setKeyPins("a.pinning2.example.com", true, 1000, 1,
-                          ["not a hash"]);
+    gSSService.setKeyPins("a.pinning2.example.com", true,
+                          new Date().getTime() + 1000000, 1, ["not a hash"]);
   }, /NS_ERROR_ILLEGAL_VALUE/, "Attempting to set an invalid pin should fail");
   checkFail(certFromFile('a.pinning2.example.com-badca'), "a.pinning2.example.com");
   checkOK(certFromFile('a.pinning2.example.com-pinningroot'), "a.pinning2.example.com");
   checkOK(certFromFile('x.a.pinning2.example.com-badca'), "x.a.pinning2.example.com");
   checkOK(certFromFile('x.a.pinning2.example.com-pinningroot'), "x.a.pinning2.example.com");
 
   checkFail(certFromFile('b.pinning2.example.com-badca'), "b.pinning2.example.com");
   checkOK(certFromFile('b.pinning2.example.com-pinningroot'), "b.pinning2.example.com");
   checkFail(certFromFile('x.b.pinning2.example.com-badca'), "x.b.pinning2.example.com");
   checkOK(certFromFile('x.b.pinning2.example.com-pinningroot'), "x.b.pinning2.example.com");
 
   checkDefaultSiteHPKPStatus();
 
   // Incorrect size results in failure
   throws(() => {
-    gSSService.setKeyPins("a.pinning2.example.com", true, 1000, 2,
-                          ["not a hash"]);
+    gSSService.setKeyPins("a.pinning2.example.com", true,
+                          new Date().getTime() + 1000000, 2, ["not a hash"]);
   }, /NS_ERROR_XPC_NOT_ENOUGH_ELEMENTS_IN_ARRAY/,
      "Attempting to set a pin with an incorrect size should fail");
 
   // Ensure built-in pins work as expected
   ok(!gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP,
                               "nonexistent.example.com", 0),
      "Not built-in nonexistent.example.com should not have HPKP status");
   ok(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HPKP,
                              "include-subdomains.pinning.example.com", 0),
      "Built-in include-subdomains.pinning.example.com should have HPKP status");
 
-  gSSService.setKeyPins("a.pinning2.example.com", false, 0, 1,
-                        [NON_ISSUED_KEY_HASH]);
+  gSSService.setKeyPins("a.pinning2.example.com", false, new Date().getTime(),
+                        1, [NON_ISSUED_KEY_HASH]);
+
+  // Check that a preload pin loaded from file works as expected
+  checkFail(certFromFile("a.preload.example.com-badca"), "a.preload.example.com");
+  checkOK(certFromFile("a.preload.example.com-pinningroot"), "a.preload.example.com");
+
+  // Check a dynamic addition works as expected
+  // first, it should succeed with the badCA - because there's no pin
+  checkOK(certFromFile('b.preload.example.com-badca'), "b.preload.example.com");
+  // then we add a pin, and we should get a failure (ensuring the expiry is
+  // after the test timeout)
+  gSSService.setKeyPins("b.preload.example.com", false,
+                        new Date().getTime() + 1000000, 2,
+                        [NON_ISSUED_KEY_HASH, PINNING_ROOT_KEY_HASH], true);
+  checkFail(certFromFile('b.preload.example.com-badca'), "b.preload.example.com");
 
   do_timeout(1250, checkExpiredState);
 }
 
 function checkExpiredState() {
   checkOK(certFromFile('a.pinning2.example.com-badca'), "a.pinning2.example.com");
   checkOK(certFromFile('a.pinning2.example.com-pinningroot'), "a.pinning2.example.com");
   checkOK(certFromFile('x.a.pinning2.example.com-badca'), "x.a.pinning2.example.com");
   checkOK(certFromFile('x.a.pinning2.example.com-pinningroot'), "x.a.pinning2.example.com");
 
   checkFail(certFromFile('b.pinning2.example.com-badca'), "b.pinning2.example.com");
   checkOK(certFromFile('b.pinning2.example.com-pinningroot'), "b.pinning2.example.com");
   checkFail(certFromFile('x.b.pinning2.example.com-badca'), "x.b.pinning2.example.com");
   checkOK(certFromFile('x.b.pinning2.example.com-pinningroot'), "x.b.pinning2.example.com");
+  checkPreloadClear();
+}
+
+function checkPreloadClear() {
+  // Check that the preloaded pins still work after private data is cleared
+  gSSService.clearAll();
+  checkFail(certFromFile('b.preload.example.com-badca'), "b.preload.example.com");
+
+  // Check that the preloaded pins are cleared when we clear preloads
+  gSSService.clearPreloads();
+  checkOK(certFromFile('b.preload.example.com-badca'), "b.preload.example.com");
 
   do_test_finished();
 }
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_pinning_dynamic/a.preload.example.com-badca.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC3DCCAcagAwIBAgIUdQjNH2Vc4Gz7xsAYmk2WWMHj2egwCwYJKoZIhvcNAQEL
+MBAxDjAMBgNVBAMMBWJhZGNhMCIYDzIwMTQxMTI3MDAwMDAwWhgPMjAxNzAyMDQw
+MDAwMDBaMBoxGDAWBgNVBAMMD3Rlc3QgZW5kLWVudGl0eTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAMF1xlJmCZ93CCpnkfG4dsN/XOU4sGxKzSKxy9Rv
+plraKt1ByMJJisSjs8H2FIf0G2mJQb2ApRw8EgJExYSkxEgzBeUTjAEGzwi+moYn
+YLrmoujzbyPF2YMTud+vN4NF2s5R1Nbc0qbLPMcG680wcOyYzOQKpZHXKVp/ccW+
+ZmkdKy3+yElEWQvFo+pJ/ZOx11NAXxdzdpmVhmYlR5ftQmkIiAgRQiBpmIpD/uSM
+5oeB3SK2ppzSg3UTH5MrEozihvp9JRwGKtJ+8Bbxh83VToMrNbiTD3S6kKqLx2Fn
+JCqx/W1iFA0YxMC4xo/DdIRXMkrX3obmVS8dHhkdcSFo07sCAwEAAaMkMCIwIAYD
+VR0RBBkwF4IVYS5wcmVsb2FkLmV4YW1wbGUuY29tMAsGCSqGSIb3DQEBCwOCAQEA
+sSMSssCpgELmV5AfG5OHnm8jrx0LMP3ERMNxvmBr4z2QMywF+BSwhiJYWJTF6ukw
+frWP0CLN5oleBSjtDhSZy6XKZOkuBOSUbxM5qGZXBb/Xt+uFDPC4TFf68qa79EBg
+aE4rQ0Vdeitswvl3CGd9qR1WN+507y+TNwPFPMjtgCfoXQkxIfQKUrLWiCtT9XZU
+ETRl33mIWqCnu4A6MVXUA1pT2tcreARtErxTs8zZfcT0KGIahCeZRR8E+C+KIqpy
+K6vh7BhXlKyGBWKyHYQbojHXP/tUmsLjd71VHXToE89tPgUT+Ic+6RDk3mLEtsGd
+N67nb14XDOhFBqI3dSDv+Q==
+-----END CERTIFICATE-----
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_pinning_dynamic/a.preload.example.com-badca.pem.certspec
@@ -0,0 +1,5 @@
+issuer:badca
+subject:test end-entity
+issuerKey:alternate
+subjectKey:alternate
+extension:subjectAlternativeName:a.preload.example.com
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_pinning_dynamic/a.preload.example.com-pinningroot.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC4jCCAcygAwIBAgIUd8Jru54+Q3mk8MRoEuJa+5ZmtScwCwYJKoZIhvcNAQEL
+MBYxFDASBgNVBAMMC3Bpbm5pbmdyb290MCIYDzIwMTQxMTI3MDAwMDAwWhgPMjAx
+NzAyMDQwMDAwMDBaMBoxGDAWBgNVBAMMD3Rlc3QgZW5kLWVudGl0eTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMF1xlJmCZ93CCpnkfG4dsN/XOU4sGxK
+zSKxy9RvplraKt1ByMJJisSjs8H2FIf0G2mJQb2ApRw8EgJExYSkxEgzBeUTjAEG
+zwi+moYnYLrmoujzbyPF2YMTud+vN4NF2s5R1Nbc0qbLPMcG680wcOyYzOQKpZHX
+KVp/ccW+ZmkdKy3+yElEWQvFo+pJ/ZOx11NAXxdzdpmVhmYlR5ftQmkIiAgRQiBp
+mIpD/uSM5oeB3SK2ppzSg3UTH5MrEozihvp9JRwGKtJ+8Bbxh83VToMrNbiTD3S6
+kKqLx2FnJCqx/W1iFA0YxMC4xo/DdIRXMkrX3obmVS8dHhkdcSFo07sCAwEAAaMk
+MCIwIAYDVR0RBBkwF4IVYS5wcmVsb2FkLmV4YW1wbGUuY29tMAsGCSqGSIb3DQEB
+CwOCAQEADFLPsTqfQBQGZmIT9RdQbLA6wwCxz2pyD7uAMADHIb/2cW/qrM14VEmD
+T2k0QLEgiY4qQzq9FtOr+dagH4EH7eQgg2P832I+Ofw84jaT+9htZOTjcquaNjD2
+BYjeAgXKF4hi1l31UKiysrGFKq5w/KBxEJ/qf3tkQUok+0Ns5PA2M3je3g4WXV5g
+32/mivABQ3o8XEc39cvtBKwJKwHp0nBMmfUavowfPvtca2FVEoOIWPXyHAiRmbp8
+1WGBCMZ4eTxC9Rsb64yG9QaZDIQkX+JZAeeBvqiMQCUki7jD3ycD/rCKxjkPFEp0
+Cuh+dHH06CO/e4xKVBV4+M7cmqpElA==
+-----END CERTIFICATE-----
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_pinning_dynamic/a.preload.example.com-pinningroot.pem.certspec
@@ -0,0 +1,4 @@
+issuer:pinningroot
+subject:test end-entity
+subjectKey:alternate
+extension:subjectAlternativeName:a.preload.example.com
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_pinning_dynamic/b.preload.example.com-badca.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC3DCCAcagAwIBAgIUHXRy6Hn2zmZm180jDXeS1VCXV2kwCwYJKoZIhvcNAQEL
+MBAxDjAMBgNVBAMMBWJhZGNhMCIYDzIwMTQxMTI3MDAwMDAwWhgPMjAxNzAyMDQw
+MDAwMDBaMBoxGDAWBgNVBAMMD3Rlc3QgZW5kLWVudGl0eTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAMF1xlJmCZ93CCpnkfG4dsN/XOU4sGxKzSKxy9Rv
+plraKt1ByMJJisSjs8H2FIf0G2mJQb2ApRw8EgJExYSkxEgzBeUTjAEGzwi+moYn
+YLrmoujzbyPF2YMTud+vN4NF2s5R1Nbc0qbLPMcG680wcOyYzOQKpZHXKVp/ccW+
+ZmkdKy3+yElEWQvFo+pJ/ZOx11NAXxdzdpmVhmYlR5ftQmkIiAgRQiBpmIpD/uSM
+5oeB3SK2ppzSg3UTH5MrEozihvp9JRwGKtJ+8Bbxh83VToMrNbiTD3S6kKqLx2Fn
+JCqx/W1iFA0YxMC4xo/DdIRXMkrX3obmVS8dHhkdcSFo07sCAwEAAaMkMCIwIAYD
+VR0RBBkwF4IVYi5wcmVsb2FkLmV4YW1wbGUuY29tMAsGCSqGSIb3DQEBCwOCAQEA
+aF/5/1oFnoQIrst6NqmPSbV4thJhYGVUlxDteXjtdr/JSOeI/0CiSEM8TSKqe7Kp
+a2X4lMd2IxDOTGwKH6Kf9a19gpBE7sfDOTUEHEliXhqaNBlJ/NlQejLJg5fwDdZY
+NVPLtJAa88m9VIuw5vrzrNc4QKuf9YdIPDCjU8l/FA+d0puRQsyl161+0EyKn0GF
+X3McirP9OMD7od3URyKI0YLxQOGKrnxCaAlANkV5B9Y9IuaDCdOdlfU9qasXEmGg
+xpopsqviT/PkbbENsncdnVujd1KjTzRKgcCesr+wcwFvtME3bz5bRiN5MUxoX/uV
+AnPfLFW187AVFCTuPBCa9w==
+-----END CERTIFICATE-----
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_pinning_dynamic/b.preload.example.com-badca.pem.certspec
@@ -0,0 +1,5 @@
+issuer:badca
+subject:test end-entity
+issuerKey:alternate
+subjectKey:alternate
+extension:subjectAlternativeName:b.preload.example.com
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_pinning_dynamic/b.preload.example.com-pinningroot.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC4jCCAcygAwIBAgIUdeK/iWA0AXtD6Wtgli7w7YOzrQQwCwYJKoZIhvcNAQEL
+MBYxFDASBgNVBAMMC3Bpbm5pbmdyb290MCIYDzIwMTQxMTI3MDAwMDAwWhgPMjAx
+NzAyMDQwMDAwMDBaMBoxGDAWBgNVBAMMD3Rlc3QgZW5kLWVudGl0eTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMF1xlJmCZ93CCpnkfG4dsN/XOU4sGxK
+zSKxy9RvplraKt1ByMJJisSjs8H2FIf0G2mJQb2ApRw8EgJExYSkxEgzBeUTjAEG
+zwi+moYnYLrmoujzbyPF2YMTud+vN4NF2s5R1Nbc0qbLPMcG680wcOyYzOQKpZHX
+KVp/ccW+ZmkdKy3+yElEWQvFo+pJ/ZOx11NAXxdzdpmVhmYlR5ftQmkIiAgRQiBp
+mIpD/uSM5oeB3SK2ppzSg3UTH5MrEozihvp9JRwGKtJ+8Bbxh83VToMrNbiTD3S6
+kKqLx2FnJCqx/W1iFA0YxMC4xo/DdIRXMkrX3obmVS8dHhkdcSFo07sCAwEAAaMk
+MCIwIAYDVR0RBBkwF4IVYi5wcmVsb2FkLmV4YW1wbGUuY29tMAsGCSqGSIb3DQEB
+CwOCAQEAMREuv9I4XtY9fzxxiXqqV0D1ZRwxo+JKoX/eCoosKAUv9AUnoMp0HLVA
+yyOPA/Vx7lG/svQ/FvfbPLZ2vaU8ORdPP/drkkXQEkGgamqqK9jBMCYGoETseQSi
+zOy0Gm0rP3jcdQa1c5X0LItT8gJvFMJaj8NiuVrqvAbSnUHUKPK3okqkyZn8iX+8
++8RhCZ2lZybwuvPewXvpgFxTMNZNV5wxRHcEk/9LYA2BxNsAqHj5sAOrR2JEPzYE
+L8ux8pBWfhJ43TOVWoZj4W9Dff9m31V6JNQDgNFMzXQYlxzZBb1tTM5GNvWv0EkC
+IzWM/TTTRRU/p3cDrbzxtQYEz5/ELA==
+-----END CERTIFICATE-----
new file mode 100644
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_pinning_dynamic/b.preload.example.com-pinningroot.pem.certspec
@@ -0,0 +1,4 @@
+issuer:pinningroot
+subject:test end-entity
+subjectKey:alternate
+extension:subjectAlternativeName:b.preload.example.com
--- a/security/manager/ssl/tests/unit/test_pinning_dynamic/moz.build
+++ b/security/manager/ssl/tests/unit/test_pinning_dynamic/moz.build
@@ -4,18 +4,22 @@
 # 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/.
 
 # Temporarily disabled. See bug 1256495.
 #test_certificates = (
 #    'badca.pem',
 #    'a.pinning2.example.com-badca.pem',
 #    'a.pinning2.example.com-pinningroot.pem',
+#    'a.preload.example.com-badca.pem',
+#    'a.preload.example.com-pinningroot.pem',
 #    'b.pinning2.example.com-badca.pem',
 #    'b.pinning2.example.com-pinningroot.pem',
+#    'b.preload.example.com-badca.pem',
+#    'b.preload.example.com-pinningroot.pem',
 #    'x.a.pinning2.example.com-badca.pem',
 #    'x.a.pinning2.example.com-pinningroot.pem',
 #    'x.b.pinning2.example.com-badca.pem',
 #    'x.b.pinning2.example.com-pinningroot.pem',
 #    'pinningroot.pem',
 #)
 #
 #for test_certificate in test_certificates:
--- a/security/manager/ssl/tests/unit/test_sss_eviction.js
+++ b/security/manager/ssl/tests/unit/test_sss_eviction.js
@@ -5,16 +5,20 @@
 
 // The purpose of this test is to check that a frequently visited site
 // will not be evicted over an infrequently visited site.
 
 var gSSService = null;
 var gProfileDir = null;
 
 function do_state_written(aSubject, aTopic, aData) {
+  if (aData == PRELOAD_STATE_FILE_NAME) {
+    return;
+  }
+
   equal(aData, SSS_STATE_FILE_NAME);
 
   let stateFile = gProfileDir.clone();
   stateFile.append(SSS_STATE_FILE_NAME);
   ok(stateFile.exists());
   let stateFileContents = readFile(stateFile);
   // the last part is removed because it's the empty string after the final \n
   let lines = stateFileContents.split('\n').slice(0, -1);
@@ -36,16 +40,20 @@ function do_state_written(aSubject, aTop
     }
   }
 
   ok(foundLegitSite);
   do_test_finished();
 }
 
 function do_state_read(aSubject, aTopic, aData) {
+  if (aData == PRELOAD_STATE_FILE_NAME) {
+    return;
+  }
+
   equal(aData, SSS_STATE_FILE_NAME);
 
   ok(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
                              "frequentlyused.example.com", 0));
   let sslStatus = new FakeSSLStatus();
   for (let i = 0; i < 2000; i++) {
     let uri = Services.io.newURI("http://bad" + i + ".example.com", null, null);
     gSSService.processHeader(Ci.nsISiteSecurityService.HEADER_HSTS, uri,
--- a/security/manager/ssl/tests/unit/test_sss_readstate.js
+++ b/security/manager/ssl/tests/unit/test_sss_readstate.js
@@ -8,16 +8,20 @@
 
 function writeLine(aLine, aOutputStream) {
   aOutputStream.write(aLine, aLine.length);
 }
 
 var gSSService = null;
 
 function checkStateRead(aSubject, aTopic, aData) {
+  if (aData == PRELOAD_STATE_FILE_NAME) {
+    return;
+  }
+
   equal(aData, SSS_STATE_FILE_NAME);
 
   ok(!gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
                               "expired.example.com", 0));
   ok(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
                              "notexpired.example.com", 0));
   ok(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
                              "bugzilla.mozilla.org", 0));
--- a/security/manager/ssl/tests/unit/test_sss_readstate_garbage.js
+++ b/security/manager/ssl/tests/unit/test_sss_readstate_garbage.js
@@ -8,16 +8,20 @@
 
 function writeLine(aLine, aOutputStream) {
   aOutputStream.write(aLine, aLine.length);
 }
 
 var gSSService = null;
 
 function checkStateRead(aSubject, aTopic, aData) {
+  if (aData == PRELOAD_STATE_FILE_NAME) {
+    return;
+  }
+
   equal(aData, SSS_STATE_FILE_NAME);
 
   ok(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
                              "example1.example.com", 0));
   ok(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
                              "example2.example.com", 0));
   ok(!gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
                               "example.com", 0));
--- a/security/manager/ssl/tests/unit/test_sss_readstate_huge.js
+++ b/security/manager/ssl/tests/unit/test_sss_readstate_huge.js
@@ -9,16 +9,20 @@
 
 function writeLine(aLine, aOutputStream) {
   aOutputStream.write(aLine, aLine.length);
 }
 
 var gSSService = null;
 
 function checkStateRead(aSubject, aTopic, aData) {
+  if (aData == PRELOAD_STATE_FILE_NAME) {
+    return;
+  }
+
   equal(aData, SSS_STATE_FILE_NAME);
 
   ok(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
                              "example0.example.com", 0));
   ok(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
                              "example423.example.com", 0));
   ok(gSSService.isSecureHost(Ci.nsISiteSecurityService.HEADER_HSTS,
                              "example1023.example.com", 0));
--- a/security/manager/ssl/tests/unit/test_sss_savestate.js
+++ b/security/manager/ssl/tests/unit/test_sss_savestate.js
@@ -13,16 +13,20 @@ var gProfileDir = null;
 
 const NON_ISSUED_KEY_HASH = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
 
 // For reference, the format of the state file is a list of:
 // <domain name> <expiration time in milliseconds>,<sts status>,<includeSubdomains>
 // separated by newlines ('\n')
 
 function checkStateWritten(aSubject, aTopic, aData) {
+  if (aData == PRELOAD_STATE_FILE_NAME) {
+    return;
+  }
+
   equal(aData, SSS_STATE_FILE_NAME);
 
   let stateFile = gProfileDir.clone();
   stateFile.append(SSS_STATE_FILE_NAME);
   ok(stateFile.exists());
   let stateFileContents = readFile(stateFile);
   // the last line is removed because it's just a trailing newline
   let lines = stateFileContents.split('\n').slice(0, -1);
@@ -92,17 +96,18 @@ function checkStateWritten(aSubject, aTo
 }
 
 function run_test() {
   Services.prefs.setIntPref("test.datastorage.write_timer_ms", 100);
   gProfileDir = do_get_profile();
   let SSService = Cc["@mozilla.org/ssservice;1"]
                     .getService(Ci.nsISiteSecurityService);
   // Put an HPKP entry
-  SSService.setKeyPins("dynamic-pin.example.com", true, 1000, 1,
+  SSService.setKeyPins("dynamic-pin.example.com", true,
+                       new Date().getTime() + 1000000, 1,
                        [NON_ISSUED_KEY_HASH]);
 
   let uris = [ Services.io.newURI("http://bugzilla.mozilla.org", null, null),
                Services.io.newURI("http://a.example.com", null, null),
                Services.io.newURI("http://b.example.com", null, null),
                Services.io.newURI("http://c.c.example.com", null, null),
                Services.io.newURI("http://d.example.com", null, null) ];