Give mojo_shell a TransportSecurityPersister.

This required moving TransportSecurityPersister into net.

BUG=310293
[email protected], [email protected]

Review URL: https://codereview.chromium.org/59693008

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@234667 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/net/http/transport_security_persister.cc b/net/http/transport_security_persister.cc
new file mode 100644
index 0000000..a7e72fd3
--- /dev/null
+++ b/net/http/transport_security_persister.cc
@@ -0,0 +1,315 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/http/transport_security_persister.h"
+
+#include "base/base64.h"
+#include "base/bind.h"
+#include "base/file_util.h"
+#include "base/files/file_path.h"
+#include "base/json/json_reader.h"
+#include "base/json/json_writer.h"
+#include "base/message_loop/message_loop.h"
+#include "base/message_loop/message_loop_proxy.h"
+#include "base/sequenced_task_runner.h"
+#include "base/task_runner_util.h"
+#include "base/values.h"
+#include "crypto/sha2.h"
+#include "net/cert/x509_certificate.h"
+#include "net/http/transport_security_state.h"
+
+using net::HashValue;
+using net::HashValueTag;
+using net::HashValueVector;
+using net::TransportSecurityState;
+
+namespace {
+
+ListValue* SPKIHashesToListValue(const HashValueVector& hashes) {
+  ListValue* pins = new ListValue;
+  for (size_t i = 0; i != hashes.size(); i++)
+    pins->Append(new StringValue(hashes[i].ToString()));
+  return pins;
+}
+
+void SPKIHashesFromListValue(const ListValue& pins, HashValueVector* hashes) {
+  size_t num_pins = pins.GetSize();
+  for (size_t i = 0; i < num_pins; ++i) {
+    std::string type_and_base64;
+    HashValue fingerprint;
+    if (pins.GetString(i, &type_and_base64) &&
+        fingerprint.FromString(type_and_base64)) {
+      hashes->push_back(fingerprint);
+    }
+  }
+}
+
+// This function converts the binary hashes to a base64 string which we can
+// include in a JSON file.
+std::string HashedDomainToExternalString(const std::string& hashed) {
+  std::string out;
+  base::Base64Encode(hashed, &out);
+  return out;
+}
+
+// This inverts |HashedDomainToExternalString|, above. It turns an external
+// string (from a JSON file) into an internal (binary) string.
+std::string ExternalStringToHashedDomain(const std::string& external) {
+  std::string out;
+  if (!base::Base64Decode(external, &out) ||
+      out.size() != crypto::kSHA256Length) {
+    return std::string();
+  }
+
+  return out;
+}
+
+const char kIncludeSubdomains[] = "include_subdomains";
+const char kStsIncludeSubdomains[] = "sts_include_subdomains";
+const char kPkpIncludeSubdomains[] = "pkp_include_subdomains";
+const char kMode[] = "mode";
+const char kExpiry[] = "expiry";
+const char kDynamicSPKIHashesExpiry[] = "dynamic_spki_hashes_expiry";
+const char kStaticSPKIHashes[] = "static_spki_hashes";
+const char kPreloadedSPKIHashes[] = "preloaded_spki_hashes";
+const char kDynamicSPKIHashes[] = "dynamic_spki_hashes";
+const char kForceHTTPS[] = "force-https";
+const char kStrict[] = "strict";
+const char kDefault[] = "default";
+const char kPinningOnly[] = "pinning-only";
+const char kCreated[] = "created";
+
+std::string LoadState(const base::FilePath& path) {
+  std::string result;
+  if (!base::ReadFileToString(path, &result)) {
+    return "";
+  }
+  return result;
+}
+
+}  // namespace
+
+
+namespace net {
+
+TransportSecurityPersister::TransportSecurityPersister(
+    TransportSecurityState* state,
+    const base::FilePath& profile_path,
+    base::SequencedTaskRunner* background_runner,
+    bool readonly)
+    : transport_security_state_(state),
+      writer_(profile_path.AppendASCII("TransportSecurity"), background_runner),
+      foreground_runner_(base::MessageLoop::current()->message_loop_proxy()),
+      background_runner_(background_runner),
+      readonly_(readonly),
+      weak_ptr_factory_(this) {
+  transport_security_state_->SetDelegate(this);
+
+  base::PostTaskAndReplyWithResult(
+      background_runner_,
+      FROM_HERE,
+      base::Bind(&::LoadState, writer_.path()),
+      base::Bind(&TransportSecurityPersister::CompleteLoad,
+                 weak_ptr_factory_.GetWeakPtr()));
+}
+
+TransportSecurityPersister::~TransportSecurityPersister() {
+  DCHECK(foreground_runner_->RunsTasksOnCurrentThread());
+
+  if (writer_.HasPendingWrite())
+    writer_.DoScheduledWrite();
+
+  transport_security_state_->SetDelegate(NULL);
+}
+
+void TransportSecurityPersister::StateIsDirty(
+    TransportSecurityState* state) {
+  DCHECK(foreground_runner_->RunsTasksOnCurrentThread());
+  DCHECK_EQ(transport_security_state_, state);
+
+  if (!readonly_)
+    writer_.ScheduleWrite(this);
+}
+
+bool TransportSecurityPersister::SerializeData(std::string* output) {
+  DCHECK(foreground_runner_->RunsTasksOnCurrentThread());
+
+  DictionaryValue toplevel;
+  base::Time now = base::Time::Now();
+  TransportSecurityState::Iterator state(*transport_security_state_);
+  for (; state.HasNext(); state.Advance()) {
+    const std::string& hostname = state.hostname();
+    const TransportSecurityState::DomainState& domain_state =
+        state.domain_state();
+
+    DictionaryValue* serialized = new DictionaryValue;
+    serialized->SetBoolean(kStsIncludeSubdomains,
+                           domain_state.sts_include_subdomains);
+    serialized->SetBoolean(kPkpIncludeSubdomains,
+                           domain_state.pkp_include_subdomains);
+    serialized->SetDouble(kCreated, domain_state.created.ToDoubleT());
+    serialized->SetDouble(kExpiry, domain_state.upgrade_expiry.ToDoubleT());
+    serialized->SetDouble(kDynamicSPKIHashesExpiry,
+                          domain_state.dynamic_spki_hashes_expiry.ToDoubleT());
+
+    switch (domain_state.upgrade_mode) {
+      case TransportSecurityState::DomainState::MODE_FORCE_HTTPS:
+        serialized->SetString(kMode, kForceHTTPS);
+        break;
+      case TransportSecurityState::DomainState::MODE_DEFAULT:
+        serialized->SetString(kMode, kDefault);
+        break;
+      default:
+        NOTREACHED() << "DomainState with unknown mode";
+        delete serialized;
+        continue;
+    }
+
+    serialized->Set(kStaticSPKIHashes,
+                    SPKIHashesToListValue(domain_state.static_spki_hashes));
+
+    if (now < domain_state.dynamic_spki_hashes_expiry) {
+      serialized->Set(kDynamicSPKIHashes,
+                      SPKIHashesToListValue(domain_state.dynamic_spki_hashes));
+    }
+
+    toplevel.Set(HashedDomainToExternalString(hostname), serialized);
+  }
+
+  base::JSONWriter::WriteWithOptions(&toplevel,
+                                     base::JSONWriter::OPTIONS_PRETTY_PRINT,
+                                     output);
+  return true;
+}
+
+bool TransportSecurityPersister::LoadEntries(const std::string& serialized,
+                                             bool* dirty) {
+  DCHECK(foreground_runner_->RunsTasksOnCurrentThread());
+
+  transport_security_state_->ClearDynamicData();
+  return Deserialize(serialized, dirty, transport_security_state_);
+}
+
+// static
+bool TransportSecurityPersister::Deserialize(const std::string& serialized,
+                                             bool* dirty,
+                                             TransportSecurityState* state) {
+  scoped_ptr<Value> value(base::JSONReader::Read(serialized));
+  DictionaryValue* dict_value = NULL;
+  if (!value.get() || !value->GetAsDictionary(&dict_value))
+    return false;
+
+  const base::Time current_time(base::Time::Now());
+  bool dirtied = false;
+
+  for (DictionaryValue::Iterator i(*dict_value); !i.IsAtEnd(); i.Advance()) {
+    const DictionaryValue* parsed = NULL;
+    if (!i.value().GetAsDictionary(&parsed)) {
+      LOG(WARNING) << "Could not parse entry " << i.key() << "; skipping entry";
+      continue;
+    }
+
+    std::string mode_string;
+    double created;
+    double expiry;
+    double dynamic_spki_hashes_expiry = 0.0;
+    TransportSecurityState::DomainState domain_state;
+
+    // kIncludeSubdomains is a legacy synonym for kStsIncludeSubdomains and
+    // kPkpIncludeSubdomains. Parse at least one of these properties,
+    // preferably the new ones.
+    bool include_subdomains = false;
+    bool parsed_include_subdomains = parsed->GetBoolean(kIncludeSubdomains,
+                                                        &include_subdomains);
+    domain_state.sts_include_subdomains = include_subdomains;
+    domain_state.pkp_include_subdomains = include_subdomains;
+    if (parsed->GetBoolean(kStsIncludeSubdomains, &include_subdomains)) {
+      domain_state.sts_include_subdomains = include_subdomains;
+      parsed_include_subdomains = true;
+    }
+    if (parsed->GetBoolean(kPkpIncludeSubdomains, &include_subdomains)) {
+      domain_state.pkp_include_subdomains = include_subdomains;
+      parsed_include_subdomains = true;
+    }
+
+    if (!parsed_include_subdomains ||
+        !parsed->GetString(kMode, &mode_string) ||
+        !parsed->GetDouble(kExpiry, &expiry)) {
+      LOG(WARNING) << "Could not parse some elements of entry " << i.key()
+                   << "; skipping entry";
+      continue;
+    }
+
+    // Don't fail if this key is not present.
+    parsed->GetDouble(kDynamicSPKIHashesExpiry,
+                      &dynamic_spki_hashes_expiry);
+
+    const ListValue* pins_list = NULL;
+    // preloaded_spki_hashes is a legacy synonym for static_spki_hashes.
+    if (parsed->GetList(kStaticSPKIHashes, &pins_list))
+      SPKIHashesFromListValue(*pins_list, &domain_state.static_spki_hashes);
+    else if (parsed->GetList(kPreloadedSPKIHashes, &pins_list))
+      SPKIHashesFromListValue(*pins_list, &domain_state.static_spki_hashes);
+
+    if (parsed->GetList(kDynamicSPKIHashes, &pins_list))
+      SPKIHashesFromListValue(*pins_list, &domain_state.dynamic_spki_hashes);
+
+    if (mode_string == kForceHTTPS || mode_string == kStrict) {
+      domain_state.upgrade_mode =
+          TransportSecurityState::DomainState::MODE_FORCE_HTTPS;
+    } else if (mode_string == kDefault || mode_string == kPinningOnly) {
+      domain_state.upgrade_mode =
+          TransportSecurityState::DomainState::MODE_DEFAULT;
+    } else {
+      LOG(WARNING) << "Unknown TransportSecurityState mode string "
+                   << mode_string << " found for entry " << i.key()
+                   << "; skipping entry";
+      continue;
+    }
+
+    domain_state.upgrade_expiry = base::Time::FromDoubleT(expiry);
+    domain_state.dynamic_spki_hashes_expiry =
+        base::Time::FromDoubleT(dynamic_spki_hashes_expiry);
+    if (parsed->GetDouble(kCreated, &created)) {
+      domain_state.created = base::Time::FromDoubleT(created);
+    } else {
+      // We're migrating an old entry with no creation date. Make sure we
+      // write the new date back in a reasonable time frame.
+      dirtied = true;
+      domain_state.created = base::Time::Now();
+    }
+
+    if (domain_state.upgrade_expiry <= current_time &&
+        domain_state.dynamic_spki_hashes_expiry <= current_time) {
+      // Make sure we dirty the state if we drop an entry.
+      dirtied = true;
+      continue;
+    }
+
+    std::string hashed = ExternalStringToHashedDomain(i.key());
+    if (hashed.empty()) {
+      dirtied = true;
+      continue;
+    }
+
+    state->AddOrUpdateEnabledHosts(hashed, domain_state);
+  }
+
+  *dirty = dirtied;
+  return true;
+}
+
+void TransportSecurityPersister::CompleteLoad(const std::string& state) {
+  DCHECK(foreground_runner_->RunsTasksOnCurrentThread());
+
+  bool dirty = false;
+  if (!LoadEntries(state, &dirty)) {
+    LOG(ERROR) << "Failed to deserialize state: " << state;
+    return;
+  }
+  if (dirty)
+    StateIsDirty(transport_security_state_);
+}
+
+}  // namespace net
diff --git a/net/http/transport_security_persister.h b/net/http/transport_security_persister.h
new file mode 100644
index 0000000..7725ba1
--- /dev/null
+++ b/net/http/transport_security_persister.h
@@ -0,0 +1,138 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// TransportSecurityState maintains an in memory database containing the
+// list of hosts that currently have transport security enabled. This
+// singleton object deals with writing that data out to disk as needed and
+// loading it at startup.
+
+// At startup we need to load the transport security state from the
+// disk. For the moment, we don't want to delay startup for this load, so we
+// let the TransportSecurityState run for a while without being loaded.
+// This means that it's possible for pages opened very quickly not to get the
+// correct transport security information.
+//
+// To load the state, we schedule a Task on file_task_runner, which
+// deserializes and configures the TransportSecurityState.
+//
+// The TransportSecurityState object supports running a callback function
+// when it changes. This object registers the callback, pointing at itself.
+//
+// TransportSecurityState calls...
+// TransportSecurityPersister::StateIsDirty
+//   since the callback isn't allowed to block or reenter, we schedule a Task
+//   on the file task runner after some small amount of time
+//
+// ...
+//
+// TransportSecurityPersister::SerializeState
+//   copies the current state of the TransportSecurityState, serializes
+//   and writes to disk.
+
+#ifndef NET_HTTP_TRANSPORT_SECURITY_PERSISTER_H_
+#define NET_HTTP_TRANSPORT_SECURITY_PERSISTER_H_
+
+#include <string>
+
+#include "base/files/file_path.h"
+#include "base/files/important_file_writer.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "net/base/net_export.h"
+#include "net/http/transport_security_state.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace net {
+
+// Reads and updates on-disk TransportSecurity state. Clients of this class
+// should create, destroy, and call into it from one thread.
+//
+// file_task_runner is the task runner this class should use internally to
+// perform file IO, and can optionally be associated with a different thread.
+class NET_EXPORT TransportSecurityPersister
+    : public TransportSecurityState::Delegate,
+      public base::ImportantFileWriter::DataSerializer {
+ public:
+  TransportSecurityPersister(TransportSecurityState* state,
+                             const base::FilePath& profile_path,
+                             base::SequencedTaskRunner* file_task_runner,
+                             bool readonly);
+  virtual ~TransportSecurityPersister();
+
+  // Called by the TransportSecurityState when it changes its state.
+  virtual void StateIsDirty(TransportSecurityState*) OVERRIDE;
+
+  // ImportantFileWriter::DataSerializer:
+  //
+  // Serializes |transport_security_state_| into |*output|. Returns true if
+  // all DomainStates were serialized correctly.
+  //
+  // The serialization format is JSON; the JSON represents a dictionary of
+  // host:DomainState pairs (host is a string). The DomainState is
+  // represented as a dictionary containing the following keys and value
+  // types (not all keys will always be present):
+  //
+  //     "sts_include_subdomains": true|false
+  //     "pkp_include_subdomains": true|false
+  //     "created": double
+  //     "expiry": double
+  //     "dynamic_spki_hashes_expiry": double
+  //     "mode": "default"|"force-https"
+  //             legacy value synonyms "strict" = "force-https"
+  //                                   "pinning-only" = "default"
+  //             legacy value "spdy-only" is unused and ignored
+  //     "static_spki_hashes": list of strings
+  //         legacy key synonym "preloaded_spki_hashes"
+  //     "bad_static_spki_hashes": list of strings
+  //         legacy key synonym "bad_preloaded_spki_hashes"
+  //     "dynamic_spki_hashes": list of strings
+  //
+  // The JSON dictionary keys are strings containing
+  // Base64(SHA256(TransportSecurityState::CanonicalizeHost(domain))).
+  // The reason for hashing them is so that the stored state does not
+  // trivially reveal a user's browsing history to an attacker reading the
+  // serialized state on disk.
+  virtual bool SerializeData(std::string* data) OVERRIDE;
+
+  // Clears any existing non-static entries, and then re-populates
+  // |transport_security_state_|.
+  //
+  // Sets |*dirty| to true if the new state differs from the persisted
+  // state; false otherwise.
+  bool LoadEntries(const std::string& serialized, bool* dirty);
+
+ private:
+  // Populates |state| from the JSON string |serialized|. Returns true if
+  // all entries were parsed and deserialized correctly.
+  //
+  // Sets |*dirty| to true if the new state differs from the persisted
+  // state; false otherwise.
+  static bool Deserialize(const std::string& serialized,
+                          bool* dirty,
+                          TransportSecurityState* state);
+
+  void CompleteLoad(const std::string& state);
+
+  TransportSecurityState* transport_security_state_;
+
+  // Helper for safely writing the data.
+  base::ImportantFileWriter writer_;
+
+  scoped_refptr<base::SequencedTaskRunner> foreground_runner_;
+  scoped_refptr<base::SequencedTaskRunner> background_runner_;
+
+  // Whether or not we're in read-only mode.
+  const bool readonly_;
+
+  base::WeakPtrFactory<TransportSecurityPersister> weak_ptr_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(TransportSecurityPersister);
+};
+
+}  // namespace net
+
+#endif  // NET_HTTP_TRANSPORT_SECURITY_PERSISTER_H_
diff --git a/net/http/transport_security_persister_unittest.cc b/net/http/transport_security_persister_unittest.cc
new file mode 100644
index 0000000..8c41f9e
--- /dev/null
+++ b/net/http/transport_security_persister_unittest.cc
@@ -0,0 +1,201 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/http/transport_security_persister.h"
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/file_util.h"
+#include "base/files/file_path.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/message_loop/message_loop.h"
+#include "net/http/transport_security_state.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using net::TransportSecurityPersister;
+using net::TransportSecurityState;
+
+class TransportSecurityPersisterTest : public testing::Test {
+ public:
+  TransportSecurityPersisterTest() {
+  }
+
+  virtual ~TransportSecurityPersisterTest() {
+    base::MessageLoopForIO::current()->RunUntilIdle();
+  }
+
+  virtual void SetUp() OVERRIDE {
+    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+    persister_.reset(new TransportSecurityPersister(
+        &state_,
+        temp_dir_.path(),
+        base::MessageLoopForIO::current()->message_loop_proxy(),
+        false));
+  }
+
+ protected:
+  base::ScopedTempDir temp_dir_;
+  TransportSecurityState state_;
+  scoped_ptr<TransportSecurityPersister> persister_;
+};
+
+TEST_F(TransportSecurityPersisterTest, SerializeData1) {
+  std::string output;
+  bool dirty;
+
+  EXPECT_TRUE(persister_->SerializeData(&output));
+  EXPECT_TRUE(persister_->LoadEntries(output, &dirty));
+  EXPECT_FALSE(dirty);
+}
+
+TEST_F(TransportSecurityPersisterTest, SerializeData2) {
+  TransportSecurityState::DomainState domain_state;
+  const base::Time current_time(base::Time::Now());
+  const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000);
+  static const char kYahooDomain[] = "yahoo.com";
+
+  EXPECT_FALSE(state_.GetDomainState(kYahooDomain, true, &domain_state));
+
+  bool include_subdomains = true;
+  state_.AddHSTS(kYahooDomain, expiry, include_subdomains);
+
+  std::string output;
+  bool dirty;
+  EXPECT_TRUE(persister_->SerializeData(&output));
+  EXPECT_TRUE(persister_->LoadEntries(output, &dirty));
+
+  EXPECT_TRUE(state_.GetDomainState(kYahooDomain, true, &domain_state));
+  EXPECT_EQ(domain_state.upgrade_mode,
+            TransportSecurityState::DomainState::MODE_FORCE_HTTPS);
+  EXPECT_TRUE(state_.GetDomainState("foo.yahoo.com", true, &domain_state));
+  EXPECT_EQ(domain_state.upgrade_mode,
+            TransportSecurityState::DomainState::MODE_FORCE_HTTPS);
+  EXPECT_TRUE(state_.GetDomainState("foo.bar.yahoo.com", true, &domain_state));
+  EXPECT_EQ(domain_state.upgrade_mode,
+            TransportSecurityState::DomainState::MODE_FORCE_HTTPS);
+  EXPECT_TRUE(state_.GetDomainState("foo.bar.baz.yahoo.com", true,
+                                   &domain_state));
+  EXPECT_EQ(domain_state.upgrade_mode,
+            TransportSecurityState::DomainState::MODE_FORCE_HTTPS);
+  EXPECT_FALSE(state_.GetDomainState("com", true, &domain_state));
+}
+
+TEST_F(TransportSecurityPersisterTest, SerializeData3) {
+  // Add an entry.
+  net::HashValue fp1(net::HASH_VALUE_SHA1);
+  memset(fp1.data(), 0, fp1.size());
+  net::HashValue fp2(net::HASH_VALUE_SHA1);
+  memset(fp2.data(), 1, fp2.size());
+  base::Time expiry =
+      base::Time::Now() + base::TimeDelta::FromSeconds(1000);
+  net::HashValueVector dynamic_spki_hashes;
+  dynamic_spki_hashes.push_back(fp1);
+  dynamic_spki_hashes.push_back(fp2);
+  bool include_subdomains = false;
+  state_.AddHSTS("www.example.com", expiry, include_subdomains);
+  state_.AddHPKP("www.example.com", expiry, include_subdomains,
+                 dynamic_spki_hashes);
+
+  // Add another entry.
+  memset(fp1.data(), 2, fp1.size());
+  memset(fp2.data(), 3, fp2.size());
+  expiry =
+      base::Time::Now() + base::TimeDelta::FromSeconds(3000);
+  dynamic_spki_hashes.push_back(fp1);
+  dynamic_spki_hashes.push_back(fp2);
+  state_.AddHSTS("www.example.net", expiry, include_subdomains);
+  state_.AddHPKP("www.example.net", expiry, include_subdomains,
+                 dynamic_spki_hashes);
+
+  // Save a copy of everything.
+  std::map<std::string, TransportSecurityState::DomainState> saved;
+  TransportSecurityState::Iterator i(state_);
+  while (i.HasNext()) {
+    saved[i.hostname()] = i.domain_state();
+    i.Advance();
+  }
+
+  std::string serialized;
+  EXPECT_TRUE(persister_->SerializeData(&serialized));
+
+  // Persist the data to the file. For the test to be fast and not flaky, we
+  // just do it directly rather than call persister_->StateIsDirty. (That uses
+  // ImportantFileWriter, which has an asynchronous commit interval rather
+  // than block.) Use a different basename just for cleanliness.
+  base::FilePath path =
+      temp_dir_.path().AppendASCII("TransportSecurityPersisterTest");
+  EXPECT_TRUE(file_util::WriteFile(path, serialized.c_str(),
+                                   serialized.size()));
+
+  // Read the data back.
+  std::string persisted;
+  EXPECT_TRUE(base::ReadFileToString(path, &persisted));
+  EXPECT_EQ(persisted, serialized);
+  bool dirty;
+  EXPECT_TRUE(persister_->LoadEntries(persisted, &dirty));
+  EXPECT_FALSE(dirty);
+
+  // Check that states are the same as saved.
+  size_t count = 0;
+  TransportSecurityState::Iterator j(state_);
+  while (j.HasNext()) {
+    count++;
+    j.Advance();
+  }
+  EXPECT_EQ(count, saved.size());
+}
+
+TEST_F(TransportSecurityPersisterTest, SerializeDataOld) {
+  // This is an old-style piece of transport state JSON, which has no creation
+  // date.
+  std::string output =
+      "{ "
+      "\"NiyD+3J1r6z1wjl2n1ALBu94Zj9OsEAMo0kCN8js0Uk=\": {"
+      "\"expiry\": 1266815027.983453, "
+      "\"include_subdomains\": false, "
+      "\"mode\": \"strict\" "
+      "}"
+      "}";
+  bool dirty;
+  EXPECT_TRUE(persister_->LoadEntries(output, &dirty));
+  EXPECT_TRUE(dirty);
+}
+
+TEST_F(TransportSecurityPersisterTest, PublicKeyHashes) {
+  TransportSecurityState::DomainState domain_state;
+  static const char kTestDomain[] = "example.com";
+  EXPECT_FALSE(state_.GetDomainState(kTestDomain, false, &domain_state));
+  net::HashValueVector hashes;
+  EXPECT_FALSE(domain_state.CheckPublicKeyPins(hashes));
+
+  net::HashValue sha1(net::HASH_VALUE_SHA1);
+  memset(sha1.data(), '1', sha1.size());
+  domain_state.dynamic_spki_hashes.push_back(sha1);
+
+  EXPECT_FALSE(domain_state.CheckPublicKeyPins(hashes));
+
+  hashes.push_back(sha1);
+  EXPECT_TRUE(domain_state.CheckPublicKeyPins(hashes));
+
+  hashes[0].data()[0] = '2';
+  EXPECT_FALSE(domain_state.CheckPublicKeyPins(hashes));
+
+  const base::Time current_time(base::Time::Now());
+  const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000);
+  bool include_subdomains = false;
+  state_.AddHSTS(kTestDomain, expiry, include_subdomains);
+  state_.AddHPKP(kTestDomain, expiry, include_subdomains,
+                 domain_state.dynamic_spki_hashes);
+  std::string ser;
+  EXPECT_TRUE(persister_->SerializeData(&ser));
+  bool dirty;
+  EXPECT_TRUE(persister_->LoadEntries(ser, &dirty));
+  EXPECT_TRUE(state_.GetDomainState(kTestDomain, false, &domain_state));
+  EXPECT_EQ(1u, domain_state.dynamic_spki_hashes.size());
+  EXPECT_EQ(sha1.tag, domain_state.dynamic_spki_hashes[0].tag);
+  EXPECT_EQ(0, memcmp(domain_state.dynamic_spki_hashes[0].data(), sha1.data(),
+                      sha1.size()));
+}
diff --git a/net/http/transport_security_state.h b/net/http/transport_security_state.h
index ccbc53a0..97b4d7c 100644
--- a/net/http/transport_security_state.h
+++ b/net/http/transport_security_state.h
@@ -35,7 +35,7 @@
 class NET_EXPORT TransportSecurityState
     : NON_EXPORTED_BASE(public base::NonThreadSafe) {
  public:
-  class Delegate {
+  class NET_EXPORT Delegate {
    public:
     // This function may not block and may be called with internal locks held.
     // Thus it must not reenter the TransportSecurityState object.