| // Copyright 2016 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 "components/security_state/content/content_utils.h" |
| |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "base/strings/string16.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/security_state/content/ssl_status_input_event_data.h" |
| #include "components/security_state/core/security_state.h" |
| #include "components/strings/grit/components_chromium_strings.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/security_style_explanation.h" |
| #include "content/public/browser/security_style_explanations.h" |
| #include "content/public/browser/ssl_status.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/url_constants.h" |
| #include "net/base/net_errors.h" |
| #include "net/cert/x509_certificate.h" |
| #include "net/ssl/ssl_cipher_suite_names.h" |
| #include "net/ssl/ssl_connection_status_flags.h" |
| #include "third_party/blink/public/platform/web_mixed_content_context_type.h" |
| #include "third_party/boringssl/src/include/openssl/ssl.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| namespace security_state { |
| |
| namespace { |
| |
| // Note: This is a lossy operation. Not all of the policies that can be |
| // expressed by a SecurityLevel can be expressed by a blink::WebSecurityStyle. |
| blink::WebSecurityStyle SecurityLevelToSecurityStyle( |
| security_state::SecurityLevel security_level) { |
| switch (security_level) { |
| case security_state::NONE: |
| case security_state::HTTP_SHOW_WARNING: |
| return blink::kWebSecurityStyleNeutral; |
| case security_state::SECURE_WITH_POLICY_INSTALLED_CERT: |
| case security_state::EV_SECURE: |
| case security_state::SECURE: |
| return blink::kWebSecurityStyleSecure; |
| case security_state::DANGEROUS: |
| return blink::kWebSecurityStyleInsecure; |
| case security_state::SECURITY_LEVEL_COUNT: |
| NOTREACHED(); |
| return blink::kWebSecurityStyleNeutral; |
| } |
| |
| NOTREACHED(); |
| return blink::kWebSecurityStyleUnknown; |
| } |
| |
| void ExplainHTTPSecurity( |
| const security_state::SecurityInfo& security_info, |
| content::SecurityStyleExplanations* security_style_explanations) { |
| if (security_info.security_level != security_state::HTTP_SHOW_WARNING) |
| return; |
| |
| if (security_info.field_edit_downgraded_security_level) { |
| security_style_explanations->neutral_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_EDITED_NONSECURE), |
| l10n_util::GetStringUTF8(IDS_EDITED_NONSECURE_DESCRIPTION))); |
| } |
| if (security_info.insecure_input_events.password_field_shown || |
| security_info.insecure_input_events.credit_card_field_edited) { |
| security_style_explanations->neutral_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_PRIVATE_USER_DATA_INPUT), |
| l10n_util::GetStringUTF8(IDS_PRIVATE_USER_DATA_INPUT_DESCRIPTION))); |
| } |
| if (security_info.incognito_downgraded_security_level) { |
| security_style_explanations->neutral_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_INCOGNITO_NONSECURE), |
| l10n_util::GetStringUTF8(IDS_INCOGNITO_NONSECURE_DESCRIPTION))); |
| } |
| } |
| |
| void ExplainSafeBrowsingSecurity( |
| const security_state::SecurityInfo& security_info, |
| content::SecurityStyleExplanations* security_style_explanations) { |
| if (security_info.malicious_content_status == |
| security_state::MALICIOUS_CONTENT_STATUS_NONE) { |
| return; |
| } |
| // Override the main summary for the page. |
| security_style_explanations->summary = |
| l10n_util::GetStringUTF8(IDS_SAFEBROWSING_WARNING); |
| // Add a bullet describing the issue. |
| content::SecurityStyleExplanation explanation( |
| l10n_util::GetStringUTF8(IDS_SAFEBROWSING_WARNING_SUMMARY), |
| l10n_util::GetStringUTF8(IDS_SAFEBROWSING_WARNING_DESCRIPTION)); |
| security_style_explanations->insecure_explanations.push_back(explanation); |
| } |
| |
| void ExplainCertificateSecurity( |
| const security_state::SecurityInfo& security_info, |
| content::SecurityStyleExplanations* security_style_explanations) { |
| if (security_info.sha1_in_chain) { |
| content::SecurityStyleExplanation explanation( |
| l10n_util::GetStringUTF8(IDS_CERTIFICATE_TITLE), |
| l10n_util::GetStringUTF8(IDS_SHA1), |
| l10n_util::GetStringUTF8(IDS_SHA1_DESCRIPTION), |
| security_info.certificate, |
| blink::WebMixedContentContextType::kNotMixedContent); |
| // The impact of SHA1 on the certificate status depends on |
| // the EnableSHA1ForLocalAnchors policy. |
| if (security_info.cert_status & net::CERT_STATUS_WEAK_SIGNATURE_ALGORITHM) { |
| security_style_explanations->insecure_explanations.push_back(explanation); |
| } else { |
| security_style_explanations->neutral_explanations.push_back(explanation); |
| } |
| } |
| |
| if (security_info.cert_missing_subject_alt_name) { |
| security_style_explanations->insecure_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_CERTIFICATE_TITLE), |
| l10n_util::GetStringUTF8(IDS_SUBJECT_ALT_NAME_MISSING), |
| l10n_util::GetStringUTF8(IDS_SUBJECT_ALT_NAME_MISSING_DESCRIPTION), |
| security_info.certificate, |
| blink::WebMixedContentContextType::kNotMixedContent)); |
| } |
| |
| bool is_cert_status_error = net::IsCertStatusError(security_info.cert_status); |
| bool is_cert_status_minor_error = |
| net::IsCertStatusMinorError(security_info.cert_status); |
| |
| if (is_cert_status_error) { |
| base::string16 error_string = base::UTF8ToUTF16(net::ErrorToString( |
| net::MapCertStatusToNetError(security_info.cert_status))); |
| |
| content::SecurityStyleExplanation explanation( |
| l10n_util::GetStringUTF8(IDS_CERTIFICATE_TITLE), |
| l10n_util::GetStringUTF8(IDS_CERTIFICATE_CHAIN_ERROR), |
| l10n_util::GetStringFUTF8( |
| IDS_CERTIFICATE_CHAIN_ERROR_DESCRIPTION_FORMAT, error_string), |
| security_info.certificate, |
| blink::WebMixedContentContextType::kNotMixedContent); |
| |
| if (is_cert_status_minor_error) { |
| security_style_explanations->neutral_explanations.push_back(explanation); |
| } else { |
| security_style_explanations->insecure_explanations.push_back(explanation); |
| } |
| } else { |
| // If the certificate does not have errors and is not using SHA1, then add |
| // an explanation that the certificate is valid. |
| |
| base::string16 issuer_name; |
| if (security_info.certificate) { |
| // This results in the empty string if there is no relevant display name. |
| issuer_name = base::UTF8ToUTF16( |
| security_info.certificate->issuer().GetDisplayName()); |
| } else { |
| issuer_name = base::string16(); |
| } |
| if (issuer_name.empty()) { |
| issuer_name.assign( |
| l10n_util::GetStringUTF16(IDS_PAGE_INFO_SECURITY_TAB_UNKNOWN_PARTY)); |
| } |
| |
| if (!security_info.sha1_in_chain) { |
| security_style_explanations->secure_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_CERTIFICATE_TITLE), |
| l10n_util::GetStringUTF8(IDS_VALID_SERVER_CERTIFICATE), |
| l10n_util::GetStringFUTF8( |
| IDS_VALID_SERVER_CERTIFICATE_DESCRIPTION, issuer_name), |
| security_info.certificate, |
| blink::WebMixedContentContextType::kNotMixedContent)); |
| } |
| } |
| |
| security_style_explanations->pkp_bypassed = security_info.pkp_bypassed; |
| if (security_info.pkp_bypassed) { |
| security_style_explanations->info_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_CERTIFICATE_TITLE), |
| l10n_util::GetStringUTF8(IDS_PRIVATE_KEY_PINNING_BYPASSED), |
| l10n_util::GetStringUTF8( |
| IDS_PRIVATE_KEY_PINNING_BYPASSED_DESCRIPTION))); |
| } |
| |
| if (security_info.certificate && |
| !security_info.certificate->valid_expiry().is_null() && |
| (security_info.certificate->valid_expiry() - base::Time::Now()) |
| .InHours() < 48 && |
| (security_info.certificate->valid_expiry() > base::Time::Now())) { |
| security_style_explanations->info_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_CERTIFICATE_EXPIRING_SOON), |
| l10n_util::GetStringUTF8( |
| IDS_CERTIFICATE_EXPIRING_SOON_DESCRIPTION))); |
| } |
| } |
| |
| void ExplainConnectionSecurity( |
| const security_state::SecurityInfo& security_info, |
| content::SecurityStyleExplanations* security_style_explanations) { |
| // Avoid showing TLS details when we couldn't even establish a TLS connection |
| // (e.g. for net errors) or if there was no real connection (some tests). We |
| // check the |connection_status| to see if there was a connection. |
| if (security_info.connection_status == 0) { |
| return; |
| } |
| |
| int ssl_version = |
| net::SSLConnectionStatusToVersion(security_info.connection_status); |
| const char* protocol; |
| net::SSLVersionToString(&protocol, ssl_version); |
| const char* key_exchange; |
| const char* cipher; |
| const char* mac; |
| bool is_aead; |
| bool is_tls13; |
| uint16_t cipher_suite = |
| net::SSLConnectionStatusToCipherSuite(security_info.connection_status); |
| net::SSLCipherSuiteToStrings(&key_exchange, &cipher, &mac, &is_aead, |
| &is_tls13, cipher_suite); |
| base::string16 protocol_name = base::ASCIIToUTF16(protocol); |
| const base::string16 cipher_name = |
| (mac == nullptr) ? base::ASCIIToUTF16(cipher) |
| : l10n_util::GetStringFUTF16(IDS_CIPHER_WITH_MAC, |
| base::ASCIIToUTF16(cipher), |
| base::ASCIIToUTF16(mac)); |
| |
| // Include the key exchange group (previously known as curve) if specified. |
| base::string16 key_exchange_name; |
| if (is_tls13) { |
| key_exchange_name = base::ASCIIToUTF16( |
| SSL_get_curve_name(security_info.key_exchange_group)); |
| } else if (security_info.key_exchange_group != 0) { |
| key_exchange_name = l10n_util::GetStringFUTF16( |
| IDS_SSL_KEY_EXCHANGE_WITH_GROUP, base::ASCIIToUTF16(key_exchange), |
| base::ASCIIToUTF16( |
| SSL_get_curve_name(security_info.key_exchange_group))); |
| } else { |
| key_exchange_name = base::ASCIIToUTF16(key_exchange); |
| } |
| |
| if (security_info.obsolete_ssl_status == net::OBSOLETE_SSL_NONE) { |
| security_style_explanations->secure_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_SSL_CONNECTION_TITLE), |
| l10n_util::GetStringFUTF8(IDS_STRONG_SSL_SUMMARY, protocol_name), |
| l10n_util::GetStringFUTF8(IDS_STRONG_SSL_DESCRIPTION, protocol_name, |
| key_exchange_name, cipher_name))); |
| return; |
| } |
| |
| std::vector<base::string16> description_replacements; |
| int status = security_info.obsolete_ssl_status; |
| int str_id; |
| |
| str_id = (status & net::OBSOLETE_SSL_MASK_PROTOCOL) |
| ? IDS_SSL_AN_OBSOLETE_PROTOCOL |
| : IDS_SSL_A_STRONG_PROTOCOL; |
| description_replacements.push_back(protocol_name); |
| description_replacements.push_back(l10n_util::GetStringUTF16(str_id)); |
| |
| str_id = (status & net::OBSOLETE_SSL_MASK_KEY_EXCHANGE) |
| ? IDS_SSL_AN_OBSOLETE_KEY_EXCHANGE |
| : IDS_SSL_A_STRONG_KEY_EXCHANGE; |
| description_replacements.push_back(key_exchange_name); |
| description_replacements.push_back(l10n_util::GetStringUTF16(str_id)); |
| |
| str_id = (status & net::OBSOLETE_SSL_MASK_CIPHER) ? IDS_SSL_AN_OBSOLETE_CIPHER |
| : IDS_SSL_A_STRONG_CIPHER; |
| description_replacements.push_back(cipher_name); |
| description_replacements.push_back(l10n_util::GetStringUTF16(str_id)); |
| |
| security_style_explanations->info_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_SSL_CONNECTION_TITLE), |
| l10n_util::GetStringUTF8(IDS_OBSOLETE_SSL_SUMMARY), |
| base::UTF16ToUTF8( |
| l10n_util::GetStringFUTF16(IDS_OBSOLETE_SSL_DESCRIPTION, |
| description_replacements, nullptr)))); |
| } |
| |
| void ExplainContentSecurity( |
| const security_state::SecurityInfo& security_info, |
| content::SecurityStyleExplanations* security_style_explanations) { |
| security_style_explanations->ran_insecure_content_style = |
| SecurityLevelToSecurityStyle(security_state::kRanInsecureContentLevel); |
| security_style_explanations->displayed_insecure_content_style = |
| SecurityLevelToSecurityStyle( |
| security_state::kDisplayedInsecureContentLevel); |
| |
| // Add the secure explanation unless there is an issue. |
| bool add_secure_explanation = true; |
| |
| security_style_explanations->ran_mixed_content = |
| security_info.mixed_content_status == |
| security_state::CONTENT_STATUS_RAN || |
| security_info.mixed_content_status == |
| security_state::CONTENT_STATUS_DISPLAYED_AND_RAN; |
| if (security_style_explanations->ran_mixed_content) { |
| add_secure_explanation = false; |
| security_style_explanations->insecure_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_RESOURCE_SECURITY_TITLE), |
| l10n_util::GetStringUTF8(IDS_MIXED_ACTIVE_CONTENT_SUMMARY), |
| l10n_util::GetStringUTF8(IDS_MIXED_ACTIVE_CONTENT_DESCRIPTION), |
| nullptr, blink::WebMixedContentContextType::kBlockable)); |
| } |
| |
| security_style_explanations->displayed_mixed_content = |
| security_info.mixed_content_status == |
| security_state::CONTENT_STATUS_DISPLAYED || |
| security_info.mixed_content_status == |
| security_state::CONTENT_STATUS_DISPLAYED_AND_RAN; |
| if (security_style_explanations->displayed_mixed_content) { |
| add_secure_explanation = false; |
| security_style_explanations->neutral_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_RESOURCE_SECURITY_TITLE), |
| l10n_util::GetStringUTF8(IDS_MIXED_PASSIVE_CONTENT_SUMMARY), |
| l10n_util::GetStringUTF8(IDS_MIXED_PASSIVE_CONTENT_DESCRIPTION), |
| nullptr, blink::WebMixedContentContextType::kOptionallyBlockable)); |
| } |
| |
| security_style_explanations->contained_mixed_form = |
| security_info.contained_mixed_form; |
| if (security_style_explanations->contained_mixed_form) { |
| add_secure_explanation = false; |
| security_style_explanations->neutral_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_RESOURCE_SECURITY_TITLE), |
| l10n_util::GetStringUTF8(IDS_NON_SECURE_FORM_SUMMARY), |
| l10n_util::GetStringUTF8(IDS_NON_SECURE_FORM_DESCRIPTION))); |
| } |
| |
| // If the main resource was loaded with no certificate errors or only minor |
| // certificate errors, then record the presence of subresources with |
| // certificate errors. Subresource certificate errors aren't recorded when the |
| // main resource was loaded with major certificate errors because, in the |
| // common case, these subresource certificate errors would be duplicative with |
| // the main resource's error. |
| bool is_cert_status_error = net::IsCertStatusError(security_info.cert_status); |
| bool is_cert_status_minor_error = |
| net::IsCertStatusMinorError(security_info.cert_status); |
| if (!is_cert_status_error || is_cert_status_minor_error) { |
| security_style_explanations->ran_content_with_cert_errors = |
| security_info.content_with_cert_errors_status == |
| security_state::CONTENT_STATUS_RAN || |
| security_info.content_with_cert_errors_status == |
| security_state::CONTENT_STATUS_DISPLAYED_AND_RAN; |
| if (security_style_explanations->ran_content_with_cert_errors) { |
| add_secure_explanation = false; |
| security_style_explanations->insecure_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_RESOURCE_SECURITY_TITLE), |
| l10n_util::GetStringUTF8(IDS_CERT_ERROR_ACTIVE_CONTENT_SUMMARY), |
| l10n_util::GetStringUTF8( |
| IDS_CERT_ERROR_ACTIVE_CONTENT_DESCRIPTION))); |
| } |
| |
| security_style_explanations->displayed_content_with_cert_errors = |
| security_info.content_with_cert_errors_status == |
| security_state::CONTENT_STATUS_DISPLAYED || |
| security_info.content_with_cert_errors_status == |
| security_state::CONTENT_STATUS_DISPLAYED_AND_RAN; |
| if (security_style_explanations->displayed_content_with_cert_errors) { |
| add_secure_explanation = false; |
| security_style_explanations->neutral_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_RESOURCE_SECURITY_TITLE), |
| l10n_util::GetStringUTF8(IDS_CERT_ERROR_PASSIVE_CONTENT_SUMMARY), |
| l10n_util::GetStringUTF8( |
| IDS_CERT_ERROR_PASSIVE_CONTENT_DESCRIPTION))); |
| } |
| } |
| |
| if (add_secure_explanation) { |
| DCHECK(security_info.scheme_is_cryptographic); |
| security_style_explanations->secure_explanations.push_back( |
| content::SecurityStyleExplanation( |
| l10n_util::GetStringUTF8(IDS_RESOURCE_SECURITY_TITLE), |
| l10n_util::GetStringUTF8(IDS_SECURE_RESOURCES_SUMMARY), |
| l10n_util::GetStringUTF8(IDS_SECURE_RESOURCES_DESCRIPTION))); |
| } |
| } |
| |
| } // namespace |
| |
| std::unique_ptr<security_state::VisibleSecurityState> GetVisibleSecurityState( |
| content::WebContents* web_contents) { |
| auto state = std::make_unique<security_state::VisibleSecurityState>(); |
| |
| content::NavigationEntry* entry = |
| web_contents->GetController().GetVisibleEntry(); |
| if (!entry) |
| return state; |
| // Set fields that are not dependent on the connection info. |
| state->is_error_page = entry->GetPageType() == content::PAGE_TYPE_ERROR; |
| state->is_view_source = |
| entry->GetVirtualURL().SchemeIs(content::kViewSourceScheme); |
| state->url = entry->GetURL(); |
| |
| if (!entry->GetSSL().initialized) |
| return state; |
| state->connection_info_initialized = true; |
| const content::SSLStatus& ssl = entry->GetSSL(); |
| state->certificate = ssl.certificate; |
| state->cert_status = ssl.cert_status; |
| state->connection_status = ssl.connection_status; |
| state->key_exchange_group = ssl.key_exchange_group; |
| state->security_bits = ssl.security_bits; |
| state->pkp_bypassed = ssl.pkp_bypassed; |
| state->displayed_mixed_content = |
| !!(ssl.content_status & content::SSLStatus::DISPLAYED_INSECURE_CONTENT); |
| state->ran_mixed_content = |
| !!(ssl.content_status & content::SSLStatus::RAN_INSECURE_CONTENT); |
| state->displayed_content_with_cert_errors = |
| !!(ssl.content_status & |
| content::SSLStatus::DISPLAYED_CONTENT_WITH_CERT_ERRORS); |
| state->ran_content_with_cert_errors = |
| !!(ssl.content_status & content::SSLStatus::RAN_CONTENT_WITH_CERT_ERRORS); |
| state->contained_mixed_form = |
| !!(ssl.content_status & |
| content::SSLStatus::DISPLAYED_FORM_WITH_INSECURE_ACTION); |
| |
| SSLStatusInputEventData* input_events = |
| static_cast<SSLStatusInputEventData*>(ssl.user_data.get()); |
| |
| if (input_events) |
| state->insecure_input_events = *input_events->input_events(); |
| |
| return state; |
| } |
| |
| blink::WebSecurityStyle GetSecurityStyle( |
| const security_state::SecurityInfo& security_info, |
| content::SecurityStyleExplanations* security_style_explanations) { |
| const blink::WebSecurityStyle security_style = |
| SecurityLevelToSecurityStyle(security_info.security_level); |
| |
| ExplainHTTPSecurity(security_info, security_style_explanations); |
| ExplainSafeBrowsingSecurity(security_info, security_style_explanations); |
| |
| // Check if the page is HTTP; if so, no more explanations are needed. Note |
| // that SecurityStyleUnauthenticated does not necessarily mean that |
| // the page is loaded over HTTP, because the security style merely |
| // represents how the embedder wishes to display the security state of |
| // the page, and the embedder can choose to display HTTPS page as HTTP |
| // if it wants to (for example, displaying deprecated crypto |
| // algorithms with the same UI treatment as HTTP pages). |
| security_style_explanations->scheme_is_cryptographic = |
| security_info.scheme_is_cryptographic; |
| if (!security_info.scheme_is_cryptographic) { |
| return security_style; |
| } |
| |
| ExplainCertificateSecurity(security_info, security_style_explanations); |
| ExplainConnectionSecurity(security_info, security_style_explanations); |
| ExplainContentSecurity(security_info, security_style_explanations); |
| |
| return security_style; |
| } |
| |
| } // namespace security_state |