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