// Copyright 2022 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/crosapi/cert_provisioning_ash.h"
#include "base/base64.h"
#include "base/test/test_future.h"
#include "chrome/browser/ash/cert_provisioning/mock_cert_provisioning_scheduler.h"
#include "chrome/browser/ash/cert_provisioning/mock_cert_provisioning_worker.h"
#include "chromeos/crosapi/mojom/cert_provisioning.mojom.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::base::test::TestFuture;
using ::testing::ByMove;
using ::testing::DoAll;
using ::testing::Invoke;
using ::testing::Mock;
using ::testing::Return;
using ::testing::ReturnPointee;
using ::testing::ReturnRef;
using ::testing::SaveArg;
namespace crosapi {
namespace {
// Fake failure message used for tests. The exact content of the message can be
// chosen arbitrarily.
const char kFakeFailureMessage[] = "Failure Message";
// Extracted from a X.509 certificate using the command:
// openssl x509 -pubkey -noout -in cert.pem
// and reformatted as a single line.
// This represents a RSA public key.
constexpr char kDerEncodedSpkiBase64[] =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1na7r6WiaL5slsyHI7bEpP5ad9ffsz"
"T0mBi8yc03hJpxaA3/2/"
"PX7esUdTSGoZr1XVBxjjJc4AypzZKlsPqYKZ+lPHZPpXlp8JVHn8w8+"
"zmPKl319vVYdJv5AE0HOuJZ6a19fXxItgzoB+"
"oXgkA0mhyPygJwF3HMJfJHRrkxJ73c23R6kKvKTxqRKswvzTo5O5AzZFLdCe+"
"GVTJuPo4VToGd+ZhS7QvsY38nAYG57fMnzzs5jjMF042AzzWiMt9gGbeuqCE6LXqFuSJYPo+"
"TLaN7pwQx68PK5pd/lv58B7jjxCIAai0BX1rV6bl/Am3EukhTSuIcQiTr5c1G4E6bKwIDAQAB";
constexpr char kCertProfileVersion[] = "cert_profile_version_1";
constexpr base::TimeDelta kCertProfileRenewalPeriod = base::Days(30);
constexpr char kDeviceCertProfileId[] = "device_cert_profile_1";
constexpr char kDeviceCertProfileName[] = "Device Certificate Profile 1";
constexpr char kUserCertProfileId[] = "user_cert_profile_1";
constexpr char kUserCertProfileName[] = "User Certificate Profile 1";
constexpr char kFailedDeviceCertProfileId[] = "failed_device_cert_profile_1";
constexpr char kFailedDeviceCertProfileName[] =
"Failed Device Certificate Profile 1";
constexpr char kFailedUserCertProfileId[] = "failed_user_cert_profile_1";
constexpr char kFailedUserCertProfileName[] =
"Failed User Certificate Profile 1";
void SetupMockCertProvisioningWorker(
ash::cert_provisioning::MockCertProvisioningWorker* worker,
ash::cert_provisioning::CertProvisioningWorkerState state,
const std::vector<uint8_t>* public_key,
ash::cert_provisioning::CertProfile& cert_profile,
base::Time last_update_time,
std::optional<ash::cert_provisioning::BackendServerError>& backend_error) {
EXPECT_CALL(*worker, GetState).WillRepeatedly(Return(state));
EXPECT_CALL(*worker, GetLastUpdateTime)
.WillRepeatedly(Return(last_update_time));
EXPECT_CALL(*worker, GetPublicKey).WillRepeatedly(ReturnPointee(public_key));
ON_CALL(*worker, GetCertProfile).WillByDefault(ReturnRef(cert_profile));
ON_CALL(*worker, GetLastBackendServerError)
.WillByDefault(ReturnRef(backend_error));
}
class MockMojoObserver : public mojom::CertProvisioningObserver {
public:
MOCK_METHOD(void, OnStateChanged, (), (override));
auto GetRemote() { return receiver_.BindNewPipeAndPassRemote(); }
private:
mojo::Receiver<crosapi::mojom::CertProvisioningObserver> receiver_{this};
};
class CertProvisioningAshTest : public ::testing::Test {
public:
void SetUp() override {
der_encoded_spki_ = base::Base64Decode(kDerEncodedSpkiBase64).value();
ON_CALL(user_scheduler_, GetWorkers)
.WillByDefault(ReturnRef(user_workers_));
ON_CALL(user_scheduler_, GetFailedCertProfileIds)
.WillByDefault(ReturnRef(user_failed_workers_));
ON_CALL(user_scheduler_, AddObserver)
.WillByDefault(
Invoke(this, &CertProvisioningAshTest::SaveUserObserver));
ON_CALL(device_scheduler_, GetWorkers)
.WillByDefault(ReturnRef(device_workers_));
ON_CALL(device_scheduler_, GetFailedCertProfileIds)
.WillByDefault(ReturnRef(device_failed_workers_));
ON_CALL(device_scheduler_, AddObserver)
.WillByDefault(
Invoke(this, &CertProvisioningAshTest::SaveDeviceObserver));
}
base::CallbackListSubscription SaveUserObserver(
base::RepeatingClosure callback) {
user_scheduler_observer_ = std::move(callback);
return base::CallbackListSubscription();
}
base::CallbackListSubscription SaveDeviceObserver(
base::RepeatingClosure callback) {
device_scheduler_observer_ = std::move(callback);
return base::CallbackListSubscription();
}
protected:
// The mojo service is async, so the tests need to explicitly give it an
// opportunity to execute its scheduled tasks within their synchronous
// sequences of calls.
void ExecuteAsyncTasks() { task_environment_.RunUntilIdle(); }
std::vector<uint8_t> der_encoded_spki_;
content::BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
ash::cert_provisioning::MockCertProvisioningScheduler user_scheduler_;
ash::cert_provisioning::WorkerMap user_workers_;
base::flat_map<ash::cert_provisioning::CertProfileId,
ash::cert_provisioning::FailedWorkerInfo>
user_failed_workers_;
base::RepeatingClosure user_scheduler_observer_;
ash::cert_provisioning::MockCertProvisioningScheduler device_scheduler_;
ash::cert_provisioning::WorkerMap device_workers_;
base::flat_map<ash::cert_provisioning::CertProfileId,
ash::cert_provisioning::FailedWorkerInfo>
device_failed_workers_;
base::RepeatingClosure device_scheduler_observer_;
// The CertProvisioningAsh mojo service under test.
CertProvisioningAsh service_;
};
TEST_F(CertProvisioningAshTest, ObserveSchedulersWhenNeeded) {
service_.InjectForTesting(&user_scheduler_, &device_scheduler_);
auto observer_1 = std::make_unique<MockMojoObserver>();
auto observer_2 = std::make_unique<MockMojoObserver>();
auto observer_3 = std::make_unique<MockMojoObserver>();
{
// AddObserver is called when the mojo service needs to start tracking
// changes. One observer per scheduler is enough for any number of mojo
// observers.
EXPECT_CALL(user_scheduler_, AddObserver);
EXPECT_CALL(device_scheduler_, AddObserver);
service_.AddObserver(observer_1->GetRemote());
service_.AddObserver(observer_2->GetRemote());
service_.AddObserver(observer_3->GetRemote());
ExecuteAsyncTasks();
Mock::VerifyAndClearExpectations(&user_scheduler_);
Mock::VerifyAndClearExpectations(&device_scheduler_);
}
{
// The mojo service is supposed to stop observing the schedulers without its
// own observers. It's indirectly verified by the fact that later it adds
// observers again.
observer_1.reset();
observer_2.reset();
observer_3.reset();
ExecuteAsyncTasks();
}
// Check that the mojo service can start and stop observering several times
// within its lifetime.
{
auto observer_4 = std::make_unique<MockMojoObserver>();
EXPECT_CALL(user_scheduler_, AddObserver);
EXPECT_CALL(device_scheduler_, AddObserver);
service_.AddObserver(observer_4->GetRemote());
ExecuteAsyncTasks();
Mock::VerifyAndClearExpectations(&user_scheduler_);
Mock::VerifyAndClearExpectations(&device_scheduler_);
observer_4.reset();
ExecuteAsyncTasks();
}
}
TEST_F(CertProvisioningAshTest, ChangeNotificationsForwarded) {
service_.InjectForTesting(&user_scheduler_, &device_scheduler_);
EXPECT_FALSE(user_scheduler_observer_);
EXPECT_FALSE(device_scheduler_observer_);
MockMojoObserver observer_1;
MockMojoObserver observer_2;
service_.AddObserver(observer_1.GetRemote());
service_.AddObserver(observer_2.GetRemote());
ASSERT_TRUE(user_scheduler_observer_);
ASSERT_TRUE(device_scheduler_observer_);
{
// User changes are propagated.
EXPECT_CALL(observer_1, OnStateChanged);
EXPECT_CALL(observer_2, OnStateChanged);
user_scheduler_observer_.Run();
ExecuteAsyncTasks();
Mock::VerifyAndClearExpectations(&observer_1);
Mock::VerifyAndClearExpectations(&observer_2);
}
{
// Device changes are propagated.
EXPECT_CALL(observer_1, OnStateChanged);
EXPECT_CALL(observer_2, OnStateChanged);
device_scheduler_observer_.Run();
ExecuteAsyncTasks();
Mock::VerifyAndClearExpectations(&observer_1);
Mock::VerifyAndClearExpectations(&observer_2);
}
}
TEST_F(CertProvisioningAshTest, GetStatusEmpty) {
service_.InjectForTesting(&user_scheduler_, &device_scheduler_);
TestFuture<std::vector<mojom::CertProvisioningProcessStatusPtr>> result;
service_.GetStatus(result.GetCallback());
EXPECT_EQ(0u, result.Get().size());
}
TEST_F(CertProvisioningAshTest, GetStatusAliveUserWorker) {
service_.InjectForTesting(&user_scheduler_, &device_scheduler_);
// Setup a user mock worker.
ash::cert_provisioning::CertProfile user_cert_profile(
kUserCertProfileId, kUserCertProfileName, kCertProfileVersion,
/*is_va_enabled=*/true, kCertProfileRenewalPeriod,
ash::cert_provisioning::ProtocolVersion::kStatic);
// Any time should work. Any time in the past is a realistic value.
base::Time last_update_time = base::Time::Now() - base::Hours(1);
std::optional<ash::cert_provisioning::BackendServerError> backend_error =
ash::cert_provisioning::BackendServerError(
policy::DM_STATUS_REQUEST_INVALID, last_update_time);
auto user_cert_worker =
std::make_unique<ash::cert_provisioning::MockCertProvisioningWorker>();
SetupMockCertProvisioningWorker(
user_cert_worker.get(),
ash::cert_provisioning::CertProvisioningWorkerState::kKeypairGenerated,
&der_encoded_spki_, user_cert_profile, last_update_time, backend_error);
user_workers_[kUserCertProfileId] = std::move(user_cert_worker);
auto expected_user_status = mojom::CertProvisioningProcessStatus::New();
expected_user_status->cert_profile_id = kUserCertProfileId;
expected_user_status->cert_profile_name = kUserCertProfileName;
expected_user_status->public_key = der_encoded_spki_;
expected_user_status->last_update_time = last_update_time;
expected_user_status->last_backend_server_error =
crosapi::mojom::CertProvisioningBackendServerError::New(
last_update_time, policy::DM_STATUS_REQUEST_INVALID);
expected_user_status->state =
mojom::CertProvisioningProcessState::kKeypairGenerated;
expected_user_status->did_fail = false;
expected_user_status->is_device_wide = false;
expected_user_status->failure_message = std::nullopt;
TestFuture<std::vector<mojom::CertProvisioningProcessStatusPtr>> result;
service_.GetStatus(result.GetCallback());
ASSERT_EQ(1u, result.Get().size());
EXPECT_EQ(*result.Get()[0], *expected_user_status);
}
TEST_F(CertProvisioningAshTest, GetStatusAliveDeviceWorker) {
service_.InjectForTesting(&user_scheduler_, &device_scheduler_);
// Setup a device mock worker.
ash::cert_provisioning::CertProfile device_cert_profile(
kDeviceCertProfileId, kDeviceCertProfileName, kCertProfileVersion,
/*is_va_enabled=*/true, kCertProfileRenewalPeriod,
ash::cert_provisioning::ProtocolVersion::kStatic);
base::Time last_update_time = base::Time::Now() - base::Hours(2);
std::optional<ash::cert_provisioning::BackendServerError> backend_error =
ash::cert_provisioning::BackendServerError(
policy::DM_STATUS_REQUEST_FAILED, last_update_time);
auto device_cert_worker =
std::make_unique<ash::cert_provisioning::MockCertProvisioningWorker>();
SetupMockCertProvisioningWorker(
device_cert_worker.get(),
ash::cert_provisioning::CertProvisioningWorkerState::kSignCsrFinished,
&der_encoded_spki_, device_cert_profile, last_update_time, backend_error);
device_workers_[kDeviceCertProfileId] = std::move(device_cert_worker);
auto expected_device_status = mojom::CertProvisioningProcessStatus::New();
expected_device_status->cert_profile_id = kDeviceCertProfileId;
expected_device_status->cert_profile_name = kDeviceCertProfileName;
expected_device_status->public_key = der_encoded_spki_;
expected_device_status->last_update_time = last_update_time;
expected_device_status->last_backend_server_error =
crosapi::mojom::CertProvisioningBackendServerError::New(
last_update_time, policy::DM_STATUS_REQUEST_FAILED);
expected_device_status->state =
mojom::CertProvisioningProcessState::kSignCsrFinished;
expected_device_status->did_fail = false;
expected_device_status->is_device_wide = true;
expected_device_status->failure_message = std::nullopt;
TestFuture<std::vector<mojom::CertProvisioningProcessStatusPtr>> result;
service_.GetStatus(result.GetCallback());
ASSERT_EQ(1u, result.Get().size());
EXPECT_EQ(*result.Get()[0], *expected_device_status);
}
TEST_F(CertProvisioningAshTest, GetStatusFailedUserWorker) {
service_.InjectForTesting(&user_scheduler_, &device_scheduler_);
base::Time last_update_time = base::Time::Now() - base::Hours(3);
ash::cert_provisioning::FailedWorkerInfo& info =
user_failed_workers_[kFailedUserCertProfileId];
info.state_before_failure =
ash::cert_provisioning::CertProvisioningWorkerState::kVaChallengeFinished;
info.public_key = der_encoded_spki_;
info.cert_profile_name = kFailedUserCertProfileName;
info.last_update_time = last_update_time;
info.failure_message = kFakeFailureMessage;
auto expected_user_status = mojom::CertProvisioningProcessStatus::New();
expected_user_status->cert_profile_id = kFailedUserCertProfileId;
expected_user_status->cert_profile_name = kFailedUserCertProfileName;
expected_user_status->public_key = der_encoded_spki_;
expected_user_status->last_update_time = last_update_time;
expected_user_status->state =
mojom::CertProvisioningProcessState::kVaChallengeFinished;
expected_user_status->did_fail = true;
expected_user_status->is_device_wide = false;
expected_user_status->failure_message = kFakeFailureMessage;
TestFuture<std::vector<mojom::CertProvisioningProcessStatusPtr>> result;
service_.GetStatus(result.GetCallback());
ASSERT_EQ(1u, result.Get().size());
EXPECT_EQ(*result.Get()[0], *expected_user_status);
}
TEST_F(CertProvisioningAshTest, GetStatusFailedDeviceWorker) {
service_.InjectForTesting(&user_scheduler_, &device_scheduler_);
base::Time last_update_time = base::Time::Now() - base::Hours(4);
ash::cert_provisioning::FailedWorkerInfo& info =
device_failed_workers_[kFailedDeviceCertProfileId];
info.state_before_failure = ash::cert_provisioning::
CertProvisioningWorkerState::kFinishCsrResponseReceived;
info.public_key = der_encoded_spki_;
info.cert_profile_name = kFailedDeviceCertProfileName;
info.last_update_time = last_update_time;
info.failure_message = kFakeFailureMessage;
auto expected_device_status = mojom::CertProvisioningProcessStatus::New();
expected_device_status->cert_profile_id = kFailedDeviceCertProfileId;
expected_device_status->cert_profile_name = kFailedDeviceCertProfileName;
expected_device_status->public_key = der_encoded_spki_;
expected_device_status->last_update_time = last_update_time;
expected_device_status->state =
mojom::CertProvisioningProcessState::kFinishCsrResponseReceived;
expected_device_status->did_fail = true;
expected_device_status->is_device_wide = true;
expected_device_status->failure_message = kFakeFailureMessage;
TestFuture<std::vector<mojom::CertProvisioningProcessStatusPtr>> result;
service_.GetStatus(result.GetCallback());
ASSERT_EQ(1u, result.Get().size());
EXPECT_EQ(*result.Get()[0], *expected_device_status);
}
TEST_F(CertProvisioningAshTest, UpdateOneProcess) {
service_.InjectForTesting(&user_scheduler_, &device_scheduler_);
{
// The service will try different schedulers until it finds the one that
// contains the profile id.
EXPECT_CALL(user_scheduler_, UpdateOneWorker("111")).WillOnce(Return(true));
EXPECT_CALL(device_scheduler_, UpdateOneWorker).Times(0);
service_.UpdateOneProcess("111");
ExecuteAsyncTasks();
Mock::VerifyAndClearExpectations(&user_scheduler_);
Mock::VerifyAndClearExpectations(&device_scheduler_);
}
{
// If the first one reports that it doesn't own the id, the service will try
// another one.
EXPECT_CALL(user_scheduler_, UpdateOneWorker("222"))
.WillOnce(Return(false));
EXPECT_CALL(device_scheduler_, UpdateOneWorker("222"))
.WillOnce(Return(false));
service_.UpdateOneProcess("222");
ExecuteAsyncTasks();
Mock::VerifyAndClearExpectations(&user_scheduler_);
Mock::VerifyAndClearExpectations(&device_scheduler_);
}
}
} // namespace
} // namespace crosapi