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

// 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 <stddef.h>

#include <algorithm>
#include <string>
#include <vector>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/ash/attestation/platform_verification_flow.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
#include "chrome/browser/profiles/profile_impl.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/attestation/fake_certificate.h"
#include "chromeos/ash/components/attestation/mock_attestation_flow.h"
#include "chromeos/ash/components/dbus/attestation/attestation.pb.h"
#include "chromeos/ash/components/dbus/attestation/fake_attestation_client.h"
#include "chromeos/ash/components/dbus/attestation/interface.pb.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

using testing::_;
using testing::DoAll;
using testing::Invoke;
using testing::Return;
using testing::SetArgPointee;
using testing::StrictMock;
using testing::WithArgs;

namespace ash {
namespace attestation {

namespace {

const char kTestID[] = "test_id";
const char kTestChallenge[] = "test_challenge";
const char kTestCertificate[] = "test_certificate";
const char kTestEmail[] = "[email protected]";

class FakeDelegate : public PlatformVerificationFlow::Delegate {
 public:
  FakeDelegate() : is_in_supported_mode_(true) {}

  FakeDelegate(const FakeDelegate&) = delete;
  FakeDelegate& operator=(const FakeDelegate&) = delete;

  ~FakeDelegate() override {}

  bool IsInSupportedMode() override { return is_in_supported_mode_; }

  void set_is_in_supported_mode(bool is_in_supported_mode) {
    is_in_supported_mode_ = is_in_supported_mode;
  }

 private:
  bool is_in_supported_mode_;
};

}  // namespace

class PlatformVerificationFlowTest : public ::testing::Test {
 public:
  PlatformVerificationFlowTest()
      : certificate_status_(ATTESTATION_SUCCESS),
        fake_certificate_index_(0),
        result_(PlatformVerificationFlow::INTERNAL_ERROR) {
    AttestationClient::InitializeFake();
  }
  ~PlatformVerificationFlowTest() override { AttestationClient::Shutdown(); }

  void SetUp() override {
    // Create a verifier for tests to call.
    verifier_ = new PlatformVerificationFlow(
        &mock_attestation_flow_, AttestationClient::Get(), &fake_delegate_);

    // Create callbacks for tests to use with verifier_.
    settings_helper_.ReplaceDeviceSettingsProviderWithStub();
    settings_helper_.SetBoolean(kAttestationForContentProtectionEnabled, true);

    // Configure the fake user.
    user_ = user_manager_.AddUser(AccountId::FromUserEmail(kTestEmail));
  }

  PlatformVerificationFlow::ChallengeCallback CreateChallengeCallback() {
    return base::BindOnce(&PlatformVerificationFlowTest::FakeChallengeCallback,
                          base::Unretained(this));
  }

  void ExpectAttestationFlow() {
    // When consent is not given or the feature is disabled, it is important
    // that there are no calls to the attestation service.  Thus, a test must
    // explicitly expect these calls or the mocks will fail the test.

    const AccountId account_id = AccountId::FromUserEmail(kTestEmail);
    // Configure the mock AttestationFlow to call FakeGetCertificate.
    EXPECT_CALL(mock_attestation_flow_,
                GetCertificate(PROFILE_CONTENT_PROTECTION_CERTIFICATE,
                               account_id, kTestID, _, _, _, _, _))
        .WillRepeatedly(WithArgs<7>(
            Invoke(this, &PlatformVerificationFlowTest::FakeGetCertificate)));

    const std::string expected_key_name =
        std::string(kContentProtectionKeyPrefix) + std::string(kTestID);
    AttestationClient::Get()
        ->GetTestInterface()
        ->AllowlistSignSimpleChallengeKey(kTestEmail, expected_key_name);
  }

  void FakeGetCertificate(AttestationFlow::CertificateCallback callback) {
    std::string certificate =
        (fake_certificate_index_ < fake_certificate_list_.size()) ?
            fake_certificate_list_[fake_certificate_index_] : kTestCertificate;
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback), certificate_status_, certificate));
    ++fake_certificate_index_;
  }

  void FakeChallengeCallback(PlatformVerificationFlow::Result result,
                             const std::string& salt,
                             const std::string& signature,
                             const std::string& certificate) {
    result_ = result;
    challenge_salt_ = salt;
    challenge_signature_ = signature;
    certificate_ = certificate;
  }

 protected:
  content::BrowserTaskEnvironment task_environment_;
  StrictMock<MockAttestationFlow> mock_attestation_flow_;
  FakeDelegate fake_delegate_;
  ScopedCrosSettingsTestHelper settings_helper_;

  // Used to create a fake user.
  FakeChromeUserManager user_manager_;
  raw_ptr<user_manager::User> user_;

  scoped_refptr<PlatformVerificationFlow> verifier_;

  // Controls result of FakeGetCertificate.
  AttestationStatus certificate_status_;
  std::vector<std::string> fake_certificate_list_;
  size_t fake_certificate_index_;

  // Callback functions and data.
  PlatformVerificationFlow::Result result_;
  std::string challenge_salt_;
  std::string challenge_signature_;
  std::string certificate_;
};

TEST_F(PlatformVerificationFlowTest, Success) {
  ExpectAttestationFlow();
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::SUCCESS, result_);
  EXPECT_EQ(kTestCertificate, certificate_);
  ::attestation::SignedData challenge_respoonse;
  challenge_respoonse.set_data(challenge_salt_);
  challenge_respoonse.set_signature(challenge_signature_);
  EXPECT_TRUE(
      AttestationClient::Get()
          ->GetTestInterface()
          ->VerifySimpleChallengeResponse(kTestChallenge, challenge_respoonse));
}

TEST_F(PlatformVerificationFlowTest, FeatureDisabledByPolicy) {
  settings_helper_.SetBoolean(kAttestationForContentProtectionEnabled, false);
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::POLICY_REJECTED, result_);
}

TEST_F(PlatformVerificationFlowTest, NotVerifiedDueToUnspeciedFailure) {
  certificate_status_ = ATTESTATION_UNSPECIFIED_FAILURE;
  ExpectAttestationFlow();
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::PLATFORM_NOT_VERIFIED, result_);
}

TEST_F(PlatformVerificationFlowTest, NotVerifiedDueToBadRequestFailure) {
  certificate_status_ = ATTESTATION_SERVER_BAD_REQUEST_FAILURE;
  ExpectAttestationFlow();
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::PLATFORM_NOT_VERIFIED, result_);
}

TEST_F(PlatformVerificationFlowTest, ChallengeSigningError) {
  // Force the signing operation to fail.
  AttestationClient::Get()
      ->GetTestInterface()
      ->set_sign_simple_challenge_status(
          ::attestation::STATUS_UNEXPECTED_DEVICE_ERROR);
  ExpectAttestationFlow();
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::INTERNAL_ERROR, result_);
}

TEST_F(PlatformVerificationFlowTest, DBusFailure) {
  AttestationClient::Get()
      ->GetTestInterface()
      ->ConfigureEnrollmentPreparationsStatus(::attestation::STATUS_DBUS_ERROR);
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::INTERNAL_ERROR, result_);
}

TEST_F(PlatformVerificationFlowTest, AttestationServiceInternalError) {
  AttestationClient::Get()
      ->GetTestInterface()
      ->ConfigureEnrollmentPreparationsStatus(
          ::attestation::STATUS_UNEXPECTED_DEVICE_ERROR);
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::INTERNAL_ERROR, result_);
}

TEST_F(PlatformVerificationFlowTest, Timeout) {
  verifier_->set_timeout_delay(base::Seconds(0));
  ExpectAttestationFlow();
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::TIMEOUT, result_);
}

TEST_F(PlatformVerificationFlowTest, ExpiredCert) {
  ExpectAttestationFlow();
  fake_certificate_list_.resize(3);
  ASSERT_TRUE(
      GetFakeCertificatePEM(base::Days(-1), &fake_certificate_list_[0]));
  ASSERT_TRUE(GetFakeCertificatePEM(base::Days(1), &fake_certificate_list_[1]));
  // This is the opportunistic renewal certificate. Send it back expired to test
  // that it does not pass through the certificate expiry check again.
  ASSERT_TRUE(
      GetFakeCertificatePEM(base::Days(-1), &fake_certificate_list_[2]));
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::SUCCESS, result_);
  EXPECT_EQ(fake_certificate_list_[1], certificate_);
  // Once for the expired certificate, once for the almost expired certificate,
  // and once for the opportunistic renewal attempt.
  EXPECT_EQ(3ul, fake_certificate_index_);
}

TEST_F(PlatformVerificationFlowTest, ExpiredIntermediateCert) {
  ExpectAttestationFlow();
  fake_certificate_list_.resize(2);
  std::string leaf_cert;
  ASSERT_TRUE(GetFakeCertificatePEM(base::Days(60), &leaf_cert));
  std::string intermediate_cert;
  ASSERT_TRUE(GetFakeCertificatePEM(base::Days(-1), &intermediate_cert));
  fake_certificate_list_[0] = leaf_cert + intermediate_cert;
  ASSERT_TRUE(
      GetFakeCertificatePEM(base::Days(90), &fake_certificate_list_[1]));
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::SUCCESS, result_);
  EXPECT_EQ(fake_certificate_list_[1], certificate_);
  // Once for the expired intermediate, once for the renewal.
  EXPECT_EQ(2ul, fake_certificate_index_);
}

TEST_F(PlatformVerificationFlowTest, AsyncRenewalMultipleHits) {
  ExpectAttestationFlow();
  fake_certificate_list_.resize(4);
  ASSERT_TRUE(GetFakeCertificatePEM(base::Days(1), &fake_certificate_list_[0]));
  std::fill(fake_certificate_list_.begin() + 1, fake_certificate_list_.end(),
            fake_certificate_list_[0]);
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::SUCCESS, result_);
  EXPECT_EQ(fake_certificate_list_[0], certificate_);
  // Once per challenge and only one renewal.
  EXPECT_EQ(4ul, fake_certificate_index_);
}

TEST_F(PlatformVerificationFlowTest, CertificateNotPEM) {
  ExpectAttestationFlow();
  fake_certificate_list_.push_back("invalid_pem");
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::SUCCESS, result_);
  EXPECT_EQ(fake_certificate_list_[0], certificate_);
}

TEST_F(PlatformVerificationFlowTest, CertificateNotX509) {
  ExpectAttestationFlow();
  std::string not_x509 =
      "-----BEGIN CERTIFICATE-----\n"
      "Vm0wd2QyUXlWa1pOVldoVFYwZDRWVll3WkRSV1JteFZVMjA1VjFadGVEQmFWVll3WVd4YWMx"
      "TnNiRlZXYkhCUVdWZHplRll5VGtWUwpiSEJPVWpKb1RWZFhkR0ZUTWs1eVRsWmtZUXBTYlZK"
      "d1ZXcEtiMDFzWkZkV2JVWlVZbFpHTTFSc1dsZFZaM0JwVTBWS2RsWkdZM2hpCk1rbDRWMnhX"
      "VkdGc1NsaFpiRnBIVGtaYVNFNVZkRmRhTTBKd1ZteGFkMVpXWkZobFIzUnBDazFXY0VoV01X"
      "aHpZV3hLV1ZWc1ZscGkKUm5Cb1dsZDRXbVZWTlZkYVIyaFdWMFZLVlZacVFsZFRNVnBYV2ta"
      "b2JGSXpVbGREYlVwWFYydG9WMDF1VW5aWmExcExZMnMxVjFScwpjRmdLVTBWS1dWWnRjRWRq"
      "TWs1elYyNVNVRll5YUZkV01GWkxWbXhhVlZGc1pGUk5Wa3BJVmpKNGIyRnNTbGxWYkVKRVlr"
      "VndWbFZ0CmVHOVdNVWw2WVVkb1dGWnNjRXhXTUZwWFpGWk9jd3BhUjJkTFdWUkNkMDVzV2to"
      "TlZGSmFWbTFTUjFSV1ZsZFdNa3BKVVd4a1YwMUcKV2t4V01uaGhWMGRXU0dSRk9WTk5WWEJa"
      "Vm1wR2IySXhXblJTV0hCV1lrWktSVmxZY0VkbGJGbDVDbU5GVGxkTlZtdzJWbGMxWVZkdApS"
      "WGhqUlhSaFZucEdTRlZ0TVZOU2QzQmhVbTFPVEZkWGVGWmtNbEY0VjJ0V1UySkhVbFpVVjNS"
      "M1pXeFdXR1ZHWkZWaVJYQmFWa2QwCk5GSkdjRFlLVFVSc1JGcDZNRGxEWnowOUNnPT0K\n"
      "-----END CERTIFICATE-----\n";
  fake_certificate_list_.push_back(not_x509);
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::SUCCESS, result_);
  EXPECT_EQ(fake_certificate_list_[0], certificate_);
}

TEST_F(PlatformVerificationFlowTest, UnsupportedMode) {
  fake_delegate_.set_is_in_supported_mode(false);
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::PLATFORM_NOT_VERIFIED, result_);
}

TEST_F(PlatformVerificationFlowTest, AttestationNotPrepared) {
  AttestationClient::Get()->GetTestInterface()->ConfigureEnrollmentPreparations(
      false);
  verifier_->ChallengePlatformKey(user_, kTestID, kTestChallenge,
                                  CreateChallengeCallback());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(PlatformVerificationFlow::PLATFORM_NOT_VERIFIED, result_);
}

}  // namespace attestation
}  // namespace ash