1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "net/base/transport_security_state.h" 6 7 #include "base/base64.h" 8 #include "base/json/json_reader.h" 9 #include "base/json/json_writer.h" 10 #include "base/logging.h" 11 #include "base/scoped_ptr.h" 12 #include "base/sha2.h" 13 #include "base/string_tokenizer.h" 14 #include "base/string_util.h" 15 #include "base/values.h" 16 #include "googleurl/src/gurl.h" 17 #include "net/base/dns_util.h" 18 19 namespace net { 20 21 TransportSecurityState::TransportSecurityState() 22 : delegate_(NULL) { 23 } 24 25 void TransportSecurityState::EnableHost(const std::string& host, 26 const DomainState& state) { 27 const std::string canonicalised_host = CanonicaliseHost(host); 28 if (canonicalised_host.empty()) 29 return; 30 char hashed[base::SHA256_LENGTH]; 31 base::SHA256HashString(canonicalised_host, hashed, sizeof(hashed)); 32 33 AutoLock lock(lock_); 34 35 enabled_hosts_[std::string(hashed, sizeof(hashed))] = state; 36 DirtyNotify(); 37 } 38 39 bool TransportSecurityState::IsEnabledForHost(DomainState* result, 40 const std::string& host) { 41 const std::string canonicalised_host = CanonicaliseHost(host); 42 if (canonicalised_host.empty()) 43 return false; 44 45 base::Time current_time(base::Time::Now()); 46 AutoLock lock(lock_); 47 48 for (size_t i = 0; canonicalised_host[i]; i += canonicalised_host[i] + 1) { 49 char hashed_domain[base::SHA256_LENGTH]; 50 51 base::SHA256HashString(&canonicalised_host[i], &hashed_domain, 52 sizeof(hashed_domain)); 53 std::map<std::string, DomainState>::iterator j = 54 enabled_hosts_.find(std::string(hashed_domain, sizeof(hashed_domain))); 55 if (j == enabled_hosts_.end()) 56 continue; 57 58 if (current_time > j->second.expiry) { 59 enabled_hosts_.erase(j); 60 DirtyNotify(); 61 continue; 62 } 63 64 *result = j->second; 65 66 // If we matched the domain exactly, it doesn't matter what the value of 67 // include_subdomains is. 68 if (i == 0) 69 return true; 70 71 return j->second.include_subdomains; 72 } 73 74 return false; 75 } 76 77 // "Strict-Transport-Security" ":" 78 // "max-age" "=" delta-seconds [ ";" "includeSubDomains" ] 79 bool TransportSecurityState::ParseHeader(const std::string& value, 80 int* max_age, 81 bool* include_subdomains) { 82 DCHECK(max_age); 83 DCHECK(include_subdomains); 84 85 int max_age_candidate; 86 87 enum ParserState { 88 START, 89 AFTER_MAX_AGE_LABEL, 90 AFTER_MAX_AGE_EQUALS, 91 AFTER_MAX_AGE, 92 AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER, 93 AFTER_INCLUDE_SUBDOMAINS, 94 } state = START; 95 96 StringTokenizer tokenizer(value, " \t=;"); 97 tokenizer.set_options(StringTokenizer::RETURN_DELIMS); 98 while (tokenizer.GetNext()) { 99 DCHECK(!tokenizer.token_is_delim() || tokenizer.token().length() == 1); 100 switch (state) { 101 case START: 102 if (IsAsciiWhitespace(*tokenizer.token_begin())) 103 continue; 104 if (!LowerCaseEqualsASCII(tokenizer.token(), "max-age")) 105 return false; 106 state = AFTER_MAX_AGE_LABEL; 107 break; 108 109 case AFTER_MAX_AGE_LABEL: 110 if (IsAsciiWhitespace(*tokenizer.token_begin())) 111 continue; 112 if (*tokenizer.token_begin() != '=') 113 return false; 114 DCHECK(tokenizer.token().length() == 1); 115 state = AFTER_MAX_AGE_EQUALS; 116 break; 117 118 case AFTER_MAX_AGE_EQUALS: 119 if (IsAsciiWhitespace(*tokenizer.token_begin())) 120 continue; 121 if (!StringToInt(tokenizer.token(), &max_age_candidate)) 122 return false; 123 if (max_age_candidate < 0) 124 return false; 125 state = AFTER_MAX_AGE; 126 break; 127 128 case AFTER_MAX_AGE: 129 if (IsAsciiWhitespace(*tokenizer.token_begin())) 130 continue; 131 if (*tokenizer.token_begin() != ';') 132 return false; 133 state = AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER; 134 break; 135 136 case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER: 137 if (IsAsciiWhitespace(*tokenizer.token_begin())) 138 continue; 139 if (!LowerCaseEqualsASCII(tokenizer.token(), "includesubdomains")) 140 return false; 141 state = AFTER_INCLUDE_SUBDOMAINS; 142 break; 143 144 case AFTER_INCLUDE_SUBDOMAINS: 145 if (!IsAsciiWhitespace(*tokenizer.token_begin())) 146 return false; 147 break; 148 149 default: 150 NOTREACHED(); 151 } 152 } 153 154 // We've consumed all the input. Let's see what state we ended up in. 155 switch (state) { 156 case START: 157 case AFTER_MAX_AGE_LABEL: 158 case AFTER_MAX_AGE_EQUALS: 159 return false; 160 case AFTER_MAX_AGE: 161 *max_age = max_age_candidate; 162 *include_subdomains = false; 163 return true; 164 case AFTER_MAX_AGE_INCLUDE_SUB_DOMAINS_DELIMITER: 165 return false; 166 case AFTER_INCLUDE_SUBDOMAINS: 167 *max_age = max_age_candidate; 168 *include_subdomains = true; 169 return true; 170 default: 171 NOTREACHED(); 172 return false; 173 } 174 } 175 176 void TransportSecurityState::SetDelegate( 177 TransportSecurityState::Delegate* delegate) { 178 AutoLock lock(lock_); 179 180 delegate_ = delegate; 181 } 182 183 // This function converts the binary hashes, which we store in 184 // |enabled_hosts_|, to a base64 string which we can include in a JSON file. 185 static std::wstring HashedDomainToExternalString(const std::string& hashed) { 186 std::string out; 187 CHECK(base::Base64Encode(hashed, &out)); 188 return ASCIIToWide(out); 189 } 190 191 // This inverts |HashedDomainToExternalString|, above. It turns an external 192 // string (from a JSON file) into an internal (binary) string. 193 static std::string ExternalStringToHashedDomain(const std::wstring& external) { 194 std::string external_ascii = WideToASCII(external); 195 std::string out; 196 if (!base::Base64Decode(external_ascii, &out) || 197 out.size() != base::SHA256_LENGTH) { 198 return std::string(); 199 } 200 201 return out; 202 } 203 204 bool TransportSecurityState::Serialise(std::string* output) { 205 AutoLock lock(lock_); 206 207 DictionaryValue toplevel; 208 for (std::map<std::string, DomainState>::const_iterator 209 i = enabled_hosts_.begin(); i != enabled_hosts_.end(); ++i) { 210 DictionaryValue* state = new DictionaryValue; 211 state->SetBoolean(L"include_subdomains", i->second.include_subdomains); 212 state->SetReal(L"expiry", i->second.expiry.ToDoubleT()); 213 214 switch (i->second.mode) { 215 case DomainState::MODE_STRICT: 216 state->SetString(L"mode", "strict"); 217 break; 218 case DomainState::MODE_OPPORTUNISTIC: 219 state->SetString(L"mode", "opportunistic"); 220 break; 221 case DomainState::MODE_SPDY_ONLY: 222 state->SetString(L"mode", "spdy-only"); 223 break; 224 default: 225 NOTREACHED() << "DomainState with unknown mode"; 226 delete state; 227 continue; 228 } 229 230 toplevel.Set(HashedDomainToExternalString(i->first), state); 231 } 232 233 base::JSONWriter::Write(&toplevel, true /* pretty print */, output); 234 return true; 235 } 236 237 bool TransportSecurityState::Deserialise(const std::string& input) { 238 AutoLock lock(lock_); 239 240 enabled_hosts_.clear(); 241 242 scoped_ptr<Value> value( 243 base::JSONReader::Read(input, false /* do not allow trailing commas */)); 244 if (!value.get() || !value->IsType(Value::TYPE_DICTIONARY)) 245 return false; 246 247 DictionaryValue* dict_value = reinterpret_cast<DictionaryValue*>(value.get()); 248 const base::Time current_time(base::Time::Now()); 249 250 for (DictionaryValue::key_iterator i = dict_value->begin_keys(); 251 i != dict_value->end_keys(); ++i) { 252 DictionaryValue* state; 253 if (!dict_value->GetDictionaryWithoutPathExpansion(*i, &state)) 254 continue; 255 256 bool include_subdomains; 257 std::string mode_string; 258 double expiry; 259 260 if (!state->GetBoolean(L"include_subdomains", &include_subdomains) || 261 !state->GetString(L"mode", &mode_string) || 262 !state->GetReal(L"expiry", &expiry)) { 263 continue; 264 } 265 266 DomainState::Mode mode; 267 if (mode_string == "strict") { 268 mode = DomainState::MODE_STRICT; 269 } else if (mode_string == "opportunistic") { 270 mode = DomainState::MODE_OPPORTUNISTIC; 271 } else if (mode_string == "spdy-only") { 272 mode = DomainState::MODE_SPDY_ONLY; 273 } else { 274 LOG(WARNING) << "Unknown TransportSecurityState mode string found: " 275 << mode_string; 276 continue; 277 } 278 279 base::Time expiry_time = base::Time::FromDoubleT(expiry); 280 if (expiry_time <= current_time) 281 continue; 282 283 std::string hashed = ExternalStringToHashedDomain(*i); 284 if (hashed.empty()) 285 continue; 286 287 DomainState new_state; 288 new_state.mode = mode; 289 new_state.expiry = expiry_time; 290 new_state.include_subdomains = include_subdomains; 291 enabled_hosts_[hashed] = new_state; 292 } 293 294 return true; 295 } 296 297 void TransportSecurityState::DirtyNotify() { 298 if (delegate_) 299 delegate_->StateIsDirty(this); 300 } 301 302 // static 303 std::string TransportSecurityState::CanonicaliseHost(const std::string& host) { 304 // We cannot perform the operations as detailed in the spec here as |host| 305 // has already undergone IDN processing before it reached us. Thus, we check 306 // that there are no invalid characters in the host and lowercase the result. 307 308 std::string new_host; 309 if (!DNSDomainFromDot(host, &new_host)) { 310 NOTREACHED(); 311 return std::string(); 312 } 313 314 for (size_t i = 0; new_host[i]; i += new_host[i] + 1) { 315 const unsigned label_length = static_cast<unsigned>(new_host[i]); 316 if (!label_length) 317 break; 318 319 for (size_t j = 0; j < label_length; ++j) { 320 // RFC 3490, 4.1, step 3 321 if (!IsSTD3ASCIIValidCharacter(new_host[i + 1 + j])) 322 return std::string(); 323 324 new_host[i + 1 + j] = tolower(new_host[i + 1 + j]); 325 } 326 327 // step 3(b) 328 if (new_host[i + 1] == '-' || 329 new_host[i + label_length] == '-') { 330 return std::string(); 331 } 332 } 333 334 return new_host; 335 } 336 337 } // namespace 338