| // Copyright 2013 The Chromium Authors |
| // 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 <optional> |
| #include <string_view> |
| #include <utility> |
| |
| #include "ash/constants/ash_switches.h" |
| #include "base/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/strcat.h" |
| #include "base/time/time.h" |
| #include "base/timer/timer.h" |
| #include "chrome/browser/ash/attestation/attestation_ca_client.h" |
| #include "chrome/browser/ash/attestation/certificate_util.h" |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chromeos/ash/components/attestation/attestation_flow.h" |
| #include "chromeos/ash/components/attestation/attestation_flow_adaptive.h" |
| #include "chromeos/ash/components/cryptohome/cryptohome_parameters.h" |
| #include "chromeos/ash/components/dbus/attestation/attestation.pb.h" |
| #include "chromeos/ash/components/dbus/attestation/attestation_client.h" |
| #include "chromeos/ash/components/dbus/attestation/interface.pb.h" |
| #include "chromeos/ash/components/dbus/constants/attestation_constants.h" |
| #include "chromeos/ash/components/dbus/dbus_thread_manager.h" |
| #include "chromeos/ash/components/settings/cros_settings.h" |
| #include "chromeos/dbus/constants/dbus_switches.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 "media/base/media_switches.h" |
| |
| namespace ash::attestation { |
| |
| namespace { |
| |
| const int kTimeoutInSeconds = 8; |
| const char kAttestationResultHistogram[] = |
| "ChromeOS.PlatformVerification.Result2"; |
| constexpr base::TimeDelta kOpportunisticRenewalThreshold = base::Days(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()); |
| } |
| |
| std::string GetKeyName(std::string_view request_origin) { |
| return base::StrCat( |
| {ash::attestation::kContentProtectionKeyPrefix, request_origin}); |
| } |
| |
| } // namespace |
| |
| // A default implementation of the Delegate interface. |
| class DefaultDelegate : public PlatformVerificationFlow::Delegate { |
| public: |
| DefaultDelegate() = default; |
| |
| DefaultDelegate(const DefaultDelegate&) = delete; |
| DefaultDelegate& operator=(const DefaultDelegate&) = delete; |
| |
| ~DefaultDelegate() override = default; |
| |
| bool IsInSupportedMode() override { |
| base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); |
| return !command_line->HasSwitch(chromeos::switches::kSystemDevMode) || |
| command_line->HasSwitch(::switches::kAllowRAInDevMode); |
| } |
| }; |
| |
| 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_(nullptr), |
| attestation_client_(AttestationClient::Get()), |
| delegate_(nullptr), |
| timeout_delay_(base::Seconds(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::Seconds(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); |
| |
| 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)); |
| |
| const std::string key_name = |
| GetKeyName(/*request_origin=*/context->data.service_id); |
| AttestationFlow::CertificateCallback certificate_callback = |
| base::BindOnce(&PlatformVerificationFlow::OnCertificateReady, this, |
| context, context->data.account_id, std::move(timer)); |
| attestation_flow_->GetCertificate( |
| /*certificate_profile=*/PROFILE_CONTENT_PROTECTION_CERTIFICATE, |
| /*account_id=*/context->data.account_id, |
| /*request_origin=*/context->data.service_id, |
| /*force_new_key=*/force_new_key, |
| /*key_crypto_type=*/::attestation::KEY_TYPE_RSA, |
| /*key_name=*/key_name, /*profile_specific_data=*/std::nullopt, |
| /*callback=*/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; |
| } |
| // EXPIRY_STATUS_INVALID_PEM_CHAIN and EXPIRY_STATUS_INVALID_X509 are not |
| // handled intentionally. |
| // 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. |
| 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; |
| } |
| 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. |
| const std::string key_name = |
| GetKeyName(/*request_origin=*/context.service_id); |
| AttestationFlow::CertificateCallback renew_callback = |
| base::BindOnce(&PlatformVerificationFlow::RenewCertificateCallback, |
| this, std::move(certificate_chain)); |
| attestation_flow_->GetCertificate( |
| /*certificate_profile=*/PROFILE_CONTENT_PROTECTION_CERTIFICATE, |
| /*account_id=*/context.account_id, |
| /*request_origin=*/context.service_id, |
| /*force_new_key=*/true, // force_new_key |
| /*key_crypto_type=*/::attestation::KEY_TYPE_RSA, |
| /*key_name=*/key_name, |
| /*profile_specific_data=*/std::nullopt, |
| /*callback=*/std::move(renew_callback)); |
| } |
| } |
| |
| PlatformVerificationFlow::ExpiryStatus PlatformVerificationFlow::CheckExpiry( |
| const std::string& certificate_chain) { |
| CertificateExpiryStatus cert_status = |
| CheckCertificateExpiry(certificate_chain, kOpportunisticRenewalThreshold); |
| LOG_IF(ERROR, cert_status != CertificateExpiryStatus::kValid) |
| << "Failed to parse certificate, cannot check expiry: " |
| << CertificateExpiryStatusToString(cert_status); |
| switch (cert_status) { |
| case CertificateExpiryStatus::kValid: |
| return EXPIRY_STATUS_OK; |
| case CertificateExpiryStatus::kExpiringSoon: |
| return EXPIRY_STATUS_EXPIRING_SOON; |
| case CertificateExpiryStatus::kExpired: |
| return EXPIRY_STATUS_EXPIRED; |
| case CertificateExpiryStatus::kInvalidPemChain: |
| return EXPIRY_STATUS_INVALID_PEM_CHAIN; |
| case CertificateExpiryStatus::kInvalidX509: |
| return EXPIRY_STATUS_INVALID_X509; |
| } |
| |
| NOTREACHED() << "Unknown certificate status"; |
| } |
| |
| 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 ash::attestation |