Index: chrome/browser/transport_security_persister.cc |
=================================================================== |
--- chrome/browser/transport_security_persister.cc (revision 134551) |
+++ chrome/browser/transport_security_persister.cc (working copy) |
@@ -4,17 +4,90 @@ |
#include "chrome/browser/transport_security_persister.h" |
+#include "base/base64.h" |
#include "base/bind.h" |
#include "base/file_path.h" |
#include "base/file_util.h" |
+#include "base/json/json_reader.h" |
+#include "base/json/json_writer.h" |
#include "base/message_loop.h" |
#include "base/path_service.h" |
+#include "base/values.h" |
#include "chrome/common/chrome_paths.h" |
#include "content/public/browser/browser_thread.h" |
+#include "crypto/sha2.h" |
#include "net/base/transport_security_state.h" |
+#include "net/base/x509_certificate.h" |
using content::BrowserThread; |
+using net::Fingerprint; |
+using net::FingerprintVector; |
+using net::TransportSecurityState; |
+namespace { |
+ |
+ListValue* SPKIHashesToListValue(const FingerprintVector& hashes) { |
+ ListValue* pins = new ListValue; |
+ |
+ for (FingerprintVector::const_iterator i = hashes.begin(); |
+ i != hashes.end(); ++i) { |
+ std::string hash_str(reinterpret_cast<const char*>(i->data), |
+ sizeof(i->data)); |
+ std::string b64; |
+ base::Base64Encode(hash_str, &b64); |
+ pins->Append(new StringValue("sha1/" + b64)); |
+ } |
+ |
+ return pins; |
+} |
+ |
+void SPKIHashesFromListValue(const ListValue& pins, FingerprintVector* hashes) { |
+ size_t num_pins = pins.GetSize(); |
+ for (size_t i = 0; i < num_pins; ++i) { |
+ std::string type_and_base64; |
+ Fingerprint fingerprint; |
+ if (pins.GetString(i, &type_and_base64) && |
+ TransportSecurityState::ParsePin(type_and_base64, &fingerprint)) { |
+ 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 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"; |
+ |
+} // anonymous namespce |
+ |
class TransportSecurityPersister::Loader { |
public: |
Loader(const base::WeakPtr<TransportSecurityPersister>& persister, |
@@ -52,7 +125,7 @@ |
}; |
TransportSecurityPersister::TransportSecurityPersister( |
- net::TransportSecurityState* state, |
+ TransportSecurityState* state, |
const FilePath& profile_path, |
bool readonly) |
: transport_security_state_(state), |
@@ -80,28 +153,196 @@ |
transport_security_state_->SetDelegate(NULL); |
} |
-void TransportSecurityPersister::CompleteLoad(const std::string& state) { |
+void TransportSecurityPersister::StateIsDirty( |
+ TransportSecurityState* state) { |
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ DCHECK_EQ(transport_security_state_, state); |
- bool dirty = false; |
- if (!transport_security_state_->LoadEntries(state, &dirty)) { |
- LOG(ERROR) << "Failed to deserialize state: " << state; |
- return; |
+ if (!readonly_) |
+ writer_.ScheduleWrite(this); |
+} |
+ |
+bool TransportSecurityPersister::SerializeData(std::string* output) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ |
+ 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(kIncludeSubdomains, |
+ domain_state.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); |
} |
- if (dirty) |
- StateIsDirty(transport_security_state_); |
+ |
+ base::JSONWriter::WriteWithOptions(&toplevel, |
+ base::JSONWriter::OPTIONS_PRETTY_PRINT, |
+ output); |
+ return true; |
} |
-void TransportSecurityPersister::StateIsDirty( |
- net::TransportSecurityState* state) { |
+bool TransportSecurityPersister::DeserializeFromCommandLine( |
+ const std::string& serialized) { |
+ // Purposefully ignore |dirty| because we do not want to persist entries |
+ // deserialized in this way. |
+ bool dirty; |
+ return Deserialize(serialized, true, &dirty, transport_security_state_); |
+} |
+ |
+bool TransportSecurityPersister::LoadEntries(const std::string& serialized, |
+ bool* dirty) { |
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
- DCHECK_EQ(transport_security_state_, state); |
- if (!readonly_) |
- writer_.ScheduleWrite(this); |
+ transport_security_state_->Clear(); |
+ return Deserialize(serialized, false, dirty, transport_security_state_); |
} |
-bool TransportSecurityPersister::SerializeData(std::string* data) { |
+// static |
+bool TransportSecurityPersister::Deserialize(const std::string& serialized, |
+ bool forced, |
+ bool* dirty, |
+ TransportSecurityState* state) { |
+ scoped_ptr<Value> value(base::JSONReader::Read(serialized)); |
+ DictionaryValue* dict_value; |
+ if (!value.get() || !value->GetAsDictionary(&dict_value)) |
+ return false; |
+ |
+ const base::Time current_time(base::Time::Now()); |
+ bool dirtied = false; |
+ |
+ for (DictionaryValue::key_iterator i = dict_value->begin_keys(); |
+ i != dict_value->end_keys(); ++i) { |
+ DictionaryValue* parsed; |
+ if (!dict_value->GetDictionaryWithoutPathExpansion(*i, &parsed)) { |
+ LOG(WARNING) << "Could not parse entry " << *i << "; skipping entry"; |
+ continue; |
+ } |
+ |
+ bool include_subdomains; |
+ std::string mode_string; |
+ double created; |
+ double expiry; |
+ double dynamic_spki_hashes_expiry = 0.0; |
+ |
+ if (!parsed->GetBoolean(kIncludeSubdomains, &include_subdomains) || |
+ !parsed->GetString(kMode, &mode_string) || |
+ !parsed->GetDouble(kExpiry, &expiry)) { |
+ LOG(WARNING) << "Could not parse some elements of entry " << *i |
+ << "; skipping entry"; |
+ continue; |
+ } |
+ |
+ // Don't fail if this key is not present. |
+ parsed->GetDouble(kDynamicSPKIHashesExpiry, |
+ &dynamic_spki_hashes_expiry); |
+ |
+ ListValue* pins_list = NULL; |
+ FingerprintVector static_spki_hashes; |
+ // preloaded_spki_hashes is a legacy synonym for static_spki_hashes. |
+ if (parsed->GetList(kStaticSPKIHashes, &pins_list)) |
+ SPKIHashesFromListValue(*pins_list, &static_spki_hashes); |
+ else if (parsed->GetList(kPreloadedSPKIHashes, &pins_list)) |
+ SPKIHashesFromListValue(*pins_list, &static_spki_hashes); |
+ |
+ FingerprintVector dynamic_spki_hashes; |
+ if (parsed->GetList(kDynamicSPKIHashes, &pins_list)) |
+ SPKIHashesFromListValue(*pins_list, &dynamic_spki_hashes); |
+ |
+ TransportSecurityState::DomainState::UpgradeMode mode; |
+ if (mode_string == kForceHTTPS || mode_string == kStrict) { |
+ mode = TransportSecurityState::DomainState::MODE_FORCE_HTTPS; |
+ } else if (mode_string == kDefault || mode_string == kPinningOnly) { |
+ mode = TransportSecurityState::DomainState::MODE_DEFAULT; |
+ } else { |
+ LOG(WARNING) << "Unknown TransportSecurityState mode string " |
+ << mode_string << " found for entry " << *i |
+ << "; skipping entry"; |
+ continue; |
+ } |
+ |
+ base::Time expiry_time = base::Time::FromDoubleT(expiry); |
+ base::Time dynamic_spki_hashes_expiry_time = |
+ base::Time::FromDoubleT(dynamic_spki_hashes_expiry); |
+ base::Time created_time; |
+ if (parsed->GetDouble(kCreated, &created)) { |
+ created_time = 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; |
+ created_time = base::Time::Now(); |
+ } |
+ |
+ if (expiry_time <= current_time && |
+ dynamic_spki_hashes_expiry_time <= current_time) { |
+ // Make sure we dirty the state if we drop an entry. |
+ dirtied = true; |
+ continue; |
+ } |
+ |
+ std::string hashed = ExternalStringToHashedDomain(*i); |
+ if (hashed.empty()) { |
+ dirtied = true; |
+ continue; |
+ } |
+ |
+ TransportSecurityState::DomainState domain_state; |
+ domain_state.upgrade_mode = mode; |
+ domain_state.created = created_time; |
+ domain_state.upgrade_expiry = expiry_time; |
+ domain_state.include_subdomains = include_subdomains; |
+ domain_state.static_spki_hashes = static_spki_hashes; |
+ domain_state.dynamic_spki_hashes = dynamic_spki_hashes; |
+ domain_state.dynamic_spki_hashes_expiry = dynamic_spki_hashes_expiry_time; |
+ |
+ if (forced) |
+ state->AddOrUpdateForcedHosts(hashed, domain_state); |
+ else |
+ state->AddOrUpdateEnabledHosts(hashed, domain_state); |
+ } |
+ |
+ *dirty = dirtied; |
+ return true; |
+} |
+ |
+void TransportSecurityPersister::CompleteLoad(const std::string& state) { |
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
- return transport_security_state_->Serialise(data); |
+ |
+ bool dirty = false; |
+ if (!LoadEntries(state, &dirty)) { |
+ LOG(ERROR) << "Failed to deserialize state: " << state; |
+ return; |
+ } |
+ if (dirty) |
+ StateIsDirty(transport_security_state_); |
} |