summaryrefslogtreecommitdiffstats
path: root/net/base/transport_security_state.cc
diff options
context:
space:
mode:
authoragl@chromium.org <agl@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-12-11 21:04:42 +0000
committeragl@chromium.org <agl@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-12-11 21:04:42 +0000
commit326e67907033c1e8db115327d59482b1ae6db3ec (patch)
tree2940d88e6de6ce6ba528c4671b6574b4ff6bd1bd /net/base/transport_security_state.cc
parent5973945e4c3d2baf2b92d11be55c1692a09b12e3 (diff)
downloadchromium_src-326e67907033c1e8db115327d59482b1ae6db3ec.zip
chromium_src-326e67907033c1e8db115327d59482b1ae6db3ec.tar.gz
chromium_src-326e67907033c1e8db115327d59482b1ae6db3ec.tar.bz2
SPDY: augment Strict Transport Security with the beginnings of SPDY upgrade.
This adds an opportunistic flag to the information that we store in the Strict Transport Security State. Given this, STSS might be misnamed now, but renaming it in this patch would add huge amounts of noise. We process the 'X-Bodge-Transport-Security' header which has the same format as the STS header. When we see this on an HTTP connection, we'll probe for a clean HTTPS path to the host and then remember it. This header should be considered mutually exclusive with STS, although this isn't enforced in the code. The remembered flag is currently ignored by the rest of the code. This will be addressed in a future patch. The header should be called 'Opportunistic-Transport-Security' in the future, but we have some issues to work out before we take that name. http://codereview.chromium.org/456011 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@34380 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net/base/transport_security_state.cc')
-rw-r--r--net/base/transport_security_state.cc337
1 files changed, 337 insertions, 0 deletions
diff --git a/net/base/transport_security_state.cc b/net/base/transport_security_state.cc
new file mode 100644
index 0000000..35b930c
--- /dev/null
+++ b/net/base/transport_security_state.cc
@@ -0,0 +1,337 @@
+// Copyright (c) 2009 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/base/transport_security_state.h"
+
+#include "base/base64.h"
+#include "base/json/json_reader.h"
+#include "base/json/json_writer.h"
+#include "base/logging.h"
+#include "base/scoped_ptr.h"
+#include "base/sha2.h"
+#include "base/string_tokenizer.h"
+#include "base/string_util.h"
+#include "base/values.h"
+#include "googleurl/src/gurl.h"
+#include "net/base/dns_util.h"
+
+namespace net {
+
+TransportSecurityState::TransportSecurityState()
+ : delegate_(NULL) {
+}
+
+void TransportSecurityState::EnableHost(const std::string& host,
+ const DomainState& state) {
+ const std::string canonicalised_host = CanonicaliseHost(host);
+ if (canonicalised_host.empty())
+ return;
+ char hashed[base::SHA256_LENGTH];
+ base::SHA256HashString(canonicalised_host, hashed, sizeof(hashed));
+
+ AutoLock lock(lock_);
+
+ enabled_hosts_[std::string(hashed, sizeof(hashed))] = state;
+ DirtyNotify();
+}
+
+bool TransportSecurityState::IsEnabledForHost(DomainState* result,
+ const std::string& host) {
+ const std::string canonicalised_host = CanonicaliseHost(host);
+ if (canonicalised_host.empty())
+ return false;
+
+ base::Time current_time(base::Time::Now());
+ AutoLock lock(lock_);
+
+ for (size_t i = 0; canonicalised_host[i]; i += canonicalised_host[i] + 1) {
+ char hashed_domain[base::SHA256_LENGTH];
+
+ base::SHA256HashString(&canonicalised_host[i], &hashed_domain,
+ sizeof(hashed_domain));
+ std::map<std::string, DomainState>::iterator j =
+ enabled_hosts_.find(std::string(hashed_domain, sizeof(hashed_domain)));
+ if (j == enabled_hosts_.end())
+ continue;
+
+ if (current_time > j->second.expiry) {
+ enabled_hosts_.erase(j);
+ DirtyNotify();
+ continue;
+ }
+
+ *result = j->second;
+
+ // If we matched the domain exactly, it doesn't matter what the value of
+ // include_subdomains is.
+ if (i == 0)
+ return true;
+
+ return j->second.include_subdomains;
+ }
+
+ return false;
+}
+
+// "Strict-Transport-Security" ":"
+// "max-age" "=" delta-seconds [ ";" "includeSubDomains" ]
+bool TransportSecurityState::ParseHeader(const std::string& value,
+ int* max_age,
+ bool* include_subdomains) {
+ DCHECK(max_age);
+ DCHECK(include_subdomains);
+
+ int max_age_candidate;
+
+ enum ParserState {
+ START,
+ AFTER_MAX_AGE_LABEL,
+ AFTER_MAX_AGE_EQUALS,
+ AFTER_MAX_AGE,
+ AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER,
+ AFTER_INCLUDE_SUBDOMAINS,
+ } state = START;
+
+ StringTokenizer tokenizer(value, " \t=;");
+ tokenizer.set_options(StringTokenizer::RETURN_DELIMS);
+ while (tokenizer.GetNext()) {
+ DCHECK(!tokenizer.token_is_delim() || tokenizer.token().length() == 1);
+ switch (state) {
+ case START:
+ if (IsAsciiWhitespace(*tokenizer.token_begin()))
+ continue;
+ if (!LowerCaseEqualsASCII(tokenizer.token(), "max-age"))
+ return false;
+ state = AFTER_MAX_AGE_LABEL;
+ break;
+
+ case AFTER_MAX_AGE_LABEL:
+ if (IsAsciiWhitespace(*tokenizer.token_begin()))
+ continue;
+ if (*tokenizer.token_begin() != '=')
+ return false;
+ DCHECK(tokenizer.token().length() == 1);
+ state = AFTER_MAX_AGE_EQUALS;
+ break;
+
+ case AFTER_MAX_AGE_EQUALS:
+ if (IsAsciiWhitespace(*tokenizer.token_begin()))
+ continue;
+ if (!StringToInt(tokenizer.token(), &max_age_candidate))
+ return false;
+ if (max_age_candidate < 0)
+ return false;
+ state = AFTER_MAX_AGE;
+ break;
+
+ case AFTER_MAX_AGE:
+ if (IsAsciiWhitespace(*tokenizer.token_begin()))
+ continue;
+ if (*tokenizer.token_begin() != ';')
+ return false;
+ state = AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER;
+ break;
+
+ case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER:
+ if (IsAsciiWhitespace(*tokenizer.token_begin()))
+ continue;
+ if (!LowerCaseEqualsASCII(tokenizer.token(), "includesubdomains"))
+ return false;
+ state = AFTER_INCLUDE_SUBDOMAINS;
+ break;
+
+ case AFTER_INCLUDE_SUBDOMAINS:
+ if (!IsAsciiWhitespace(*tokenizer.token_begin()))
+ return false;
+ break;
+
+ default:
+ NOTREACHED();
+ }
+ }
+
+ // We've consumed all the input. Let's see what state we ended up in.
+ switch (state) {
+ case START:
+ case AFTER_MAX_AGE_LABEL:
+ case AFTER_MAX_AGE_EQUALS:
+ return false;
+ case AFTER_MAX_AGE:
+ *max_age = max_age_candidate;
+ *include_subdomains = false;
+ return true;
+ case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER:
+ return false;
+ case AFTER_INCLUDE_SUBDOMAINS:
+ *max_age = max_age_candidate;
+ *include_subdomains = true;
+ return true;
+ default:
+ NOTREACHED();
+ return false;
+ }
+}
+
+void TransportSecurityState::SetDelegate(
+ TransportSecurityState::Delegate* delegate) {
+ AutoLock lock(lock_);
+
+ delegate_ = delegate;
+}
+
+// This function converts the binary hashes, which we store in
+// |enabled_hosts_|, to a base64 string which we can include in a JSON file.
+static std::wstring HashedDomainToExternalString(const std::string& hashed) {
+ std::string out;
+ CHECK(base::Base64Encode(hashed, &out));
+ return ASCIIToWide(out);
+}
+
+// This inverts |HashedDomainToExternalString|, above. It turns an external
+// string (from a JSON file) into an internal (binary) string.
+static std::string ExternalStringToHashedDomain(const std::wstring& external) {
+ std::string external_ascii = WideToASCII(external);
+ std::string out;
+ if (!base::Base64Decode(external_ascii, &out) ||
+ out.size() != base::SHA256_LENGTH) {
+ return std::string();
+ }
+
+ return out;
+}
+
+bool TransportSecurityState::Serialise(std::string* output) {
+ AutoLock lock(lock_);
+
+ DictionaryValue toplevel;
+ for (std::map<std::string, DomainState>::const_iterator
+ i = enabled_hosts_.begin(); i != enabled_hosts_.end(); ++i) {
+ DictionaryValue* state = new DictionaryValue;
+ state->SetBoolean(L"include_subdomains", i->second.include_subdomains);
+ state->SetReal(L"expiry", i->second.expiry.ToDoubleT());
+
+ switch (i->second.mode) {
+ case DomainState::MODE_STRICT:
+ state->SetString(L"mode", "strict");
+ break;
+ case DomainState::MODE_OPPORTUNISTIC:
+ state->SetString(L"mode", "opportunistic");
+ break;
+ case DomainState::MODE_SPDY_ONLY:
+ state->SetString(L"mode", "spdy-only");
+ break;
+ default:
+ NOTREACHED() << "DomainState with unknown mode";
+ delete state;
+ continue;
+ }
+
+ toplevel.Set(HashedDomainToExternalString(i->first), state);
+ }
+
+ base::JSONWriter::Write(&toplevel, true /* pretty print */, output);
+ return true;
+}
+
+bool TransportSecurityState::Deserialise(const std::string& input) {
+ AutoLock lock(lock_);
+
+ enabled_hosts_.clear();
+
+ scoped_ptr<Value> value(
+ base::JSONReader::Read(input, false /* do not allow trailing commas */));
+ if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY))
+ return false;
+
+ DictionaryValue* dict_value = reinterpret_cast<DictionaryValue*>(value.get());
+ const base::Time current_time(base::Time::Now());
+
+ for (DictionaryValue::key_iterator i = dict_value->begin_keys();
+ i != dict_value->end_keys(); ++i) {
+ DictionaryValue* state;
+ if (!dict_value->GetDictionaryWithoutPathExpansion(*i, &state))
+ continue;
+
+ bool include_subdomains;
+ std::string mode_string;
+ double expiry;
+
+ if (!state->GetBoolean(L"include_subdomains", &include_subdomains) ||
+ !state->GetString(L"mode", &mode_string) ||
+ !state->GetReal(L"expiry", &expiry)) {
+ continue;
+ }
+
+ DomainState::Mode mode;
+ if (mode_string == "strict") {
+ mode = DomainState::MODE_STRICT;
+ } else if (mode_string == "opportunistic") {
+ mode = DomainState::MODE_OPPORTUNISTIC;
+ } else if (mode_string == "spdy-only") {
+ mode = DomainState::MODE_SPDY_ONLY;
+ } else {
+ LOG(WARNING) << "Unknown TransportSecurityState mode string found: "
+ << mode_string;
+ continue;
+ }
+
+ base::Time expiry_time = base::Time::FromDoubleT(expiry);
+ if (expiry_time <= current_time)
+ continue;
+
+ std::string hashed = ExternalStringToHashedDomain(*i);
+ if (hashed.empty())
+ continue;
+
+ DomainState new_state;
+ new_state.mode = mode;
+ new_state.expiry = expiry_time;
+ new_state.include_subdomains = include_subdomains;
+ enabled_hosts_[hashed] = new_state;
+ }
+
+ return true;
+}
+
+void TransportSecurityState::DirtyNotify() {
+ if (delegate_)
+ delegate_->StateIsDirty(this);
+}
+
+// static
+std::string TransportSecurityState::CanonicaliseHost(const std::string& host) {
+ // We cannot perform the operations as detailed in the spec here as |host|
+ // has already undergone IDN processing before it reached us. Thus, we check
+ // that there are no invalid characters in the host and lowercase the result.
+
+ std::string new_host;
+ if (!DNSDomainFromDot(host, &new_host)) {
+ NOTREACHED();
+ return std::string();
+ }
+
+ for (size_t i = 0; new_host[i]; i += new_host[i] + 1) {
+ const unsigned label_length = static_cast<unsigned>(new_host[i]);
+ if (!label_length)
+ break;
+
+ for (size_t j = 0; j < label_length; ++j) {
+ // RFC 3490, 4.1, step 3
+ if (!IsSTD3ASCIIValidCharacter(new_host[i + 1 + j]))
+ return std::string();
+
+ new_host[i + 1 + j] = tolower(new_host[i + 1 + j]);
+ }
+
+ // step 3(b)
+ if (new_host[i + 1] == '-' ||
+ new_host[i + label_length] == '-') {
+ return std::string();
+ }
+ }
+
+ return new_host;
+}
+
+} // namespace