|  | // 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, | 
|  | absl::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); | 
|  | absl::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 = absl::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); | 
|  | absl::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 = absl::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 |