chromium/chrome/browser/ash/attestation/enrollment_id_upload_manager.cc

// Copyright 2017 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/enrollment_id_upload_manager.h"

#include <optional>
#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "chrome/browser/ash/attestation/attestation_ca_client.h"
#include "chrome/browser/ash/attestation/attestation_key_payload.pb.h"
#include "chromeos/ash/components/cryptohome/cryptohome_parameters.h"
#include "chromeos/ash/components/dbus/attestation/attestation_client.h"
#include "chromeos/ash/components/dbus/attestation/interface.pb.h"
#include "chromeos/ash/components/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/common/dbus_callback.h"
#include "components/account_id/account_id.h"
#include "components/policy/core/common/cloud/cloud_policy_client.h"
#include "components/policy/core/common/cloud/cloud_policy_manager.h"
#include "components/user_manager/known_user.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "net/cert/x509_certificate.h"

namespace {

const int kRetryDelay = 5;  // Seconds.
const int kRetryLimit = 100;

}  // namespace

namespace ash {
namespace attestation {

EnrollmentIdUploadManager::EnrollmentIdUploadManager(
    policy::CloudPolicyClient* policy_client,
    EnrollmentCertificateUploader* certificate_uploader)
    : EnrollmentIdUploadManager(policy_client,
                                DeviceSettingsService::Get(),
                                certificate_uploader) {}

EnrollmentIdUploadManager::EnrollmentIdUploadManager(
    policy::CloudPolicyClient* policy_client,
    DeviceSettingsService* device_settings_service,
    EnrollmentCertificateUploader* certificate_uploader)
    : device_settings_service_(device_settings_service),
      policy_client_(policy_client),
      certificate_uploader_(certificate_uploader),
      num_retries_(0),
      retry_limit_(kRetryLimit),
      retry_delay_(kRetryDelay) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  device_settings_service_->AddObserver(this);
  Start();
}

EnrollmentIdUploadManager::~EnrollmentIdUploadManager() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(device_settings_service_);
  device_settings_service_->RemoveObserver(this);
}

void EnrollmentIdUploadManager::DeviceSettingsUpdated() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  num_retries_ = 0;
  Start();
}

void EnrollmentIdUploadManager::Start() {
  // If identification for enrollment isn't needed, there is nothing to do.
  const enterprise_management::PolicyData* policy_data =
      device_settings_service_->policy_data();
  if (!policy_data || !policy_data->enrollment_id_needed())
    return;

  // Do not check the result of the upload request, because DMServer will
  // inform us if the enrollment ID is needed the next time device policies
  // are fetched.
  ObtainAndUploadEnrollmentId(base::DoNothing());
}

void EnrollmentIdUploadManager::ObtainAndUploadEnrollmentId(
    UploadManagerCallback callback) {
  DCHECK(!callback.is_null());

  bool start = upload_manager_callbacks_.empty();
  upload_manager_callbacks_.push(std::move(callback));

  // Do not allow multiple concurrent requests.
  if (start) {
    certificate_uploader_->ObtainAndUploadCertificate(base::BindOnce(
        &EnrollmentIdUploadManager::OnEnrollmentCertificateUploaded,
        weak_factory_.GetWeakPtr()));
  }
}

void EnrollmentIdUploadManager::OnEnrollmentCertificateUploaded(
    EnrollmentCertificateUploader::Status status) {
  switch (status) {
    case EnrollmentCertificateUploader::Status::kSuccess:
      // Enrollment certificate uploaded successfully. No need to compute EID.
      RunCallbacks(/*status=*/true);
      break;
    case EnrollmentCertificateUploader::Status::kFailedToFetch:
      LOG(WARNING) << "EnrollmentIdUploadManager: Failed to fetch certificate. "
                      "Trying to compute and upload enrollment ID.";
      GetEnrollmentId();
      break;
    case EnrollmentCertificateUploader::Status::kFailedToUpload:
      // Enrollment certificate was fetched but not uploaded. It can be uploaded
      // later so we will not proceed with computed EID.
      RunCallbacks(/*status=*/false);
      break;
    case EnrollmentCertificateUploader::Status::kInvalidClient:
      // Enrollment certificate was not uploaded due to invalid
      // `CloudPolicyClient`. The certificate can be uploaded later when the
      // client is working again. The manager is also not able to upload EID
      // with invalid `CloudPolicyClient` so there is no reason to fall back to
      // EID computation.
      RunCallbacks(/*status=*/false);
      break;
  }
}

void EnrollmentIdUploadManager::GetEnrollmentId() {
  // If we already uploaded an empty identification, we are done, because
  // we asked to compute and upload it only if the PCA refused to give
  // us an enrollment certificate, an error that will happen again (the
  // AIK certificate sent to request an enrollment certificate does not
  // contain an EID).
  if (did_upload_empty_eid_) {
    RunCallbacks(/*status=*/false);
    return;
  }

  // We expect a registered CloudPolicyClient.
  if (!policy_client_->is_registered()) {
    LOG(ERROR) << "EnrollmentIdUploadManager: Invalid CloudPolicyClient.";
    RunCallbacks(/*status=*/false);
    return;
  }

  ::attestation::GetEnrollmentIdRequest request;
  request.set_ignore_cache(true);
  AttestationClient::Get()->GetEnrollmentId(
      request, base::BindOnce(&EnrollmentIdUploadManager::OnGetEnrollmentId,
                              weak_factory_.GetWeakPtr()));
}

void EnrollmentIdUploadManager::OnGetEnrollmentId(
    const ::attestation::GetEnrollmentIdReply& reply) {
  if (reply.status() != ::attestation::STATUS_SUCCESS) {
    LOG(WARNING) << "Failed to get enrollment ID: " << reply.status();
    RescheduleGetEnrollmentId();
    return;
  }
  if (reply.enrollment_id().empty()) {
    LOG(WARNING) << "EnrollmentIdUploadManager: The enrollment identifier "
                    "obtained is empty.";
  }
  policy_client_->UploadEnterpriseEnrollmentId(
      reply.enrollment_id(),
      base::BindOnce(&EnrollmentIdUploadManager::OnUploadComplete,
                     weak_factory_.GetWeakPtr(), reply.enrollment_id()));
}

void EnrollmentIdUploadManager::RescheduleGetEnrollmentId() {
  if (++num_retries_ < retry_limit_) {
    content::GetUIThreadTaskRunner({})->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&EnrollmentIdUploadManager::GetEnrollmentId,
                       weak_factory_.GetWeakPtr()),
        base::Seconds(retry_delay_));
  } else {
    LOG(WARNING) << "EnrollmentIdUploadManager: Retry limit exceeded.";
    RunCallbacks(/*status=*/false);
  }
}

void EnrollmentIdUploadManager::OnUploadComplete(
    const std::string& enrollment_id,
    policy::CloudPolicyClient::Result result) {
  const std::string printable_enrollment_id =
      base::ToLowerASCII(base::HexEncode(enrollment_id));

  if (!result.IsSuccess()) {
    LOG(ERROR) << "Failed to upload Enrollment Identifier \""
               << printable_enrollment_id << "\" to DMServer.";
    RunCallbacks(/*status=*/false);
    return;
  }

  if (enrollment_id.empty())
    did_upload_empty_eid_ = true;

  VLOG(1) << "Enrollment Identifier \"" << printable_enrollment_id
          << "\" uploaded to DMServer.";
  RunCallbacks(/*status=*/true);
}

void EnrollmentIdUploadManager::RunCallbacks(bool status) {
  std::queue<UploadManagerCallback> callbacks;
  callbacks.swap(upload_manager_callbacks_);

  while (!callbacks.empty()) {
    std::move(callbacks.front()).Run(status);
    callbacks.pop();
  }
}

}  // namespace attestation
}  // namespace ash