diff --git a/libwvdrmengine/cdm/core/include/certificate_provisioning.h b/libwvdrmengine/cdm/core/include/certificate_provisioning.h index dde4d1e8..097a8006 100644 --- a/libwvdrmengine/cdm/core/include/certificate_provisioning.h +++ b/libwvdrmengine/cdm/core/include/certificate_provisioning.h @@ -82,7 +82,8 @@ class CertificateProvisioning { const std::string& origin, const std::string& spoid, CdmProvisioningRequest* request, std::string* default_url); CdmResponseType GetProvisioning40RequestInternal( - wvutil::FileSystem* file_system, CdmProvisioningRequest* request); + wvutil::FileSystem* file_system, CdmProvisioningRequest* request, + std::string* default_url); CdmResponseType FillEncryptedClientId( const std::string& client_token, video_widevine::ProvisioningRequest& provisioning_request); diff --git a/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp b/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp index f7ce8efe..77e20747 100644 --- a/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp +++ b/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp @@ -26,6 +26,11 @@ const std::string kProvisioningServerUrl = "https://www.googleapis.com/" "certificateprovisioning/v1/devicecertificates/create" "?key=AIzaSyB-5OLKTx2iU5mko18DfdwK5611JIjbUhE"; +// In case of provisioning 4, the default url is used as a way to inform app of +// the current provisioning stage. In the first stage, this suffix is appended +// to kProvisioningServerUrl; in the second stage, there is no change to +// kProvisioningServerUrl. +const std::string kProv40FirstStageServerUrlSuffix = "&preProvisioning=true"; // NOTE: Provider ID = widevine.com const std::string kCpProductionServiceCertificate = wvutil::a2bs_hex( @@ -207,7 +212,7 @@ CdmResponseType CertificateProvisioning::GetProvisioningRequestInternal( if (crypto_session_->GetPreProvisionTokenType() == kClientTokenBootCertChain) { - return GetProvisioning40RequestInternal(file_system, request); + return GetProvisioning40RequestInternal(file_system, request, default_url); } // Prepare device provisioning request. @@ -298,7 +303,8 @@ CdmResponseType CertificateProvisioning::GetProvisioningRequestInternal( } CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal( - wvutil::FileSystem* file_system, CdmProvisioningRequest* request) { + wvutil::FileSystem* file_system, CdmProvisioningRequest* request, + std::string* default_url) { if (!crypto_session_->IsOpen()) { LOGE("Crypto session is not open"); return PROVISIONING_4_CRYPTO_SESSION_NOT_OPEN; @@ -333,6 +339,15 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal( } } + if (stored_oem_cert.empty()) { + // This is the first stage provisioning. + default_url->assign(kProvisioningServerUrl + + kProv40FirstStageServerUrlSuffix); + } else { + // This is the second stage provisioning. + default_url->assign(kProvisioningServerUrl); + } + // If this is the first stage, |stored_oem_cert| remains empty. In this case, // the client identification token will be retrieved from OEMCrypto, which is // the BCC in this case. diff --git a/libwvdrmengine/cdm/core/test/cdm_engine_test.cpp b/libwvdrmengine/cdm/core/test/cdm_engine_test.cpp index 09f99a9e..0ed472e7 100644 --- a/libwvdrmengine/cdm/core/test/cdm_engine_test.cpp +++ b/libwvdrmengine/cdm/core/test/cdm_engine_test.cpp @@ -18,6 +18,7 @@ #include "device_files.h" #include "file_store.h" #include "initialization_data.h" +#include "license_holder.h" #include "license_request.h" #include "log.h" #include "properties.h" @@ -387,14 +388,11 @@ TEST_F(WvCdmEngineTest, DISABLED_NormalDecryptionWebm) { TEST_F(WvCdmEngineTest, LoadKey) { EnsureProvisioned(); - TestLicenseHolder holder(&cdm_engine_); - holder.OpenSession(config_.key_system()); - holder.GenerateKeyRequest(binary_key_id(), ISO_BMFF_VIDEO_MIME_TYPE); - holder.CreateDefaultLicense(); - std::vector key_data(CONTENT_KEY_SIZE, '1'); - wvoec::KeyControlBlock block = {}; - holder.AddKey("key_one", key_data, block); - holder.SignAndLoadLicense(); + LicenseHolder holder("CDM_Streaming", &cdm_engine_, config_); + ASSERT_NO_FATAL_FAILURE(holder.OpenSession()); + ASSERT_NO_FATAL_FAILURE(holder.FetchLicense()); + ASSERT_NO_FATAL_FAILURE(holder.LoadLicense()); + ASSERT_NO_FATAL_FAILURE(holder.CloseSession()); } // This test generates a renewal and then requests the renewal using the server @@ -402,6 +400,7 @@ TEST_F(WvCdmEngineTest, LoadKey) { // skip this test when you want to set the license and renewal server on the // command line. TEST_F(WvCdmEngineTest, LicenseRenewalSpecifiedServer) { + EnsureProvisioned(); GenerateKeyRequest(binary_key_id(), kCencMimeType); VerifyNewKeyResponse(config_.license_server(), config_.client_auth()); @@ -423,6 +422,7 @@ TEST_F(WvCdmEngineTest, LicenseRenewalSpecifiedServer) { // This test generates a renewal and then requests it from the server specified // by the current test configuration. TEST_F(WvCdmEngineTest, LicenseRenewal) { + EnsureProvisioned(); GenerateKeyRequest(binary_key_id(), kCencMimeType); VerifyNewKeyResponse(config_.license_server(), config_.client_auth()); diff --git a/libwvdrmengine/cdm/core/test/duration_use_case_test.cpp b/libwvdrmengine/cdm/core/test/duration_use_case_test.cpp index 25fa6925..31b8fd7b 100644 --- a/libwvdrmengine/cdm/core/test/duration_use_case_test.cpp +++ b/libwvdrmengine/cdm/core/test/duration_use_case_test.cpp @@ -18,7 +18,7 @@ #include "clock.h" #include "config_test_env.h" #include "initialization_data.h" -#include "license_request.h" +#include "license_holder.h" #include "log.h" #include "metrics_collections.h" #include "odk_structs.h" @@ -36,8 +36,6 @@ namespace wvcdm { namespace { -constexpr int kHttpOk = 200; -const std::string kCencMimeType = "cenc"; // How many seconds to fudge the start or stop playback times. We fudge these // times because there might be a little round off when sleeping, and because // the time that the license was signed or loaded might be off a little bit due @@ -86,27 +84,6 @@ const RenewalPolicy kHeartbeatRenewal = {"CDM_Heartbeat_renewal", 10, 30}; // Key ID in all duration tests. const KeyId kKeyId = "Duration_Key===="; - -class SimpleEventListener : public wvcdm::WvCdmEventListener { - public: - SimpleEventListener() { renewal_needed_ = false; } - // We will want to know when a renewal is needed. - void OnSessionRenewalNeeded(const CdmSessionId& /*session*/) override { - renewal_needed_ = true; - } - void OnSessionKeysChange(const CdmSessionId&, const CdmKeyStatusMap&, - bool) override {} - void OnExpirationUpdate(const CdmSessionId&, - int64_t expiry_time UNUSED) override {} - bool renewal_needed() { return renewal_needed_; } - void set_renewal_needed(bool renewal_needed) { - renewal_needed_ = renewal_needed; - } - - private: - bool renewal_needed_; -}; - } // namespace // All duration tests are parameterized by can_persist = true or false. @@ -114,7 +91,7 @@ class CdmDurationTest : public WvCdmTestBaseWithEngine, public ::testing::WithParamInterface { public: CdmDurationTest(const std::string& content_id) - : content_id_(content_id), + : license_holder_(content_id, &cdm_engine_, config_), first_load_occurred_(false), allow_lenience_(false) { // These are reasonable initial values for most tests. This is an unlimited @@ -150,24 +127,22 @@ class CdmDurationTest : public WvCdmTestBaseWithEngine, void SetUp() override { WvCdmTestBase::SetUp(); EnsureProvisioned(); - ASSERT_NO_FATAL_FAILURE(OpenSession(&session_id_)); - can_persist_ = GetParam(); - if (can_persist_) { + license_holder_.set_can_persist(GetParam()); + ASSERT_NO_FATAL_FAILURE(license_holder_.OpenSession()); + if (license_holder_.can_persist()) { // Each content/policy in a test class below should match two policies in // UAT. One policy id matches the string exactly, and one has // _can_persist appended. - content_id_ = content_id_ + "_can_persist"; - license_type_ = kLicenseTypeOffline; - } else { - license_type_ = kLicenseTypeStreaming; + license_holder_.set_content_id(license_holder_.content_id() + + "_can_persist"); } // All times in the license are relative to the rental clock. start_of_rental_clock_ = wvutil::Clock().GetCurrentTime(); - FetchLicense(); + license_holder_.FetchLicense(); } void TearDown() override { - cdm_engine_.CloseSession(session_id_); + license_holder_.CloseSession(); // Log the time used in this test suite. When this comment was written, // these tests took over three hours. If we want to improve that, we need to // track these times. @@ -223,99 +198,20 @@ class CdmDurationTest : public WvCdmTestBaseWithEngine, return now - start_of_rental_clock_; } - void OpenSession(CdmSessionId* session_id) { - CdmResponseType status = cdm_engine_.OpenSession( - config_.key_system(), nullptr, &event_listener_, session_id); - ASSERT_EQ(NO_ERROR, status); - ASSERT_TRUE(cdm_engine_.IsOpenSession(*session_id)); - } - - void CloseSession(const CdmSessionId& session_id) { - CdmResponseType status = cdm_engine_.CloseSession(session_id); - ASSERT_EQ(NO_ERROR, status); - ASSERT_FALSE(cdm_engine_.IsOpenSession(session_id)); - } - - void FetchLicense() { - video_widevine::WidevinePsshData pssh; - pssh.set_content_id(content_id_); - const std::string init_data_string = MakePSSH(pssh); - const InitializationData init_data(kCencMimeType, init_data_string); - init_data.DumpToLogs(); - CdmKeyRequest key_request; - ASSERT_NO_FATAL_FAILURE(GenerateKeyRequest(init_data, &key_request)); - ASSERT_NO_FATAL_FAILURE(GetKeyResponse(key_request)); - } - - void GenerateKeyRequest(const InitializationData& init_data, - CdmKeyRequest* key_request) { - CdmAppParameterMap empty_app_parameters; - CdmKeySetId empty_key_set_id; - CdmResponseType result = cdm_engine_.GenerateKeyRequest( - session_id_, empty_key_set_id, init_data, license_type_, - empty_app_parameters, key_request); - ASSERT_EQ(KEY_MESSAGE, result); - ASSERT_EQ(kKeyRequestTypeInitial, key_request->type); - } - - // Append the content/policy id for the test to the URL. - std::string MakeUrl(const std::string& server_url, - const std::string& policy_id) { - // For these tests, we want to specify the policy, but the UAT server only - // allows us to set the content id as the video_id. So each policy is - // matched to a single license with the same name. The local license - // server, on the other hand, wants to see the policy id in the url. So we - // have to guess which format to use based on the name of the server. - const std::string path = server_url + config_.client_auth(); - std::string video_query; - if (path.find("proxy.uat") != std::string::npos) { - // This is uat or uat-nightly. Set the video_id. - video_query = "video_id=" + policy_id; - } else { - // This is probably a local license server. Set the policy. - video_query = "policy=" + policy_id; - } - // If there is already a parameter, then we don't need to add another - // question mark. - return path + ((path.find("?") == std::string::npos) ? "?" : "&") + - video_query; - } - - void GetKeyResponse(const CdmKeyRequest& key_request) { - // The content id matches the policy id used on UAT. - const std::string url = MakeUrl(config_.license_server(), content_id_); - UrlRequest url_request(url); - ASSERT_TRUE(url_request.is_connected()); - - std::string http_response; - url_request.PostRequest(key_request.message); - ASSERT_TRUE(url_request.GetResponse(&http_response)); - int status_code = url_request.GetStatusCode(http_response); - ASSERT_EQ(kHttpOk, status_code) << "Error with url = " << url; - - LicenseRequest license_request; - license_request.GetDrmMessage(http_response, key_response_); - } - // This ensures that the licenses are loaded into the sessions. void LoadLicense() { if (!first_load_occurred_) { - CdmLicenseType license_type; - ASSERT_EQ(KEY_ADDED, cdm_engine_.AddKey(session_id_, key_response_, - &license_type, &key_set_id_)); - ASSERT_EQ(license_type_, license_type); + license_holder_.LoadLicense(); first_load_occurred_ = true; - } else if (can_persist_) { - // For the persistent license, we use restore key. - ASSERT_NO_FATAL_FAILURE(OpenSession(&session_id_)); - CdmResponseType status = cdm_engine_.RestoreKey(session_id_, key_set_id_); - ASSERT_EQ(KEY_ADDED, status); + } else if (license_holder_.can_persist()) { + ASSERT_NO_FATAL_FAILURE(license_holder_.OpenSession()); + license_holder_.ReloadLicense(); } } void UnloadLicense() { - if (can_persist_) { - CloseSession(session_id_); + if (license_holder_.can_persist()) { + license_holder_.CloseSession(); } } @@ -380,12 +276,8 @@ class CdmDurationTest : public WvCdmTestBaseWithEngine, void AllowLenience() { allow_lenience_ = true; } void Decrypt() { - constexpr size_t buffer_size = 500; - const std::vector input(buffer_size, 0); - std::vector output(buffer_size, 0); - const std::vector iv(KEY_IV_SIZE, 0); const uint64_t now = CurrentRentalTime(); - EXPECT_EQ(NO_ERROR, Decrypt(session_id_, kKeyId, input, iv, &output)) + EXPECT_EQ(NO_ERROR, license_holder_.Decrypt(kKeyId)) << "Failed to decrypt when rental clock = " << now << ", and playback clock = " << ((now < start_of_playback_) ? 0 : (now - start_of_playback_)); @@ -403,17 +295,12 @@ class CdmDurationTest : public WvCdmTestBaseWithEngine, const bool low_end_device = (wvoec::global_features.api_version < wvoec::kCoreMessagesAPI || !wvoec::global_features.usage_table); - if (allow_lenience_ && low_end_device && can_persist_) { + if (allow_lenience_ && low_end_device && license_holder_.can_persist()) { allow_success = true; } allow_lenience_ = false; // Only allow lenience once per test. - - constexpr size_t buffer_size = 500; - const std::vector input(buffer_size, 0); - std::vector output(buffer_size, 0); - const std::vector iv(KEY_IV_SIZE, 0); const uint64_t now = CurrentRentalTime(); - CdmResponseType status = Decrypt(session_id_, kKeyId, input, iv, &output); + CdmResponseType status = license_holder_.Decrypt(kKeyId); // We always allow failure. that's what we usually expect. if (status == NEED_KEY) return; // No other error code is allowed: either NO_ERROR or NEED_KEY. @@ -428,26 +315,7 @@ class CdmDurationTest : public WvCdmTestBaseWithEngine, << (now < start_of_playback_ ? 0 : now - start_of_playback_); } - CdmResponseType Decrypt(const CdmSessionId& session_id, const KeyId& key_id, - const std::vector& input, - const std::vector& iv, - std::vector* output) { - CdmDecryptionParametersV16 params(key_id); - params.is_secure = false; - CdmDecryptionSample sample(input.data(), output->data(), 0, input.size(), - iv); - CdmDecryptionSubsample subsample(0, input.size()); - sample.subsamples.push_back(subsample); - params.samples.push_back(sample); - - return cdm_engine_.DecryptV16(session_id, params); - } - - CdmSessionId session_id_; - std::string content_id_; - CdmLicenseType license_type_; - CdmKeySetId key_set_id_; - std::string key_response_; + LicenseHolder license_holder_; // Time license requests generated. All test times are relative to this value. uint64_t start_of_rental_clock_; // The start of playback. This is set to the planned start at the beginning of @@ -457,9 +325,7 @@ class CdmDurationTest : public WvCdmTestBaseWithEngine, // not have to reload the streaming license, and we should use RestoreKey // instead of AddKey for the offline license. bool first_load_occurred_; - bool can_persist_; ODK_TimerLimits timer_limits_; - SimpleEventListener event_listener_; // If this is set, then the next time we expect a playback to be terminated, // we will allow lenient failure. bool allow_lenience_; @@ -751,7 +617,8 @@ TEST_P(CdmUseCase_SevenHardTwoSoft, Case4) { LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 4 * kPlayDuration); UnloadLicense(); - if (!can_persist_) return; // streaming license cannot restart. + // streaming license cannot restart. + if (!license_holder_.can_persist()) return; AllowLenience(); ForbidPlayback(EndOfPlaybackWindow() + kFudge); } @@ -947,7 +814,8 @@ TEST_P(CdmUseCase_SevenSoftTwoSoft, Case4) { LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 4 * kPlayDuration); UnloadLicense(); - if (!can_persist_) return; // streaming license cannot restart. + // streaming license cannot restart. + if (!license_holder_.can_persist()) return; AllowLenience(); ForbidPlayback(EndOfPlaybackWindow() + kFudge); } @@ -989,7 +857,7 @@ TEST_P(CdmUseCase_SevenSoftTwoSoft, Case7) { EndOfPlaybackWindow() + kPlayDuration); UnloadLicense(); // But forbid restart after playback window. - if (!can_persist_) return; // streaming license cannot restart. + if (!license_holder_.can_persist()) return; ForbidPlayback(EndOfPlaybackWindow() + 2 * kPlayDuration); } @@ -1028,7 +896,7 @@ class RenewalTest : public CdmDurationTest { // Sleep until a renewal request event is generated. If one does not occur // before cutoff, an error happens. void SleepUntilRenewalNeeded(uint64_t cutoff) { - while (!event_listener_.renewal_needed()) { + while (!license_holder_.event_listener().renewal_needed()) { uint64_t now = CurrentRentalTime(); ASSERT_LT(now, cutoff); SleepUntil(now + 1); @@ -1042,34 +910,18 @@ class RenewalTest : public CdmDurationTest { } void RequestRenewal(const RenewalPolicy& renewal_policy) { - event_listener_.set_renewal_needed(false); - CdmKeyRequest request; - const CdmResponseType result = - cdm_engine_.GenerateRenewalRequest(session_id_, &request); - ASSERT_EQ(KEY_MESSAGE, result); - const std::string url = - MakeUrl(config_.renewal_server(), renewal_policy.policy_id); - renewal_in_flight_.reset(new UrlRequest(url)); - ASSERT_TRUE(renewal_in_flight_->is_connected()); - renewal_in_flight_->PostRequest(request.message); + license_holder_.GenerateAndPostRenewalRequest(renewal_policy.policy_id); } void LoadRenewal(uint64_t time_of_load, const RenewalPolicy& renewal_policy) { - ASSERT_NE(renewal_in_flight_, nullptr); - std::string http_response; // Most of the network latency will probably show up in the next few // commands. I think the tests have enough slop to account for reasonable // latency with the current value of kRoundTripTime. But we'll know I made a // mistake if we see errors about "Test Clock skew..." in the SleepUntil // call below. - ASSERT_TRUE(renewal_in_flight_->GetResponse(&http_response)); - int status_code = renewal_in_flight_->GetStatusCode(http_response); - ASSERT_EQ(kHttpOk, status_code); - + license_holder_.FetchRenewal(); SleepUntil(time_of_load); - LicenseRequest license_request; - license_request.GetDrmMessage(http_response, renewal_message_); - EXPECT_EQ(KEY_ADDED, cdm_engine_.RenewKey(session_id_, renewal_message_)); + license_holder_.LoadRenewal(); ComputeCutoff(time_of_load, renewal_policy); } @@ -1260,7 +1112,7 @@ TEST_P(CdmUseCase_LicenseWithRenewal, Case4) { RenewAndContinue(start, load_time, stop, kShortRenewal); } UnloadLicense(); - if (can_persist_) { + if (license_holder_.can_persist()) { // Reload license after timer expired. const uint64_t reload_time = current_cutoff_ + 20; ReloadAndAllowPlayback( @@ -1464,7 +1316,7 @@ TEST_P(CdmUseCase_LicenseWithRenewalPlayback, Case4) { RenewAndContinue(start, load_time, stop, kShortRenewal); } UnloadLicense(); - if (can_persist_) { + if (license_holder_.can_persist()) { // Reload license after timer expired. const uint64_t reload_time = current_cutoff_ + 20; ReloadAndAllowPlayback( diff --git a/libwvdrmengine/cdm/core/test/generic_crypto_unittest.cpp b/libwvdrmengine/cdm/core/test/generic_crypto_unittest.cpp index 46882800..1b8422bc 100644 --- a/libwvdrmengine/cdm/core/test/generic_crypto_unittest.cpp +++ b/libwvdrmengine/cdm/core/test/generic_crypto_unittest.cpp @@ -13,7 +13,7 @@ #include "cdm_engine.h" #include "config_test_env.h" -#include "license_request.h" +#include "license_holder.h" #include "log.h" #include "oec_session_util.h" #include "oemcrypto_session_tests_helper.h" @@ -27,31 +27,35 @@ #include "wv_cdm_constants.h" #include "wv_cdm_types.h" +using wvutil::a2b_hex; + namespace wvcdm { class WvGenericCryptoTest : public WvCdmTestBaseWithEngine { public: - WvGenericCryptoTest() : holder_(&cdm_engine_) {} + WvGenericCryptoTest() : holder_("CDM_GenericCrypto", &cdm_engine_, config_) {} void SetUp() override { WvCdmTestBase::SetUp(); EnsureProvisioned(); - holder_.OpenSession(config_.key_system()); - holder_.GenerateKeyRequest(binary_key_id(), ISO_BMFF_VIDEO_MIME_TYPE); - holder_.CreateDefaultLicense(); + ASSERT_NO_FATAL_FAILURE(holder_.OpenSession()); + ASSERT_NO_FATAL_FAILURE(holder_.FetchLicense()); - ency_id_ = "ency"; - StripeBuffer(&ency_key_, CONTENT_KEY_SIZE, 'e'); - AddOneKey(ency_id_, ency_key_, wvoec::kControlAllowEncrypt); - dency_id_ = "dency"; - StripeBuffer(&dency_key_, CONTENT_KEY_SIZE, 'd'); - AddOneKey(dency_id_, dency_key_, wvoec::kControlAllowDecrypt); - siggy_id_ = "siggy"; - StripeBuffer(&siggy_key_, MAC_KEY_SIZE, 's'); - AddOneKey(siggy_id_, siggy_key_, wvoec::kControlAllowSign); - vou_id_ = "vou"; - StripeBuffer(&vou_key_, MAC_KEY_SIZE, 'v'); - AddOneKey(vou_id_, vou_key_, wvoec::kControlAllowVerify); + ency_id_ = "encrypt-key-----"; + ency_key_ = a2b_hex("0102030405060708090a0b0c0d0e0f10"); + dency_id_ = "decrypt-key-----"; + dency_key_ = a2b_hex("AA02030405060708090a0b0c0d0e0f10"); + siggy_id_ = "sign-key--------"; + siggy_key_ = a2b_hex( + "BB02030405060708090a0b0c0d0e0f100102030405060708090a0b0c0d0e0f10"); + vou_id_ = "verify-key------"; + vou_key_ = a2b_hex( + "CC02030405060708090a0b0c0d0e0f100102030405060708090a0b0c0d0e0f10"); + both_id_ = "enc-and-dec-key-"; + both_key_ = a2b_hex("DD02030405060708090a0b0c0d0e0f10"); + sign_and_verify_id_ = "sign-and-verify-"; + sign_and_verify_key_ = a2b_hex( + "EE02030405060708090a0b0c0d0e0f100102030405060708090a0b0c0d0e0f10"); StripeBuffer(&in_vector_, CONTENT_KEY_SIZE * 15, '1'); in_buffer_ = std::string(in_vector_.begin(), in_vector_.end()); @@ -62,26 +66,22 @@ class WvGenericCryptoTest : public WvCdmTestBaseWithEngine { void TearDown() override { holder_.CloseSession(); } - // Create a single key, and add it to the license. - void AddOneKey(const KeyId& key_id, const std::vector& key_data, - uint32_t key_control_block) { - wvoec::KeyControlBlock block = {}; - block.control_bits = htonl(key_control_block); - holder_.AddKey(key_id, key_data, block); - } - protected: - TestLicenseHolder holder_; + LicenseHolder holder_; KeyId ency_id_; KeyId dency_id_; KeyId siggy_id_; KeyId vou_id_; + KeyId both_id_; + KeyId sign_and_verify_id_; std::vector ency_key_; std::vector dency_key_; std::vector siggy_key_; std::vector vou_key_; + std::vector both_key_; + std::vector sign_and_verify_key_; std::vector in_vector_; std::vector iv_vector_; @@ -89,210 +89,155 @@ class WvGenericCryptoTest : public WvCdmTestBaseWithEngine { std::string iv_; }; -TEST_F(WvGenericCryptoTest, LoadSpecialKeys) { holder_.SignAndLoadLicense(); } +TEST_F(WvGenericCryptoTest, LoadSpecialKeys) { + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); +} TEST_F(WvGenericCryptoTest, GenericEncryptGood) { - CdmResponseType cdm_sts; + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); std::string encrypted = Aes128CbcEncrypt(ency_key_, in_vector_, iv_vector_); std::string out_buffer; - - holder_.SignAndLoadLicense(); - cdm_sts = cdm_engine_.GenericEncrypt( - holder_.session_id(), in_buffer_, ency_id_, iv_, - wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer); - - EXPECT_EQ(NO_ERROR, cdm_sts); + EXPECT_EQ(NO_ERROR, cdm_engine_.GenericEncrypt( + holder_.session_id(), in_buffer_, ency_id_, iv_, + wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer)); EXPECT_EQ(encrypted, out_buffer); } TEST_F(WvGenericCryptoTest, GenericEncryptNoKey) { - CdmResponseType cdm_sts; + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); std::string encrypted = Aes128CbcEncrypt(ency_key_, in_vector_, iv_vector_); std::string out_buffer; KeyId key_id("no_key"); - - holder_.SignAndLoadLicense(); - cdm_sts = cdm_engine_.GenericEncrypt( - holder_.session_id(), in_buffer_, key_id, iv_, - wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer); - EXPECT_NE(NO_ERROR, cdm_sts); + EXPECT_NE(NO_ERROR, cdm_engine_.GenericEncrypt( + holder_.session_id(), in_buffer_, key_id, iv_, + wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer)); EXPECT_NE(encrypted, out_buffer); } TEST_F(WvGenericCryptoTest, GenericEncryptKeyNotAllowed) { - CdmResponseType cdm_sts; + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); // Trying to use Decrypt key to encrypt, which is not allowed. KeyId key_id = dency_id_; std::string encrypted = Aes128CbcEncrypt(dency_key_, in_vector_, iv_vector_); std::string out_buffer; - - holder_.SignAndLoadLicense(); - cdm_sts = cdm_engine_.GenericEncrypt( - holder_.session_id(), in_buffer_, key_id, iv_, - wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer); - EXPECT_EQ(UNKNOWN_ERROR, cdm_sts); + EXPECT_EQ(UNKNOWN_ERROR, + cdm_engine_.GenericEncrypt( + holder_.session_id(), in_buffer_, key_id, iv_, + wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer)); EXPECT_NE(encrypted, out_buffer); } TEST_F(WvGenericCryptoTest, GenericDecryptGood) { - CdmResponseType cdm_sts; + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); std::string decrypted = Aes128CbcDecrypt(dency_key_, in_vector_, iv_vector_); std::string out_buffer; - - holder_.SignAndLoadLicense(); - cdm_sts = cdm_engine_.GenericDecrypt( - holder_.session_id(), in_buffer_, dency_id_, iv_, - wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer); - - EXPECT_EQ(NO_ERROR, cdm_sts); + EXPECT_EQ(NO_ERROR, cdm_engine_.GenericDecrypt( + holder_.session_id(), in_buffer_, dency_id_, iv_, + wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer)); EXPECT_EQ(decrypted, out_buffer); } TEST_F(WvGenericCryptoTest, GenericDecryptNoKey) { - CdmResponseType cdm_sts; + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); std::string decrypted = Aes128CbcDecrypt(dency_key_, in_vector_, iv_vector_); std::string out_buffer; KeyId key_id = "no_key"; - - holder_.SignAndLoadLicense(); - cdm_sts = cdm_engine_.GenericDecrypt( - holder_.session_id(), in_buffer_, key_id, iv_, - wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer); - EXPECT_NE(NO_ERROR, cdm_sts); + EXPECT_NE(NO_ERROR, cdm_engine_.GenericDecrypt( + holder_.session_id(), in_buffer_, key_id, iv_, + wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer)); EXPECT_NE(decrypted, out_buffer); } TEST_F(WvGenericCryptoTest, GenericDecryptKeyNotAllowed) { - CdmResponseType cdm_sts; + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); // Trying to use Encrypt key to decrypt, which is not allowed. KeyId key_id = ency_id_; std::string decrypted = Aes128CbcDecrypt(ency_key_, in_vector_, iv_vector_); std::string out_buffer; - - holder_.SignAndLoadLicense(); - cdm_sts = cdm_engine_.GenericDecrypt( - holder_.session_id(), in_buffer_, key_id, iv_, - wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer); - EXPECT_EQ(UNKNOWN_ERROR, cdm_sts); + EXPECT_EQ(UNKNOWN_ERROR, + cdm_engine_.GenericDecrypt( + holder_.session_id(), in_buffer_, key_id, iv_, + wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer)); EXPECT_NE(decrypted, out_buffer); } +TEST_F(WvGenericCryptoTest, GenericSignGood) { + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); + std::string out_buffer; + std::string signature = SignHMAC(in_buffer_, siggy_key_); + EXPECT_EQ(NO_ERROR, cdm_engine_.GenericSign( + holder_.session_id(), in_buffer_, siggy_id_, + wvcdm::kSigningAlgorithmHmacSha256, &out_buffer)); + EXPECT_EQ(signature, out_buffer); +} + TEST_F(WvGenericCryptoTest, GenericSignKeyNotAllowed) { - CdmResponseType cdm_sts; + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); // Wrong key std::string key_id = vou_id_; std::string out_buffer; std::string signature = SignHMAC(in_buffer_, siggy_key_); - - holder_.SignAndLoadLicense(); - cdm_sts = + EXPECT_EQ( + UNKNOWN_ERROR, cdm_engine_.GenericSign(holder_.session_id(), in_buffer_, key_id, - wvcdm::kSigningAlgorithmHmacSha256, &out_buffer); - EXPECT_EQ(UNKNOWN_ERROR, cdm_sts); + wvcdm::kSigningAlgorithmHmacSha256, &out_buffer)); EXPECT_NE(signature, out_buffer); } -TEST_F(WvGenericCryptoTest, GenericSignGood) { - CdmResponseType cdm_sts; - std::string out_buffer; - std::string signature = SignHMAC(in_buffer_, siggy_key_); - - holder_.SignAndLoadLicense(); - cdm_sts = - cdm_engine_.GenericSign(holder_.session_id(), in_buffer_, siggy_id_, - wvcdm::kSigningAlgorithmHmacSha256, &out_buffer); - EXPECT_EQ(NO_ERROR, cdm_sts); - EXPECT_EQ(signature, out_buffer); +TEST_F(WvGenericCryptoTest, GenericVerifyGood) { + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); + std::string signature = SignHMAC(in_buffer_, vou_key_); + EXPECT_EQ(NO_ERROR, cdm_engine_.GenericVerify( + holder_.session_id(), in_buffer_, vou_id_, + wvcdm::kSigningAlgorithmHmacSha256, signature)); } TEST_F(WvGenericCryptoTest, GenericVerifyKeyNotAllowed) { - CdmResponseType cdm_sts; + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); // Wrong key std::string key_id = siggy_id_; std::string signature = SignHMAC(in_buffer_, siggy_key_); - - holder_.SignAndLoadLicense(); - cdm_sts = - cdm_engine_.GenericVerify(holder_.session_id(), in_buffer_, key_id, - wvcdm::kSigningAlgorithmHmacSha256, signature); - EXPECT_EQ(UNKNOWN_ERROR, cdm_sts); + EXPECT_EQ(UNKNOWN_ERROR, cdm_engine_.GenericVerify( + holder_.session_id(), in_buffer_, key_id, + wvcdm::kSigningAlgorithmHmacSha256, signature)); } -TEST_F(WvGenericCryptoTest, GenericVerifyBadSignautre) { - CdmResponseType cdm_sts; +TEST_F(WvGenericCryptoTest, GenericVerifyBadSignature) { + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); std::string signature(MAC_KEY_SIZE, 's'); - - holder_.SignAndLoadLicense(); - cdm_sts = - cdm_engine_.GenericVerify(holder_.session_id(), in_buffer_, vou_id_, - wvcdm::kSigningAlgorithmHmacSha256, signature); // OEMCrypto error is OEMCrypto_ERROR_SIGNATURE_FAILURE - EXPECT_EQ(UNKNOWN_ERROR, cdm_sts); -} - -TEST_F(WvGenericCryptoTest, GenericVerifyGood) { - CdmResponseType cdm_sts; - std::string signature = SignHMAC(in_buffer_, vou_key_); - - holder_.SignAndLoadLicense(); - cdm_sts = - cdm_engine_.GenericVerify(holder_.session_id(), in_buffer_, vou_id_, - wvcdm::kSigningAlgorithmHmacSha256, signature); - EXPECT_EQ(NO_ERROR, cdm_sts); + EXPECT_EQ(UNKNOWN_ERROR, cdm_engine_.GenericVerify( + holder_.session_id(), in_buffer_, vou_id_, + wvcdm::kSigningAlgorithmHmacSha256, signature)); } TEST_F(WvGenericCryptoTest, GenericEncryptDecrypt) { - CdmResponseType cdm_sts; + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); + std::string encrypted = Aes128CbcEncrypt(both_key_, in_vector_, iv_vector_); std::string out_buffer; - std::string clear_buffer; + EXPECT_EQ(NO_ERROR, cdm_engine_.GenericEncrypt( + holder_.session_id(), in_buffer_, both_id_, iv_, + wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer)); + EXPECT_EQ(encrypted, out_buffer); - KeyId key_id = "enc and dec"; - - std::vector key_data; - StripeBuffer(&key_data, CONTENT_KEY_SIZE, '3'); - AddOneKey(key_id, key_data, - wvoec::kControlAllowEncrypt | wvoec::kControlAllowDecrypt); - - holder_.SignAndLoadLicense(); - - cdm_sts = cdm_engine_.GenericEncrypt( - holder_.session_id(), in_buffer_, key_id, iv_, - wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer); - EXPECT_EQ(NO_ERROR, cdm_sts); - - cdm_sts = cdm_engine_.GenericDecrypt( - holder_.session_id(), out_buffer, key_id, iv_, - wvcdm::kEncryptionAlgorithmAesCbc128, &clear_buffer); - EXPECT_EQ(NO_ERROR, cdm_sts); - - EXPECT_EQ(in_buffer_, clear_buffer); + std::string decrypted = Aes128CbcDecrypt(dency_key_, in_vector_, iv_vector_); + EXPECT_EQ(NO_ERROR, cdm_engine_.GenericDecrypt( + holder_.session_id(), encrypted, both_id_, iv_, + wvcdm::kEncryptionAlgorithmAesCbc128, &out_buffer)); + EXPECT_EQ(in_buffer_, out_buffer); } TEST_F(WvGenericCryptoTest, GenericSignVerify) { - CdmResponseType cdm_sts; - std::string signature_buffer; + ASSERT_NO_FATAL_FAILURE(holder_.LoadLicense()); - KeyId key_id = "sign and ver"; - - std::vector key_data; - StripeBuffer(&key_data, MAC_KEY_SIZE, '4'); - AddOneKey(key_id, key_data, - wvoec::kControlAllowSign | wvoec::kControlAllowVerify); - std::string signature = SignHMAC(in_buffer_, key_data); - - holder_.SignAndLoadLicense(); - - cdm_sts = cdm_engine_.GenericSign(holder_.session_id(), in_buffer_, key_id, - wvcdm::kSigningAlgorithmHmacSha256, - &signature_buffer); - EXPECT_EQ(NO_ERROR, cdm_sts); - EXPECT_EQ(MAC_KEY_SIZE, signature_buffer.size()); - - cdm_sts = cdm_engine_.GenericVerify(holder_.session_id(), in_buffer_, key_id, - wvcdm::kSigningAlgorithmHmacSha256, - signature_buffer); - EXPECT_EQ(NO_ERROR, cdm_sts); - EXPECT_EQ(signature, signature_buffer); + std::string out_buffer; + std::string signature = SignHMAC(in_buffer_, sign_and_verify_key_); + EXPECT_EQ(NO_ERROR, cdm_engine_.GenericSign( + holder_.session_id(), in_buffer_, sign_and_verify_id_, + wvcdm::kSigningAlgorithmHmacSha256, &out_buffer)); + EXPECT_EQ(signature, out_buffer); + EXPECT_EQ(NO_ERROR, cdm_engine_.GenericVerify( + holder_.session_id(), in_buffer_, sign_and_verify_id_, + wvcdm::kSigningAlgorithmHmacSha256, signature)); } - } // namespace wvcdm diff --git a/libwvdrmengine/cdm/core/test/license_holder.cpp b/libwvdrmengine/cdm/core/test/license_holder.cpp new file mode 100644 index 00000000..140029de --- /dev/null +++ b/libwvdrmengine/cdm/core/test/license_holder.cpp @@ -0,0 +1,227 @@ +// Copyright 2022 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine License +// Agreement. + +#include "license_holder.h" + +#include "license_request.h" +#include "oec_device_features.h" +#include "test_base.h" + +namespace wvcdm { +namespace { +constexpr int kHttpOk = 200; +const std::string kCencMimeType = "cenc"; +} // namespace + +LicenseHolder::~LicenseHolder() {} + +void LicenseHolder::OpenSession() { + CdmResponseType status = cdm_engine_->OpenSession( + config_.key_system(), nullptr, &event_listener_, &session_id_); + ASSERT_EQ(NO_ERROR, status); + ASSERT_TRUE(cdm_engine_->IsOpenSession(session_id_)); +} + +void LicenseHolder::CloseSession() { + CdmResponseType status = cdm_engine_->CloseSession(session_id_); + ASSERT_EQ(NO_ERROR, status); + ASSERT_FALSE(cdm_engine_->IsOpenSession(session_id_)); +} + +void LicenseHolder::FetchLicense() { + video_widevine::WidevinePsshData pssh; + pssh.set_content_id(content_id_); + const std::string init_data_string = MakePSSH(pssh); + const InitializationData init_data(kCencMimeType, init_data_string); + init_data.DumpToLogs(); + CdmKeyRequest key_request; + ASSERT_NO_FATAL_FAILURE(GenerateKeyRequest(init_data, &key_request)) + << "Failed for " << content_id(); + ASSERT_NO_FATAL_FAILURE(GetKeyResponse(key_request)) + << "Failed for " << content_id(); +} + +void LicenseHolder::LoadLicense() { + CdmLicenseType license_type; + ASSERT_EQ(KEY_ADDED, cdm_engine_->AddKey(session_id_, key_response_, + &license_type, &key_set_id_)) + << "Failed to load license for " << content_id(); + if (can_persist_) { + ASSERT_EQ(license_type, kLicenseTypeOffline) + << "Failed for " << content_id(); + } else { + ASSERT_EQ(license_type, kLicenseTypeStreaming) + << "Failed for " << content_id(); + } +} + +void LicenseHolder::ReloadLicense() { + CdmResponseType status = cdm_engine_->RestoreKey(session_id_, key_set_id_); + ASSERT_EQ(KEY_ADDED, status) + << "Failed to reload license for " << content_id(); +} + +void LicenseHolder::GenerateAndPostRenewalRequest( + const std::string& policy_id) { + event_listener_.set_renewal_needed(false); + CdmKeyRequest request; + const CdmResponseType result = + cdm_engine_->GenerateRenewalRequest(session_id_, &request); + ASSERT_EQ(KEY_MESSAGE, result) << "Failed for " << content_id(); + const std::string url = MakeUrl(config_.renewal_server(), policy_id); + renewal_in_flight_.reset(new UrlRequest(url)); + ASSERT_TRUE(renewal_in_flight_->is_connected()) + << "Failed for " << content_id(); + renewal_in_flight_->PostRequest(request.message); +} +void LicenseHolder::FetchRenewal() { + ASSERT_NE(renewal_in_flight_, nullptr) << "Failed for " << content_id(); + ASSERT_TRUE(renewal_in_flight_->GetResponse(&renewal_response_)) + << "Failed for " << content_id(); + int status_code = renewal_in_flight_->GetStatusCode(renewal_response_); + ASSERT_EQ(kHttpOk, status_code) << "Failed for " << content_id(); +} + +void LicenseHolder::LoadRenewal() { + LicenseRequest license_request; + license_request.GetDrmMessage(renewal_response_, renewal_message_); + EXPECT_EQ(KEY_ADDED, cdm_engine_->RenewKey(session_id_, renewal_message_)) + << "Failed for " << content_id(); +} + +void LicenseHolder::RemoveLicense() { + EXPECT_EQ(NO_ERROR, cdm_engine_->RemoveLicense(session_id_)) + << "Failed for " << content_id(); +} + +CdmResponseType LicenseHolder::Decrypt(const std::string& key_id) { + constexpr size_t buffer_size = 500; + const std::vector input(buffer_size, 0); + std::vector output(buffer_size, 0); + const std::vector iv(KEY_IV_SIZE, 0); + CdmDecryptionParametersV16 params(key_id); + params.is_secure = false; + CdmDecryptionSample sample(input.data(), output.data(), 0, input.size(), iv); + CdmDecryptionSubsample subsample(0, input.size()); + sample.subsamples.push_back(subsample); + params.samples.push_back(sample); + return cdm_engine_->DecryptV16(session_id_, params); +} + +void LicenseHolder::DecryptSecure(const KeyId& key_id) { + ASSERT_TRUE(wvoec::global_features.test_secure_buffers); + constexpr size_t buffer_size = 500; + const std::vector input(buffer_size, 0); + const std::vector iv(KEY_IV_SIZE, 0); + + // To create a secure buffer, we need to know the OEMCrypto session id. + CdmQueryMap query_map; + cdm_engine_->QueryOemCryptoSessionId(session_id_, &query_map); + const std::string oec_session_id_string = + query_map[QUERY_KEY_OEMCRYPTO_SESSION_ID]; + uint32_t oec_session_id = std::stoi(oec_session_id_string); + + int secure_buffer_fid; + OEMCrypto_DestBufferDesc output_descriptor; + output_descriptor.type = OEMCrypto_BufferType_Secure; + output_descriptor.buffer.secure.secure_buffer_length = buffer_size; + ASSERT_EQ( + OEMCrypto_AllocateSecureBuffer(oec_session_id, buffer_size, + &output_descriptor, &secure_buffer_fid), + OEMCrypto_SUCCESS); + + ASSERT_NE(output_descriptor.buffer.secure.secure_buffer, nullptr); + // It is OK if OEMCrypto changes the maximum size, but there must + // still be enough room for our data. + ASSERT_GE(output_descriptor.buffer.secure.secure_buffer_length, buffer_size); + output_descriptor.buffer.secure.offset = 0; + + // Now create a sample array for the CDM layer. + CdmDecryptionParametersV16 params(key_id); + params.is_secure = true; + CdmDecryptionSample sample(input.data(), + output_descriptor.buffer.secure.secure_buffer, 0, + input.size(), iv); + CdmDecryptionSubsample subsample(0, input.size()); + sample.subsamples.push_back(subsample); + params.samples.push_back(sample); + CdmResponseType status = cdm_engine_->DecryptV16(session_id_, params); + + // Free the secure buffer before we check the return status. + EXPECT_EQ(OEMCrypto_FreeSecureBuffer(oec_session_id, &output_descriptor, + secure_buffer_fid), + OEMCrypto_SUCCESS); + + ASSERT_EQ(status, NO_ERROR); +} + +void LicenseHolder::FailDecrypt(const KeyId& key_id, + CdmResponseType expected_status) { + CdmResponseType status = Decrypt(key_id); + // If the server knows we cannot handle the key, it would not have given us + // the key. In that case, the status should indicate no key. + if (status != NEED_KEY) { + // Otherwise, we should have gotten the expected error. + ASSERT_EQ(expected_status, status); + } +} + +void LicenseHolder::GenerateKeyRequest(const InitializationData& init_data, + CdmKeyRequest* key_request) { + CdmAppParameterMap empty_app_parameters; + CdmKeySetId empty_key_set_id; + CdmLicenseType license_type = + can_persist_ ? kLicenseTypeOffline : kLicenseTypeStreaming; + CdmResponseType result = cdm_engine_->GenerateKeyRequest( + session_id_, empty_key_set_id, init_data, license_type, + empty_app_parameters, key_request); + ASSERT_EQ(KEY_MESSAGE, result); + ASSERT_EQ(kKeyRequestTypeInitial, key_request->type); +} + +std::string LicenseHolder::MakeUrl(const std::string& server_url, + const std::string& policy_id) { + // For tests, we want to specify the policy, but the UAT server only allows us + // to set the content id as the video_id. So each policy is matched to a + // single license with the same name. The local license server, on the other + // hand, wants to see the policy id in the url. So we have to guess which + // format to use based on the name of the server. + const std::string path = server_url + config_.client_auth(); + std::string video_query; + if (!policy_id.empty()) { + if (path.find("proxy.uat") != std::string::npos) { + // This is uat or uat-nightly. Set the video_id. + video_query = "video_id=" + policy_id; + } else { + // This is probably a local license server. Set the policy. + video_query = "policy=" + policy_id; + } + // If there is already a parameter, then we don't need to add another + // question mark. + return path + ((path.find("?") == std::string::npos) ? "?" : "&") + + video_query; + } else { + return path; + } +} + +void LicenseHolder::GetKeyResponse(const CdmKeyRequest& key_request) { + // The content id matches the policy id used on UAT. + const std::string url = MakeUrl(config_.license_server(), ""); + UrlRequest url_request(url); + ASSERT_TRUE(url_request.is_connected()); + + std::string http_response; + url_request.PostRequest(key_request.message); + ASSERT_TRUE(url_request.GetResponse(&http_response)); + int status_code = url_request.GetStatusCode(http_response); + ASSERT_EQ(kHttpOk, status_code) << "Error with url = " << url << "\n" + << "content_id = " << content_id() << "\n" + << "response = " << http_response; + + LicenseRequest license_request; + license_request.GetDrmMessage(http_response, key_response_); +} + +} // namespace wvcdm diff --git a/libwvdrmengine/cdm/core/test/license_holder.h b/libwvdrmengine/cdm/core/test/license_holder.h new file mode 100644 index 00000000..eb1e5e37 --- /dev/null +++ b/libwvdrmengine/cdm/core/test/license_holder.h @@ -0,0 +1,133 @@ +// Copyright 2022 Google LLC. All Rights Reserved. This file and proprietary +// source code may only be used and distributed under the Widevine License +// Agreement. + +#ifndef WVCDM_CORE_TEST_LICENSE_HOLDER_H_ +#define WVCDM_CORE_TEST_LICENSE_HOLDER_H_ + +#include +#include + +#include + +#include "cdm_engine.h" +#include "config_test_env.h" +#include "url_request.h" +#include "wv_attributes.h" +#include "wv_cdm_event_listener.h" + +#include "log.h" + +namespace wvcdm { + +// An event listener to tell when a renewal is needed. +class SimpleEventListener : public wvcdm::WvCdmEventListener { + public: + SimpleEventListener() { renewal_needed_ = false; } + // We will want to know when a renewal is needed. + void OnSessionRenewalNeeded(const CdmSessionId& /*session*/) override { + renewal_needed_ = true; + } + void OnSessionKeysChange(const CdmSessionId&, const CdmKeyStatusMap&, + bool) override {} + void OnExpirationUpdate(const CdmSessionId&, + int64_t expiry_time UNUSED) override {} + bool renewal_needed() { return renewal_needed_; } + void set_renewal_needed(bool renewal_needed) { + renewal_needed_ = renewal_needed; + } + + private: + bool renewal_needed_ = false; +}; + +class LicenseHolder { + public: + // Set up a new license holder with the specified content id. + LicenseHolder(const std::string& content_id, CdmEngine* cdm_engine, + const ConfigTestEnv& config) + : content_id_(content_id), cdm_engine_(cdm_engine), config_(config) {} + ~LicenseHolder(); + // Open a session for the license. Must be called before FetchLicense or + // ReloadLicense. + void OpenSession(); + // Close the session and release resources. + void CloseSession(); + // Generate a license request, send it to the license server, and wait for the + // response. The response is *not* loaded into the cdm engine. + void FetchLicense(); + // Load the license response into the CDM engine. A call to FetchLicense() + // must be made first. + void LoadLicense(); + // Reload the license. Call OpenSession() before calling + // ReloadLicense(). Also, the key_set_id must have been set previously. The + // key_set_id is set by calling LoadLicense(), or by calling set_key_set_id(). + void ReloadLicense(); + // Generate the renewal request, and send it to the server. + void GenerateAndPostRenewalRequest(const std::string& policy_id); + // Fetch the renewal response. This can add a few seconds of latency. + void FetchRenewal(); + // Load the renewal response that was fetched in FetchRenewal(). + void LoadRenewal(); + // Releases the license and frees up entry in usage table. + void RemoveLicense(); + + // Try to decrypt some random data. This does not verify that the data is + // decrypted correctly. Returns the result of the decrypt operation. + CdmResponseType Decrypt(const std::string& key_id); + // Try to decrypt some random data to a secure buffer. If the test harness + // does not allow creating a secure buffer, then this function fails + // immediately. Otherwise, a secure buffer is created and used for a + // decryption operation. + void DecryptSecure(const KeyId& key_id); + // Try to decrypt some random data, but expect failure. The failure may + // be either the expected_status, or NEED_KEY. We allow NEED_KEY in case + // the server recognized that we cannot support the given key. + void FailDecrypt(const KeyId& key_id, CdmResponseType expected_status); + + const std::string& content_id() const { return content_id_; } + void set_content_id(const std::string& content_id) { + content_id_ = content_id; + } + // The session id. This is only valid after a call to OpenSession. + const std::string& session_id() { return session_id_; } + // Returns true if the license is offline. + bool can_persist() const { return can_persist_; } + // Sets whether the license is offline or not. + void set_can_persist(bool can_persist) { can_persist_ = can_persist; } + uint64_t start_of_rental_clock() const { return start_of_rental_clock_; } + const std::string& key_set_id() const { return key_set_id_; } + void set_key_set_id(const std::string& key_set_id) { + key_set_id_ = key_set_id; + } + SimpleEventListener& event_listener() { return event_listener_; } + + private: + std::string content_id_; + CdmSessionId session_id_; + CdmKeySetId key_set_id_; + std::string key_response_; + bool can_persist_ = false; + uint64_t start_of_rental_clock_ = 0u; + CdmEngine* cdm_engine_ = nullptr; + const ConfigTestEnv& config_; + SimpleEventListener event_listener_; + std::unique_ptr renewal_in_flight_; + std::string renewal_message_; + std::string renewal_response_; + + // Generate the license request. + void GenerateKeyRequest(const InitializationData& init_data, + CdmKeyRequest* key_request); + + // Generate a URL for the specified policy. The license request should be sent + // to this url. + std::string MakeUrl(const std::string& server_url, + const std::string& policy_id); + // Fetch the key response from the server. + void GetKeyResponse(const CdmKeyRequest& key_request); +}; + +} // namespace wvcdm + +#endif // WVCDM_CORE_TEST_LICENSE_HOLDER_H_ diff --git a/libwvdrmengine/cdm/core/test/policy_integration_test.cpp b/libwvdrmengine/cdm/core/test/policy_integration_test.cpp index f3318f4e..2135b951 100644 --- a/libwvdrmengine/cdm/core/test/policy_integration_test.cpp +++ b/libwvdrmengine/cdm/core/test/policy_integration_test.cpp @@ -13,27 +13,15 @@ #include #include "cdm_engine.h" -#include "clock.h" -#include "config_test_env.h" -#include "initialization_data.h" -#include "license_request.h" +#include "license_holder.h" #include "log.h" -#include "metrics_collections.h" #include "oec_device_features.h" #include "test_base.h" #include "test_printers.h" -#include "test_sleep.h" -#include "url_request.h" -#include "wv_cdm_constants.h" + #include "wv_cdm_types.h" namespace wvcdm { - -namespace { -constexpr int kHttpOk = 200; -const std::string kCencMimeType = "cenc"; -} // namespace - // Core Policy Integration Test class CorePIGTest : public WvCdmTestBaseWithEngine { protected: @@ -41,210 +29,44 @@ class CorePIGTest : public WvCdmTestBaseWithEngine { WvCdmTestBase::SetUp(); EnsureProvisioned(); } - - void OpenSession(CdmSessionId* session_id) { - CdmResponseType status = cdm_engine_.OpenSession( - config_.key_system(), nullptr, nullptr, session_id); - ASSERT_EQ(NO_ERROR, status); - ASSERT_TRUE(cdm_engine_.IsOpenSession(*session_id)); - } - - void CloseSession(const CdmSessionId& session_id) { - CdmResponseType status = cdm_engine_.CloseSession(session_id); - ASSERT_EQ(NO_ERROR, status); - ASSERT_FALSE(cdm_engine_.IsOpenSession(session_id)); - } - - // Create a license request for the given content_id and requesting the - // specified license_type. - void GenerateKeyRequest(const CdmSessionId& session_id, - const std::string& content_id, - CdmKeyRequest* key_request, - CdmLicenseType license_type) { - video_widevine::WidevinePsshData pssh; - pssh.set_content_id(content_id); - const std::string init_data_string = MakePSSH(pssh); - const InitializationData init_data(kCencMimeType, init_data_string); - init_data.DumpToLogs(); - CdmAppParameterMap empty_app_parameters; - CdmKeySetId empty_key_set_id; - CdmResponseType result = cdm_engine_.GenerateKeyRequest( - session_id, empty_key_set_id, init_data, license_type, - empty_app_parameters, key_request); - ASSERT_EQ(KEY_MESSAGE, result); - ASSERT_EQ(kKeyRequestTypeInitial, key_request->type); - } - - // Send the request to the server and get the response. - void GetKeyResponse(const CdmKeyRequest& key_request, - std::string* key_response) { - const std::string url = config_.license_server() + config_.client_auth(); - UrlRequest url_request(url); - ASSERT_TRUE(url_request.is_connected()); - - std::string http_response; - url_request.PostRequest(key_request.message); - ASSERT_TRUE(url_request.GetResponse(&http_response)); - int status_code = url_request.GetStatusCode(http_response); - ASSERT_EQ(kHttpOk, status_code); - - LicenseRequest license_request; - license_request.GetDrmMessage(http_response, *key_response); - } - - // Load the license response into the specified session. Verify it has the - // correct license type (either streaming or offline). - void AddKey(const CdmSessionId& session_id, const std::string& key_response, - CdmLicenseType expected_license_type, CdmKeySetId* key_set_id) { - CdmLicenseType license_type; - CdmResponseType status = - cdm_engine_.AddKey(session_id, key_response, &license_type, key_set_id); - ASSERT_EQ(KEY_ADDED, status); - ASSERT_EQ(expected_license_type, license_type); - } - - // Reload the license response into the specified session. - void RestoreKey(const CdmSessionId& session_id, - const CdmKeySetId& key_set_id) { - CdmResponseType status = cdm_engine_.RestoreKey(session_id, key_set_id); - ASSERT_EQ(KEY_ADDED, status); - } - - // Use the key to decrypt. - void Decrypt(const CdmSessionId& session_id, const KeyId& key_id) { - constexpr size_t buffer_size = 500; - const std::vector input(buffer_size, 0); - std::vector output(buffer_size, 0); - const std::vector iv(KEY_IV_SIZE, 0); - ASSERT_EQ(NO_ERROR, Decrypt(session_id, key_id, input, iv, &output)); - } - - // Try to use the key to decrypt, but expect the key has expired. - void FailDecrypt(const CdmSessionId& session_id, const KeyId& key_id, - CdmResponseType expected_status) { - constexpr size_t buffer_size = 500; - const std::vector input(buffer_size, 0); - std::vector output(buffer_size, 0); - const std::vector iv(KEY_IV_SIZE, 0); - CdmResponseType status = Decrypt(session_id, key_id, input, iv, &output); - // If the server knows we cannot handle the key, it would not have given us - // the key. In that case, the status should indicate no key. - if (status != NEED_KEY) { - // Otherwise, we should have gotten the expected error. - ASSERT_EQ(expected_status, status); - } - } - - // Decrypt or fail to decrypt, with the expected status. - CdmResponseType Decrypt(const CdmSessionId& session_id, const KeyId& key_id, - const std::vector& input, - const std::vector& iv, - std::vector* output) { - CdmDecryptionParametersV16 params(key_id); - params.is_secure = false; - CdmDecryptionSample sample(input.data(), output->data(), 0, input.size(), - iv); - CdmDecryptionSubsample subsample(0, input.size()); - sample.subsamples.push_back(subsample); - params.samples.push_back(sample); - return cdm_engine_.DecryptV16(session_id, params); - } - - // Use the key to decrypt to a secure buffer. - void DecryptSecure(const CdmSessionId& session_id, const KeyId& key_id) { - ASSERT_TRUE(wvoec::global_features.test_secure_buffers); - constexpr size_t buffer_size = 500; - const std::vector input(buffer_size, 0); - const std::vector iv(KEY_IV_SIZE, 0); - - // To create a secure buffer, we need to know the OEMCrypto session id. - CdmQueryMap query_map; - cdm_engine_.QueryOemCryptoSessionId(session_id, &query_map); - const std::string oec_session_id_string = - query_map[QUERY_KEY_OEMCRYPTO_SESSION_ID]; - uint32_t oec_session_id = std::stoi(oec_session_id_string); - - int secure_buffer_fid; - OEMCrypto_DestBufferDesc output_descriptor; - output_descriptor.type = OEMCrypto_BufferType_Secure; - output_descriptor.buffer.secure.secure_buffer_length = buffer_size; - ASSERT_EQ( - OEMCrypto_AllocateSecureBuffer(oec_session_id, buffer_size, - &output_descriptor, &secure_buffer_fid), - OEMCrypto_SUCCESS); - - ASSERT_NE(output_descriptor.buffer.secure.secure_buffer, nullptr); - // It is OK if OEMCrypto changes the maximum size, but there must - // still be enough room for our data. - ASSERT_GE(output_descriptor.buffer.secure.secure_buffer_length, - buffer_size); - output_descriptor.buffer.secure.offset = 0; - - // Now create a sample array for the CDM layer. - CdmDecryptionParametersV16 params(key_id); - params.is_secure = true; - CdmDecryptionSample sample(input.data(), - output_descriptor.buffer.secure.secure_buffer, 0, - input.size(), iv); - CdmDecryptionSubsample subsample(0, input.size()); - sample.subsamples.push_back(subsample); - params.samples.push_back(sample); - CdmResponseType status = cdm_engine_.DecryptV16(session_id, params); - - // Free the secure buffer before we check the return status. - OEMCrypto_FreeSecureBuffer(oec_session_id, &output_descriptor, - secure_buffer_fid); - - ASSERT_EQ(status, NO_ERROR); - } }; // An offline license with nonce not required. TEST_F(CorePIGTest, OfflineNoNonce) { - const std::string content_id = "2015_tears"; + LicenseHolder holder("CDM_OfflineNoNonce", &cdm_engine_, config_); + holder.set_can_persist(true); const KeyId key_id = "0000000000000000"; - const CdmLicenseType license_type = kLicenseTypeOffline; - CdmSessionId session_id; - ASSERT_NO_FATAL_FAILURE(OpenSession(&session_id)); - CdmKeyRequest key_request; - ASSERT_NO_FATAL_FAILURE( - GenerateKeyRequest(session_id, content_id, &key_request, license_type)); - std::string key_response; - ASSERT_NO_FATAL_FAILURE(GetKeyResponse(key_request, &key_response)); - CdmKeySetId key_set_id; - ASSERT_NO_FATAL_FAILURE( - AddKey(session_id, key_response, license_type, &key_set_id)); - ASSERT_NO_FATAL_FAILURE(Decrypt(session_id, key_id)); - ASSERT_NO_FATAL_FAILURE(CloseSession(session_id)); - ASSERT_NO_FATAL_FAILURE(OpenSession(&session_id)); - ASSERT_NO_FATAL_FAILURE(RestoreKey(session_id, key_set_id)); - ASSERT_NO_FATAL_FAILURE(Decrypt(session_id, key_id)); - ASSERT_NO_FATAL_FAILURE(CloseSession(session_id)); + ASSERT_NO_FATAL_FAILURE(holder.OpenSession()); + ASSERT_NO_FATAL_FAILURE(holder.FetchLicense()); + ASSERT_NO_FATAL_FAILURE(holder.LoadLicense()); + EXPECT_EQ(NO_ERROR, holder.Decrypt(key_id)); + ASSERT_NO_FATAL_FAILURE(holder.CloseSession()); + // Should be able to close the previous session, open a new session, + // and reload the license. + ASSERT_NO_FATAL_FAILURE(holder.OpenSession()); + ASSERT_NO_FATAL_FAILURE(holder.ReloadLicense()); + EXPECT_EQ(NO_ERROR, holder.Decrypt(key_id)); + ASSERT_NO_FATAL_FAILURE(holder.CloseSession()); } // An offline license with nonce and provider session token. TEST_F(CorePIGTest, OfflineWithPST) { - const std::string content_id = "offline_clip2"; - const KeyId key_id = - "\x32\x60\xF3\x9E\x12\xCC\xF6\x53\x52\x99\x90\x16\x8A\x35\x83\xFF"; - const CdmLicenseType license_type = kLicenseTypeOffline; - CdmSessionId session_id; - ASSERT_NO_FATAL_FAILURE(OpenSession(&session_id)); - CdmKeyRequest key_request; - ASSERT_NO_FATAL_FAILURE( - GenerateKeyRequest(session_id, content_id, &key_request, license_type)); - std::string key_response; - ASSERT_NO_FATAL_FAILURE(GetKeyResponse(key_request, &key_response)); - CdmKeySetId key_set_id; - ASSERT_NO_FATAL_FAILURE( - AddKey(session_id, key_response, license_type, &key_set_id)); - ASSERT_NO_FATAL_FAILURE(Decrypt(session_id, key_id)); - ASSERT_NO_FATAL_FAILURE(CloseSession(session_id)); - ASSERT_NO_FATAL_FAILURE(OpenSession(&session_id)); - ASSERT_NO_FATAL_FAILURE(RestoreKey(session_id, key_set_id)); - ASSERT_NO_FATAL_FAILURE(Decrypt(session_id, key_id)); - ASSERT_NO_FATAL_FAILURE(CloseSession(session_id)); + LicenseHolder holder("CDM_OfflineWithPST", &cdm_engine_, config_); + holder.set_can_persist(true); + const KeyId key_id = "0000000000000000"; + + ASSERT_NO_FATAL_FAILURE(holder.OpenSession()); + ASSERT_NO_FATAL_FAILURE(holder.FetchLicense()); + ASSERT_NO_FATAL_FAILURE(holder.LoadLicense()); + EXPECT_EQ(NO_ERROR, holder.Decrypt(key_id)); + ASSERT_NO_FATAL_FAILURE(holder.CloseSession()); + // Should be able to close the previous session, open a new session, + // and reload the license. + ASSERT_NO_FATAL_FAILURE(holder.OpenSession()); + ASSERT_NO_FATAL_FAILURE(holder.ReloadLicense()); + EXPECT_EQ(NO_ERROR, holder.Decrypt(key_id)); + ASSERT_NO_FATAL_FAILURE(holder.CloseSession()); } // This test verifies that the system can download and install license with a @@ -252,31 +74,39 @@ TEST_F(CorePIGTest, OfflineWithPST) { // a non-secure buffer using this key, but that we can decrypt to a secure // buffer, if the test harness supports secure buffers. TEST_F(CorePIGTest, OfflineHWSecureRequired) { - const std::string content_id = "GTS_HW_SECURE_ALL"; - const KeyId key_id = "0000000000000002"; + LicenseHolder holder("CDM_OfflineHWSecureRequired", &cdm_engine_, config_); + holder.set_can_persist(true); + const KeyId sw_key_id = "0000000000000000"; + const KeyId hw_key_id = "0000000000000001"; - const CdmLicenseType license_type = kLicenseTypeOffline; - CdmSessionId session_id; - ASSERT_NO_FATAL_FAILURE(OpenSession(&session_id)); - CdmKeyRequest key_request; - ASSERT_NO_FATAL_FAILURE( - GenerateKeyRequest(session_id, content_id, &key_request, license_type)); - std::string key_response; - ASSERT_NO_FATAL_FAILURE(GetKeyResponse(key_request, &key_response)); - CdmKeySetId key_set_id; - ASSERT_NO_FATAL_FAILURE( - AddKey(session_id, key_response, license_type, &key_set_id)); - // First we try to decrypt to a non-secure buffer and verify that it fails. - // TODO(b/164517875): This error code should be something actionable. - ASSERT_NO_FATAL_FAILURE(FailDecrypt(session_id, key_id, DECRYPT_ERROR)); + ASSERT_NO_FATAL_FAILURE(holder.OpenSession()); + ASSERT_NO_FATAL_FAILURE(holder.FetchLicense()); + ASSERT_NO_FATAL_FAILURE(holder.LoadLicense()); + EXPECT_EQ(NO_ERROR, holder.Decrypt(sw_key_id)); + ASSERT_NO_FATAL_FAILURE(holder.FailDecrypt(hw_key_id, DECRYPT_ERROR)); // Next, if possible, we try to decrypt to a secure buffer, and verify // success. if (wvoec::global_features.test_secure_buffers) { - ASSERT_NO_FATAL_FAILURE(DecryptSecure(session_id, key_id)); + ASSERT_NO_FATAL_FAILURE(holder.DecryptSecure(hw_key_id)); } else { LOGI("Test harness cannot create secure buffers. test skipped."); } - ASSERT_NO_FATAL_FAILURE(CloseSession(session_id)); + ASSERT_NO_FATAL_FAILURE(holder.CloseSession()); + + // Should be able to close the previous session, open a new session, + // and reload the license. + ASSERT_NO_FATAL_FAILURE(holder.OpenSession()); + ASSERT_NO_FATAL_FAILURE(holder.ReloadLicense()); + EXPECT_EQ(NO_ERROR, holder.Decrypt(sw_key_id)); + ASSERT_NO_FATAL_FAILURE(holder.FailDecrypt(hw_key_id, DECRYPT_ERROR)); + // Next, if possible, we try to decrypt to a secure buffer, and verify + // success. + if (wvoec::global_features.test_secure_buffers) { + ASSERT_NO_FATAL_FAILURE(holder.DecryptSecure(hw_key_id)); + } else { + LOGI("Test harness cannot create secure buffers. test skipped."); + } + ASSERT_NO_FATAL_FAILURE(holder.CloseSession()); } } // namespace wvcdm diff --git a/libwvdrmengine/cdm/core/test/reboot_test.cpp b/libwvdrmengine/cdm/core/test/reboot_test.cpp index 5b9a2280..30d60b6f 100644 --- a/libwvdrmengine/cdm/core/test/reboot_test.cpp +++ b/libwvdrmengine/cdm/core/test/reboot_test.cpp @@ -10,16 +10,23 @@ #include +#include "license_holder.h" #include "log.h" +#include "test_sleep.h" using wvutil::a2b_hex; -using wvutil::b2a_hex; using wvutil::FileSystem; +using wvutil::TestSleep; +using wvutil::unlimited_b2a_hex; namespace wvcdm { FileSystem* RebootTest::file_system_; namespace { +// How much fudge or round off error do we allow in license durations for reboot +// tests. +constexpr int64_t kFudge = 10; + // We will encode a value string by wrapping it in braces, or as hex. // If the string is not printable, or if it has unmatched braces, then we use // hex. Otherwise, we surround the whole string with braces. @@ -33,11 +40,11 @@ std::string EncodeString(const std::string& data) { // If there are any unprintable characters, except whitespace, or if we // close a brace before we open it, then just use hex. if (!printable || braces_count < 0) { - return "0x" + b2a_hex(data) + ","; + return "0x" + unlimited_b2a_hex(data) + ","; } } // If we left any braces open, then use hex. - if (braces_count != 0) return "0x" + b2a_hex(data) + ","; + if (braces_count != 0) return "0x" + unlimited_b2a_hex(data) + ","; return "{" + data + "},"; } @@ -51,12 +58,12 @@ std::string EncodeKey(const std::string& data) { } // When decoding, we assume that a key starting with "0x" is in hex. So we // can't have any keys that start with "0x". - if (data.substr(0, 2) == "0x") return "0x" + b2a_hex(data) + ":"; + if (data.substr(0, 2) == "0x") return "0x" + unlimited_b2a_hex(data) + ":"; // If the key is just is not printable, or if it has unmatched braces, then // we use hex. Otherwise, we surround the whole string with braces. for (size_t i = 0; i < data.length(); i++) { if (!isprint(data[i]) || (data[i] == ':')) { - return "0x" + b2a_hex(data) + ":"; + return "0x" + unlimited_b2a_hex(data) + ":"; } } return data + ":"; @@ -227,6 +234,23 @@ void RebootTest::TearDown() { WvCdmTestBase::TearDown(); } +int64_t RebootTest::LoadTime(const std::string& key) { + int64_t value = 0; + std::istringstream input(persistent_data_[key]); + input >> value; + if (input.fail()) { + LOGE("Could not parse time '%s'", persistent_data_[key].c_str()); + } + if (!input.eof()) { + LOGE("Extra text at end of time '%s'", persistent_data_[key].c_str()); + } + return value; +} + +void RebootTest::SaveTime(const std::string& key, int64_t time) { + persistent_data_[key] = std::to_string(time); +} + /** Test the dump and restore functions above. This does not test CDM functionality. */ TEST_F(RebootTest, TestDumpUtil) { @@ -247,6 +271,10 @@ TEST_F(RebootTest, TestDumpUtil) { // This key looks like it might be hex. It should show up as hex in the // save file. map1["0x_bad_key_00"] = "value is ok"; + std::string big_string = "start with something {binary"; + // Double big_string 8 times, i.e. times 256, so it's bigger than 2k: + for (int i = 0; i < 8; i++) big_string = big_string + big_string; + map1["big_file"] = big_string; const std::string dump2 = DumpData(map1); std::map map3; EXPECT_TRUE(ParseDump(dump2, &map3)); @@ -273,4 +301,429 @@ TEST_F(RebootTest, FilesArePersistent) { } } +/** Verify that the clock moves forward over a reboot. */ +TEST_F(RebootTest, TimeMovesForward) { + wvutil::TestSleep::Sleep(2); + const int64_t start = wvutil::Clock().GetCurrentTime(); + wvutil::TestSleep::Sleep(2); + const int64_t end = wvutil::Clock().GetCurrentTime(); + EXPECT_NEAR(end - start, 2.0, 1.0); + const std::string key = "end_time"; + if (test_pass() == 0) { + // Save off the end of pass 1. + SaveTime(key, end); + } else { + int64_t previous_end = LoadTime(key); + EXPECT_LT(previous_end, start); + } +} + +/** Test offline license durations work correctly after reboot. For time + constraints, this runs four sets of tests in parallel. For each set of tests + we load a list of licenses with a range of rental or playback durations so + that we can be confident that a short wait will find at least one duration + in the range that has expired and at least one duration that has not + expired. + + RDa. Load an offline license, reboot the device, reload the license and + verify that the rental duration is enforced from the time the license was + requested. + + RDb. Load an offline license, begin playback, reboot the device, reload the + license and verify that the rental duration is enforced from the time the + license was requested. + + PDa. Load an offline license, reboot the device, reload the license and + verify that the playback duration is enforced from the time initial playback + started. + + PDb. Load an offline license, begin playback, reboot the device, reload the + license and verify that the playback duration is enforced from the time + initial playback started. + + Each of these four sets contains licenses with various durations so that the + test can run with a reasonable reboot time. We will assume that a reboot can + take anywhere from 10 seconds to almost an hour. With this in mind, we will + create a license that has a 10 second duration, one with a 20 second + duration, ... and one with an hour duration. All of these licenses will be + loaded before the reboot. Then after the reboot we should be in a situation + like this: + :-----------------------------------------> time axis. + : start of test. + :: licenses loaded. + :: : reboot starts + :: : : reboot finished. test resumes. + :: : : + :[---] 10s - expired : + :[-------] 20s expired : + : ... : + :[---------------------------] this license has not yet expired + :[----------------------------------------------] license not expired. + : ... : + :[---------------------------------------------------] 1 hour. not expired + + After the test resumes, we will have at least one license that has not yet + expired. We will then sleep we are near the expiration time of that + license. We can then carefully test that this one license from the set + enforces its duration. + + This is complicated by the fact that we are testing four different sets. So + what we really have is something like this: + :-----------------------------------------> time axis. + : start of test. + :: licenses loaded. + :: : reboot starts + :: : : reboot finished. test resumes. + :: : : + :[-----------------------------] first RDa license that has not exipred. + :[--------------------------] first RDb license that has not exipred. + :[---------------------------] first PDa license that has not exipred. + :[-------------------------------] first PDb license that has not exipred. + + Since we want to test all four of these licenses, we will compute the + interesting times for each license. These are the times just before, and + just after the expiration time. After sorting these interesting times, we + will test each one in order. + */ +class OfflineLicense { + public: + OfflineLicense(const std::string& test_type, CdmEngine* cdm_engine, + const ConfigTestEnv& config, int64_t duration, + bool play_before_reboot) + : content_id_("CDM_Reboot_" + test_type + + (play_before_reboot ? "b_" : "a_") + + std::to_string(duration)), + duration_(duration), + play_before_reboot_(play_before_reboot), + license_holder_(content_id_, cdm_engine, config) { + license_holder_.set_can_persist(true); + } + + virtual ~OfflineLicense() {} + + // Fetch and load the license. The session is left open. + void LoadLicense() { + license_holder_.OpenSession(); + start_of_rental_clock_ = wvutil::Clock().GetCurrentTime(); + license_holder_.FetchLicense(); + license_holder_.LoadLicense(); + } + + // Reload the license. The session is left open. + void ReloadLicense() { + license_holder_.OpenSession(); + license_holder_.ReloadLicense(); + } + + // Action to be taken after loading the license, but before the reboot. + virtual void BeforeReboot() { + if (play_before_reboot_) { + Decrypt(); + } + } + + // Action to be taken after reloading the license. + virtual void AfterReboot() {} + + void CloseSession() { license_holder_.CloseSession(); } + + // The time this license should be cutoff. Decrypt should succeed before this + // time and should fail after this time. + virtual int64_t cutoff() = 0; + + // Verify that the license may be used to decrypt content. + void Decrypt() { + if (start_of_playback_ == 0) { + start_of_playback_ = wvutil::Clock().GetCurrentTime(); + } + const KeyId key_id = "0000000000000000"; + EXPECT_EQ(NO_ERROR, license_holder_.Decrypt(key_id)) + << "Failed for " << content_id_ + << ", now = " << wvutil::Clock().GetCurrentTime() << ", rental_clock=" + << (wvutil::Clock().GetCurrentTime() - start_of_rental_clock_) + << ", playback_clock = " + << (wvutil::Clock().GetCurrentTime() - start_of_playback_) + << ", delta to cutoff = " + << (wvutil::Clock().GetCurrentTime() - cutoff()); + } + + // Verify that the license has expired, and may not be used to decrypt + // content. + void FailDecrypt() { + const KeyId key_id = "0000000000000000"; + EXPECT_EQ(NEED_KEY, license_holder_.Decrypt(key_id)) + << "Decrypt should have failed for " << content_id_ + << ", now = " << wvutil::Clock().GetCurrentTime() << ", rental_clock=" + << (wvutil::Clock().GetCurrentTime() - start_of_rental_clock_) + << ", playback_clock = " + << (wvutil::Clock().GetCurrentTime() - start_of_playback_) + << ", delta to cutoff = " + << (wvutil::Clock().GetCurrentTime() - cutoff()); + } + + // Save times and the key set id to persistent data. + void SaveData(RebootTest* reboot_test, + std::map* persistent_data) { + reboot_test->SaveTime("start_of_rental_" + content_id_, + start_of_rental_clock_); + reboot_test->SaveTime("start_of_playback_" + content_id_, + start_of_playback_); + (*persistent_data)["key_set_id_" + content_id_] = + license_holder_.key_set_id(); + } + + // Load times and the key set id from persistent data. + void LoadData(RebootTest* reboot_test, + std::map* persistent_data) { + start_of_rental_clock_ = + reboot_test->LoadTime("start_of_rental_" + content_id_); + start_of_playback_ = + reboot_test->LoadTime("start_of_playback_" + content_id_); + license_holder_.set_key_set_id( + (*persistent_data)["key_set_id_" + content_id_]); + } + + const std::string& content_id() const { return content_id_; } + + protected: + const std::string content_id_; + int64_t duration_; + int64_t start_of_rental_clock_ = 0; + int64_t start_of_playback_ = 0; + bool play_before_reboot_; + LicenseHolder license_holder_; +}; + +// Holds an offline license that has a limit on the rental duration. +class RentalDurationLicense : public OfflineLicense { + public: + RentalDurationLicense(CdmEngine* cdm_engine, const ConfigTestEnv& config, + int64_t duration, bool play_before_reboot) + : OfflineLicense("RD", cdm_engine, config, duration, play_before_reboot) { + } + + int64_t cutoff() override { return start_of_rental_clock_ + duration_; } +}; + +// Holds an offline license that has a limit on the playback duration. +class PlaybackDurationLicense : public OfflineLicense { + public: + PlaybackDurationLicense(CdmEngine* cdm_engine, const ConfigTestEnv& config, + int64_t duration, bool play_before_reboot) + : OfflineLicense("PD", cdm_engine, config, duration, play_before_reboot) { + } + + // If we did not start playback before the reboot, we will start playback + // just after reloading the license, post-reboot. + void AfterReboot() override { + if (!play_before_reboot_) { + Decrypt(); + } + } + + int64_t cutoff() override { return start_of_playback_ + duration_; } +}; + +// Test that the rental and playback durations are enforced across a reboot. +class OfflineLicenseTest : public RebootTest { + public: + void SetUp() override { + RebootTest::SetUp(); + EnsureProvisioned(); + // Run each of the following test cases in parallel so that we don't have to + // sleep separately for each one. These durations need to match the polices + // specified on the UAT license server. + test_case_.resize(4); + const std::vector duration_range = {10, 20, 30, 45, + 60, 300, 900, 3600}; + for (size_t i = 0; i < duration_range.size(); i++) { + test_case_[0].push_back( + std::unique_ptr(new RentalDurationLicense( + &cdm_engine_, config_, duration_range[i], false))); + test_case_[1].push_back( + std::unique_ptr(new RentalDurationLicense( + &cdm_engine_, config_, duration_range[i], true))); + test_case_[2].push_back( + std::unique_ptr(new PlaybackDurationLicense( + &cdm_engine_, config_, duration_range[i], false))); + test_case_[3].push_back( + std::unique_ptr(new PlaybackDurationLicense( + &cdm_engine_, config_, duration_range[i], true))); + } + } + + // Load all of the licenses. For the tests that require playback before the + // reboot, we start playback here. + void LoadAllLicenses() { + DeleteAllLicenses(); + // For each test case, load an offline license and save the data. + for (size_t n = 0; n < test_case_.size(); n++) { + for (size_t i = 0; i < test_case_[n].size(); i++) { + ASSERT_NO_FATAL_FAILURE(test_case_[n][i]->LoadLicense()); + test_case_[n][i]->BeforeReboot(); // For some tests, we decrypt here. + test_case_[n][i]->SaveData(this, &persistent_data_); + test_case_[n][i]->CloseSession(); + } + } + } + + // Reload all of the licenses. We also go through each license and figure out + // which ones have already expired, or close to expire. We then find the first + // license from each set that will be next to expire, so that we can test + // those more carefully. + void ReloadAllLicense() { + // first_valid is the index of the first license that has not expired and is + // not just about to expire. + first_valid_.resize(test_case_.size()); + for (size_t n = 0; n < test_case_.size(); n++) { + for (size_t i = 0; i < test_case_[n].size(); i++) { + OfflineLicense* license = test_case_[n][i].get(); + license->LoadData(this, &persistent_data_); + ASSERT_NO_FATAL_FAILURE(license->ReloadLicense()); + license->AfterReboot(); + // if past cutoff. make sure decrypt fails. + if (license->cutoff() + kFudge < wvutil::Clock().GetCurrentTime()) { + license->FailDecrypt(); + } + // If past cutoff, or near cutoff, then we don't want to use it + // as our first valid. + if (license->cutoff() - 2 * kFudge < wvutil::Clock().GetCurrentTime()) { + first_valid_[n] = i + 1; + } + license->CloseSession(); + } + // We expect there to be at least one license that has not expired yet. + // If this is not true, then the reboot time was probably longer than + // expected. If it is important to run these tests with a very long reboot + // time, then please ask a Widevine engineer to generate more license + // policies with longer reboot times. + ASSERT_LT(first_valid_[n], test_case_[n].size()) + << "For n=" << n + << ", content_id = " << test_case_[n][0]->content_id() + << ", time=" << wvutil::Clock().GetCurrentTime() << "\n" + << "This is an indication that the reboot took a very long time.\n"; + } + } + + // Take the first_valid_ array, which tells us which licenses will expire + // soon, and compute the times we are interested in: a little before and a + // little after the cutoff. We want to test that the license is valid just + // before the cutoff and that the license is not valid just after the + // cutoff. + // + // The interesting_times_ is a std::set, so that we may iterate through the + // times in order. + void FindInterestingTimes() { + for (size_t n = 0; n < test_case_.size(); n++) { + OfflineLicense* license = test_case_[n][first_valid_[n]].get(); + interesting_times_.insert(license->cutoff() - kFudge); + interesting_times_.insert(license->cutoff() + kFudge); + } + } + + // Test at each of the interesting times. These are the times just before and + // after the cutoff of the next license to expire from each list of test + // cases. + void TestInterestingTimes() { + int decrypt_count = 0; + int fail_count = 0; + for (auto time : interesting_times_) { + int64_t now = wvutil::Clock().GetCurrentTime(); + int64_t delta = (time - now); + // It is not necessarily an error for the delta to be negative. But it is + // an indication that the we are near an error condition. If the current + // time is a few seconds past the interesting time, then the license + // should still be valid or expired. If delta is very large relative to + // kFudge, then the test might fail. This is still an indication that + // something is wrong: reloading a license and decrypting some content + // should not take multiple seconds. + if (delta < 0) LOGW("Sleep delta would be negative: %ld", delta); + if (delta > 0) TestSleep::Sleep(static_cast(delta)); + // We look at each of the four license that we used to generate the + // interesting times. It's possible that two licenses share the same + // interesting time, so we have to check each of the four against each + // time instead of keeping track of which license generated which time. + for (size_t n = 0; n < test_case_.size(); n++) { + OfflineLicense* license = test_case_[n][first_valid_[n]].get(); + ASSERT_NO_FATAL_FAILURE(license->ReloadLicense()); + if (time == license->cutoff() - kFudge) { + license->Decrypt(); + decrypt_count++; + } + if (time == license->cutoff() + kFudge) { + license->FailDecrypt(); + fail_count++; + } + license->CloseSession(); + } + } + EXPECT_EQ(decrypt_count, 4) << "Test error. I missed a cutoff"; + EXPECT_EQ(fail_count, 4) << "Test error. I missed a cutoff"; + } + + // After we have tested one license from each list of test cases carefully, + // all the rest of the licenses can be tested. We do not sleep in this + // function, we only test each license that is still valid, or has already + // expired. We ignore licenses that are near the cutoff because we already + // tested those license in the previous function. + void TestAfterInterestingTimes() { + // Make sure that all the rest of the licenses are still valid. + for (size_t n = 0; n < test_case_.size(); n++) { + for (size_t i = first_valid_[n] + 1; i < test_case_[n].size(); i++) { + OfflineLicense* license = test_case_[n][i].get(); + ASSERT_NO_FATAL_FAILURE(license->ReloadLicense()); + int64_t now = wvutil::Clock().GetCurrentTime(); + if (now <= license->cutoff() - kFudge) { + license->Decrypt(); + } + if (now >= license->cutoff() + kFudge) { + license->FailDecrypt(); + } + license->CloseSession(); + } + } + } + + void TearDown() override { + // Clean up licenses if any test failed. + if (::testing::Test::HasFailure() || test_pass() == 1) { + DeleteAllLicenses(); + } + RebootTest::TearDown(); + } + + void DeleteAllLicenses() { + std::vector key_set_ids; + EXPECT_EQ(NO_ERROR, + cdm_engine_.ListStoredLicenses(kSecurityLevelL1, &key_set_ids)); + for (auto key_set : key_set_ids) { + cdm_engine_.RemoveOfflineLicense(key_set, kSecurityLevelL1); + } + // TODO(b/215230202): Is this necessary? It doesn't seem to work. + std::vector ksids; + std::vector pst; + std::string app_id = ""; + EXPECT_EQ(NO_ERROR, + cdm_engine_.ListUsageIds(app_id, kSecurityLevelL1, &ksids, &pst)); + for (auto k : ksids) { + EXPECT_EQ(NO_ERROR, + cdm_engine_.DeleteUsageRecord(app_id, kSecurityLevelL1, k)); + } + } + + std::vector>> test_case_; + std::set interesting_times_; + std::vector first_valid_; +}; + +TEST_F(OfflineLicenseTest, VariousTests) { + if (test_pass() == 0) { + LoadAllLicenses(); + } else if (test_pass() == 1) { + ReloadAllLicense(); + FindInterestingTimes(); + TestInterestingTimes(); + TestAfterInterestingTimes(); + } +} } // namespace wvcdm diff --git a/libwvdrmengine/cdm/core/test/reboot_test.h b/libwvdrmengine/cdm/core/test/reboot_test.h index a0649bb1..0a9234be 100644 --- a/libwvdrmengine/cdm/core/test/reboot_test.h +++ b/libwvdrmengine/cdm/core/test/reboot_test.h @@ -36,6 +36,12 @@ class RebootTest : public WvCdmTestBaseWithEngine { static int test_pass() { return default_config_.test_pass(); } + // Load a previously saved time. Returns 0 if the value does not exist or + // cannot be parsed. + int64_t LoadTime(const std::string& key); + // Save a time to persistent storage. + void SaveTime(const std::string& key, int64_t time); + protected: void SetUp() override; void TearDown() override; diff --git a/libwvdrmengine/cdm/core/test/test_base.cpp b/libwvdrmengine/cdm/core/test/test_base.cpp index 1543acd5..c51eb908 100644 --- a/libwvdrmengine/cdm/core/test/test_base.cpp +++ b/libwvdrmengine/cdm/core/test/test_base.cpp @@ -122,6 +122,11 @@ void show_menu(const char* prog_name, const std::string& extra_help_text) { << " be used with a real OEMCrypto." << std::endl << std::endl; + std::cout << " --initial_time=