// 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 #include #include #include #include #include #include #include #include #include #include #include #include #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(-1); const Cdm::InitDataType kBogusInitDataType = static_cast(-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 kKeyIdCtr = a2b_hex("371ea35e1a985d75d198a7f41020dc23"); // This Key ID must match the key retrieved from the license server by // kHlsInitData. const std::vector 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(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->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 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_; }; struct DecryptParam { public: DecryptParam(const std::string& short_name_param, Cdm::InitDataType init_data_type_param, const std::vector& 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* 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 {}; class CdmTestWithRemoveParam : public CdmTest, public WithParamInterface {}; 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(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(0x1); void* kCtx2 = reinterpret_cast(0x2); void* kCtx4 = reinterpret_cast(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(-1), &g_host->global_storage(), g_host, g_host, &g_stderr_logger, static_cast(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(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(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(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(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(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(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(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(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(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(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 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(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 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(version); EXPECT_THAT(key_status, AnyOf(Cdm::kUsable, Cdm::kOutputRestricted)) << "HDCP version: " << static_cast(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(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 input(12345u); for (size_t i = 0; i < input.size(); i++) input[i] = i % 256; const std::vector test_iv1(AES_BLOCK_SIZE, 42); const std::vector test_iv2(AES_BLOCK_SIZE, 74); const std::vector expected_output1 = Aes128CtrEncrypt(kEntitledKey1, test_iv1, input); const std::vector expected_output2 = Aes128CtrEncrypt(kEntitledKey2, test_iv2, input); // Set up subsample Cdm::Subsample subsample; subsample.protected_bytes = static_cast(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 output_buffer(sample.input.data_length); sample.output.data = output_buffer.data(); sample.output.data_length = static_cast(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(kKeyIdEntitlement1.c_str()); batch.key_id_length = static_cast(kKeyIdEntitlement1.size()); sample.input.iv = test_iv1.data(); sample.input.iv_length = static_cast(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(kKeyIdEntitlement2.c_str()); batch.key_id_length = static_cast(kKeyIdEntitlement2.size()); sample.input.iv = test_iv2.data(); sample.input.iv_length = static_cast(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(kKeyIdEntitlement1.c_str()); batch.key_id_length = static_cast(kKeyIdEntitlement1.size()); sample.input.iv = test_iv1.data(); sample.input.iv_length = static_cast(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 output_buffer(sample.input.data_length); sample.output.data = output_buffer.data(); sample.output.data_length = static_cast(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 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 output_buffer(sample.input.data_length); sample.output.data = output_buffer.data(); sample.output.data_length = static_cast(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(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(file), std::istreambuf_iterator()); }; 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 output_buffer(sample.input.data_length); sample.output.data = output_buffer.data(); sample.output.data_length = static_cast(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(kKeyIdCtr.size()); batch.pattern = kPatternNone; batch.encryption_scheme = Cdm::kAesCtr; std::vector 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(param.input_size); } else { subsample.protected_bytes = static_cast(param.input_size); } // Set up sample Cdm::Sample sample; sample.input.iv = param.iv; sample.input.iv_length = static_cast(param.iv_size); sample.input.data = param.input; sample.input.data_length = static_cast(param.input_size); sample.input.subsamples = &subsample; sample.input.subsamples_length = 1; std::vector output_buffer(sample.input.data_length); sample.output.data = output_buffer.data(); sample.output.data_length = static_cast(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(param.key_id->size()); batch.pattern = *param.pattern; batch.encryption_scheme = param.scheme; std::vector 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(BIO_new(BIO_s_mem()), BIO_free_all); ASSERT_NE(bio, nullptr); ASSERT_EQ(BIO_write(bio.get(), cert.data(), static_cast(cert.size())), static_cast(cert.size())); std::unique_ptr x509( PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr), X509_free); ASSERT_NE(x509, nullptr); std::unique_ptr 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(signature.size()), reinterpret_cast(signature.data()), reinterpret_cast(&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