Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2014 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 "chrome/browser/ssl/chrome_ssl_host_state_decisions.h" | |
| 6 | |
| 7 #include "base/base64.h" | |
| 8 #include "base/command_line.h" | |
| 9 #include "base/logging.h" | |
| 10 #include "base/metrics/field_trial.h" | |
| 11 #include "base/strings/string_number_conversions.h" | |
| 12 #include "base/time/default_clock.h" | |
| 13 #include "base/time/time.h" | |
| 14 #include "chrome/browser/content_settings/host_content_settings_map.h" | |
| 15 #include "chrome/browser/profiles/profile.h" | |
| 16 #include "chrome/common/chrome_switches.h" | |
| 17 #include "components/content_settings/core/common/content_settings_types.h" | |
| 18 #include "components/variations/variations_associated_data.h" | |
| 19 #include "net/base/hash_value.h" | |
| 20 #include "net/cert/x509_certificate.h" | |
| 21 #include "url/gurl.h" | |
| 22 | |
| 23 namespace { | |
| 24 | |
| 25 // Switch value that specifies that certificate decisions should be forgotten at | |
| 26 // the end of the current session. | |
| 27 const int64 kForgetAtSessionEndSwitchValue = -1; | |
| 28 | |
| 29 // Experiment information | |
| 30 const char kRememberCertificateErrorDecisionsFieldTrialName[] = | |
| 31 "RememberCertificateErrorDecisions"; | |
| 32 const char kRememberCertificateErrorDecisionsFieldTrialDefaultGroup[] = | |
| 33 "Default"; | |
| 34 const char kRememberCertificateErrorDecisionsFieldTrialLengthParam[] = "length"; | |
| 35 | |
| 36 // Keys for the per-site error + certificate finger to judgement content | |
| 37 // settings map. | |
| 38 const char kSSLCertDecisionCertErrorMapKey[] = "cert_exceptions_map"; | |
| 39 const char kSSLCertDecisionExpirationTimeKey[] = "decision_expiration_time"; | |
| 40 const char kSSLCertDecisionVersionKey[] = "version"; | |
| 41 | |
| 42 const int kDefaultSSLCertDecisionVersion = 1; | |
| 43 | |
| 44 // This is a helper function that returns the length of time before a | |
| 45 // certificate decision expires based on the command line flags. Returns a | |
| 46 // non-negative value in seconds or a value of -1 indicating that decisions | |
| 47 // should not be remembered after the current session has ended (but should be | |
| 48 // remembered indefinitely as long as the session does not end), which is the | |
| 49 // "old" style of certificate decision memory. Uses the experimental group | |
| 50 // unless overridden by a command line flag. | |
| 51 int64 GetExpirationDelta() { | |
| 52 // Check command line flags first to give them priority, then check | |
| 53 // experimental groups. | |
| 54 if (CommandLine::ForCurrentProcess()->HasSwitch( | |
| 55 switches::kRememberCertErrorDecisions)) { | |
| 56 std::string switch_value = | |
| 57 CommandLine::ForCurrentProcess()->GetSwitchValueASCII( | |
| 58 switches::kRememberCertErrorDecisions); | |
| 59 int64 expiration_delta; | |
| 60 if (!base::StringToInt64(base::StringPiece(switch_value), | |
| 61 &expiration_delta) || | |
| 62 expiration_delta < kForgetAtSessionEndSwitchValue) { | |
| 63 LOG(ERROR) << "Failed to parse the certificate error decision " | |
| 64 << "memory length: " << switch_value; | |
| 65 return kForgetAtSessionEndSwitchValue; | |
| 66 } | |
| 67 | |
| 68 return expiration_delta; | |
| 69 } | |
|
Ryan Sleevi
2014/07/31 00:31:28
Is this switch necessary, given that we can force
jww
2014/07/31 05:57:00
Yes, it's necessary because we provide flags in ch
| |
| 70 | |
| 71 // If the user is in the field trial, set the expiration to the length | |
| 72 // associated with that experimental group. The default group cannot have | |
| 73 // parameters associated with it, so it needs to be handled explictly. | |
| 74 std::string group_name = base::FieldTrialList::FindFullName( | |
| 75 kRememberCertificateErrorDecisionsFieldTrialName); | |
| 76 if (!group_name.empty() && | |
| 77 group_name.compare( | |
| 78 kRememberCertificateErrorDecisionsFieldTrialDefaultGroup) != 0) { | |
| 79 int64 field_trial_param_length; | |
| 80 std::string param = variations::GetVariationParamValue( | |
| 81 kRememberCertificateErrorDecisionsFieldTrialName, | |
| 82 kRememberCertificateErrorDecisionsFieldTrialLengthParam); | |
| 83 if (!param.empty() && base::StringToInt64(base::StringPiece(param), | |
| 84 &field_trial_param_length)) { | |
| 85 return field_trial_param_length; | |
| 86 } | |
| 87 } | |
| 88 | |
| 89 return kForgetAtSessionEndSwitchValue; | |
| 90 } | |
| 91 | |
| 92 std::string GetKey(net::X509Certificate* cert, net::CertStatus error) { | |
| 93 // Since a security decision will be made based on the fingerprint, Chrome | |
| 94 // should use the SHA-256 fingerprint for the certificate. | |
| 95 net::SHA256HashValue fingerprint = | |
| 96 net::X509Certificate::CalculateChainFingerprint256( | |
| 97 cert->os_cert_handle(), cert->GetIntermediateCertificates()); | |
| 98 std::string base64_fingerprint; | |
| 99 base::Base64Encode( | |
| 100 base::StringPiece(reinterpret_cast<const char*>(fingerprint.data), | |
| 101 sizeof(fingerprint.data)), | |
| 102 &base64_fingerprint); | |
| 103 return base::UintToString(error) + base64_fingerprint; | |
| 104 } | |
| 105 | |
| 106 } // namespace | |
| 107 | |
| 108 // This helper function gets the dictionary of certificate fingerprints to | |
| 109 // errors of certificates that have been accepted by the user from the content | |
| 110 // dictionary that has been passed in. The returned pointer is owned by the the | |
| 111 // argument dict that is passed in. | |
| 112 // | |
| 113 // If create_entries is set to |DoNotCreateDictionaryEntries|, | |
| 114 // GetValidCertDecisionsDict will return NULL if there is anything invalid about | |
| 115 // the setting, such as an invalid version or invalid value types (in addition | |
| 116 // to there not be any values in the dictionary). If create_entries is set to | |
| 117 // |CreateDictionaryEntries|, if no dictionary is found or the decisions are | |
| 118 // expired, a new dictionary will be created | |
| 119 base::DictionaryValue* ChromeSSLHostStateDecisions::GetValidCertDecisionsDict( | |
| 120 base::DictionaryValue* dict, | |
| 121 CreateDictionaryEntriesDisposition create_entries) { | |
| 122 // Extract the version of the certificate decision structure from the content | |
| 123 // setting. | |
| 124 int version; | |
| 125 bool success = dict->GetInteger(kSSLCertDecisionVersionKey, &version); | |
| 126 if (!success) { | |
| 127 if (create_entries == DoNotCreateDictionaryEntries) | |
| 128 return NULL; | |
| 129 | |
| 130 dict->SetInteger(kSSLCertDecisionVersionKey, | |
| 131 kDefaultSSLCertDecisionVersion); | |
| 132 version = kDefaultSSLCertDecisionVersion; | |
| 133 } | |
| 134 | |
| 135 // If the version is somehow a newer version than Chrome can handle, there's | |
| 136 // really nothing to do other than fail silently and pretend it doesn't exist | |
| 137 // (or is malformed). | |
| 138 if (version > kDefaultSSLCertDecisionVersion) { | |
| 139 LOG(ERROR) << "Failed to parse a certificate error exception that is in a " | |
| 140 << "newer version format (" << version << ") than is supported (" | |
| 141 << kDefaultSSLCertDecisionVersion << ")"; | |
| 142 return NULL; | |
| 143 } | |
| 144 | |
| 145 // Extract the certificate decision's expiration time from the content | |
| 146 // setting. If there is no expiration time, that means it should never expire | |
| 147 // and it should reset only at session restart, so skip all of the expiration | |
| 148 // checks. | |
| 149 bool expired = false; | |
| 150 base::Time now = clock_->Now(); | |
| 151 base::Time decision_expiration; | |
| 152 if (dict->HasKey(kSSLCertDecisionExpirationTimeKey)) { | |
| 153 std::string decision_expiration_string; | |
| 154 int64 decision_expiration_int64; | |
| 155 success = dict->GetString(kSSLCertDecisionExpirationTimeKey, | |
| 156 &decision_expiration_string); | |
| 157 if (!base::StringToInt64(base::StringPiece(decision_expiration_string), | |
| 158 &decision_expiration_int64)) { | |
| 159 LOG(ERROR) << "Failed to parse a certificate error exception that has a " | |
| 160 << "bad value for an expiration time: " | |
| 161 << decision_expiration_string; | |
| 162 return NULL; | |
| 163 } | |
| 164 decision_expiration = | |
| 165 base::Time::FromInternalValue(decision_expiration_int64); | |
| 166 } | |
| 167 | |
| 168 // Check to see if the user's certificate decision has expired. | |
| 169 // - Expired and |create_entries| is DoNotCreateDictionaryEntries, return | |
| 170 // NULL. | |
| 171 // - Expired and |create_entries| is CreateDictionaryEntries, update the | |
| 172 // expiration time. | |
| 173 if (should_remember_ssl_decisions_ != | |
| 174 ForgetSSLExceptionDecisionsAtSessionEnd && | |
| 175 decision_expiration.ToInternalValue() <= now.ToInternalValue()) { | |
| 176 if (create_entries == DoNotCreateDictionaryEntries) | |
| 177 return NULL; | |
| 178 | |
| 179 expired = true; | |
| 180 | |
| 181 base::Time expiration_time = | |
| 182 now + default_ssl_cert_decision_expiration_delta_; | |
| 183 // Unfortunately, JSON (and thus content settings) doesn't support int64 | |
| 184 // values, only doubles. Since this mildly depends on precision, it is | |
| 185 // better to store the value as a string. | |
| 186 dict->SetString(kSSLCertDecisionExpirationTimeKey, | |
| 187 base::Int64ToString(expiration_time.ToInternalValue())); | |
| 188 } | |
| 189 | |
| 190 // Extract the map of certificate fingerprints to errors from the setting. | |
| 191 base::DictionaryValue* cert_error_dict; // Will be owned by dict | |
| 192 if (expired || | |
| 193 !dict->GetDictionary(kSSLCertDecisionCertErrorMapKey, &cert_error_dict)) { | |
| 194 if (create_entries == DoNotCreateDictionaryEntries) | |
| 195 return NULL; | |
| 196 | |
| 197 cert_error_dict = new base::DictionaryValue(); | |
| 198 // dict takes ownership of cert_error_dict | |
| 199 dict->Set(kSSLCertDecisionCertErrorMapKey, cert_error_dict); | |
| 200 } | |
| 201 | |
| 202 return cert_error_dict; | |
| 203 } | |
| 204 | |
| 205 // If |should_remember_ssl_decisions_| is | |
| 206 // ForgetSSLExceptionDecisionsAtSessionEnd, that means that all invalid | |
| 207 // certificate proceed decisions should be forgotten when the session ends. At | |
| 208 // attempt is made in the destructor to remove the entries, but in the case that | |
| 209 // things didn't shut down cleanly, on start, Clear is called to guarantee a | |
| 210 // clean state. | |
| 211 ChromeSSLHostStateDecisions::ChromeSSLHostStateDecisions(Profile* profile) | |
| 212 : clock_(new base::DefaultClock()), profile_(profile) { | |
| 213 int64 expiration_delta = GetExpirationDelta(); | |
| 214 if (expiration_delta == kForgetAtSessionEndSwitchValue) { | |
| 215 should_remember_ssl_decisions_ = ForgetSSLExceptionDecisionsAtSessionEnd; | |
| 216 expiration_delta = 0; | |
| 217 Clear(); | |
| 218 } else { | |
| 219 should_remember_ssl_decisions_ = RememberSSLExceptionDecisionsForDelta; | |
| 220 } | |
| 221 default_ssl_cert_decision_expiration_delta_ = | |
| 222 base::TimeDelta::FromSeconds(expiration_delta); | |
| 223 } | |
| 224 | |
| 225 ChromeSSLHostStateDecisions::~ChromeSSLHostStateDecisions() { | |
| 226 if (should_remember_ssl_decisions_ == ForgetSSLExceptionDecisionsAtSessionEnd) | |
| 227 Clear(); | |
| 228 } | |
| 229 | |
| 230 void ChromeSSLHostStateDecisions::DenyCert(const GURL& url, | |
| 231 net::X509Certificate* cert, | |
| 232 net::CertStatus error) { | |
| 233 ChangeCertPolicy(url, cert, error, net::CertPolicy::DENIED); | |
| 234 } | |
| 235 | |
| 236 void ChromeSSLHostStateDecisions::AllowCert(const GURL& url, | |
| 237 net::X509Certificate* cert, | |
| 238 net::CertStatus error) { | |
| 239 ChangeCertPolicy(url, cert, error, net::CertPolicy::ALLOWED); | |
| 240 } | |
| 241 | |
| 242 void ChromeSSLHostStateDecisions::Clear() { | |
| 243 profile_->GetHostContentSettingsMap()->ClearSettingsForOneType( | |
| 244 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS); | |
| 245 } | |
| 246 | |
| 247 net::CertPolicy::Judgment ChromeSSLHostStateDecisions::QueryPolicy( | |
| 248 const GURL& url, | |
| 249 net::X509Certificate* cert, | |
| 250 net::CertStatus error) { | |
| 251 HostContentSettingsMap* map = profile_->GetHostContentSettingsMap(); | |
| 252 scoped_ptr<base::Value> value( | |
| 253 map->GetWebsiteSetting(url.GetWithEmptyPath(), | |
| 254 url.GetWithEmptyPath(), | |
| 255 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS, | |
| 256 std::string(), | |
| 257 NULL)); | |
| 258 | |
| 259 if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY)) | |
| 260 return net::CertPolicy::UNKNOWN; | |
| 261 | |
| 262 base::DictionaryValue* dict; // Owned by value | |
| 263 int policy_decision; | |
| 264 bool success = value->GetAsDictionary(&dict); | |
| 265 DCHECK(success); | |
| 266 | |
| 267 base::DictionaryValue* cert_error_dict; // Owned by value | |
| 268 cert_error_dict = | |
| 269 GetValidCertDecisionsDict(dict, DoNotCreateDictionaryEntries); | |
| 270 if (!cert_error_dict) | |
| 271 return net::CertPolicy::UNKNOWN; | |
| 272 | |
| 273 success = cert_error_dict->GetIntegerWithoutPathExpansion(GetKey(cert, error), | |
| 274 &policy_decision); | |
| 275 | |
| 276 if (!success) | |
| 277 return net::CertPolicy::Judgment::UNKNOWN; | |
| 278 | |
| 279 return static_cast<net::CertPolicy::Judgment>(policy_decision); | |
|
Ryan Sleevi
2014/07/31 00:31:28
SECURITY: Seems like you should bounds check this,
jww
2014/07/31 05:57:00
Done.
| |
| 280 } | |
| 281 | |
| 282 void ChromeSSLHostStateDecisions::RevokeAllowAndDenyPreferences( | |
| 283 const GURL& url) { | |
| 284 const ContentSettingsPattern pattern = | |
| 285 ContentSettingsPattern::FromURLNoWildcard(url.GetWithEmptyPath()); | |
| 286 HostContentSettingsMap* map = profile_->GetHostContentSettingsMap(); | |
| 287 | |
| 288 map->SetWebsiteSetting(pattern, | |
| 289 pattern, | |
| 290 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS, | |
| 291 std::string(), | |
| 292 NULL); | |
| 293 } | |
| 294 | |
| 295 bool ChromeSSLHostStateDecisions::HasAllowedOrDeniedCert(const GURL& url) { | |
| 296 const ContentSettingsPattern pattern = | |
| 297 ContentSettingsPattern::FromURLNoWildcard(url.GetWithEmptyPath()); | |
| 298 HostContentSettingsMap* map = profile_->GetHostContentSettingsMap(); | |
| 299 | |
| 300 scoped_ptr<base::Value> value( | |
| 301 map->GetWebsiteSetting(url.GetWithEmptyPath(), | |
| 302 url.GetWithEmptyPath(), | |
| 303 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS, | |
| 304 std::string(), | |
| 305 NULL)); | |
| 306 | |
| 307 if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY)) | |
| 308 return false; | |
| 309 | |
| 310 base::DictionaryValue* dict; // Owned by value | |
| 311 bool success = value->GetAsDictionary(&dict); | |
| 312 DCHECK(success); | |
| 313 | |
| 314 for (base::DictionaryValue::Iterator it(*dict); !it.IsAtEnd(); it.Advance()) { | |
| 315 int policy_decision; // Owned by dict | |
| 316 success = it.value().GetAsInteger(&policy_decision); | |
| 317 if (success && (static_cast<net::CertPolicy::Judgment>(policy_decision) != | |
| 318 net::CertPolicy::UNKNOWN)) | |
| 319 return true; | |
| 320 } | |
| 321 | |
| 322 return false; | |
| 323 } | |
| 324 | |
| 325 void ChromeSSLHostStateDecisions::ChangeCertPolicy( | |
| 326 const GURL& url, | |
| 327 net::X509Certificate* cert, | |
| 328 net::CertStatus error, | |
| 329 net::CertPolicy::Judgment judgment) { | |
| 330 const ContentSettingsPattern pattern = | |
| 331 ContentSettingsPattern::FromURLNoWildcard(url.GetWithEmptyPath()); | |
| 332 HostContentSettingsMap* map = profile_->GetHostContentSettingsMap(); | |
| 333 scoped_ptr<base::Value> value( | |
| 334 map->GetWebsiteSetting(url.GetWithEmptyPath(), | |
| 335 url.GetWithEmptyPath(), | |
| 336 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS, | |
| 337 std::string(), | |
| 338 NULL)); | |
| 339 | |
| 340 if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY)) | |
| 341 value.reset(new base::DictionaryValue()); | |
| 342 | |
| 343 base::DictionaryValue* dict; | |
| 344 bool success = value->GetAsDictionary(&dict); | |
| 345 DCHECK(success); | |
| 346 | |
| 347 base::DictionaryValue* cert_dict = | |
| 348 GetValidCertDecisionsDict(dict, CreateDictionaryEntries); | |
| 349 // If a a valid certificate dictionary cannot be extracted from the content | |
| 350 // setting, that means it's in an unknown format. Unfortunately, there's | |
| 351 // nothing to be done in that case, so a silent fail is the only option. | |
| 352 if (!cert_dict) | |
| 353 return; | |
| 354 | |
| 355 dict->SetIntegerWithoutPathExpansion(kSSLCertDecisionVersionKey, | |
| 356 kDefaultSSLCertDecisionVersion); | |
| 357 cert_dict->SetIntegerWithoutPathExpansion(GetKey(cert, error), judgment); | |
| 358 | |
| 359 // The map takes ownership of the value, so it is released in the call to | |
| 360 // SetWebsiteSetting. | |
| 361 map->SetWebsiteSetting(pattern, | |
| 362 pattern, | |
| 363 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS, | |
| 364 std::string(), | |
| 365 value.release()); | |
| 366 } | |
| OLD | NEW |