// 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() {}
DefaultDelegate(const DefaultDelegate&) = delete;
DefaultDelegate& operator=(const DefaultDelegate&) = delete;
~DefaultDelegate() override {}
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_IN_MIGRATION() << "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