[ Merge of http://go/wvgerrit/193190 ] This changes the persistent test storage to use protobufs instead of manual parsing. This simplifies the code but makes the files less "human readable". Files can be read using 'gqui' if needed. Bug: 312529037 Test: unit/integration tests Change-Id: I1b025eac96458c0061e0883e1e4fd05484842ff2
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
|