// Copyright 2019 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_certificate_uploader_impl.h"
#include <memory>
#include <optional>
#include <utility>
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/time/time.h"
#include "chrome/browser/ash/attestation/attestation_ca_client.h"
#include "chromeos/ash/components/attestation/attestation_features.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/constants/attestation_constants.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 "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
namespace ash::attestation {
namespace {
// Constants for retrying certificate obtention and upload.
constexpr base::TimeDelta kRetryDelay = base::Seconds(5);
const int kRetryLimit = 100;
void DBusPrivacyCACallback(
const base::RepeatingCallback<void(const std::string&)> on_success,
const base::RepeatingCallback<void(AttestationStatus)> on_failure,
const base::Location& from_here,
AttestationStatus status,
const std::string& data) {
DCHECK(on_success);
DCHECK(on_failure);
if (status == ATTESTATION_SUCCESS) {
on_success.Run(data);
return;
}
LOG(ERROR) << "Attestation DBus method or server called failed with status: "
<< status << ": " << from_here.ToString();
on_failure.Run(status);
}
} // namespace
EnrollmentCertificateUploaderImpl::EnrollmentCertificateUploaderImpl(
policy::CloudPolicyClient* policy_client)
: policy_client_(policy_client),
retry_limit_(kRetryLimit),
retry_delay_(kRetryDelay) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
EnrollmentCertificateUploaderImpl::~EnrollmentCertificateUploaderImpl() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
void EnrollmentCertificateUploaderImpl::ObtainAndUploadCertificate(
UploadCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
bool start = callbacks_.empty();
callbacks_.push(std::move(callback));
if (start) {
num_retries_ = 0;
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&EnrollmentCertificateUploaderImpl::Start,
weak_factory_.GetWeakPtr()));
}
}
void EnrollmentCertificateUploaderImpl::Start() {
if (!attestation_flow_) {
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();
}
GetCertificate();
}
void EnrollmentCertificateUploaderImpl::GetCertificate() {
if (!policy_client_->is_registered()) {
LOG(ERROR) << "CloudPolicyClient not registered.";
RunCallbacks(Status::kInvalidClient);
return;
}
auto callback = base::BindOnce(
[](base::RepeatingCallback<void(const std::string&)> on_success,
base::RepeatingCallback<void(AttestationStatus)> on_failure,
const base::Location& from_here, AttestationStatus status,
const std::string& data) {
DBusPrivacyCACallback(on_success, on_failure, from_here, status, data);
},
base::BindRepeating(
&EnrollmentCertificateUploaderImpl::UploadCertificateIfNeeded,
weak_factory_.GetWeakPtr()),
base::BindRepeating(
&EnrollmentCertificateUploaderImpl::HandleGetCertificateFailure,
weak_factory_.GetWeakPtr()),
FROM_HERE);
AttestationFeatures::GetFeatures(
base::BindOnce(&EnrollmentCertificateUploaderImpl::OnGetFeaturesReady,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void EnrollmentCertificateUploaderImpl::OnGetFeaturesReady(
AttestationFlow::CertificateCallback callback,
const AttestationFeatures* features) {
if (!features) {
LOG(ERROR) << "Failed to get AttestationFeatures.";
std::move(callback).Run(ATTESTATION_UNSPECIFIED_FAILURE, "");
return;
}
if (!features->IsAttestationAvailable()) {
LOG(ERROR) << "The Attestation is not available.";
std::move(callback).Run(ATTESTATION_UNSPECIFIED_FAILURE, "");
return;
}
// prefers ECC certificate if available
::attestation::KeyType key_crypto_type;
if (features->IsEccSupported()) {
key_crypto_type = ::attestation::KEY_TYPE_ECC;
} else if (features->IsRsaSupported()) {
key_crypto_type = ::attestation::KEY_TYPE_RSA;
} else {
LOG(ERROR) << "No appropriate crypto key type supported.";
std::move(callback).Run(ATTESTATION_UNSPECIFIED_FAILURE, "");
return;
}
// Always force a new key to obtain a fresh certificate.
// Expired certificates are rejected by the server. It is easier to force
// the certificate refresh rather than ensure certificate expiry status, since
// the certificate upload is not expected to happen too often. See b/163817801
// and b/216220722 for the context.
attestation_flow_->GetCertificate(
/*certificate_profile=*/PROFILE_ENTERPRISE_ENROLLMENT_CERTIFICATE,
/*account_id=*/EmptyAccountId(), // Not used.
/*request_origin=*/std::string(), // Not used.
/*force_new_key=*/true, key_crypto_type,
/*key_name=*/kEnterpriseEnrollmentKey,
/*profile_specific_data=*/std::nullopt,
/*callback=*/std::move(callback));
}
void EnrollmentCertificateUploaderImpl::HandleGetCertificateFailure(
AttestationStatus status) {
if (status == ATTESTATION_SERVER_BAD_REQUEST_FAILURE) {
LOG(ERROR)
<< "Failed to fetch Enterprise Enrollment Certificate: bad request.";
RunCallbacks(Status::kFailedToFetch);
return;
}
LOG(WARNING) << "Failed to fetch Enterprise Enrollment Certificate.";
if (!Reschedule()) {
RunCallbacks(Status::kFailedToFetch);
}
}
void EnrollmentCertificateUploaderImpl::UploadCertificateIfNeeded(
const std::string& pem_certificate_chain) {
if (has_already_uploaded_) {
RunCallbacks(Status::kSuccess);
return;
}
if (!policy_client_->is_registered()) {
LOG(ERROR) << "CloudPolicyClient not registered.";
RunCallbacks(Status::kInvalidClient);
return;
}
policy_client_->UploadEnterpriseEnrollmentCertificate(
pem_certificate_chain,
base::BindOnce(&EnrollmentCertificateUploaderImpl::OnUploadComplete,
weak_factory_.GetWeakPtr()));
}
void EnrollmentCertificateUploaderImpl::OnUploadComplete(
policy::CloudPolicyClient::Result result) {
if (result.IsSuccess()) {
has_already_uploaded_ = true;
if (num_retries_ != 0) {
LOG(WARNING) << "Enterprise Enrollment Certificate uploaded to DMServer "
"after retries: "
<< num_retries_;
} else {
VLOG(1) << "Enterprise Enrollment Certificate uploaded to DMServer.";
}
RunCallbacks(Status::kSuccess);
return;
}
LOG(WARNING)
<< "Failed to upload Enterprise Enrollment Certificate to DMServer.";
if (!Reschedule()) {
RunCallbacks(Status::kFailedToUpload);
}
}
bool EnrollmentCertificateUploaderImpl::Reschedule() {
if (num_retries_ >= retry_limit_) {
LOG(ERROR) << "Retry limit exceeded to fetch enrollment certificate.";
return false;
}
++num_retries_;
content::GetUIThreadTaskRunner({})->PostDelayedTask(
FROM_HERE,
base::BindOnce(&EnrollmentCertificateUploaderImpl::Start,
weak_factory_.GetWeakPtr()),
retry_delay_);
return true;
}
void EnrollmentCertificateUploaderImpl::RunCallbacks(Status status) {
std::queue<UploadCallback> callbacks;
callbacks.swap(callbacks_);
for (; !callbacks.empty(); callbacks.pop())
std::move(callbacks.front()).Run(status);
}
} // namespace ash::attestation