Files
ce_cdm/cdm/test/cdm_test.cpp
2024-09-05 07:02:36 +00:00

2778 lines
100 KiB
C++

// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine License
// Agreement.
//
// This source file provides a basic set of unit tests for the Content
// Decryption Module (CDM).
#include <time.h>
#include <chrono>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <memory>
#include <string>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <openssl/aes.h>
#include <openssl/err.h>
#include <openssl/rsa.h>
#include <openssl/x509.h>
#include "OEMCryptoCENC.h"
#include "cdm.h"
#include "cdm_test_printers.h"
#include "cdm_version.h"
#include "config_test_env.h"
#include "decryption_test_data.h"
#include "file_store.h"
#include "license_protocol.pb.h"
#include "license_request.h"
#include "log.h"
#include "oec_device_features.h"
#include "platform.h"
#include "properties_ce.h"
#include "service_certificate.h"
#include "string_conversions.h"
#include "test_base.h"
#include "test_host.h"
#include "test_printers.h"
#include "test_sleep.h"
#include "url_request.h"
using namespace testing;
using namespace wvcdm;
using namespace wvutil;
namespace widevine {
using video_widevine::LicenseError;
using video_widevine::SignedMessage;
namespace {
const int kHttpOk = 200;
const int kRenewalTestDelayMs = 3 * 60 * 1000;
const int kExpirationTestDelayMs = 5 * 60 * 1000;
const int kOfflineLicenseDurationMs = 604800 * 1000;
constexpr size_t kMaxFetchAttempts = 5;
const Cdm::SessionType kBogusSessionType = static_cast<Cdm::SessionType>(-1);
const Cdm::InitDataType kBogusInitDataType = static_cast<Cdm::InitDataType>(-1);
const std::string kBogusSessionId = "asdf";
const std::string kBogusServiceCertificate = "jkl;";
const std::string kCencInitData = a2bs_hex(
"00000042" // blob size
"70737368" // "pssh"
"00000000" // flags
"edef8ba979d64acea3c827dcd51d21ed" // Widevine system id
"00000022" // pssh data size
// pssh data:
"08011a0d7769646576696e655f746573" // "streaming_clip9"
"74220f73747265616d696e675f636c69"
"7039");
const std::string kCencPersistentInitData = a2bs_hex(
"00000040" // blob size
"70737368" // "pssh"
"00000000" // flags
"edef8ba979d64acea3c827dcd51d21ed" // Widevine system id
"00000020" // pssh data size
// pssh data:
"08011a0d7769646576696e655f746573" // "offline_clip6"
"74220d6f66666c696e655f636c697036");
const std::string kInvalidCencInitData = a2bs_hex(
"0000000c" // blob size
"61736466" // "asdf" (wrong box type)
"01020304"); // nonsense
const std::string kNonWidevineCencInitData = a2bs_hex(
"00000020" // blob size
"70737368" // "pssh"
"00000000" // flags
"000102030405060708090a0b0c0d0e0f" // unknown system id
"00000000"); // pssh data size
const std::string kWebMInitData = a2bs_hex("deadbeefdeadbeefdeadbeefdeadbeef");
const std::string kKeyIdsInitData =
"{\"kids\":[\"67ef0gd8pvfd0\",\"77ef0gd8pvfd0\"]}";
const std::string kHlsInitData = // content_id = "bigbuckbunny"
"#EXT-X-KEY:METHOD=SAMPLE-AES,KEYFORMAT=\"com.widevine\",KEYFORMATVERSIONS="
"\"1\",URI=\"data:text/plain;base64,ew0KICAgInByb3ZpZGVyIjogIndpZGV2aW5lX3R"
"lc3QiLA0KICAgImNvbnRlbnRfaWQiOiAiWW1sblluVmphMkoxYm01NSIsDQogICAia2V5X2lkc"
"yI6IFsNCiAgICAgICI5Yjc1OTA0MDMyMWE0MDhhNWM3NzY4YjQ1MTEyODdhNiINCiAgIF0NCn0"
"=\",IV=0x75537a79fa41abc7b598ea72aba0c26f";
const std::string kCencEntitlementInitData1 = a2bs_hex(
"000001fb" // blob size
"70737368" // "pssh"
"00000000" // flags
"edef8ba979d64acea3c827dcd51d21ed" // Widevine system id
"000001db" // pssh data size
// pssh data:
"220b47726f7570563254657374381448"
"e3dc959b065002580272580a10668093"
"381a8c5be48a0168ce372726ac1210c8"
"326486bb5d5c4a958f00b1111afc811a"
"20082cd9d3aed3ebe6239d30fbcf0b22"
"1d28cbb0360ea1295c2363973346ec00"
"512210914781334e864c8eb7f768cf26"
"49073872580a10f872d11d5b1052f2bd"
"a94e60a0e383021210450897c987a85c"
"2e9579f968554a12991a2097e603ceea"
"f35ed8cef1029eae7a0a54701e3d6db6"
"80e7da1de3b22a8db347fb2210b41c34"
"29b7bb96972bbaf6587bc0ddf172580a"
"10bac58b9fce9e5929a42a180e529f19"
"4712103f11f22988d25659b145ce4854"
"3e6b141a20416e22768e5a57b08d155e"
"5210d00658056947ff06d626668bceb3"
"5eb01c6b57221081fb2ff3fef79d332f"
"f98be46233596972580a101261c8036d"
"ae5c8caa968858aa0ca9cc12106d583c"
"b37c1456519843a81cf49912221a20c2"
"1116bb54a226e8d879a4cd41d8879920"
"2ae85b80d83b1b4447e5d7fcad6f6a22"
"100b27a4c3f44771d2b0c7c34c66af35"
"b572580a10ab1c8c259c6b5967991389"
"65bff5ac0c1210b5b4473658565d3786"
"efaf4b85d8e6e21a203ce6a9085285c2"
"ece0b650dc83dd7aa8ac849611a8e3f8"
"3c8f389223c0f3621522101946f0c2a3"
"d543101cc842bbec2d0b30");
const std::string kCencEntitlementInitData2 = a2bs_hex(
"000001fb" // blob size
"70737368" // "pssh"
"00000000" // flags
"edef8ba979d64acea3c827dcd51d21ed" // Widevine system id
"000001db" // pssh data size
// pssh data:
"220b47726f7570563254657374381548"
"e3dc959b065002580272580a10668093"
"381a8c5be48a0168ce372726ac1210f8"
"488775a99855ff94b93ec5bd4993561a"
"20d15ba631c20e95da0d4857f6a1d25a"
"a3bccbd3fde18b3fdc1dd8c4f0ede76f"
"402210d6dd3675f0d1150052e81b9107"
"6d7fc172580a10f872d11d5b1052f2bd"
"a94e60a0e383021210ad1f93ad921e53"
"b097c415b2bf1ef1c61a20b2087b60a2"
"d253ac2158a1bfa789b150b79701b29e"
"c852a2662560f8b8977a4c2210051ed3"
"2628671fbda58f506ba5ea713972580a"
"10bac58b9fce9e5929a42a180e529f19"
"47121027cdda7bfe5e5fd4bff2ebc9c7"
"c020701a20f2cb1184d648a2404517e6"
"7a39d698332aae6bb890a69bf7ddb536"
"75b8ac41c62210a80ed7f9b728fdd566"
"0b01b173ace26372580a101261c8036d"
"ae5c8caa968858aa0ca9cc1210769a70"
"0442a25bf5ae17174c70f4cb8e1a206c"
"7b2012723fc47c83b003ea214204915f"
"9a63dc373bf219f36ccf5697589aa422"
"10bcc3c16e836cca264d5493a0c334d3"
"4872580a10ab1c8c259c6b5967991389"
"65bff5ac0c1210894b04aef78557c6a7"
"e6e8855febbcc91a2025cc545ee3cd0c"
"c323586610ff6a8f8f22a78f5fade2f2"
"1083f152c52208f16d2210257aacacec"
"512a2e769396b10e6d9dfa");
// This Key ID must match the key retrieved from the license server by
// kCencInitData.
const std::vector<uint8_t> kKeyIdCtr =
a2b_hex("371ea35e1a985d75d198a7f41020dc23");
// This Key ID must match the key retrieved from the license server by
// kHlsInitData.
const std::vector<uint8_t> kKeyIdCbc =
a2b_hex("9b759040321a408a5c7768b4511287a6");
const std::string kEntitlementContentId = "CDM_Entitlement";
// This entitlement key id has to match the key id in the license data for
// content id "CDM_Entitlement" as seen in the integration console. And it
// also has to match the key id derived by UAT for the content id
// CDM_Entitlement, for the AUDIO track. When running backwards compatibility
// tests, the SDK servers use the data in the integration console, and UAT
// derives key data.
const std::string kKeyIdEntitlement =
wvutil::a2bs_hex("972F75C583835AABA6778E2565948825");
// The key id and encrypted key are made up. The decrypted key is golden data
// from a working system. This is TEST_ONLY data, so we may include it in source
// code in the clear.
const std::string kKeyIdEntitlement1 = // Key ID for entitled key 1.
a2bs_hex("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
const std::string kEncEntitledKey1 = // Encrypted key data for entitled key 1.
a2bs_hex("11111111111111111111111111111111");
// Clear key data for entitled key 1, used to encrypt test data.
const std::vector kEntitledKey1 = a2b_hex("AD789E1309DD67E55965679E72CE2328");
const std::string kKeyIdEntitlement2 = // Key ID for entitled key 2.
a2bs_hex("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
const std::string kEncEntitledKey2 = // Encrypted key data for entitled key 2.
a2bs_hex("22222222222222222222222222222222");
// Clear key data for entitled key 2, used to encrypt test data.
const std::vector kEntitledKey2 = a2b_hex("EA1E37D89066BF0B6FEF181DD5373580");
// A default pattern object disables patterns during decryption.
const Cdm::Pattern kPatternNone;
// The recommended pattern from CENC 3.0, which is also the pattern used by
// HLS. Encrypts 1 in every 10 crypto blocks.
const Cdm::Pattern kPatternRecommended(1, 9);
// The recommended pattern for HLS Audio, which should be decrypted in CENC 3.0
// cbcs mode despite not using patterns. This pattern disables patterned
// decryption by having one encrypted block and no clear blocks.
const Cdm::Pattern kPatternHlsAudio(1, 0);
const std::string kValue = "A Value";
const std::string kNewValue = "A New Value";
const std::string kParamName = "PARAM";
const std::string kParamName2 = "PARAM2";
const std::string kFakeCastMessage = a2bs_hex(
// ASN.1 SHA-1 identifier
"3021300906052b0e03021a05000414"
// Fake SHA-1 digest (actually just random bytes)
"96b34d11727bb41089e989ea51588666f924a40e");
class CdmTest : public WvCdmTestBase, public Cdm::IEventListener {
public:
CdmTest() {}
~CdmTest() override {}
// IEventListener mocks:
MOCK_METHOD(void, onMessage,
(const std::string& session_id, Cdm::MessageType message_type,
const std::string& message, const std::string& server_url),
(override));
MOCK_METHOD(void, onKeyStatusesChange,
(const std::string& session_id, bool has_new_usable_key),
(override));
MOCK_METHOD(void, onExpirationChange,
(const std::string& session_id, int64_t new_expiry_time_seconds),
(override));
MOCK_METHOD(void, onRemoveComplete, (const std::string& session_id),
(override));
protected:
void SetUp() override {
WvCdmTestBase::SetUp();
// Clear anything stored, load default device cert.
g_host->Reset();
// Reinit the library.
Cdm::Status status = Cdm::initialize(
Cdm::kNoSecureOutput, &g_host->global_storage(), g_host, g_host,
&g_stderr_logger, static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
ASSERT_EQ(Cdm::kSuccess, status);
// Make a fresh CDM.
RecreateCdm(true /* privacy_mode */);
}
void TearDown() override {}
void RecreateCdm(bool privacy_mode) {
CreateAdditionalCdm(privacy_mode, &cdm_);
cdm_->setServiceCertificate(Cdm::kProvisioningService,
config_.provisioning_service_certificate());
cdm_->setServiceCertificate(Cdm::kLicensingService,
config_.license_service_certificate());
}
void CreateAdditionalCdm(bool privacy_mode, std::unique_ptr<Cdm>* cdm) {
cdm->reset(Cdm::create(this, &g_host->per_origin_storage(), privacy_mode));
ASSERT_NE(nullptr, cdm->get());
}
bool Fetch(const std::string& url, const std::string& message,
std::string* response, int* status_code) {
std::string http_response;
for (size_t attempt = 1; attempt <= kMaxFetchAttempts; ++attempt) {
UrlRequest url_request(url);
if (!url_request.is_connected()) {
sleep(1);
continue;
}
url_request.PostRequest(message);
if (!url_request.GetResponse(&http_response)) {
sleep(1);
continue;
}
break;
}
// Some license servers return 400 for invalid message, some
// return 500; treat anything other than 200 as an invalid message.
int http_status_code = UrlRequest::GetStatusCode(http_response);
if (status_code) {
*status_code = http_status_code;
}
if (response) {
if (http_status_code == kHttpOk) {
// Parse out HTTP and server headers and return the body only.
std::string reply_body;
LicenseRequest lic_request;
lic_request.GetDrmMessage(http_response, reply_body);
*response = std::move(reply_body);
} else {
*response = std::move(http_response);
}
LOGV("Reply body(hex): \n%s\n", b2a_hex(*response).c_str());
LOGV("Reply body(b64): \n%s\n", Base64SafeEncode(*response).c_str());
}
return true;
}
void FetchCertificate(const std::string& url, std::string* response) {
int status_code;
bool ok = Fetch(url, "", response, &status_code);
ASSERT_TRUE(ok);
if (ok)
ASSERT_EQ(kHttpOk, status_code) << "Error response: " << *response << "\n"
<< "url: " << url;
}
bool FetchServiceCertificate(const std::string& url, std::string* response) {
std::string request;
int status;
if (!ServiceCertificate::GetRequest(&request)) {
LOGE("FAILED to generate service certificate request.");
return false;
}
if (!Fetch(url, request, response, &status)) {
LOGE("FAILED to get service certificate response: sts=%d", status);
return false;
}
LOGV("Reply body(hex): \n%s\n", b2a_hex(*response).c_str());
LOGV("Reply body(b64): \n%s\n", Base64SafeEncode(*response).c_str());
return true;
}
void FetchLicense(const std::string& license_server,
const std::string& message, std::string* response) {
int status_code;
const bool ok = Fetch(license_server, message, response, &status_code);
ASSERT_TRUE(ok);
if (!ok) return;
if (kHttpOk != status_code) {
std::map<std::string, std::string> fields;
if (UrlRequest::GetDebugHeaderFields(*response, &fields)) {
LOGD("Unexpected status code: code = %d", status_code);
for (const auto& field : fields) {
LOGD("- %s: %s", field.first.c_str(), field.second.c_str());
}
}
ASSERT_EQ(kHttpOk, status_code) << "Error response: " << *response << "\n"
<< "license_server: " << license_server;
}
}
void FetchLicenseFailure(const std::string& message) {
int status_code;
const bool ok =
Fetch(config_.license_server(), message, nullptr, &status_code);
ASSERT_TRUE(ok);
ASSERT_THAT(status_code, AllOf(Ge(400), Le(599)));
}
void CreateSessionAndGenerateRequest(Cdm::SessionType session_type,
Cdm::InitDataType init_data_type,
std::string* session_id,
std::string* message) {
Cdm::Status status = cdm_->createSession(session_type, session_id);
ASSERT_EQ(Cdm::kSuccess, status);
std::string init_data_type_name;
std::string init_data;
if (session_type == Cdm::kTemporary) {
if (init_data_type == Cdm::kCenc) {
init_data_type_name = CENC_INIT_DATA_FORMAT;
init_data = kCencInitData;
} else if (init_data_type == Cdm::kHls) {
init_data_type_name = HLS_INIT_DATA_FORMAT;
init_data = kHlsInitData;
}
} else if (session_type == Cdm::kPersistentLicense) {
if (init_data_type == Cdm::kCenc) {
init_data = kCencPersistentInitData;
init_data_type_name = CENC_INIT_DATA_FORMAT;
}
}
if (g_cutoff >= CDM_LOG_DEBUG) {
InitializationData parsed_init_data(init_data_type_name, init_data);
parsed_init_data.DumpToLogs();
}
ASSERT_FALSE(init_data.empty());
EXPECT_CALL(*this, onMessage(*session_id, Cdm::kLicenseRequest, _, _))
.WillOnce(SaveArg<2>(message));
status = generateRequestWithRetry(*session_id, init_data_type, init_data);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
}
void CreateSessionAndFetchLicense(Cdm::SessionType session_type,
Cdm::InitDataType init_data_type,
std::string* session_id,
std::string* response) {
std::string message;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndGenerateRequest(
session_type, init_data_type, session_id, &message));
std::string license_server;
if (init_data_type == Cdm::kCenc) {
license_server = config_.license_server();
} else if (init_data_type == Cdm::kHls) {
license_server = config_.license_server();
}
ASSERT_FALSE(license_server.empty());
FetchLicense(license_server, message, response);
}
void CreateSessionAndUpdate(Cdm::SessionType session_type,
Cdm::InitDataType init_data_type,
std::string* session_id) {
std::string response;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
session_type, init_data_type, session_id, &response));
EXPECT_CALL(*this, onKeyStatusesChange(*session_id, true));
int64_t event_expiry_time = 0;
EXPECT_CALL(*this, onExpirationChange(*session_id, _))
.WillOnce(SaveArg<1>(&event_expiry_time));
Cdm::Status status = updateWithRetry(*session_id, response);
ASSERT_EQ(Cdm::kSuccess, status);
ASSERT_TRUE(Mock::VerifyAndClear(this));
// The new expiry time should be consistent.
int64_t function_expiry_time = 0;
ASSERT_EQ(cdm_->getExpiration(*session_id, &function_expiry_time),
Cdm::kSuccess);
EXPECT_EQ(event_expiry_time, function_expiry_time);
}
void FetchLicenseAndUpdate(const std::string& session_id,
const std::string& message) {
// Acquire a license.
std::string response;
ASSERT_NO_FATAL_FAILURE(
FetchLicense(config_.license_server(), message, &response));
// This license should be accepted, but the keys are not expected to change.
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
Cdm::Status status = updateWithRetry(session_id, response);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
}
std::string GetProvisioningResponse(const std::string& message) {
std::string uri = config_.provisioning_server();
LOGV("GetProvisioningResponse: URI: %s", uri.c_str());
LOGV("GetProvisioningResponse: message:\n%s\n", b2a_hex(message).c_str());
uri += "&signedRequest=" + message;
std::string reply;
FetchCertificate(uri, &reply);
if (HasFatalFailure()) {
LOGE("Failed to get provisioning response");
reply.clear();
} else {
LOGV("GetProvisioningResponse: response:\n%s\n", reply.c_str());
}
return reply;
}
// This calls cdm->update, and retries several times if there is a
// failure due to nonce flood errors.
Cdm::Status updateWithRetry(const std::string& session_id,
const std::string& response) {
const int num_retries = 5;
for (int i = 0; i < num_retries; i++) {
Cdm::Status status = cdm_->update(session_id, response);
if (status == Cdm::kQuotaExceeded) {
sleep(1);
continue;
}
return status;
}
return Cdm::kQuotaExceeded;
}
// This calls cdm->generateRequest, and retries several times if there is a
// failure due to nonce flood errors.
Cdm::Status generateRequestWithRetry(const std::string& session_id,
Cdm::InitDataType init_data_type,
const std::string& init_data) {
const int num_retries = 5;
if (init_data_type == Cdm::kCenc && g_cutoff >= CDM_LOG_DEBUG) {
InitializationData parsed_init_data(CENC_INIT_DATA_FORMAT, init_data);
parsed_init_data.DumpToLogs();
}
for (int i = 0; i < num_retries; i++) {
LOGD("attempt %d", i);
Cdm::Status status =
cdm_->generateRequest(session_id, init_data_type, init_data);
if (status == Cdm::kQuotaExceeded) {
sleep(1);
continue;
}
return status;
}
return Cdm::kQuotaExceeded;
}
std::unique_ptr<Cdm> cdm_;
};
struct DecryptParam {
public:
DecryptParam(const std::string& short_name_param,
Cdm::InitDataType init_data_type_param,
const std::vector<uint8_t>& key_id_param,
const uint8_t* iv_param, size_t iv_size_param,
Cdm::EncryptionScheme scheme_param,
const Cdm::Pattern& pattern_param, const uint8_t* input_param,
size_t input_size_param, const uint8_t* output_param,
size_t output_size_param)
: short_name(short_name_param),
init_data_type(init_data_type_param),
key_id(&key_id_param),
iv(iv_param),
iv_size(iv_size_param),
scheme(scheme_param),
pattern(&pattern_param),
input(input_param),
input_size(input_size_param),
output(output_param),
output_size(output_size_param) {}
const std::string short_name;
const Cdm::InitDataType init_data_type;
const std::vector<uint8_t>* const key_id;
const uint8_t* const iv;
const size_t iv_size;
const Cdm::EncryptionScheme scheme;
const Cdm::Pattern* const pattern;
const uint8_t* const input;
const size_t input_size;
const uint8_t* const output;
const size_t output_size;
};
void PrintTo(const DecryptParam& value, ::std::ostream* os) {
*os << value.short_name << " DecryptParam";
}
class CdmTestWithDecryptParam : public CdmTest,
public WithParamInterface<DecryptParam> {};
class CdmTestWithRemoveParam : public CdmTest,
public WithParamInterface<bool> {};
class MockTimerClient : public Cdm::ITimer::IClient {
public:
MockTimerClient() {}
~MockTimerClient() override {}
MOCK_METHOD(void, onTimerExpired, (void*), (override));
};
const char* describe(Cdm::RobustnessLevel value) {
switch (value) {
case Cdm::kL1:
return "L1";
case Cdm::kL2:
return "L2";
case Cdm::kL3:
return "L3";
}
return "Invalid Value";
}
const char* describe(OEMCrypto_WatermarkingSupport value) {
switch (value) {
case OEMCrypto_WatermarkingError:
return "Error";
case OEMCrypto_WatermarkingNotSupported:
return "Not Supported";
case OEMCrypto_WatermarkingConfigurable:
return "Configurable";
case OEMCrypto_WatermarkingAlwaysOn:
return "Always-On";
}
return "Invalid Value";
}
// Prints out various pieces of information about the client running the tests.
// This information is parsed by the integration console when partners upload
// their test results.
TEST_F(CdmTest, PrintClientInformation) {
using std::cout;
using std::endl;
const time_t c_now =
std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
cout << "CE CDM Unit Tests for version " CDM_VERSION << endl;
cout << "Tests run at " << std::put_time(localtime(&c_now), "%F %T %Z")
<< endl;
// Collect CDM info
Cdm::ClientInfo client_info;
ASSERT_EQ(Cdm::getClientInfo(&client_info), Cdm::kSuccess);
cout << endl << "CDM Information:" << endl;
cout << " CDM Version = " << Cdm::version() << endl;
cout << " Company Name = " << client_info.company_name << endl;
cout << " Model Name = " << client_info.model_name << endl;
cout << " Model Year = " << client_info.model_year << endl;
cout << " Device Name = " << client_info.device_name << endl;
cout << " Product Name = " << client_info.product_name << endl;
cout << " Arch Name = " << client_info.arch_name << endl;
cout << " Build Info = " << client_info.build_info << endl;
// Collect OEMCrypto info
ASSERT_EQ(OEMCrypto_SUCCESS, OEMCrypto_Initialize());
(void)OEMCrypto_SetMaxAPIVersion(CDM_VERSION_MAJOR);
(void)OEMCrypto_EnterTestMode();
const uint32_t oec_major = OEMCrypto_APIVersion();
const uint32_t oec_minor = OEMCrypto_MinorAPIVersion();
const char* const supports_usage_tables =
OEMCrypto_SupportsUsageTable() ? "Supported" : "Not Supported";
const char* const production_ready =
(OEMCrypto_ProductionReady() == OEMCrypto_SUCCESS) ? "Production Ready"
: "Not Ready";
const OEMCrypto_WatermarkingSupport wm_support =
OEMCrypto_GetWatermarkingSupport();
EXPECT_EQ(OEMCrypto_SUCCESS, OEMCrypto_Terminate());
Cdm::RobustnessLevel robustness_level = static_cast<Cdm::RobustnessLevel>(0);
EXPECT_EQ(cdm_->getRobustnessLevel(&robustness_level), Cdm::kSuccess);
uint32_t system_id = 0;
EXPECT_EQ(cdm_->getSystemId(&system_id), Cdm::kSuccess);
uint32_t resource_rating_tier = 0;
EXPECT_EQ(cdm_->getResourceRatingTier(&resource_rating_tier), Cdm::kSuccess);
std::string oec_build_info;
EXPECT_EQ(cdm_->getOemCryptoBuildInfo(&oec_build_info), Cdm::kSuccess);
cout << endl << "OEMCrypto Information:" << endl;
cout << " OEMCrypto Version = " << oec_major << "." << oec_minor << endl;
cout << " Robustness Level = " << describe(robustness_level) << endl;
cout << " System ID = " << system_id << endl;
cout << " Usage Tables = " << supports_usage_tables << endl;
cout << " Resource Rating Tier = " << resource_rating_tier << endl;
cout << " Production Readiness = " << production_ready << endl;
cout << " Watermarking = " << describe(wm_support) << endl;
cout << " Build Info = " << oec_build_info << endl;
}
TEST_F(CdmTest, TestHostTimer) {
// Validate that the TestHost timers are processed in the correct order and
// on the correct timeouts.
const int64_t kTimerDelayMs = 1000;
void* kCtx1 = reinterpret_cast<void*>(0x1);
void* kCtx2 = reinterpret_cast<void*>(0x2);
void* kCtx4 = reinterpret_cast<void*>(0x4);
MockTimerClient client;
g_host->setTimeout(kTimerDelayMs * 1, &client, kCtx1);
g_host->setTimeout(kTimerDelayMs * 2, &client, kCtx2);
g_host->setTimeout(kTimerDelayMs * 4, &client, kCtx4);
EXPECT_CALL(client, onTimerExpired(kCtx1));
g_host->ElapseTime(kTimerDelayMs);
Mock::VerifyAndClear(&client);
EXPECT_CALL(client, onTimerExpired(kCtx2));
g_host->ElapseTime(kTimerDelayMs);
Mock::VerifyAndClear(&client);
EXPECT_CALL(client, onTimerExpired(_)).Times(0);
g_host->ElapseTime(kTimerDelayMs);
Mock::VerifyAndClear(&client);
EXPECT_CALL(client, onTimerExpired(kCtx4));
g_host->ElapseTime(kTimerDelayMs);
Mock::VerifyAndClear(&client);
EXPECT_CALL(client, onTimerExpired(_)).Times(0);
g_host->ElapseTime(kTimerDelayMs);
Mock::VerifyAndClear(&client);
}
TEST_F(CdmTest, Initialize) {
// Try with an invalid output type.
Cdm::Status status = Cdm::initialize(
static_cast<Cdm::SecureOutputType>(-1), &g_host->global_storage(), g_host,
g_host, &g_stderr_logger, static_cast<Cdm::LogLevel>(g_cutoff),
g_sandbox_id);
EXPECT_EQ(Cdm::kTypeError, status);
// Try with various host interfaces missing.
status = Cdm::initialize(Cdm::kNoSecureOutput, nullptr, g_host, g_host,
&g_stderr_logger,
static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
EXPECT_EQ(Cdm::kTypeError, status);
status = Cdm::initialize(Cdm::kNoSecureOutput, &g_host->global_storage(),
nullptr, g_host, &g_stderr_logger,
static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
EXPECT_EQ(Cdm::kTypeError, status);
status = Cdm::initialize(Cdm::kNoSecureOutput, &g_host->global_storage(),
g_host, nullptr, &g_stderr_logger,
static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
EXPECT_EQ(Cdm::kTypeError, status);
status = Cdm::initialize(Cdm::kNoSecureOutput, &g_host->global_storage(),
g_host, g_host, nullptr,
static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
EXPECT_EQ(Cdm::kTypeError, status);
// Try all output types.
status = Cdm::initialize(Cdm::kDirectRender, &g_host->global_storage(),
g_host, g_host, &g_stderr_logger,
static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
EXPECT_EQ(Cdm::kSuccess, status);
status = Cdm::initialize(Cdm::kOpaqueHandle, &g_host->global_storage(),
g_host, g_host, &g_stderr_logger,
static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
EXPECT_EQ(Cdm::kSuccess, status);
// One last init with everything correct and working.
status = Cdm::initialize(Cdm::kNoSecureOutput, &g_host->global_storage(),
g_host, g_host, &g_stderr_logger,
static_cast<Cdm::LogLevel>(g_cutoff), g_sandbox_id);
EXPECT_EQ(Cdm::kSuccess, status);
}
TEST_F(CdmTest, GetServiceCertificateRequest) {
// Set a server certificate with privacy mode disabled - should work.
ASSERT_NO_FATAL_FAILURE(RecreateCdm(false /* privacy_mode */));
std::string message;
Cdm::Status status = cdm_->getServiceCertificateRequest(&message);
EXPECT_EQ(Cdm::kSuccess, status);
}
TEST_F(CdmTest, ParseAndLoadServiceCertificateResponse) {
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
std::string message;
ASSERT_EQ(cdm_->getServiceCertificateRequest(&message), Cdm::kSuccess);
ConfigTestEnv uat_config(kContentProtectionUatServer);
std::string response;
int code;
ASSERT_TRUE(Fetch(uat_config.license_server(), message, &response, &code));
ASSERT_EQ(code, 200);
std::string cert;
EXPECT_EQ(cdm_->parseAndLoadServiceCertificateResponse(Cdm::kAllServices,
response, &cert),
Cdm::kSuccess);
EXPECT_EQ(cdm_->parseAndLoadServiceCertificateResponse(Cdm::kLicensingService,
response, &cert),
Cdm::kSuccess);
// Can pass NULL and ignore the parsed certificate.
EXPECT_EQ(cdm_->parseAndLoadServiceCertificateResponse(Cdm::kLicensingService,
response, nullptr),
Cdm::kSuccess);
EXPECT_EQ(cdm_->parseAndLoadServiceCertificateResponse(
Cdm::kLicensingService, kBogusServiceCertificate, &cert),
Cdm::kTypeError);
}
TEST_F(CdmTest, ServiceCertificateRequestResponseUat) {
ConfigTestEnv uat_config(kContentProtectionUatServer);
std::string response;
ASSERT_TRUE(FetchServiceCertificate(uat_config.license_server(), &response));
LOGV("Response size = %zu", response.size());
#if 0 // enable to extract the service certificate in byte form
size_t done = 0;
while (done < response.size()) {
for (int i = 0; i < 12; i++) {
if (done >= response.size()) {
break;
}
uint32_t x = static_cast<uint8_t>(response.data()[done]);
printf("0x%02x, ", x);
done++;
}
printf ("\n");
}
#endif
}
TEST_F(CdmTest, ServiceCertificateRequestResponseStaging) {
ConfigTestEnv staging_config(kContentProtectionStagingServer);
std::string response;
ASSERT_TRUE(
FetchServiceCertificate(staging_config.license_server(), &response));
LOGV("Response: size = %zu", response.size());
#if 0 // enable to extract the service certificate in byte form
size_t done = 0;
while (done < response.size()) {
for (int i = 0; i < 12; i++) {
if (done >= response.size()) {
break;
}
uint32_t x = static_cast<uint8_t>(response.data()[done]);
printf("0x%02x, ", x);
done++;
}
printf ("\n");
}
#endif
}
TEST_F(CdmTest, SetServiceCertificate) {
// Set a server certificate with privacy mode disabled - should work.
ASSERT_NO_FATAL_FAILURE(RecreateCdm(false /* privacy_mode */));
Cdm::Status status = cdm_->setServiceCertificate(
Cdm::kAllServices, config_.license_service_certificate());
EXPECT_EQ(Cdm::kSuccess, status);
status = cdm_->setServiceCertificate(
Cdm::kProvisioningService, config_.provisioning_service_certificate());
EXPECT_EQ(Cdm::kSuccess, status);
status = cdm_->setServiceCertificate(Cdm::kLicensingService,
config_.license_service_certificate());
EXPECT_EQ(Cdm::kSuccess, status);
// Can set a server certificate if privacy mode is enabled.
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
status = cdm_->setServiceCertificate(Cdm::kAllServices,
config_.license_service_certificate());
EXPECT_EQ(Cdm::kSuccess, status);
status = cdm_->setServiceCertificate(
Cdm::kProvisioningService, config_.provisioning_service_certificate());
EXPECT_EQ(Cdm::kSuccess, status);
status = cdm_->setServiceCertificate(Cdm::kLicensingService,
config_.license_service_certificate());
EXPECT_EQ(Cdm::kSuccess, status);
// It is invalid to set a malformed cert.
status =
cdm_->setServiceCertificate(Cdm::kAllServices, kBogusServiceCertificate);
EXPECT_EQ(Cdm::kTypeError, status);
status = cdm_->setServiceCertificate(Cdm::kProvisioningService,
kBogusServiceCertificate);
EXPECT_EQ(Cdm::kTypeError, status);
status = cdm_->setServiceCertificate(Cdm::kLicensingService,
kBogusServiceCertificate);
EXPECT_EQ(Cdm::kTypeError, status);
}
TEST_F(CdmTest, OpenSessionWithoutServiceCertificate) {
// Create a CDM instance that does not have any service certificates
// installed.
ASSERT_NO_FATAL_FAILURE(CreateAdditionalCdm(true /* privacy_mode */, &cdm_));
EnsureProvisioned();
// Verify that sessions can be opened.
std::string session_id;
ASSERT_EQ(Cdm::kSuccess, cdm_->createSession(Cdm::kTemporary, &session_id));
// License request generation, however, should fail.
EXPECT_CALL(*this, onMessage(session_id, _, _, _)).Times(0);
EXPECT_EQ(Cdm::kNeedsServiceCertificate,
generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData));
Mock::VerifyAndClear(this);
ASSERT_EQ(Cdm::kSuccess, cdm_->close(session_id));
ASSERT_EQ(Cdm::kSuccess, cdm_->createSession(Cdm::kTemporary, &session_id));
// Once a service certificate has been set, the existing session should be
// able to generate a request.
ASSERT_EQ(Cdm::kSuccess,
cdm_->setServiceCertificate(Cdm::kLicensingService,
config_.license_service_certificate()));
EXPECT_CALL(*this, onMessage(session_id, _, _, _)).Times(AtLeast(1));
EXPECT_EQ(Cdm::kSuccess,
generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData));
Mock::VerifyAndClear(this);
ASSERT_EQ(Cdm::kSuccess, cdm_->close(session_id));
}
TEST_F(CdmTest, GetRobustnessLevel) {
Cdm::RobustnessLevel level;
const Cdm::Status status = cdm_->getRobustnessLevel(&level);
ASSERT_EQ(Cdm::kSuccess, status);
LOGI("Got robustness level %d", static_cast<int>(level));
}
TEST_F(CdmTest, GetSystemId) {
uint32_t id;
const Cdm::Status status = cdm_->getSystemId(&id);
ASSERT_EQ(Cdm::kSuccess, status);
LOGI("Got system ID %u", id);
}
TEST_F(CdmTest, GetResourceRatingTier) {
uint32_t tier;
const Cdm::Status status = cdm_->getResourceRatingTier(&tier);
ASSERT_EQ(Cdm::kSuccess, status);
LOGI("Got resource rating tier %u", tier);
}
TEST_F(CdmTest, GetOemCryptoBuildInfo) {
std::string build_info;
const Cdm::Status status = cdm_->getOemCryptoBuildInfo(&build_info);
ASSERT_EQ(Cdm::kSuccess, status);
LOGI("Got OEMCrypto build info: %s", build_info.c_str());
}
TEST_F(CdmTest, CreateSession) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
EXPECT_EQ(Cdm::kSuccess, status);
EXPECT_FALSE(session_id.empty());
}
TEST_F(CdmTest, CreateSession_ReuseSessionId) {
EnsureProvisioned();
// Create a temporary session.
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
EXPECT_EQ(Cdm::kSuccess, status);
// Create another using the same pointer to an already-filled-out string,
// and expect the session ID to change.
std::string original_session_id = session_id;
status = cdm_->createSession(Cdm::kTemporary, &session_id);
EXPECT_EQ(Cdm::kSuccess, status);
EXPECT_NE(original_session_id, session_id);
}
TEST_F(CdmTest, CreateSession_Persistent) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status =
cdm_->createSession(Cdm::kPersistentLicense, &session_id);
EXPECT_EQ(Cdm::kSuccess, status);
}
TEST_F(CdmTest, CreateSession_NullArgument) {
EnsureProvisioned();
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, nullptr);
EXPECT_EQ(Cdm::kTypeError, status);
}
TEST_F(CdmTest, CreateSession_BogusType) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(kBogusSessionType, &session_id);
EXPECT_EQ(Cdm::kNotSupported, status);
}
TEST_F(CdmTest, GenerateRequest) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
std::string url;
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.WillOnce(SaveArg<3>(&url));
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
EXPECT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
EXPECT_TRUE(url.empty());
}
TEST_F(CdmTest, GenerateRequest_ReuseSession) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _));
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
EXPECT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// Can't call generateRequest more than once on a session.
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.Times(0);
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
EXPECT_EQ(Cdm::kInvalidState, status);
}
TEST_F(CdmTest, GenerateRequest_WebM) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
std::string url;
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.WillOnce(SaveArg<3>(&url));
status = generateRequestWithRetry(session_id, Cdm::kWebM, kWebMInitData);
EXPECT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
EXPECT_TRUE(url.empty());
}
TEST_F(CdmTest, GenerateRequest_KeyIds) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.Times(0);
status = generateRequestWithRetry(session_id, Cdm::kKeyIds, kKeyIdsInitData);
EXPECT_EQ(Cdm::kNotSupported, status);
}
TEST_F(CdmTest, GenerateRequest_HLS) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
std::string url;
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.WillOnce(SaveArg<3>(&url));
status = generateRequestWithRetry(session_id, Cdm::kHls, kHlsInitData);
EXPECT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
EXPECT_TRUE(url.empty());
}
TEST_F(CdmTest, GenerateRequest_BogusType) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.Times(0);
status = generateRequestWithRetry(session_id, kBogusInitDataType, "asdf");
EXPECT_EQ(Cdm::kTypeError, status);
Mock::VerifyAndClear(this);
// This same session should still be usable with a supported init data type
// after failing with an unsupported or bogus type.
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _));
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
EXPECT_EQ(Cdm::kSuccess, status);
}
TEST_F(CdmTest, GenerateRequest_Empty) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.Times(0);
status = generateRequestWithRetry(session_id, Cdm::kCenc, "");
EXPECT_EQ(Cdm::kTypeError, status);
}
TEST_F(CdmTest, GenerateRequest_InvalidCenc) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.Times(0);
status =
generateRequestWithRetry(session_id, Cdm::kCenc, kInvalidCencInitData);
EXPECT_EQ(Cdm::kNotSupported, status);
}
TEST_F(CdmTest, GenerateRequest_NonWidevineCenc) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.Times(0);
status = generateRequestWithRetry(session_id, Cdm::kCenc,
kNonWidevineCencInitData);
EXPECT_EQ(Cdm::kNotSupported, status);
}
TEST_F(CdmTest, GenerateRequest_BogusSessionId) {
EnsureProvisioned();
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_CALL(*this, onMessage(_, _, _, _)).Times(0);
status = generateRequestWithRetry(kBogusSessionId, Cdm::kCenc, kCencInitData);
EXPECT_EQ(Cdm::kSessionNotFound, status);
}
TEST_F(CdmTest, Update) {
EnsureProvisioned();
std::string session_id;
std::string message;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndGenerateRequest(
Cdm::kTemporary, Cdm::kCenc, &session_id, &message));
std::string response;
ASSERT_NO_FATAL_FAILURE(
FetchLicense(config_.license_server(), message, &response));
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
Cdm::Status status = updateWithRetry(session_id, response);
EXPECT_EQ(Cdm::kSuccess, status);
}
TEST_F(CdmTest, Update_BogusSession) {
EnsureProvisioned();
std::string session_id;
std::string message;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndGenerateRequest(
Cdm::kTemporary, Cdm::kCenc, &session_id, &message));
std::string response;
ASSERT_NO_FATAL_FAILURE(
FetchLicense(config_.license_server(), message, &response));
Cdm::Status status = updateWithRetry(kBogusSessionId, response);
EXPECT_EQ(Cdm::kSessionNotFound, status);
}
TEST_F(CdmTest, Update_Empty) {
EnsureProvisioned();
std::string session_id;
std::string message;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndGenerateRequest(
Cdm::kTemporary, Cdm::kCenc, &session_id, &message));
Cdm::Status status = updateWithRetry(session_id, "");
EXPECT_EQ(Cdm::kTypeError, status);
}
TEST_F(CdmTest, Update_InvalidDrmCertError) {
EnsureProvisioned();
std::string session_id;
std::string message;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndGenerateRequest(
Cdm::kTemporary, Cdm::kCenc, &session_id, &message));
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true)).Times(0);
LicenseError license_error;
std::string error_msg;
SignedMessage signed_message;
signed_message.set_type(SignedMessage::ERROR_RESPONSE);
std::string error_response;
// Invalid device certificate
license_error.set_error_code(LicenseError::INVALID_DRM_DEVICE_CERTIFICATE);
license_error.SerializeToString(&error_msg);
signed_message.set_msg(error_msg);
signed_message.SerializeToString(&error_response);
Cdm::Status status = updateWithRetry(session_id, error_response);
EXPECT_EQ(Cdm::kNeedsDeviceCertificate, status);
}
TEST_F(CdmTest, Update_RevokedDrmCertError) {
EnsureProvisioned();
std::string session_id;
std::string message;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndGenerateRequest(
Cdm::kTemporary, Cdm::kCenc, &session_id, &message));
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true)).Times(0);
LicenseError license_error;
std::string error_msg;
SignedMessage signed_message;
signed_message.set_type(SignedMessage::ERROR_RESPONSE);
std::string error_response;
// Revoked device certificate
license_error.set_error_code(LicenseError::REVOKED_DRM_DEVICE_CERTIFICATE);
license_error.SerializeToString(&error_msg);
signed_message.set_msg(error_msg);
signed_message.SerializeToString(&error_response);
Cdm::Status status = updateWithRetry(session_id, error_response);
EXPECT_EQ(Cdm::kDeviceRevoked, status);
}
TEST_F(CdmTest, Update_UnexpectedUpdate) {
EnsureProvisioned();
std::string session_id;
std::string message;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndGenerateRequest(
Cdm::kTemporary, Cdm::kCenc, &session_id, &message));
std::string response;
ASSERT_NO_FATAL_FAILURE(
FetchLicense(config_.license_server(), message, &response));
// Create a new session and try updating before generating a request.
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
status = updateWithRetry(session_id, response);
ASSERT_EQ(Cdm::kInvalidState, status);
}
TEST_F(CdmTest, Close) {
EnsureProvisioned();
// Create a temporary session.
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
// Close it.
status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
// Can't generate a license request after close.
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.Times(0);
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
EXPECT_EQ(Cdm::kSessionNotFound, status);
Mock::VerifyAndClear(this);
// Try to close the same session again.
status = cdm_->close(session_id);
EXPECT_EQ(Cdm::kSessionNotFound, status);
// Try to close a bogus session.
status = cdm_->close(kBogusSessionId);
EXPECT_EQ(Cdm::kSessionNotFound, status);
}
TEST_F(CdmTest, LoadTemporary) {
EnsureProvisioned();
std::string session_id;
std::string response;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
Cdm::kTemporary, Cdm::kCenc, &session_id, &response));
// Update the temporary session.
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
Cdm::Status status = updateWithRetry(session_id, response);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// Close the session.
status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
// Can't load a temporary session.
status = cdm_->load(session_id);
EXPECT_EQ(Cdm::kSessionNotFound, status);
}
TEST_F(CdmTest, LoadPersistent) {
EnsureProvisioned();
std::string session_id;
std::string response;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
Cdm::kPersistentLicense, Cdm::kCenc, &session_id, &response));
// Update the persistent session.
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
Cdm::Status status = updateWithRetry(session_id, response);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// Should be able to load the session again after closing it.
status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
status = cdm_->load(session_id);
EXPECT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// Should be able to load the session again after recreating the CDM.
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
status = cdm_->load(session_id);
EXPECT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// Should not be able to load the session again clearing storage.
status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
g_host->Reset();
EnsureProvisioned();
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
status = cdm_->load(session_id);
EXPECT_EQ(Cdm::kSessionNotFound, status);
Mock::VerifyAndClear(this);
}
TEST_F(CdmTest, LoadWillFireExpiration) {
EnsureProvisioned();
// There was a bug where calling load() would not start the PolicyEngine timer
// because it was only started in update().
std::string session_id;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kPersistentLicense, Cdm::kCenc, &session_id));
// Should be able to load the session again after recreating the CDM.
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
ASSERT_EQ(Cdm::kSuccess, cdm_->load(session_id));
Mock::VerifyAndClear(this);
// Let the key expire, make sure we get a key status update.
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
std::string url;
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRenewal, _, _))
.WillOnce(SaveArg<3>(&url));
g_host->ElapseTime(kOfflineLicenseDurationMs);
Mock::VerifyAndClear(this);
EXPECT_FALSE(url.empty());
}
TEST_F(CdmTest, PerOriginLoadPersistent) {
EnsureProvisioned();
std::string session_id;
std::string response;
Cdm::Status status;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
Cdm::kPersistentLicense, Cdm::kCenc, &session_id, &response));
// Update and close the persistent session.
{
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(AtLeast(1));
status = updateWithRetry(session_id, response);
ASSERT_EQ(Cdm::kSuccess, status);
status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
}
// Should be able to load the session again after recreating the CDM.
{
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(AtLeast(1));
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
status = cdm_->load(session_id);
EXPECT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
}
// Create a second CDM using a different origin's storage and verify that it
// cannot load the session from above.
{
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
// Create another host to use its storage. This will simulate another
// origin.
TestHost other_host;
// EnsureProvisioned uses the global host, so set that temporarily to
// provision the new host (if needed).
TestHost* cur_host = g_host;
g_host = &other_host;
EnsureProvisioned();
g_host = cur_host;
// Create a new CDM that uses the new host and new storage.
std::unique_ptr<Cdm> other_cdm(Cdm::create(
this, &other_host.per_origin_storage(), /* privacy_mode */ true));
ASSERT_TRUE(other_cdm.get());
status = other_cdm->setServiceCertificate(
Cdm::kLicensingService, config_.license_service_certificate());
ASSERT_EQ(Cdm::kSuccess, status);
// Should not be able to see sessions from another origin.
status = other_cdm->load(session_id);
EXPECT_EQ(Cdm::kSessionNotFound, status);
Mock::VerifyAndClear(this);
}
}
TEST_F(CdmTest, LoadBogus) {
EnsureProvisioned();
EXPECT_CALL(*this, onKeyStatusesChange(_, _)).Times(0);
Cdm::Status status = cdm_->load(kBogusSessionId);
EXPECT_EQ(Cdm::kSessionNotFound, status);
}
TEST_F(CdmTest, GetKeyStatuses) {
EnsureProvisioned();
std::string session_id;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
// We should be able to query status and see a usable key.
Cdm::KeyStatusMap map;
Cdm::Status status = cdm_->getKeyStatuses(session_id, &map);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_EQ(Cdm::kUsable, map.begin()->second);
// The key ID should be the one we are expecting.
const std::string expected_key_id(
reinterpret_cast<const char*>(kKeyIdCtr.data()), kKeyIdCtr.size());
EXPECT_EQ(expected_key_id, map.begin()->first);
// Let the key expire.
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRenewal, _, _))
.Times(AtLeast(1));
g_host->ElapseTime(kExpirationTestDelayMs);
Mock::VerifyAndClear(this);
// We should see expiration reflected in the map.
status = cdm_->getKeyStatuses(session_id, &map);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_EQ(Cdm::kExpired, map.begin()->second);
// We can't get status after closing a session.
status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
status = cdm_->getKeyStatuses(session_id, &map);
ASSERT_EQ(Cdm::kSessionNotFound, status);
}
TEST_F(CdmTest, GetExpiration) {
EnsureProvisioned();
std::string session_id;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
// We should be able to query expiration and get a value in the future.
int64_t expiration;
Cdm::Status status = cdm_->getExpiration(session_id, &expiration);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_GT(expiration, g_host->now());
int64_t original_expiration = expiration;
// Let the key expire.
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRenewal, _, _))
.Times(AtLeast(1));
g_host->ElapseTime(kExpirationTestDelayMs);
Mock::VerifyAndClear(this);
// We should see expiration in the past now.
status = cdm_->getExpiration(session_id, &expiration);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_LE(expiration, g_host->now());
// Expiration should not have changed.
EXPECT_EQ(original_expiration, expiration);
// We can't get expiration after closing a session.
status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
status = cdm_->getExpiration(session_id, &expiration);
ASSERT_EQ(Cdm::kSessionNotFound, status);
}
TEST_P(CdmTestWithRemoveParam, Remove) {
if (!wvoec::global_features.usage_table) {
GTEST_SKIP() << "Test for usage table devices only.";
}
const bool intermediate_close = GetParam();
EnsureProvisioned();
std::string session_id;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kPersistentLicense, Cdm::kCenc, &session_id));
// Remove the session. This causes a release message to be generated.
std::string message;
std::string url;
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _, _))
.WillOnce(DoAll(SaveArg<2>(&message), SaveArg<3>(&url)));
Cdm::Status status = cdm_->remove(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
EXPECT_FALSE(url.empty());
// The keys should already be unusable.
Cdm::KeyStatusMap map;
status = cdm_->getKeyStatuses(session_id, &map);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_EQ(Cdm::kReleased, map.begin()->second);
// If we are testing intermediate closing, close the session.
if (intermediate_close) {
status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
}
// Post the release message to the license server.
std::string response;
ASSERT_NO_FATAL_FAILURE(
FetchLicense(config_.license_server(), message, &response));
// If we are testing intermediate closing, reopen the session.
if (intermediate_close) {
status = cdm_->load(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
}
// Update the session.
EXPECT_CALL(*this, onRemoveComplete(session_id));
status = updateWithRetry(session_id, response);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// The session is now completely gone.
status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSessionNotFound, status);
status = cdm_->load(session_id);
ASSERT_EQ(Cdm::kSessionNotFound, status);
// Try a bogus session ID.
status = cdm_->remove(kBogusSessionId);
EXPECT_EQ(Cdm::kSessionNotFound, status);
// Try a new session.
status = cdm_->createSession(Cdm::kPersistentLicense, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
status = cdm_->remove(session_id);
EXPECT_EQ(Cdm::kInvalidState, status);
// Try a temporary session.
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
status = cdm_->remove(session_id);
EXPECT_EQ(Cdm::kRangeError, status);
}
INSTANTIATE_TEST_SUITE_P(CdmRemoveTest, CdmTestWithRemoveParam, Bool(),
testing::PrintToStringParamName());
TEST_F(CdmTest, ForceRemove) {
EnsureProvisioned();
std::string session_id;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kPersistentLicense, Cdm::kCenc, &session_id));
// Forcibly remove the session. This should immediately trigger a removal
// callback and should *not* cause a release message to be generated.
EXPECT_CALL(*this, onRemoveComplete(session_id)).Times(1);
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _, _))
.Times(0);
Cdm::Status status = cdm_->forceRemove(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// The session is now completely gone.
status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSessionNotFound, status);
status = cdm_->load(session_id);
ASSERT_EQ(Cdm::kSessionNotFound, status);
// Try a bogus session ID.
status = cdm_->forceRemove(kBogusSessionId);
EXPECT_EQ(Cdm::kSessionNotFound, status);
// Try a new session.
status = cdm_->createSession(Cdm::kPersistentLicense, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
status = cdm_->forceRemove(session_id);
EXPECT_EQ(Cdm::kInvalidState, status);
// Try a temporary session.
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
status = cdm_->forceRemove(session_id);
EXPECT_EQ(Cdm::kRangeError, status);
}
// Reload an offline license that does not have a usage entry.
TEST_F(CdmTest, LoadPersistentNoNonce) {
EnsureProvisioned();
std::string session_id;
ASSERT_EQ(Cdm::kSuccess,
cdm_->createSession(Cdm::kPersistentLicense, &session_id));
video_widevine::WidevinePsshData pssh;
// offline_clip1 does not have a provider session token, so it will not
// generate a usage table entry.
pssh.set_content_id("offline_clip1");
const std::string init_data = MakePSSH(pssh);
std::string license_request;
{
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.WillOnce(SaveArg<2>(&license_request));
ASSERT_EQ(Cdm::kSuccess,
generateRequestWithRetry(session_id, Cdm::kCenc, init_data));
Mock::VerifyAndClear(this);
}
// Send the request to the license server and receive the license response.
std::string license_response;
FetchLicense(config_.license_server(), license_request, &license_response);
// Update the session with the new keys.
{
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
ASSERT_EQ(Cdm::kSuccess, updateWithRetry(session_id, license_response));
Mock::VerifyAndClear(this);
}
// Should be able to load the session again after closing it.
Cdm::Status status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
status = cdm_->load(session_id);
EXPECT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// Should be able to load the session again after recreating the CDM.
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
status = cdm_->load(session_id);
EXPECT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// Should not be able to load the session again after clearing storage.
status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
g_host->Reset();
EnsureProvisioned();
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
status = cdm_->load(session_id);
EXPECT_EQ(Cdm::kSessionNotFound, status);
Mock::VerifyAndClear(this);
}
TEST_F(CdmTest, RemoveIncomplete) {
EnsureProvisioned();
std::string session_id;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kPersistentLicense, Cdm::kCenc, &session_id));
// Remove the session. This causes a release message to be generated.
std::string message;
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _, _))
.WillOnce(SaveArg<2>(&message));
Cdm::Status status = cdm_->remove(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// The keys should already be unusable, but they should still exist.
Cdm::KeyStatusMap map;
status = cdm_->getKeyStatuses(session_id, &map);
ASSERT_EQ(Cdm::kSuccess, status);
ASSERT_FALSE(map.empty());
EXPECT_EQ(Cdm::kReleased, map.begin()->second);
// Recreate the CDM.
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
// Load the partially removed session.
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _, _))
.Times(0);
status = cdm_->load(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// This session has no keys.
status = cdm_->getKeyStatuses(session_id, &map);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_TRUE(map.empty());
// Remove the session again to fire the release message.
message.clear();
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRelease, _, _))
.WillOnce(SaveArg<2>(&message));
status = cdm_->remove(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
ASSERT_FALSE(message.empty());
Mock::VerifyAndClear(this);
// Post the release message to the license server.
std::string response;
ASSERT_NO_FATAL_FAILURE(
FetchLicense(config_.license_server(), message, &response));
// Update the session.
EXPECT_CALL(*this, onKeyStatusesChange(session_id, _)).Times(0);
EXPECT_CALL(*this, onRemoveComplete(session_id));
status = updateWithRetry(session_id, response);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// The session is now completely gone.
status = cdm_->load(session_id);
ASSERT_EQ(Cdm::kSessionNotFound, status);
}
TEST_F(CdmTest, RemoveNotLoaded) {
EnsureProvisioned();
// Create a persistent session and then close it.
std::string session_id;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kPersistentLicense, Cdm::kCenc, &session_id));
Cdm::Status status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
// A session must be loaded before removing it. Remove only works on active
// sessions.
status = cdm_->remove(session_id);
EXPECT_EQ(Cdm::kSessionNotFound, status);
}
TEST_F(CdmTest, RequestPersistentLicenseWithWrongInitData) {
EnsureProvisioned();
// Generate a request for a persistent license without using the correct
// persistent content init data.
std::string session_id;
Cdm::Status status =
cdm_->createSession(Cdm::kPersistentLicense, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
std::string message;
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.WillOnce(SaveArg<2>(&message));
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// The license server will reject this.
FetchLicenseFailure(message);
}
// TODO(b/34949512): Fix this test so it can be re-enabled.
TEST_F(CdmTest, DISABLED_RequestTemporaryLicenseWithWrongInitData) {
EnsureProvisioned();
// Generate a request for a temporary license using persistent init data.
std::string session_id;
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
std::string message;
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.WillOnce(SaveArg<2>(&message));
status =
generateRequestWithRetry(session_id, Cdm::kCenc, kCencPersistentInitData);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// Acquire a license.
std::string response;
ASSERT_NO_FATAL_FAILURE(
FetchLicense(config_.license_server(), message, &response));
// This license should not be accepted.
EXPECT_CALL(*this, onKeyStatusesChange(session_id, false));
status = updateWithRetry(session_id, response);
EXPECT_EQ(Cdm::kRangeError, status);
Mock::VerifyAndClear(this);
}
TEST_F(CdmTest, Renewal) {
EnsureProvisioned();
std::string session_id;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
// We should have a timer.
EXPECT_NE(0u, g_host->NumTimers());
// When we elapse time, we should get a renewal message.
std::string message;
std::string url;
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRenewal, _, _))
.WillOnce(DoAll(SaveArg<2>(&message), SaveArg<3>(&url)));
g_host->ElapseTime(kRenewalTestDelayMs);
Mock::VerifyAndClear(this);
ASSERT_FALSE(message.empty()); // Stop the test if no message came through.
EXPECT_FALSE(url.empty());
// When should still have a timer.
EXPECT_NE(0u, g_host->NumTimers());
// We should be able to update the session.
ASSERT_NO_FATAL_FAILURE(FetchLicenseAndUpdate(session_id, message));
// After closing the session, there should be no more renewals.
Cdm::Status status = cdm_->close(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRenewal, _, _))
.Times(0);
g_host->ElapseTime(kRenewalTestDelayMs * 10);
Mock::VerifyAndClear(this);
}
TEST_F(CdmTest, SetAppParameters) {
EnsureProvisioned();
// Must use privacy_mode = false to ensure that the message is in plain-text.
std::string session_id;
ASSERT_NO_FATAL_FAILURE(RecreateCdm(false /* privacy_mode */));
Cdm::Status status = cdm_->createSession(Cdm::kTemporary, &session_id);
ASSERT_EQ(Cdm::kSuccess, status);
// Set a new app parameter, and check by getting.
std::string result;
status = cdm_->setAppParameter(kParamName, kValue);
ASSERT_EQ(Cdm::kSuccess, status);
status = cdm_->getAppParameter(kParamName, &result);
ASSERT_EQ(Cdm::kSuccess, status);
ASSERT_EQ(kValue, result);
// Try to get using a null result.
status = cdm_->getAppParameter(kParamName, nullptr);
ASSERT_EQ(Cdm::kTypeError, status);
// Try to get using an empty key.
status = cdm_->getAppParameter("", &result);
ASSERT_EQ(Cdm::kTypeError, status);
// Try to set using an empty key.
status = cdm_->setAppParameter("", kValue);
ASSERT_EQ(Cdm::kTypeError, status);
// Try to remove using an empty key.
status = cdm_->removeAppParameter("");
ASSERT_EQ(Cdm::kTypeError, status);
// Change an existing app parameter.
status = cdm_->setAppParameter(kParamName, kNewValue);
ASSERT_EQ(Cdm::kSuccess, status);
status = cdm_->getAppParameter(kParamName, &result);
ASSERT_EQ(Cdm::kSuccess, status);
ASSERT_EQ(kNewValue, result);
// Remove an existing app parameter, check for invalid access when it's gone.
status = cdm_->removeAppParameter(kParamName);
ASSERT_EQ(Cdm::kSuccess, status);
status = cdm_->getAppParameter(kParamName, &result);
ASSERT_EQ(Cdm::kTypeError, status);
// Try to remove an absent value.
status = cdm_->removeAppParameter(kParamName2);
ASSERT_EQ(Cdm::kTypeError, status);
// Set some values to check for.
status = cdm_->setAppParameter(kParamName, kValue);
ASSERT_EQ(Cdm::kSuccess, status);
status = cdm_->setAppParameter(kParamName2, kNewValue);
ASSERT_EQ(Cdm::kSuccess, status);
// Send a generate request to ensure the parameter is in the message.
std::string message;
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.WillOnce(SaveArg<2>(&message));
status = generateRequestWithRetry(session_id, Cdm::kCenc, kCencInitData);
EXPECT_EQ(Cdm::kSuccess, status);
EXPECT_TRUE(!message.empty() && message.find(kValue) != std::string::npos);
Mock::VerifyAndClear(this);
// Ensure that the value is still present and correct.
status = cdm_->getAppParameter(kParamName, &result);
ASSERT_EQ(Cdm::kSuccess, status);
ASSERT_EQ(kValue, result);
status = cdm_->getAppParameter(kParamName2, &result);
ASSERT_EQ(Cdm::kSuccess, status);
ASSERT_EQ(kNewValue, result);
// Clear all the parameters.
status = cdm_->clearAppParameters();
ASSERT_EQ(Cdm::kSuccess, status);
status = cdm_->getAppParameter(kParamName, &result);
ASSERT_EQ(Cdm::kTypeError, status);
status = cdm_->getAppParameter(kParamName2, &result);
ASSERT_EQ(Cdm::kTypeError, status);
}
TEST_F(CdmTest, SetVideoResolutionBadSession) {
EnsureProvisioned();
std::string session_id;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
Cdm::Status status = cdm_->setVideoResolution(kBogusSessionId, 100, 100);
ASSERT_EQ(Cdm::kSessionNotFound, status);
cdm_->close(session_id);
}
TEST_F(CdmTest, SetVideoResolutionOK) {
EnsureProvisioned();
std::string session_id;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
Cdm::Status status = cdm_->setVideoResolution(session_id, 100, 100);
ASSERT_EQ(Cdm::kSuccess, status);
cdm_->close(session_id);
}
TEST_F(CdmTest, SetVideoResolutionOverflow) {
EnsureProvisioned();
std::string session_id;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &session_id));
const uint32_t kUintTooLarge = (1 << 16);
Cdm::Status status =
cdm_->setVideoResolution(session_id, kUintTooLarge, kUintTooLarge);
ASSERT_EQ(Cdm::kRangeError, status);
cdm_->close(session_id);
}
TEST_F(CdmTest, GetStatusForHdcpResolution) {
// Unfortunately, since we cannot mock the HDCP state, we cannot validate
// the validity of the values returned here, only that meaningful values are
// returned.
const std::vector<Cdm::HdcpVersion> kSupportedHdcpVersions = {
// Legacy 1.x version
Cdm::kHdcp1_x,
// v17 1.x versions
Cdm::kHdcp1_0, Cdm::kHdcp1_1, Cdm::kHdcp1_2, Cdm::kHdcp1_3, Cdm::kHdcp1_4,
// 2.x versions.
Cdm::kHdcp2_0, Cdm::kHdcp2_1, Cdm::kHdcp2_2, Cdm::kHdcp2_3};
for (const auto version : kSupportedHdcpVersions) {
Cdm::KeyStatus key_status;
ASSERT_EQ(Cdm::kSuccess,
cdm_->getStatusForHdcpVersion(version, &key_status))
<< "HDCP version: " << static_cast<int>(version);
EXPECT_THAT(key_status, AnyOf(Cdm::kUsable, Cdm::kOutputRestricted))
<< "HDCP version: " << static_cast<int>(version);
}
}
// Make some init data for an entitled license. The entitled key information is
// in this PSSH, and the entitlement key information is in the license data on
// UAT.
std::string MakeEntitledData(const std::string& content_key_id,
const std::string& encrypted_content_key) {
video_widevine::WidevinePsshData pssh;
pssh.set_content_id(kEntitlementContentId);
const uint32_t kFourCcCenc = 0x63656e63;
pssh.set_protection_scheme(kFourCcCenc);
pssh.set_type(video_widevine::WidevinePsshData_Type_ENTITLED_KEY);
pssh.set_crypto_period_index(20);
pssh.set_crypto_period_seconds(2);
video_widevine::WidevinePsshData_EntitledKey* key = pssh.add_entitled_keys();
// The key id for the entitlement id. This id is in the license, too.
key->set_entitlement_key_id(kKeyIdEntitlement);
key->set_key_id(content_key_id); // Entitled content key id.
key->set_key(encrypted_content_key); // encrypted entitled content key.
key->set_iv(wvutil::a2bs_hex("1234567890abcdef1234567890abcdef"));
for (int i = 0; i < 3; i++) {
// The other key ids are just to pad out the init data. Only the first one
// is used.
key = pssh.add_entitled_keys();
char x = 'G' + static_cast<char>(i);
key->set_entitlement_key_id(std::string(AES_BLOCK_SIZE, x));
key->set_key_id(std::string(AES_BLOCK_SIZE, x));
key->set_key(std::string(AES_BLOCK_SIZE, x));
key->set_iv(std::string(AES_BLOCK_SIZE, x));
}
return MakePSSH(pssh);
}
TEST_F(CdmTest, HandlesKeyRotationWithOnlyOneLicenseRequest) {
EnsureProvisioned();
std::string session_id;
// TODO(b/77152154):
// The CreateSessionAndX helpers need to be reworked so this function can use
// them.
ASSERT_EQ(Cdm::kSuccess, cdm_->createSession(Cdm::kTemporary, &session_id));
// Generate a license request for the 1st entitlement init data.
const std::string init_data_string1 =
MakeEntitledData(kKeyIdEntitlement1, kEncEntitledKey1);
std::string license_request;
{
EXPECT_CALL(*this, onMessage(session_id, Cdm::kLicenseRequest, _, _))
.WillOnce(SaveArg<2>(&license_request));
ASSERT_EQ(Cdm::kSuccess, generateRequestWithRetry(session_id, Cdm::kCenc,
init_data_string1));
Mock::VerifyAndClear(this);
}
// Send the request to the license server and receive the license response.
std::string license_response;
FetchLicense(config_.license_server(), license_request, &license_response);
// Update the session with the new keys.
{
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
ASSERT_EQ(Cdm::kSuccess, updateWithRetry(session_id, license_response));
Mock::VerifyAndClear(this);
}
// Set up input and expected output.
std::vector<uint8_t> input(12345u);
for (size_t i = 0; i < input.size(); i++) input[i] = i % 256;
const std::vector<uint8_t> test_iv1(AES_BLOCK_SIZE, 42);
const std::vector<uint8_t> test_iv2(AES_BLOCK_SIZE, 74);
const std::vector<uint8_t> expected_output1 =
Aes128CtrEncrypt(kEntitledKey1, test_iv1, input);
const std::vector<uint8_t> expected_output2 =
Aes128CtrEncrypt(kEntitledKey2, test_iv2, input);
// Set up subsample
Cdm::Subsample subsample;
subsample.protected_bytes = static_cast<uint32_t>(input.size());
// Set up sample
Cdm::Sample sample;
sample.input.data = input.data();
sample.input.data_length = subsample.protected_bytes;
sample.input.subsamples = &subsample;
sample.input.subsamples_length = 1;
std::vector<uint8_t> output_buffer(sample.input.data_length);
sample.output.data = output_buffer.data();
sample.output.data_length = static_cast<uint32_t>(output_buffer.size());
// Set up batch to decrypt
Cdm::DecryptionBatch batch;
batch.samples = &sample;
batch.samples_length = 1;
batch.encryption_scheme = Cdm::kAesCtr;
// Attempt multiple decrypts with a key from the first license.
batch.key_id = reinterpret_cast<const uint8_t*>(kKeyIdEntitlement1.c_str());
batch.key_id_length = static_cast<uint32_t>(kKeyIdEntitlement1.size());
sample.input.iv = test_iv1.data();
sample.input.iv_length = static_cast<uint32_t>(test_iv1.size());
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
EXPECT_EQ(expected_output1, output_buffer);
memset(&(output_buffer[0]), 0, output_buffer.size());
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
EXPECT_EQ(expected_output1, output_buffer);
// Load the second entitled license using the first. This should not
// require any server roundtrip.
const std::string init_data_string2 =
MakeEntitledData(kKeyIdEntitlement2, kEncEntitledKey2);
ASSERT_EQ(Cdm::kSuccess,
cdm_->loadEmbeddedKeys(session_id, Cdm::kCenc, init_data_string2));
// Attempt multiple decrypts with a key from the second license.
batch.key_id = reinterpret_cast<const uint8_t*>(kKeyIdEntitlement2.c_str());
batch.key_id_length = static_cast<uint32_t>(kKeyIdEntitlement2.size());
sample.input.iv = test_iv2.data();
sample.input.iv_length = static_cast<uint32_t>(test_iv2.size());
memset(&(output_buffer[0]), 0, output_buffer.size());
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
EXPECT_EQ(expected_output2, output_buffer);
memset(&(output_buffer[0]), 0, output_buffer.size());
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
EXPECT_EQ(expected_output2, output_buffer);
// Attempt multiple decrypts with a key from the first license again.
batch.key_id = reinterpret_cast<const uint8_t*>(kKeyIdEntitlement1.c_str());
batch.key_id_length = static_cast<uint32_t>(kKeyIdEntitlement1.size());
sample.input.iv = test_iv1.data();
sample.input.iv_length = static_cast<uint32_t>(test_iv1.size());
memset(&(output_buffer[0]), 0, output_buffer.size());
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
EXPECT_EQ(expected_output1, output_buffer);
memset(&(output_buffer[0]), 0, output_buffer.size());
ASSERT_EQ(Cdm::kSuccess, cdm_->decrypt(batch));
EXPECT_EQ(expected_output1, output_buffer);
}
TEST_F(CdmTest, ClearPlaybackWithoutAKeyIdOrIv) {
EnsureProvisioned();
// Set up subsample
Cdm::Subsample subsample;
subsample.clear_bytes = kInputSize;
// Set up sample
Cdm::Sample sample;
sample.input.data = kInput;
sample.input.data_length = subsample.clear_bytes;
sample.input.subsamples = &subsample;
sample.input.subsamples_length = 1;
std::vector<uint8_t> output_buffer(sample.input.data_length);
sample.output.data = output_buffer.data();
sample.output.data_length = static_cast<uint32_t>(output_buffer.size());
// Set up batch to decrypt
Cdm::DecryptionBatch batch;
batch.samples = &sample;
batch.samples_length = 1;
// Note that the use of kInput for the expected output is not an error. This
// is clear playback, so the data should pass through unchanged.
std::vector<uint8_t> expected_output(kInput, kInput + kInputSize);
// Create a session.
std::string session_id;
cdm_->createSession(Cdm::kTemporary, &session_id);
// Decrypt without specifying a session or key ID should fail.
Cdm::Status status = cdm_->decrypt(batch);
EXPECT_EQ(Cdm::kNoKey, status);
// Decrypt with a known session should succeed.
status = cdm_->decrypt(session_id, batch);
EXPECT_EQ(Cdm::kSuccess, status);
}
TEST_F(CdmTest, EncryptedPlaybackWithoutALicense) {
EnsureProvisioned();
// Set up subsample
Cdm::Subsample subsample;
subsample.protected_bytes = kInputSize;
// Set up sample
Cdm::Sample sample;
sample.input.iv = kIvCenc;
sample.input.iv_length = kIvCencSize;
sample.input.data = kInput;
sample.input.data_length = subsample.protected_bytes;
sample.input.subsamples = &subsample;
sample.input.subsamples_length = 1;
std::vector<uint8_t> output_buffer(sample.input.data_length);
sample.output.data = output_buffer.data();
sample.output.data_length = static_cast<uint32_t>(output_buffer.size());
// Set up batch to decrypt
Cdm::DecryptionBatch batch;
batch.samples = &sample;
batch.samples_length = 1;
batch.key_id = kKeyIdCtr.data();
batch.key_id_length = static_cast<uint32_t>(kKeyIdCtr.size());
batch.encryption_scheme = Cdm::kAesCtr;
// Create a session.
std::string session_id;
cdm_->createSession(Cdm::kTemporary, &session_id);
// Decrypt without specifying a session should fail.
Cdm::Status status = cdm_->decrypt(batch);
EXPECT_EQ(Cdm::kNoKey, status);
// Decrypt with a known session should fail.
status = cdm_->decrypt(session_id, batch);
EXPECT_EQ(Cdm::kNoKey, status);
}
TEST_F(CdmTest, CheckInitDataEmbeddedKeys) {
bool contains_embedded_keys;
// WebM (cannot contain embedded keys)
EXPECT_EQ(cdm_->initDataContainsEmbeddedKeys(Cdm::kWebM, kWebMInitData,
&contains_embedded_keys),
Cdm::kSuccess);
EXPECT_FALSE(contains_embedded_keys);
// PSSH without embedded keys
EXPECT_EQ(cdm_->initDataContainsEmbeddedKeys(Cdm::kCenc, kCencInitData,
&contains_embedded_keys),
Cdm::kSuccess);
EXPECT_FALSE(contains_embedded_keys);
// PSSH with embedded keys
EXPECT_EQ(cdm_->initDataContainsEmbeddedKeys(
Cdm::kCenc, kCencEntitlementInitData1, &contains_embedded_keys),
Cdm::kSuccess);
EXPECT_TRUE(contains_embedded_keys);
// Null out pointer
EXPECT_NE(
cdm_->initDataContainsEmbeddedKeys(Cdm::kCenc, kCencInitData, nullptr),
Cdm::kSuccess);
}
TEST_F(CdmTest, GetEmptyMetrics) {
std::string metrics;
Cdm::Status status = cdm_->getMetrics(&metrics);
EXPECT_EQ(status, Cdm::kSuccess);
EXPECT_NE(metrics.length(), 0u);
}
TEST_F(CdmTest, GetMetrics) {
EnsureProvisioned();
std::string ignored;
ASSERT_NO_FATAL_FAILURE(
CreateSessionAndUpdate(Cdm::kTemporary, Cdm::kCenc, &ignored));
std::string metrics;
Cdm::Status status = cdm_->getMetrics(&metrics);
EXPECT_EQ(status, Cdm::kSuccess);
EXPECT_NE(metrics.length(), 0u);
}
// These test reverse-compatibility of offline licenses. These tests are
// disabled by default as they require multiple runs using different builds.
// First run the *Setup* test with the old build to save the files into the test
// data path. Then copy the *.dat files into the new build directory. Finally
// run the remaining tests on the new build.
// You can set GTEST_ALSO_RUN_DISABLED_TESTS variable to run these tests too.
class OfflineReverseCompatTest : public CdmTest {
public:
void SaveToFiles(const std::string& session_id) {
auto save = [](const std::string& data, const std::string& path) {
std::ofstream os(path);
os.write(data.data(), data.size());
ASSERT_TRUE(os);
};
ASSERT_NO_FATAL_FAILURE(save(session_id, kSessionIdFile));
std::string data;
ASSERT_TRUE(g_host->global_storage().SaveToString(&data));
ASSERT_NO_FATAL_FAILURE(save(data, kGlobalStorageFile));
ASSERT_TRUE(g_host->per_origin_storage().SaveToString(&data));
ASSERT_NO_FATAL_FAILURE(save(data, kPerOriginStorageFile));
}
void LoadFromFiles(std::string* session_id) {
auto load = [](const std::string& path, std::string* data) {
std::ifstream file(path);
ASSERT_TRUE(file);
data->assign(std::istreambuf_iterator<char>(file),
std::istreambuf_iterator<char>());
};
ASSERT_NO_FATAL_FAILURE(load(kSessionIdFile, session_id));
std::string data;
ASSERT_NO_FATAL_FAILURE(load(kGlobalStorageFile, &data));
ASSERT_TRUE(g_host->global_storage().LoadFromString(data));
ASSERT_NO_FATAL_FAILURE(load(kPerOriginStorageFile, &data));
ASSERT_TRUE(g_host->per_origin_storage().LoadFromString(data));
}
private:
static constexpr const char* kSessionIdFile = "session_id.dat";
static constexpr const char* kGlobalStorageFile = "global.dat";
static constexpr const char* kPerOriginStorageFile = "per_origin.dat";
};
TEST_F(OfflineReverseCompatTest, DISABLED_Setup) {
constexpr const size_t kNumSessions = 20;
std::string first_session_id;
std::string session_id;
std::string response;
EnsureProvisioned();
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
Cdm::kPersistentLicense, Cdm::kCenc, &first_session_id, &response));
ASSERT_EQ(Cdm::kSuccess, updateWithRetry(first_session_id, response));
ASSERT_EQ(Cdm::kSuccess, cdm_->close(first_session_id));
for (size_t i = 0; i < kNumSessions; i++) {
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
Cdm::kPersistentLicense, Cdm::kCenc, &session_id, &response));
ASSERT_EQ(Cdm::kSuccess, updateWithRetry(session_id, response));
ASSERT_EQ(Cdm::kSuccess, cdm_->close(session_id));
}
ASSERT_NO_FATAL_FAILURE(SaveToFiles(first_session_id));
}
TEST_F(OfflineReverseCompatTest, DISABLED_LoadAndDecrypt) {
std::string session_id;
ASSERT_NO_FATAL_FAILURE(LoadFromFiles(&session_id));
ASSERT_EQ(cdm_->getProvisioningStatus(), Cdm::kProvisioned);
Cdm::Status status = cdm_->load(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
Cdm::Subsample subsample;
subsample.protected_bytes = kInputSize;
// Set up sample
Cdm::Sample sample;
sample.input.iv = kIvCenc;
sample.input.iv_length = kIvCencSize;
sample.input.data = kInput;
sample.input.data_length = kInputSize;
sample.input.subsamples = &subsample;
sample.input.subsamples_length = 1;
std::vector<uint8_t> output_buffer(sample.input.data_length);
sample.output.data = output_buffer.data();
sample.output.data_length = static_cast<uint32_t>(output_buffer.size());
// Set up batch to decrypt
Cdm::DecryptionBatch batch;
batch.samples = &sample;
batch.samples_length = 1;
batch.key_id = kKeyIdCtr.data();
batch.key_id_length = static_cast<uint32_t>(kKeyIdCtr.size());
batch.pattern = kPatternNone;
batch.encryption_scheme = Cdm::kAesCtr;
std::vector<uint8_t> expected_output(kOutputCenc,
kOutputCenc + kOutputCencSize);
status = cdm_->decrypt(session_id, batch);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_EQ(expected_output, output_buffer);
}
TEST_F(OfflineReverseCompatTest, DISABLED_CreateSessionThenLoad) {
std::string session_id;
ASSERT_NO_FATAL_FAILURE(LoadFromFiles(&session_id));
ASSERT_EQ(cdm_->getProvisioningStatus(), Cdm::kProvisioned);
// This will cause us to re-provision.
EnsureProvisioned();
// Create a new streaming license to use the new CDM/OEMCrypto.
std::string new_session_id;
std::string response;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
Cdm::kTemporary, Cdm::kCenc, &new_session_id, &response));
EXPECT_CALL(*this, onKeyStatusesChange(new_session_id, true));
Cdm::Status status = updateWithRetry(new_session_id, response);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// Loading the offline license should still succeed.
status = cdm_->load(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
}
TEST_F(OfflineReverseCompatTest, DISABLED_LoadThenCreateSession) {
std::string session_id;
ASSERT_NO_FATAL_FAILURE(LoadFromFiles(&session_id));
ASSERT_EQ(cdm_->getProvisioningStatus(), Cdm::kProvisioned);
// Loading the offline license should not break new sessions.
Cdm::Status status = cdm_->load(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
ASSERT_EQ(cdm_->close(session_id), Cdm::kSuccess);
// This will cause us to re-provision.
EnsureProvisioned();
std::string new_session_id;
std::string response;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
Cdm::kTemporary, Cdm::kCenc, &new_session_id, &response));
EXPECT_CALL(*this, onKeyStatusesChange(new_session_id, true));
status = updateWithRetry(new_session_id, response);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
}
TEST_F(OfflineReverseCompatTest, DISABLED_LoadThenCreateOfflineSession) {
std::string session_id;
ASSERT_NO_FATAL_FAILURE(LoadFromFiles(&session_id));
ASSERT_EQ(cdm_->getProvisioningStatus(), Cdm::kProvisioned);
// Loading the offline license should not break new sessions.
Cdm::Status status = cdm_->load(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
ASSERT_EQ(cdm_->close(session_id), Cdm::kSuccess);
// This will cause us to re-provision.
EnsureProvisioned();
std::string new_session_id;
std::string response;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
Cdm::kPersistentLicense, Cdm::kCenc, &new_session_id, &response));
EXPECT_CALL(*this, onKeyStatusesChange(new_session_id, true));
status = updateWithRetry(new_session_id, response);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
}
TEST_F(OfflineReverseCompatTest, DISABLED_NewCdmInstance) {
std::string session_id;
ASSERT_NO_FATAL_FAILURE(LoadFromFiles(&session_id));
ASSERT_EQ(cdm_->getProvisioningStatus(), Cdm::kProvisioned);
// This will cause us to re-provision.
EnsureProvisioned();
std::string new_session_id;
std::string response;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndFetchLicense(
Cdm::kTemporary, Cdm::kCenc, &new_session_id, &response));
EXPECT_CALL(*this, onKeyStatusesChange(new_session_id, true));
Cdm::Status status = updateWithRetry(new_session_id, response);
ASSERT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
// Loading should still work with an entirely new CDM instance.
ASSERT_NO_FATAL_FAILURE(RecreateCdm(true /* privacy_mode */));
EnsureProvisioned();
status = cdm_->load(session_id);
ASSERT_EQ(Cdm::kSuccess, status);
}
TEST_P(CdmTestWithDecryptParam, DecryptToClearBuffer) {
EnsureProvisioned();
DecryptParam param = GetParam();
// Set up subsample
Cdm::Subsample subsample;
if (param.scheme == Cdm::kClear) {
subsample.clear_bytes = static_cast<uint32_t>(param.input_size);
} else {
subsample.protected_bytes = static_cast<uint32_t>(param.input_size);
}
// Set up sample
Cdm::Sample sample;
sample.input.iv = param.iv;
sample.input.iv_length = static_cast<uint32_t>(param.iv_size);
sample.input.data = param.input;
sample.input.data_length = static_cast<uint32_t>(param.input_size);
sample.input.subsamples = &subsample;
sample.input.subsamples_length = 1;
std::vector<uint8_t> output_buffer(sample.input.data_length);
sample.output.data = output_buffer.data();
sample.output.data_length = static_cast<uint32_t>(output_buffer.size());
// Set up batch to decrypt
Cdm::DecryptionBatch batch;
batch.samples = &sample;
batch.samples_length = 1;
batch.key_id = param.key_id->data();
batch.key_id_length = static_cast<uint32_t>(param.key_id->size());
batch.pattern = *param.pattern;
batch.encryption_scheme = param.scheme;
std::vector<uint8_t> expected_output(param.output,
param.output + param.output_size);
// Decrypt without keys loaded should fail.
Cdm::Status status = cdm_->decrypt(batch);
ASSERT_EQ(Cdm::kNoKey, status);
// Create a session with the right keys.
std::string session_id;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndUpdate(
Cdm::kTemporary, param.init_data_type, &session_id));
// Decrypt should now succeed.
status = cdm_->decrypt(session_id, batch);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_EQ(expected_output, output_buffer);
// Decrypt should succeed even without specifying the session ID.
status = cdm_->decrypt(batch);
ASSERT_EQ(Cdm::kSuccess, status);
EXPECT_EQ(expected_output, output_buffer);
}
INSTANTIATE_TEST_SUITE_P(
CdmDecryptTest, CdmTestWithDecryptParam,
Values(DecryptParam("CENC 3.0 cenc Mode", Cdm::kCenc, kKeyIdCtr, kIvCenc,
kIvCencSize, Cdm::kAesCtr, kPatternNone, kInput,
kInputSize, kOutputCenc, kOutputCencSize),
DecryptParam("CENC 3.0 cbcs Mode", Cdm::kHls, kKeyIdCbc, kIvCbcs,
kIvCbcsSize, Cdm::kAesCbc, kPatternRecommended, kInput,
kInputSize, kOutputCbcs, kOutputCbcsSize),
DecryptParam("HLS Audio (CENC 3.0 cbcs Mode Without a Pattern)",
Cdm::kHls, kKeyIdCbc, kIvCbc1, kIvCbc1Size,
Cdm::kAesCbc, kPatternHlsAudio, kInput, kInputSize,
kOutputCbc1, kOutputCbc1Size),
DecryptParam("Clear Data w/ Known Key ID", Cdm::kCenc, kKeyIdCtr,
kIvCenc, kIvCencSize, Cdm::kClear, kPatternNone, kInput,
kInputSize, kInput, kInputSize)));
// TODO (b/119200745):
// add infrastructure to test secure buffer decrypt for some platforms
class CdmIndividualizationTest : public CdmTest {
protected:
bool CheckProvisioningSupport() {
if (wvoec::global_features.loads_certificate) return true;
LOGW(
"WARNING: Skipping device provisioning tests because the device "
"does not support provisioning. If you are using a baked-in "
"certificate, this is expected. Otherwise, something is wrong.");
return false;
}
std::string GetProvisioningResponse(const std::string& message) {
std::string reply;
std::string uri = config_.provisioning_server();
LOGV("GetProvisioningResponse: URI: %s", uri.c_str());
LOGV("GetProvisioningResponse: message:\n%s\n", message.c_str());
uri += "&signedRequest=" + message;
FetchCertificate(uri, &reply);
if (HasFatalFailure()) {
LOGE("GetProvisioningResponse: Failed.");
return "";
}
LOGV("GetProvisioningResponse: response:\n%s\n", reply.c_str());
return reply;
}
void ProvisionDevice() {
std::string message;
Cdm::Status status = cdm_->getProvisioningRequest(&message);
EXPECT_EQ(Cdm::kSuccess, status);
std::string reply = GetProvisioningResponse(message);
ASSERT_FALSE(reply.empty());
status = cdm_->handleProvisioningResponse(reply);
EXPECT_EQ(Cdm::kSuccess, status);
// Provisioning 4.0 requires a second provisioning request to get the DRM
// cert
if (wvoec::global_features.provisioning_method ==
OEMCrypto_BootCertificateChain) {
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsDrmCertProvisioning);
status = cdm_->getProvisioningRequest(&message);
EXPECT_EQ(Cdm::kSuccess, status);
reply = GetProvisioningResponse(message);
ASSERT_FALSE(reply.empty());
status = cdm_->handleProvisioningResponse(reply);
EXPECT_EQ(Cdm::kSuccess, status);
}
}
};
TEST_F(CdmIndividualizationTest, BasicFlow) {
if (!CheckProvisioningSupport()) return;
// Clear any existing certificates.
g_host->global_storage().remove(kOemCertificateFileName);
g_host->per_origin_storage().remove(kCertificateFileName);
g_host->per_origin_storage().remove(kLegacyCertificateFileName);
ProvisionDevice();
// We should now be able to create a session and generate a request.
std::string session_id;
std::string message;
ASSERT_NO_FATAL_FAILURE(CreateSessionAndGenerateRequest(
Cdm::kTemporary, Cdm::kCenc, &session_id, &message));
// Acquire a license and update the session.
std::string reply;
ASSERT_NO_FATAL_FAILURE(
FetchLicense(config_.license_server(), message, &reply));
EXPECT_CALL(*this, onKeyStatusesChange(session_id, true));
Cdm::Status status = updateWithRetry(session_id, reply);
EXPECT_EQ(Cdm::kSuccess, status);
Mock::VerifyAndClear(this);
}
TEST_F(CdmIndividualizationTest, IsProvisioned) {
if (!CheckProvisioningSupport()) return;
// Clear any existing certificates.
g_host->global_storage().remove(kOemCertificateFileName);
g_host->per_origin_storage().remove(kCertificateFileName);
g_host->per_origin_storage().remove(kLegacyCertificateFileName);
if (wvoec::global_features.provisioning_method ==
OEMCrypto_BootCertificateChain) {
// For provisioning 4.0, an OEM cert is needed by the first stage of
// provisioning
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsOemCertProvisioning);
} else {
// For provisioning 3.0, an DRM cert is needed even though there is no OEM
// cert as well.
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsDrmCertProvisioning);
}
ProvisionDevice();
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kProvisioned);
}
TEST_F(CdmIndividualizationTest, RemoveProvisioning) {
if (!CheckProvisioningSupport()) return;
// Clear any existing certificates.
g_host->global_storage().remove(kOemCertificateFileName);
g_host->per_origin_storage().remove(kCertificateFileName);
g_host->per_origin_storage().remove(kLegacyCertificateFileName);
if (wvoec::global_features.provisioning_method ==
OEMCrypto_BootCertificateChain) {
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsOemCertProvisioning);
} else {
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsDrmCertProvisioning);
}
ProvisionDevice();
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kProvisioned);
EXPECT_EQ(Cdm::kSuccess, cdm_->removeProvisioning());
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsDrmCertProvisioning);
}
TEST_F(CdmIndividualizationTest, NoCreateSessionWithoutProvisioning) {
if (!CheckProvisioningSupport()) return;
// Clear any existing certificates.
g_host->global_storage().remove(kOemCertificateFileName);
g_host->per_origin_storage().remove(kCertificateFileName);
g_host->per_origin_storage().remove(kLegacyCertificateFileName);
if (wvoec::global_features.provisioning_method ==
OEMCrypto_BootCertificateChain) {
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsOemCertProvisioning);
} else {
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsDrmCertProvisioning);
}
std::string session_id;
EXPECT_EQ(Cdm::kNeedsDeviceCertificate,
cdm_->createSession(Cdm::kTemporary, &session_id));
EXPECT_THAT(session_id, IsEmpty());
}
TEST_F(CdmIndividualizationTest, NoLoadWithoutProvisioning) {
if (!CheckProvisioningSupport()) return;
// Clear any existing certificates.
g_host->global_storage().remove(kOemCertificateFileName);
g_host->per_origin_storage().remove(kCertificateFileName);
g_host->per_origin_storage().remove(kLegacyCertificateFileName);
if (wvoec::global_features.provisioning_method ==
OEMCrypto_BootCertificateChain) {
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsOemCertProvisioning);
} else {
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsDrmCertProvisioning);
}
EXPECT_EQ(Cdm::kNeedsDeviceCertificate, cdm_->load(kBogusSessionId));
}
TEST_F(CdmIndividualizationTest, CastReceiverProvisionAndSign) {
if (!CheckProvisioningSupport()) {
GTEST_SKIP() << "OEMCrypto does not support provisioning";
}
if (!wvoec::global_features.cast_receiver) {
GTEST_SKIP() << "OEMCrypto does not support Cast Receiver functionality";
}
ASSERT_NO_FATAL_FAILURE(ProvisionDevice());
// Perform Cast provisioning and store the public cert and wrapped private key
// for use later in the test
std::string cast_prov_request;
ASSERT_EQ(cdm_->getCastProvisioningRequest(&cast_prov_request),
Cdm::kSuccess);
const std::string cast_prov_response =
GetProvisioningResponse(cast_prov_request);
ASSERT_FALSE(cast_prov_response.empty());
std::string cert;
std::string wrapped_key;
ASSERT_EQ(cdm_->handleCastProvisioningResponse(cast_prov_response, &cert,
&wrapped_key),
Cdm::kSuccess);
EXPECT_FALSE(cert.empty());
ASSERT_FALSE(wrapped_key.empty());
// Perform cast signing using the wrapped private key
std::string signature;
ASSERT_EQ(cdm_->castSign(wrapped_key, kFakeCastMessage, &signature),
Cdm::kSuccess);
// Verify the signature against the public key
//
// 1) Load the public key into an RSA struct
std::unique_ptr<BIO, void (*)(BIO*)> bio(BIO_new(BIO_s_mem()), BIO_free_all);
ASSERT_NE(bio, nullptr);
ASSERT_EQ(BIO_write(bio.get(), cert.data(), static_cast<int>(cert.size())),
static_cast<int>(cert.size()));
std::unique_ptr<X509, void (*)(X509*)> x509(
PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr), X509_free);
ASSERT_NE(x509, nullptr);
std::unique_ptr<EVP_PKEY, void (*)(EVP_PKEY*)> pubkey(
X509_get_pubkey(x509.get()), EVP_PKEY_free);
ASSERT_NE(pubkey, nullptr);
// Not a std::unique_ptr because "get0" returns a non-owning pointer.
RSA* const rsa = EVP_PKEY_get0_RSA(pubkey.get());
ASSERT_NE(rsa, nullptr);
// 2) Decrypt the signature
std::string decrypted_digest(RSA_size(rsa), 0);
const int decrypted_length = RSA_public_decrypt(
static_cast<int>(signature.size()),
reinterpret_cast<const uint8_t*>(signature.data()),
reinterpret_cast<uint8_t*>(&decrypted_digest[0]), rsa, RSA_PKCS1_PADDING);
ASSERT_GT(decrypted_length, 0);
// 3) Compare the digests
decrypted_digest.resize(decrypted_length);
EXPECT_EQ(decrypted_digest, kFakeCastMessage);
}
class CdmProv40IndividualizationTest : public CdmTest {
void SetUp() override {
CdmTest::SetUp();
if (wvoec::global_features.provisioning_method !=
OEMCrypto_BootCertificateChain) {
GTEST_SKIP() << "Test for Prov 4.0 devices only.";
}
}
};
TEST_F(CdmProv40IndividualizationTest, NeedsOemCertProvisioning) {
// No OEM cert, no DRM cert.
g_host->global_storage().remove(kOemCertificateFileName);
g_host->per_origin_storage().remove(kCertificateFileName);
g_host->per_origin_storage().remove(kLegacyCertificateFileName);
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsOemCertProvisioning);
}
TEST_F(CdmProv40IndividualizationTest, NeedsDrmCertProvisioning) {
// Has OEM cert, but no DRM cert.
g_host->global_storage().write(kOemCertificateFileName, "oem_cert");
g_host->per_origin_storage().remove(kCertificateFileName);
g_host->per_origin_storage().remove(kLegacyCertificateFileName);
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kNeedsDrmCertProvisioning);
}
TEST_F(CdmProv40IndividualizationTest, IsProvisionedAsLongAsDrmCertExists) {
// No OEM cert, has DRM cert. We treat this as provisioned since a license
// request can be made.
g_host->global_storage().remove(kOemCertificateFileName);
g_host->per_origin_storage().write(kCertificateFileName, "drm_cert");
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kProvisioned);
}
TEST_F(CdmProv40IndividualizationTest, IsProvisioned) {
// Has OEM cert, has DRM cert.
g_host->global_storage().write(kOemCertificateFileName, "oem_cert");
g_host->per_origin_storage().write(kCertificateFileName, "drm_cert");
EXPECT_EQ(cdm_->getProvisioningStatus(), Cdm::kProvisioned);
}
} // namespace
} // namespace widevine