diff options
Diffstat (limited to 'net')
-rw-r--r-- | net/base/dns_util.cc | 71 | ||||
-rw-r--r-- | net/base/dns_util.h | 25 | ||||
-rw-r--r-- | net/base/dns_util_unittest.cc | 60 | ||||
-rw-r--r-- | net/base/strict_transport_security_state.cc | 123 | ||||
-rw-r--r-- | net/base/strict_transport_security_state.h | 6 | ||||
-rw-r--r-- | net/base/strict_transport_security_state_unittest.cc | 79 | ||||
-rw-r--r-- | net/net.gyp | 3 |
7 files changed, 343 insertions, 24 deletions
diff --git a/net/base/dns_util.cc b/net/base/dns_util.cc new file mode 100644 index 0000000..9c7e35a --- /dev/null +++ b/net/base/dns_util.cc @@ -0,0 +1,71 @@ +// 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/dns_util.h" + +namespace net { + +// Based on DJB's public domain code. +bool DNSDomainFromDot(const std::string& dotted, std::string* out) { + const char* buf = dotted.data(); + unsigned n = dotted.size(); + char label[63]; + unsigned int labellen = 0; /* <= sizeof label */ + char name[255]; + unsigned int namelen = 0; /* <= sizeof name */ + char ch; + + for (;;) { + if (!n) + break; + ch = *buf++; + --n; + if (ch == '.') { + if (labellen) { + if (namelen + labellen + 1 > sizeof name) + return false; + name[namelen++] = labellen; + memcpy(name + namelen, label, labellen); + namelen += labellen; + labellen = 0; + } + continue; + } + if (labellen >= sizeof label) + return false; + label[labellen++] = ch; + } + + if (labellen) { + if (namelen + labellen + 1 > sizeof name) + return false; + name[namelen++] = labellen; + memcpy(name + namelen, label, labellen); + namelen += labellen; + labellen = 0; + } + + if (namelen + 1 > sizeof name) + return false; + name[namelen++] = 0; + + *out = name; + return true; +} + +bool IsSTD3ASCIIValidCharacter(char c) { + if (c <= 0x2c) + return false; + if (c >= 0x7b) + return false; + if (c >= 0x2e && c <= 0x2f) + return false; + if (c >= 0x3a && c <= 0x40) + return false; + if (c >= 0x5b && c <= 0x60) + return false; + return true; +} + +} // namespace net diff --git a/net/base/dns_util.h b/net/base/dns_util.h new file mode 100644 index 0000000..8eb98f2 --- /dev/null +++ b/net/base/dns_util.h @@ -0,0 +1,25 @@ +// 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. + +#ifndef NET_BASE_DNS_UTIL_H_ +#define NET_BASE_DNS_UTIL_H_ + +#include <string> + +namespace net { + +// DNSDomainFromDot - convert a domain string to DNS format. From DJB's +// public domain DNS library. +// +// dotted: a string in dotted form: "www.google.com" +// out: a result in DNS form: "\x03www\x06google\x03com\x00" +bool DNSDomainFromDot(const std::string& dotted, std::string* out); + +// Returns true iff the given character is in the set of valid DNS label +// characters as given in RFC 3490, 4.1, 3(a) +bool IsSTD3ASCIIValidCharacter(char c); + +} // namespace net + +#endif // NET_BASE_DNS_UTIL_H_ diff --git a/net/base/dns_util_unittest.cc b/net/base/dns_util_unittest.cc new file mode 100644 index 0000000..7995b92 --- /dev/null +++ b/net/base/dns_util_unittest.cc @@ -0,0 +1,60 @@ +// 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/dns_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +class DNSUtilTest : public testing::Test { +}; + +TEST_F(DNSUtilTest, DNSDomainFromDot) { + std::string out; + + EXPECT_TRUE(DNSDomainFromDot("", &out)); + EXPECT_EQ(out, ""); + EXPECT_TRUE(DNSDomainFromDot("com", &out)); + EXPECT_EQ(out, "\003com"); + EXPECT_TRUE(DNSDomainFromDot("google.com", &out)); + EXPECT_EQ(out, "\x006google\003com"); + EXPECT_TRUE(DNSDomainFromDot("www.google.com", &out)); + EXPECT_EQ(out, "\003www\006google\003com"); + + // Label is 63 chars: still valid + EXPECT_TRUE(DNSDomainFromDot("123456789a123456789a123456789a123456789a123456789a123456789a123", &out)); + EXPECT_EQ(out, "\077123456789a123456789a123456789a123456789a123456789a123456789a123"); + + // Label is too long: invalid + EXPECT_FALSE(DNSDomainFromDot("123456789a123456789a123456789a123456789a123456789a123456789a1234", &out)); + + // 253 characters in the name: still valid + EXPECT_TRUE(DNSDomainFromDot("123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123", &out)); + EXPECT_EQ(out, "\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\011123456789\003123"); + + // 254 characters in the name: invalid + EXPECT_FALSE(DNSDomainFromDot("123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.1234", &out)); + + // Zero length labels should be dropped. + EXPECT_TRUE(DNSDomainFromDot("www.google.com.", &out)); + EXPECT_EQ(out, "\003www\006google\003com"); + + EXPECT_TRUE(DNSDomainFromDot(".google.com", &out)); + EXPECT_EQ(out, "\006google\003com"); +} + +TEST_F(DNSUtilTest, STD3ASCII) { + EXPECT_TRUE(IsSTD3ASCIIValidCharacter('a')); + EXPECT_TRUE(IsSTD3ASCIIValidCharacter('b')); + EXPECT_TRUE(IsSTD3ASCIIValidCharacter('c')); + EXPECT_TRUE(IsSTD3ASCIIValidCharacter('1')); + EXPECT_TRUE(IsSTD3ASCIIValidCharacter('2')); + EXPECT_TRUE(IsSTD3ASCIIValidCharacter('3')); + + EXPECT_FALSE(IsSTD3ASCIIValidCharacter('.')); + EXPECT_FALSE(IsSTD3ASCIIValidCharacter('/')); + EXPECT_FALSE(IsSTD3ASCIIValidCharacter('\x00')); +} + +} // namespace net diff --git a/net/base/strict_transport_security_state.cc b/net/base/strict_transport_security_state.cc index ac0b9fe..fc267c5 100644 --- a/net/base/strict_transport_security_state.cc +++ b/net/base/strict_transport_security_state.cc @@ -8,11 +8,13 @@ #include "base/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/registry_controlled_domain.h" +#include "net/base/base64.h" +#include "net/base/dns_util.h" namespace net { @@ -36,33 +38,54 @@ void StrictTransportSecurityState::DidReceiveHeader(const GURL& url, } void StrictTransportSecurityState::EnableHost(const std::string& host, - base::Time expiry, - bool include_subdomains) { - // TODO(abarth): Canonicalize host. + base::Time expiry, + bool include_subdomains) { + 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_); State state = {expiry, include_subdomains}; - enabled_hosts_[host] = state; + enabled_hosts_[std::string(hashed, sizeof(hashed))] = state; DirtyNotify(); } bool StrictTransportSecurityState::IsEnabledForHost(const std::string& host) { - // TODO(abarth): Canonicalize host. - // TODO: check for subdomains too. - - AutoLock lock(lock_); - std::map<std::string, State>::iterator i = enabled_hosts_.find(host); - if (i == enabled_hosts_.end()) + const std::string canonicalised_host = CanonicaliseHost(host); + if (canonicalised_host.empty()) return false; base::Time current_time(base::Time::Now()); - if (current_time > i->second.expiry) { - enabled_hosts_.erase(i); - DirtyNotify(); - return false; + 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, State>::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; + } + + // 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 true; + return false; } // "Strict-Transport-Security" ":" @@ -171,6 +194,27 @@ void StrictTransportSecurityState::SetDelegate( 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(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 (!Base64Decode(external_ascii, &out) || + out.size() != base::SHA256_LENGTH) { + return std::string(); + } + + return out; +} + bool StrictTransportSecurityState::Serialise(std::string* output) { AutoLock lock(lock_); @@ -181,7 +225,7 @@ bool StrictTransportSecurityState::Serialise(std::string* output) { state->SetBoolean(L"include_subdomains", i->second.include_subdomains); state->SetReal(L"expiry", i->second.expiry.ToDoubleT()); - toplevel.Set(ASCIIToWide(i->first), state); + toplevel.Set(HashedDomainToExternalString(i->first), state); } JSONWriter::Write(&toplevel, true /* pretty print */, output); @@ -207,7 +251,6 @@ bool StrictTransportSecurityState::Deserialise(const std::string& input) { if (!dict_value->GetDictionary(*i, &state)) continue; - const std::string host = WideToASCII(*i); bool include_subdomains; double expiry; @@ -220,11 +263,15 @@ bool StrictTransportSecurityState::Deserialise(const std::string& input) { if (expiry_time <= current_time) continue; + std::string hashed = ExternalStringToHashedDomain(*i); + if (hashed.empty()) + continue; + State new_state = { expiry_time, include_subdomains }; - enabled_hosts_[host] = new_state; + enabled_hosts_[hashed] = new_state; } - return enabled_hosts_.size() > 0; + return true; } void StrictTransportSecurityState::DirtyNotify() { @@ -232,4 +279,40 @@ void StrictTransportSecurityState::DirtyNotify() { delegate_->StateIsDirty(this); } +// static +std::string StrictTransportSecurityState::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 diff --git a/net/base/strict_transport_security_state.h b/net/base/strict_transport_security_state.h index b41be1e..463382c 100644 --- a/net/base/strict_transport_security_state.h +++ b/net/base/strict_transport_security_state.h @@ -70,7 +70,9 @@ class StrictTransportSecurityState : // our state is dirty. void DirtyNotify(); - // The set of hosts that have enabled StrictTransportSecurity. + // The set of hosts that have enabled StrictTransportSecurity. The keys here + // are SHA256(DNSForm(domain)) where DNSForm converts from dotted form + // ('www.google.com') to the form used in DNS: "\x03www\x06google\x03com" std::map<std::string, State> enabled_hosts_; // Protect access to our data members with this lock. @@ -79,6 +81,8 @@ class StrictTransportSecurityState : // Our delegate who gets notified when we are dirtied, or NULL. Delegate* delegate_; + static std::string CanonicaliseHost(const std::string& host); + DISALLOW_COPY_AND_ASSIGN(StrictTransportSecurityState); }; diff --git a/net/base/strict_transport_security_state_unittest.cc b/net/base/strict_transport_security_state_unittest.cc index 0077a8c..5ebd358 100644 --- a/net/base/strict_transport_security_state_unittest.cc +++ b/net/base/strict_transport_security_state_unittest.cc @@ -5,8 +5,6 @@ #include "net/base/strict_transport_security_state.h" #include "testing/gtest/include/gtest/gtest.h" -namespace { - class StrictTransportSecurityStateTest : public testing::Test { }; @@ -130,4 +128,79 @@ TEST_F(StrictTransportSecurityStateTest, ValidHeaders) { EXPECT_TRUE(include_subdomains); } -} // namespace +TEST_F(StrictTransportSecurityStateTest, SimpleMatches) { + scoped_refptr<net::StrictTransportSecurityState> state( + new net::StrictTransportSecurityState); + const base::Time current_time(base::Time::Now()); + const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000); + + EXPECT_FALSE(state->IsEnabledForHost("google.com")); + state->EnableHost("google.com", expiry, false); + EXPECT_TRUE(state->IsEnabledForHost("google.com")); +} + +TEST_F(StrictTransportSecurityStateTest, MatchesCase1) { + scoped_refptr<net::StrictTransportSecurityState> state( + new net::StrictTransportSecurityState); + const base::Time current_time(base::Time::Now()); + const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000); + + EXPECT_FALSE(state->IsEnabledForHost("google.com")); + state->EnableHost("GOOgle.coM", expiry, false); + EXPECT_TRUE(state->IsEnabledForHost("google.com")); +} + +TEST_F(StrictTransportSecurityStateTest, MatchesCase2) { + scoped_refptr<net::StrictTransportSecurityState> state( + new net::StrictTransportSecurityState); + const base::Time current_time(base::Time::Now()); + const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000); + + EXPECT_FALSE(state->IsEnabledForHost("GOOgle.coM")); + state->EnableHost("google.com", expiry, false); + EXPECT_TRUE(state->IsEnabledForHost("GOOgle.coM")); +} + +TEST_F(StrictTransportSecurityStateTest, SubdomainMatches) { + scoped_refptr<net::StrictTransportSecurityState> state( + new net::StrictTransportSecurityState); + const base::Time current_time(base::Time::Now()); + const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000); + + EXPECT_FALSE(state->IsEnabledForHost("google.com")); + state->EnableHost("google.com", expiry, true); + EXPECT_TRUE(state->IsEnabledForHost("google.com")); + EXPECT_TRUE(state->IsEnabledForHost("foo.google.com")); + EXPECT_TRUE(state->IsEnabledForHost("foo.bar.google.com")); + EXPECT_TRUE(state->IsEnabledForHost("foo.bar.baz.google.com")); + EXPECT_FALSE(state->IsEnabledForHost("com")); +} + +TEST_F(StrictTransportSecurityStateTest, Serialise1) { + scoped_refptr<net::StrictTransportSecurityState> state( + new net::StrictTransportSecurityState); + std::string output; + state->Serialise(&output); + EXPECT_TRUE(state->Deserialise(output)); +} + +TEST_F(StrictTransportSecurityStateTest, Serialise2) { + scoped_refptr<net::StrictTransportSecurityState> state( + new net::StrictTransportSecurityState); + + const base::Time current_time(base::Time::Now()); + const base::Time expiry = current_time + base::TimeDelta::FromSeconds(1000); + + EXPECT_FALSE(state->IsEnabledForHost("google.com")); + state->EnableHost("google.com", expiry, true); + + std::string output; + state->Serialise(&output); + EXPECT_TRUE(state->Deserialise(output)); + + EXPECT_TRUE(state->IsEnabledForHost("google.com")); + EXPECT_TRUE(state->IsEnabledForHost("foo.google.com")); + EXPECT_TRUE(state->IsEnabledForHost("foo.bar.google.com")); + EXPECT_TRUE(state->IsEnabledForHost("foo.bar.baz.google.com")); + EXPECT_FALSE(state->IsEnabledForHost("com")); +} diff --git a/net/net.gyp b/net/net.gyp index fb48e11..837cd13 100644 --- a/net/net.gyp +++ b/net/net.gyp @@ -52,6 +52,8 @@ 'base/data_url.h', 'base/directory_lister.cc', 'base/directory_lister.h', + 'base/dns_util.cc', + 'base/dns_util.h', 'base/effective_tld_names.cc', 'base/effective_tld_names.dat', 'base/escape.cc', @@ -450,6 +452,7 @@ 'base/cookie_policy_unittest.cc', 'base/data_url_unittest.cc', 'base/directory_lister_unittest.cc', + 'base/dns_util_unittest.cc', 'base/escape_unittest.cc', 'base/file_stream_unittest.cc', 'base/filter_unittest.cc', |