| // Copyright 2013 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 "chrome/browser/ash/attestation/platform_verification_flow.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "ash/constants/ash_switches.h" |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/containers/span.h" |
| #include "base/logging.h" |
| #include "base/macros.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/time/time.h" |
| #include "base/timer/timer.h" |
| #include "chrome/browser/ash/attestation/attestation_ca_client.h" |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #include "chrome/browser/ash/settings/cros_settings.h" |
| #include "chrome/browser/permissions/permission_manager_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chromeos/attestation/attestation_flow.h" |
| #include "chromeos/attestation/attestation_flow_adaptive.h" |
| #include "chromeos/cryptohome/cryptohome_parameters.h" |
| #include "chromeos/dbus/attestation/attestation.pb.h" |
| #include "chromeos/dbus/attestation/attestation_client.h" |
| #include "chromeos/dbus/attestation/interface.pb.h" |
| #include "chromeos/dbus/constants/dbus_switches.h" |
| #include "chromeos/dbus/dbus_thread_manager.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/content_settings/core/common/content_settings_pattern.h" |
| #include "components/content_settings/core/common/content_settings_types.h" |
| #include "components/permissions/permission_manager.h" |
| #include "components/permissions/permission_result.h" |
| #include "components/user_manager/user.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/url_constants.h" |
| #include "net/cert/pem.h" |
| #include "net/cert/x509_certificate.h" |
| #include "third_party/blink/public/mojom/permissions/permission_status.mojom.h" |
| |
| namespace { |
| |
| using ash::attestation::PlatformVerificationFlow; |
| |
| const int kTimeoutInSeconds = 8; |
| const char kAttestationResultHistogram[] = |
| "ChromeOS.PlatformVerification.Result2"; |
| const char kAttestationAvailableHistogram[] = |
| "ChromeOS.PlatformVerification.Available"; |
| const int kOpportunisticRenewalThresholdInDays = 30; |
| |
| // A helper to call a ChallengeCallback with an error result. |
| void ReportError(PlatformVerificationFlow::ChallengeCallback callback, |
| PlatformVerificationFlow::Result error) { |
| UMA_HISTOGRAM_ENUMERATION(kAttestationResultHistogram, error, |
| PlatformVerificationFlow::RESULT_MAX); |
| std::move(callback).Run(error, std::string(), std::string(), std::string()); |
| } |
| |
| } // namespace |
| |
| namespace ash { |
| namespace attestation { |
| |
| // A default implementation of the Delegate interface. |
| class DefaultDelegate : public PlatformVerificationFlow::Delegate { |
| public: |
| DefaultDelegate() {} |
| ~DefaultDelegate() override {} |
| |
| bool IsInSupportedMode() override { |
| base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); |
| return !command_line->HasSwitch(chromeos::switches::kSystemDevMode) || |
| command_line->HasSwitch(chromeos::switches::kAllowRAInDevMode); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(DefaultDelegate); |
| }; |
| |
| PlatformVerificationFlow::ChallengeContext::ChallengeContext( |
| const AccountId& account_id, |
| const std::string& service_id, |
| const std::string& challenge, |
| ChallengeCallback callback) |
| : account_id(account_id), |
| service_id(service_id), |
| challenge(challenge), |
| callback(std::move(callback)) {} |
| |
| PlatformVerificationFlow::ChallengeContext::ChallengeContext( |
| ChallengeContext&& other) = default; |
| |
| PlatformVerificationFlow::ChallengeContext::~ChallengeContext() = default; |
| |
| PlatformVerificationFlow::PlatformVerificationFlow() |
| : attestation_flow_(NULL), |
| attestation_client_(AttestationClient::Get()), |
| delegate_(NULL), |
| timeout_delay_(base::TimeDelta::FromSeconds(kTimeoutInSeconds)) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| std::unique_ptr<ServerProxy> attestation_ca_client(new AttestationCAClient()); |
| default_attestation_flow_ = std::make_unique<AttestationFlowAdaptive>( |
| std::move(attestation_ca_client)); |
| attestation_flow_ = default_attestation_flow_.get(); |
| default_delegate_ = std::make_unique<DefaultDelegate>(); |
| delegate_ = default_delegate_.get(); |
| } |
| |
| PlatformVerificationFlow::PlatformVerificationFlow( |
| AttestationFlow* attestation_flow, |
| AttestationClient* attestation_client, |
| Delegate* delegate) |
| : attestation_flow_(attestation_flow), |
| attestation_client_(attestation_client), |
| delegate_(delegate), |
| timeout_delay_(base::TimeDelta::FromSeconds(kTimeoutInSeconds)) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!delegate_) { |
| default_delegate_ = std::make_unique<DefaultDelegate>(); |
| delegate_ = default_delegate_.get(); |
| } |
| } |
| |
| PlatformVerificationFlow::~PlatformVerificationFlow() = default; |
| |
| // static |
| bool PlatformVerificationFlow::IsAttestationAllowedByPolicy() { |
| // Check the device policy for the feature. |
| bool enabled_for_device = false; |
| if (!CrosSettings::Get()->GetBoolean(kAttestationForContentProtectionEnabled, |
| &enabled_for_device)) { |
| LOG(ERROR) << "Failed to get device setting."; |
| return false; |
| } |
| if (!enabled_for_device) { |
| VLOG(1) << "Platform verification denied because Verified Access is " |
| << "disabled for the device."; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void PlatformVerificationFlow::ChallengePlatformKey( |
| content::WebContents* web_contents, |
| const std::string& service_id, |
| const std::string& challenge, |
| ChallengeCallback callback) { |
| const user_manager::User* user = ProfileHelper::Get()->GetUserByProfile( |
| Profile::FromBrowserContext(web_contents->GetBrowserContext())); |
| ChallengePlatformKey(user, service_id, challenge, std::move(callback)); |
| } |
| |
| void PlatformVerificationFlow::ChallengePlatformKey( |
| const user_manager::User* user, |
| const std::string& service_id, |
| const std::string& challenge, |
| ChallengeCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // Note: The following checks are performed when use of the protected media |
| // identifier is indicated. The first two in GetPermissionStatus and the third |
| // in DecidePermission. |
| // In Chrome, the result of the first and third could have changed in the |
| // interim, but the mode cannot change. |
| // TODO(ddorwin): Share more code for the first two checks with |
| // ProtectedMediaIdentifierPermissionContext:: |
| // IsProtectedMediaIdentifierEnabled(). |
| |
| if (!IsAttestationAllowedByPolicy()) { |
| VLOG(1) << "Platform verification not allowed by device policy."; |
| ReportError(std::move(callback), POLICY_REJECTED); |
| return; |
| } |
| |
| if (!delegate_->IsInSupportedMode()) { |
| LOG(ERROR) << "Platform verification not supported in the current mode."; |
| ReportError(std::move(callback), PLATFORM_NOT_VERIFIED); |
| return; |
| } |
| |
| if (!user) { |
| LOG(ERROR) << "Profile does not map to a valid user."; |
| ReportError(std::move(callback), INTERNAL_ERROR); |
| return; |
| } |
| |
| ChallengeContext context(user->GetAccountId(), service_id, challenge, |
| std::move(callback)); |
| |
| // Check if the device has been prepared to use attestation. |
| ::attestation::GetEnrollmentPreparationsRequest request; |
| attestation_client_->GetEnrollmentPreparations( |
| request, base::BindOnce(&PlatformVerificationFlow::OnAttestationPrepared, |
| this, std::move(context))); |
| } |
| |
| void PlatformVerificationFlow::OnAttestationPrepared( |
| ChallengeContext context, |
| const ::attestation::GetEnrollmentPreparationsReply& reply) { |
| if (reply.status() != ::attestation::STATUS_SUCCESS) { |
| LOG(ERROR) |
| << "Platform verification failed to check if attestation is prepared."; |
| ReportError(std::move(context).callback, INTERNAL_ERROR); |
| return; |
| } |
| const bool attestation_prepared = |
| AttestationClient::IsAttestationPrepared(reply); |
| UMA_HISTOGRAM_BOOLEAN(kAttestationAvailableHistogram, attestation_prepared); |
| |
| if (!attestation_prepared) { |
| // This device is not currently able to use attestation features. |
| ReportError(std::move(context).callback, PLATFORM_NOT_VERIFIED); |
| return; |
| } |
| |
| auto shared_context = |
| base::MakeRefCounted<base::RefCountedData<ChallengeContext>>( |
| std::move(context)); |
| GetCertificate(std::move(shared_context), false /* Don't force a new key */); |
| } |
| |
| void PlatformVerificationFlow::GetCertificate( |
| scoped_refptr<base::RefCountedData<ChallengeContext>> context, |
| bool force_new_key) { |
| auto timer = std::make_unique<base::OneShotTimer>(); |
| base::OnceClosure timeout_callback = base::BindOnce( |
| &PlatformVerificationFlow::OnCertificateTimeout, this, context); |
| timer->Start(FROM_HERE, timeout_delay_, std::move(timeout_callback)); |
| |
| AttestationFlow::CertificateCallback certificate_callback = |
| base::BindOnce(&PlatformVerificationFlow::OnCertificateReady, this, |
| context, context->data.account_id, std::move(timer)); |
| attestation_flow_->GetCertificate( |
| PROFILE_CONTENT_PROTECTION_CERTIFICATE, context->data.account_id, |
| context->data.service_id, force_new_key, std::string() /*key_name*/, |
| std::move(certificate_callback)); |
| } |
| |
| void PlatformVerificationFlow::OnCertificateReady( |
| scoped_refptr<base::RefCountedData<ChallengeContext>> context, |
| const AccountId& account_id, |
| std::unique_ptr<base::OneShotTimer> timer, |
| AttestationStatus operation_status, |
| const std::string& certificate_chain) { |
| // Log failure before checking the timer so all failures are logged, even if |
| // they took too long. |
| if (operation_status != ATTESTATION_SUCCESS) { |
| LOG(WARNING) << "PlatformVerificationFlow: Failed to certify platform."; |
| } |
| if (!timer->IsRunning()) { |
| LOG(WARNING) << "PlatformVerificationFlow: Certificate ready but call has " |
| << "already timed out."; |
| return; |
| } |
| timer->Stop(); |
| if (operation_status != ATTESTATION_SUCCESS) { |
| ReportError(std::move(*context).data.callback, PLATFORM_NOT_VERIFIED); |
| return; |
| } |
| ExpiryStatus expiry_status = CheckExpiry(certificate_chain); |
| if (expiry_status == EXPIRY_STATUS_EXPIRED) { |
| GetCertificate(std::move(context), true /* Force a new key */); |
| return; |
| } |
| bool is_expiring_soon = (expiry_status == EXPIRY_STATUS_EXPIRING_SOON); |
| std::string key_name = kContentProtectionKeyPrefix + context->data.service_id; |
| std::string challenge = context->data.challenge; |
| ::attestation::SignSimpleChallengeRequest request; |
| request.set_username(cryptohome::Identification(account_id).id()); |
| request.set_key_label(std::move(key_name)); |
| request.set_challenge(std::move(challenge)); |
| AttestationClient::Get()->SignSimpleChallenge( |
| request, base::BindOnce(&PlatformVerificationFlow::OnChallengeReady, this, |
| std::move(*context).data, account_id, |
| certificate_chain, is_expiring_soon)); |
| } |
| |
| void PlatformVerificationFlow::OnCertificateTimeout( |
| scoped_refptr<base::RefCountedData<ChallengeContext>> context) { |
| LOG(WARNING) << "PlatformVerificationFlow: Timing out."; |
| ReportError(std::move(*context).data.callback, TIMEOUT); |
| } |
| |
| void PlatformVerificationFlow::OnChallengeReady( |
| ChallengeContext context, |
| const AccountId& account_id, |
| const std::string& certificate_chain, |
| bool is_expiring_soon, |
| const ::attestation::SignSimpleChallengeReply& reply) { |
| if (reply.status() != ::attestation::STATUS_SUCCESS) { |
| LOG(ERROR) << "PlatformVerificationFlow: Failed to sign challenge: " |
| << reply.status(); |
| ReportError(std::move(context).callback, INTERNAL_ERROR); |
| return; |
| } |
| chromeos::attestation::SignedData signed_data_pb; |
| if (reply.challenge_response().empty() || |
| !signed_data_pb.ParseFromString(reply.challenge_response())) { |
| LOG(ERROR) << "PlatformVerificationFlow: Failed to parse response data."; |
| ReportError(std::move(context).callback, INTERNAL_ERROR); |
| return; |
| } |
| VLOG(1) << "Platform verification successful."; |
| UMA_HISTOGRAM_ENUMERATION(kAttestationResultHistogram, SUCCESS, RESULT_MAX); |
| std::move(context.callback) |
| .Run(SUCCESS, signed_data_pb.data(), signed_data_pb.signature(), |
| certificate_chain); |
| if (is_expiring_soon && renewals_in_progress_.count(certificate_chain) == 0) { |
| renewals_in_progress_.insert(certificate_chain); |
| // Fire off a certificate request so next time we'll have a new one. |
| AttestationFlow::CertificateCallback renew_callback = |
| base::BindOnce(&PlatformVerificationFlow::RenewCertificateCallback, |
| this, std::move(certificate_chain)); |
| attestation_flow_->GetCertificate( |
| PROFILE_CONTENT_PROTECTION_CERTIFICATE, context.account_id, |
| context.service_id, |
| true, // force_new_key |
| std::string(), // key_name, empty means a default one will be |
| // generated. |
| std::move(renew_callback)); |
| } |
| } |
| |
| PlatformVerificationFlow::ExpiryStatus PlatformVerificationFlow::CheckExpiry( |
| const std::string& certificate_chain) { |
| bool is_expiring_soon = false; |
| bool invalid_certificate_found = false; |
| int num_certificates = 0; |
| net::PEMTokenizer pem_tokenizer(certificate_chain, {"CERTIFICATE"}); |
| while (pem_tokenizer.GetNext()) { |
| ++num_certificates; |
| scoped_refptr<net::X509Certificate> x509 = |
| net::X509Certificate::CreateFromBytes( |
| base::as_bytes(base::make_span(pem_tokenizer.data()))); |
| if (!x509.get() || x509->valid_expiry().is_null()) { |
| // This logic intentionally fails open. In theory this should not happen |
| // but in practice parsing X.509 can be brittle and there are a lot of |
| // factors including which underlying module is parsing the certificate, |
| // whether that module performs more checks than just ASN.1/DER format, |
| // and the server module that generated the certificate(s). Renewal is |
| // expensive so we only renew certificates with good evidence that they |
| // have expired or will soon expire; if we don't know, we don't renew. |
| LOG(WARNING) << "Failed to parse certificate, cannot check expiry."; |
| invalid_certificate_found = true; |
| continue; |
| } |
| if (base::Time::Now() > x509->valid_expiry()) { |
| return EXPIRY_STATUS_EXPIRED; |
| } |
| base::TimeDelta threshold = |
| base::TimeDelta::FromDays(kOpportunisticRenewalThresholdInDays); |
| if (x509->valid_expiry() - base::Time::Now() < threshold) { |
| is_expiring_soon = true; |
| } |
| } |
| if (is_expiring_soon) { |
| return EXPIRY_STATUS_EXPIRING_SOON; |
| } |
| if (invalid_certificate_found) { |
| return EXPIRY_STATUS_INVALID_X509; |
| } |
| if (num_certificates == 0) { |
| LOG(WARNING) << "Failed to parse certificate chain, cannot check expiry."; |
| return EXPIRY_STATUS_INVALID_PEM_CHAIN; |
| } |
| return EXPIRY_STATUS_OK; |
| } |
| |
| void PlatformVerificationFlow::RenewCertificateCallback( |
| const std::string& old_certificate_chain, |
| AttestationStatus operation_status, |
| const std::string& certificate_chain) { |
| renewals_in_progress_.erase(old_certificate_chain); |
| if (operation_status != ATTESTATION_SUCCESS) { |
| LOG(WARNING) << "PlatformVerificationFlow: Failed to renew platform " |
| "certificate."; |
| return; |
| } |
| VLOG(1) << "Certificate successfully renewed."; |
| } |
| |
| } // namespace attestation |
| } // namespace ash |