531 lines
21 KiB
C++
531 lines
21 KiB
C++
// Copyright 2021 Google LLC. All Rights Reserved. This file and proprietary
|
|
// source code may only be used and distributed under the Widevine License
|
|
// Agreement.
|
|
|
|
// These tests perform various end-to-end actions similar to what an application
|
|
// would do. They verify that policies specified on UAT are honored on the
|
|
// device.
|
|
|
|
#include "reboot_test.h"
|
|
|
|
#include <sstream>
|
|
|
|
#include "create_test_file_system.h"
|
|
#include "device_files.pb.h"
|
|
#include "license_holder.h"
|
|
#include "log.h"
|
|
#include "test_sleep.h"
|
|
|
|
using video_widevine_client::sdk::SavedStorage;
|
|
using wvutil::FileSystem;
|
|
using wvutil::TestSleep;
|
|
|
|
namespace wvcdm {
|
|
FileSystem* RebootTest::file_system_;
|
|
|
|
namespace {
|
|
// How much fudge or round off error do we allow in license durations for reboot
|
|
// tests.
|
|
constexpr int64_t kFudge = 10;
|
|
} // namespace
|
|
|
|
void RebootTest::SetUp() {
|
|
WvCdmTestBase::SetUp();
|
|
if (!file_system_) file_system_ = CreateTestFileSystem();
|
|
|
|
const ::testing::TestInfo* const test_info =
|
|
::testing::UnitTest::GetInstance()->current_test_info();
|
|
std::string test_name =
|
|
std::string(test_info->test_case_name()) + "-" + test_info->name();
|
|
persistent_data_filename_ =
|
|
config_.test_data_path() + "/" + test_name + ".dat";
|
|
LOGD("Running test pass %d for %s", test_pass(), test_name.c_str());
|
|
// Don't read data on the first pass, but do read data for all other passes.
|
|
if (test_pass() > 0) {
|
|
EXPECT_TRUE(file_system_->Exists(persistent_data_filename_));
|
|
ssize_t file_size = file_system_->FileSize(persistent_data_filename_);
|
|
auto file =
|
|
file_system_->Open(persistent_data_filename_, file_system_->kReadOnly);
|
|
ASSERT_TRUE(file);
|
|
std::string dump(file_size, ' ');
|
|
ssize_t read = file->Read(&dump[0], dump.size());
|
|
EXPECT_EQ(read, file_size) << "Error reading persistent data file.";
|
|
|
|
SavedStorage proto;
|
|
EXPECT_TRUE(proto.ParseFromString(dump));
|
|
persistent_data_.insert(proto.files().begin(), proto.files().end());
|
|
}
|
|
TestSleep::SyncFakeClock();
|
|
}
|
|
|
|
void RebootTest::TearDown() {
|
|
TestSleep::SyncFakeClock();
|
|
auto file = file_system_->Open(persistent_data_filename_,
|
|
FileSystem::kCreate | FileSystem::kTruncate);
|
|
ASSERT_TRUE(file) << "Failed to open file: " << persistent_data_filename_;
|
|
|
|
SavedStorage proto;
|
|
proto.mutable_files()->insert(persistent_data_.begin(),
|
|
persistent_data_.end());
|
|
std::string dump;
|
|
ASSERT_TRUE(proto.SerializeToString(&dump));
|
|
|
|
const ssize_t bytes_written = file->Write(dump.data(), dump.length());
|
|
EXPECT_EQ(bytes_written, static_cast<ssize_t>(dump.length()));
|
|
WvCdmTestBase::TearDown();
|
|
}
|
|
|
|
int64_t RebootTest::LoadTime(const std::string& key) {
|
|
int64_t value = 0;
|
|
std::istringstream input(persistent_data_[key]);
|
|
input >> value;
|
|
if (input.fail()) {
|
|
LOGE("Could not parse time '%s'", persistent_data_[key].c_str());
|
|
}
|
|
if (!input.eof()) {
|
|
LOGE("Extra text at end of time '%s'", persistent_data_[key].c_str());
|
|
}
|
|
return value;
|
|
}
|
|
|
|
void RebootTest::SaveTime(const std::string& key, int64_t time) {
|
|
persistent_data_[key] = std::to_string(time);
|
|
}
|
|
|
|
/** Verify that the file system stores files from one test pass to the next. */
|
|
TEST_F(RebootTest, FilesArePersistent) {
|
|
const std::string key = "saved_value";
|
|
const std::string value = "the string that is saved";
|
|
if (test_pass() == 0) {
|
|
// There should be no persistent data on the first pass.
|
|
EXPECT_NE(persistent_data_[key], value);
|
|
// We will save some data for the next pass.
|
|
persistent_data_[key] = value;
|
|
} else {
|
|
// There should be persistent data set in first pass.
|
|
EXPECT_EQ(persistent_data_[key], value);
|
|
}
|
|
}
|
|
|
|
/** Verify that the clock moves forward over a reboot. */
|
|
TEST_F(RebootTest, TimeMovesForward) {
|
|
wvutil::TestSleep::Sleep(2);
|
|
const int64_t start = wvutil::Clock().GetCurrentTime();
|
|
wvutil::TestSleep::Sleep(2);
|
|
const int64_t end = wvutil::Clock().GetCurrentTime();
|
|
EXPECT_NEAR(end - start, 2.0, 1.0);
|
|
const std::string key = "end_time";
|
|
if (test_pass() == 0) {
|
|
// Save off the end of pass 1.
|
|
SaveTime(key, end);
|
|
} else {
|
|
int64_t previous_end = LoadTime(key);
|
|
EXPECT_LT(previous_end, start);
|
|
}
|
|
}
|
|
|
|
/** Test offline license durations work correctly after reboot. For time
|
|
constraints, this runs four sets of tests in parallel. For each set of tests
|
|
we load a list of licenses with a range of rental or playback durations so
|
|
that we can be confident that a short wait will find at least one duration
|
|
in the range that has expired and at least one duration that has not
|
|
expired.
|
|
|
|
RDa. Load an offline license, reboot the device, reload the license and
|
|
verify that the rental duration is enforced from the time the license was
|
|
requested.
|
|
|
|
RDb. Load an offline license, begin playback, reboot the device, reload the
|
|
license and verify that the rental duration is enforced from the time the
|
|
license was requested.
|
|
|
|
PDa. Load an offline license, reboot the device, reload the license and
|
|
verify that the playback duration is enforced from the time initial playback
|
|
started.
|
|
|
|
PDb. Load an offline license, begin playback, reboot the device, reload the
|
|
license and verify that the playback duration is enforced from the time
|
|
initial playback started.
|
|
|
|
Each of these four sets contains licenses with various durations so that the
|
|
test can run with a reasonable reboot time. We will assume that a reboot can
|
|
take anywhere from 10 seconds to almost an hour. With this in mind, we will
|
|
create a license that has a 10 second duration, one with a 20 second
|
|
duration, ... and one with an hour duration. All of these licenses will be
|
|
loaded before the reboot. Then after the reboot we should be in a situation
|
|
like this:
|
|
:-----------------------------------------> time axis.
|
|
: start of test.
|
|
:: licenses loaded.
|
|
:: : reboot starts
|
|
:: : : reboot finished. test resumes.
|
|
:: : :
|
|
:[---] 10s - expired :
|
|
:[-------] 20s expired :
|
|
: ... :
|
|
:[---------------------------] this license has not yet expired
|
|
:[----------------------------------------------] license not expired.
|
|
: ... :
|
|
:[---------------------------------------------------] 1 hour. not expired
|
|
|
|
After the test resumes, we will have at least one license that has not yet
|
|
expired. We will then sleep we are near the expiration time of that
|
|
license. We can then carefully test that this one license from the set
|
|
enforces its duration.
|
|
|
|
This is complicated by the fact that we are testing four different sets. So
|
|
what we really have is something like this:
|
|
:-----------------------------------------> time axis.
|
|
: start of test.
|
|
:: licenses loaded.
|
|
:: : reboot starts
|
|
:: : : reboot finished. test resumes.
|
|
:: : :
|
|
:[-----------------------------] first RDa license that has not exipred.
|
|
:[--------------------------] first RDb license that has not exipred.
|
|
:[---------------------------] first PDa license that has not exipred.
|
|
:[-------------------------------] first PDb license that has not exipred.
|
|
|
|
Since we want to test all four of these licenses, we will compute the
|
|
interesting times for each license. These are the times just before, and
|
|
just after the expiration time. After sorting these interesting times, we
|
|
will test each one in order.
|
|
*/
|
|
class OfflineLicense {
|
|
public:
|
|
OfflineLicense(const std::string& test_type, CdmEngine* cdm_engine,
|
|
const ConfigTestEnv& config, int64_t duration,
|
|
bool play_before_reboot)
|
|
: content_id_("CDM_Reboot_" + test_type +
|
|
(play_before_reboot ? "b_" : "a_") +
|
|
std::to_string(duration)),
|
|
duration_(duration),
|
|
play_before_reboot_(play_before_reboot),
|
|
license_holder_(content_id_, cdm_engine, config) {
|
|
license_holder_.set_can_persist(true);
|
|
}
|
|
|
|
virtual ~OfflineLicense() {}
|
|
|
|
// Fetch and load the license. The session is left open.
|
|
void LoadLicense() {
|
|
license_holder_.OpenSession();
|
|
TestSleep::SyncFakeClock();
|
|
start_of_rental_clock_ = wvutil::Clock().GetCurrentTime();
|
|
license_holder_.FetchLicense();
|
|
license_holder_.LoadLicense();
|
|
}
|
|
|
|
// Reload the license. The session is left open.
|
|
void ReloadLicense() {
|
|
license_holder_.OpenSession();
|
|
license_holder_.ReloadLicense();
|
|
}
|
|
|
|
// Action to be taken after loading the license, but before the reboot.
|
|
virtual void BeforeReboot() {
|
|
if (play_before_reboot_) {
|
|
Decrypt();
|
|
}
|
|
}
|
|
|
|
// Action to be taken after reloading the license.
|
|
virtual void AfterReboot() {}
|
|
|
|
void CloseSession() { license_holder_.CloseSession(); }
|
|
|
|
// The time this license should be cutoff. Decrypt should succeed before this
|
|
// time and should fail after this time.
|
|
virtual int64_t cutoff() = 0;
|
|
|
|
// Verify that the license may be used to decrypt content.
|
|
void Decrypt() {
|
|
TestSleep::SyncFakeClock();
|
|
if (start_of_playback_ == 0) {
|
|
start_of_playback_ = wvutil::Clock().GetCurrentTime();
|
|
}
|
|
const KeyId key_id = "0000000000000000";
|
|
EXPECT_EQ(NO_ERROR, license_holder_.Decrypt(key_id))
|
|
<< "Failed for " << content_id_
|
|
<< ", now = " << wvutil::Clock().GetCurrentTime() << ", rental_clock="
|
|
<< (wvutil::Clock().GetCurrentTime() - start_of_rental_clock_)
|
|
<< ", playback_clock = "
|
|
<< (wvutil::Clock().GetCurrentTime() - start_of_playback_)
|
|
<< ", delta to cutoff = "
|
|
<< (wvutil::Clock().GetCurrentTime() - cutoff());
|
|
}
|
|
|
|
// Verify that the license has expired, and may not be used to decrypt
|
|
// content.
|
|
void FailDecrypt() {
|
|
TestSleep::SyncFakeClock();
|
|
const KeyId key_id = "0000000000000000";
|
|
EXPECT_EQ(NEED_KEY, license_holder_.Decrypt(key_id))
|
|
<< "Decrypt should have failed for " << content_id_
|
|
<< ", now = " << wvutil::Clock().GetCurrentTime() << ", rental_clock="
|
|
<< (wvutil::Clock().GetCurrentTime() - start_of_rental_clock_)
|
|
<< ", playback_clock = "
|
|
<< (wvutil::Clock().GetCurrentTime() - start_of_playback_)
|
|
<< ", delta to cutoff = "
|
|
<< (wvutil::Clock().GetCurrentTime() - cutoff());
|
|
}
|
|
|
|
// Save times and the key set id to persistent data.
|
|
void SaveData(RebootTest* reboot_test,
|
|
std::map<std::string, std::string>* persistent_data) {
|
|
reboot_test->SaveTime("start_of_rental_" + content_id_,
|
|
start_of_rental_clock_);
|
|
reboot_test->SaveTime("start_of_playback_" + content_id_,
|
|
start_of_playback_);
|
|
(*persistent_data)["key_set_id_" + content_id_] =
|
|
license_holder_.key_set_id();
|
|
}
|
|
|
|
// Load times and the key set id from persistent data.
|
|
void LoadData(RebootTest* reboot_test,
|
|
std::map<std::string, std::string>* persistent_data) {
|
|
start_of_rental_clock_ =
|
|
reboot_test->LoadTime("start_of_rental_" + content_id_);
|
|
start_of_playback_ =
|
|
reboot_test->LoadTime("start_of_playback_" + content_id_);
|
|
license_holder_.set_key_set_id(
|
|
(*persistent_data)["key_set_id_" + content_id_]);
|
|
}
|
|
|
|
const std::string& content_id() const { return content_id_; }
|
|
|
|
protected:
|
|
const std::string content_id_;
|
|
int64_t duration_;
|
|
int64_t start_of_rental_clock_ = 0;
|
|
int64_t start_of_playback_ = 0;
|
|
bool play_before_reboot_;
|
|
LicenseHolder license_holder_;
|
|
};
|
|
|
|
// Holds an offline license that has a limit on the rental duration.
|
|
class RentalDurationLicense : public OfflineLicense {
|
|
public:
|
|
RentalDurationLicense(CdmEngine* cdm_engine, const ConfigTestEnv& config,
|
|
int64_t duration, bool play_before_reboot)
|
|
: OfflineLicense("RD", cdm_engine, config, duration, play_before_reboot) {
|
|
}
|
|
|
|
int64_t cutoff() override { return start_of_rental_clock_ + duration_; }
|
|
};
|
|
|
|
// Holds an offline license that has a limit on the playback duration.
|
|
class PlaybackDurationLicense : public OfflineLicense {
|
|
public:
|
|
PlaybackDurationLicense(CdmEngine* cdm_engine, const ConfigTestEnv& config,
|
|
int64_t duration, bool play_before_reboot)
|
|
: OfflineLicense("PD", cdm_engine, config, duration, play_before_reboot) {
|
|
}
|
|
|
|
// If we did not start playback before the reboot, we will start playback
|
|
// just after reloading the license, post-reboot.
|
|
void AfterReboot() override {
|
|
if (!play_before_reboot_) {
|
|
Decrypt();
|
|
}
|
|
}
|
|
|
|
int64_t cutoff() override { return start_of_playback_ + duration_; }
|
|
};
|
|
|
|
// Test that the rental and playback durations are enforced across a reboot.
|
|
class OfflineLicenseTest : public RebootTest {
|
|
public:
|
|
void SetUp() override {
|
|
RebootTest::SetUp();
|
|
EnsureProvisioned();
|
|
// Run each of the following test cases in parallel so that we don't have to
|
|
// sleep separately for each one. These durations need to match the polices
|
|
// specified on the UAT license server.
|
|
test_case_.resize(4);
|
|
const std::vector<int64_t> duration_range = {10, 20, 30, 45,
|
|
60, 300, 900, 3600};
|
|
for (size_t i = 0; i < duration_range.size(); i++) {
|
|
test_case_[0].push_back(
|
|
std::unique_ptr<OfflineLicense>(new RentalDurationLicense(
|
|
&cdm_engine_, config_, duration_range[i], false)));
|
|
test_case_[1].push_back(
|
|
std::unique_ptr<OfflineLicense>(new RentalDurationLicense(
|
|
&cdm_engine_, config_, duration_range[i], true)));
|
|
test_case_[2].push_back(
|
|
std::unique_ptr<OfflineLicense>(new PlaybackDurationLicense(
|
|
&cdm_engine_, config_, duration_range[i], false)));
|
|
test_case_[3].push_back(
|
|
std::unique_ptr<OfflineLicense>(new PlaybackDurationLicense(
|
|
&cdm_engine_, config_, duration_range[i], true)));
|
|
}
|
|
}
|
|
|
|
// Load all of the licenses. For the tests that require playback before the
|
|
// reboot, we start playback here.
|
|
void LoadAllLicenses() {
|
|
DeleteAllLicenses();
|
|
// For each test case, load an offline license and save the data.
|
|
for (size_t n = 0; n < test_case_.size(); n++) {
|
|
for (size_t i = 0; i < test_case_[n].size(); i++) {
|
|
ASSERT_NO_FATAL_FAILURE(test_case_[n][i]->LoadLicense());
|
|
test_case_[n][i]->BeforeReboot(); // For some tests, we decrypt here.
|
|
test_case_[n][i]->SaveData(this, &persistent_data_);
|
|
test_case_[n][i]->CloseSession();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reload all of the licenses. We also go through each license and figure out
|
|
// which ones have already expired, or close to expire. We then find the first
|
|
// license from each set that will be next to expire, so that we can test
|
|
// those more carefully.
|
|
void ReloadAllLicense() {
|
|
// first_valid is the index of the first license that has not expired and is
|
|
// not just about to expire.
|
|
first_valid_.resize(test_case_.size());
|
|
for (size_t n = 0; n < test_case_.size(); n++) {
|
|
for (size_t i = 0; i < test_case_[n].size(); i++) {
|
|
OfflineLicense* license = test_case_[n][i].get();
|
|
license->LoadData(this, &persistent_data_);
|
|
ASSERT_NO_FATAL_FAILURE(license->ReloadLicense());
|
|
license->AfterReboot();
|
|
// if past cutoff. make sure decrypt fails.
|
|
if (license->cutoff() + kFudge < wvutil::Clock().GetCurrentTime()) {
|
|
license->FailDecrypt();
|
|
}
|
|
// If past cutoff, or near cutoff, then we don't want to use it
|
|
// as our first valid.
|
|
if (license->cutoff() - 2 * kFudge < wvutil::Clock().GetCurrentTime()) {
|
|
first_valid_[n] = i + 1;
|
|
}
|
|
license->CloseSession();
|
|
}
|
|
// We expect there to be at least one license that has not expired yet.
|
|
// If this is not true, then the reboot time was probably longer than
|
|
// expected. If it is important to run these tests with a very long reboot
|
|
// time, then please ask a Widevine engineer to generate more license
|
|
// policies with longer reboot times.
|
|
ASSERT_LT(first_valid_[n], test_case_[n].size())
|
|
<< "For n=" << n
|
|
<< ", content_id = " << test_case_[n][0]->content_id()
|
|
<< ", time=" << wvutil::Clock().GetCurrentTime() << "\n"
|
|
<< "This is an indication that the reboot took a very long time.\n";
|
|
}
|
|
}
|
|
|
|
// Take the first_valid_ array, which tells us which licenses will expire
|
|
// soon, and compute the times we are interested in: a little before and a
|
|
// little after the cutoff. We want to test that the license is valid just
|
|
// before the cutoff and that the license is not valid just after the
|
|
// cutoff.
|
|
//
|
|
// The interesting_times_ is a std::set, so that we may iterate through the
|
|
// times in order.
|
|
void FindInterestingTimes() {
|
|
for (size_t n = 0; n < test_case_.size(); n++) {
|
|
OfflineLicense* license = test_case_[n][first_valid_[n]].get();
|
|
interesting_times_.insert(license->cutoff() - kFudge);
|
|
interesting_times_.insert(license->cutoff() + kFudge);
|
|
}
|
|
}
|
|
|
|
// Test at each of the interesting times. These are the times just before and
|
|
// after the cutoff of the next license to expire from each list of test
|
|
// cases.
|
|
void TestInterestingTimes() {
|
|
int decrypt_count = 0;
|
|
int fail_count = 0;
|
|
for (auto time : interesting_times_) {
|
|
TestSleep::SyncFakeClock();
|
|
int64_t now = wvutil::Clock().GetCurrentTime();
|
|
int64_t delta = (time - now);
|
|
// It is not necessarily an error for the delta to be negative. But it is
|
|
// an indication that the we are near an error condition. If the current
|
|
// time is a few seconds past the interesting time, then the license
|
|
// should still be valid or expired. If delta is very large relative to
|
|
// kFudge, then the test might fail. This is still an indication that
|
|
// something is wrong: reloading a license and decrypting some content
|
|
// should not take multiple seconds.
|
|
if (delta < 0) LOGW("Sleep delta would be negative: %ld", delta);
|
|
if (delta > 0) TestSleep::Sleep(static_cast<unsigned int>(delta));
|
|
// We look at each of the four license that we used to generate the
|
|
// interesting times. It's possible that two licenses share the same
|
|
// interesting time, so we have to check each of the four against each
|
|
// time instead of keeping track of which license generated which time.
|
|
for (size_t n = 0; n < test_case_.size(); n++) {
|
|
OfflineLicense* license = test_case_[n][first_valid_[n]].get();
|
|
ASSERT_NO_FATAL_FAILURE(license->ReloadLicense());
|
|
if (time == license->cutoff() - kFudge) {
|
|
license->Decrypt();
|
|
decrypt_count++;
|
|
}
|
|
if (time == license->cutoff() + kFudge) {
|
|
license->FailDecrypt();
|
|
fail_count++;
|
|
}
|
|
license->CloseSession();
|
|
}
|
|
}
|
|
EXPECT_EQ(decrypt_count, 4) << "Test error. I missed a cutoff";
|
|
EXPECT_EQ(fail_count, 4) << "Test error. I missed a cutoff";
|
|
}
|
|
|
|
// After we have tested one license from each list of test cases carefully,
|
|
// all the rest of the licenses can be tested. We do not sleep in this
|
|
// function, we only test each license that is still valid, or has already
|
|
// expired. We ignore licenses that are near the cutoff because we already
|
|
// tested those license in the previous function.
|
|
void TestAfterInterestingTimes() {
|
|
// Make sure that all the rest of the licenses are still valid.
|
|
for (size_t n = 0; n < test_case_.size(); n++) {
|
|
for (size_t i = first_valid_[n] + 1; i < test_case_[n].size(); i++) {
|
|
OfflineLicense* license = test_case_[n][i].get();
|
|
ASSERT_NO_FATAL_FAILURE(license->ReloadLicense());
|
|
TestSleep::SyncFakeClock();
|
|
int64_t now = wvutil::Clock().GetCurrentTime();
|
|
if (now <= license->cutoff() - kFudge) {
|
|
license->Decrypt();
|
|
}
|
|
if (now >= license->cutoff() + kFudge) {
|
|
license->FailDecrypt();
|
|
}
|
|
license->CloseSession();
|
|
}
|
|
}
|
|
}
|
|
|
|
void TearDown() override {
|
|
// Clean up licenses if any test failed.
|
|
if (::testing::Test::HasFailure() || test_pass() == 1) {
|
|
DeleteAllLicenses();
|
|
}
|
|
RebootTest::TearDown();
|
|
}
|
|
|
|
void DeleteAllLicenses() {
|
|
std::vector<std::string> key_set_ids;
|
|
EXPECT_EQ(NO_ERROR,
|
|
cdm_engine_.ListStoredLicenses(kSecurityLevelL1, &key_set_ids));
|
|
for (auto key_set : key_set_ids) {
|
|
cdm_engine_.RemoveOfflineLicense(key_set, kSecurityLevelL1);
|
|
}
|
|
}
|
|
|
|
std::vector<std::vector<std::unique_ptr<OfflineLicense>>> test_case_;
|
|
std::set<int64_t> interesting_times_;
|
|
std::vector<size_t> first_valid_;
|
|
};
|
|
|
|
TEST_F(OfflineLicenseTest, VariousTests) {
|
|
if (test_pass() == 0) {
|
|
LoadAllLicenses();
|
|
} else if (test_pass() == 1) {
|
|
ReloadAllLicense();
|
|
FindInterestingTimes();
|
|
TestInterestingTimes();
|
|
TestAfterInterestingTimes();
|
|
}
|
|
}
|
|
} // namespace wvcdm
|