// Copyright 2023 Google LLC. All Rights Reserved. This file and proprietary // source code may only be used and distributed under the Widevine License // Agreement. #include "message_dumper.h" #include #include "core_message_deserialize.h" #include "license_request.h" #include "odk.h" #include "odk_message.h" #include "odk_serialize.h" #include "odk_structs.h" #include "odk_structs_priv.h" #include "oec_device_features.h" #include "test_base.h" using video_widevine::License; using video_widevine::LicenseRequest; using video_widevine::SignedMessage; using video_widevine::SignedProvisioningMessage; namespace wvcdm { namespace { void DumpHeader(std::ofstream* out, const std::string& type) { const ::testing::TestInfo* const test_info = ::testing::UnitTest::GetInstance()->current_test_info(); const std::string suite = test_info->test_case_name(); const std::string name = test_info->name(); std::string new_test_name = suite + "_" + name; // Replace the slashes with underscores so we can use it as a name again. std::replace(new_test_name.begin(), new_test_name.end(), '/', '_'); *out << "\nTEST_F(ODKGolden" << type << "V" << ODK_MAJOR_VERSION << ", " << new_test_name << ") {\n"; } void DumpHex(std::ofstream* out, const std::string& name, const std::string& value) { const auto out_flags = out->flags(); *out << "const uint8_t " << name << "_raw[] = {\n"; *out << " "; for (size_t i = 0; i < value.length(); i++) { if ((i > 0) && (i % 10 == 0)) *out << "\n "; uint8_t c = value[i]; *out << "0x" << std::hex << std::setw(2) << std::setfill('0') << int(c) << ", "; } *out << "\n};\n"; *out << name << "_ = std::string (\n" << " reinterpret_cast(" << name << "_raw), \n" << " sizeof(" << name << "_raw));\n"; out->flags(out_flags); // Restore flags when we're done. } void LogTimer(const char* field, bool set, int64_t time) { long long total_seconds = static_cast(time); char sign = ' '; if (total_seconds < 0) { total_seconds = -total_seconds; sign = '-'; } const unsigned long long seconds = total_seconds % 60; unsigned long long minutes = total_seconds / 60; unsigned long long hours = minutes / 60; minutes = minutes % 60; unsigned long long days = hours / 24; hours = hours % 24; if (set) { LOGD("%25.25s: %c%llu seconds = %llu day %02llu:%02llu:%02llu HMS", field, sign, total_seconds, days, hours, minutes, seconds); } else { LOGD("%25.25s: %llu seconds (unset)", field, total_seconds); } } void PrintRequestProto(const char* label, const video_widevine::LicenseRequest& request) { switch (request.type()) { case video_widevine::LicenseRequest_RequestType_NEW: LOGD("%s type: NEW", label); break; case video_widevine::LicenseRequest_RequestType_RENEWAL: LOGD("%s type: RENEWAL", label); break; case video_widevine::LicenseRequest_RequestType_RELEASE: LOGD("%s type: RELEASE", label); break; } } void PrintLicenseProto(const char* label, const video_widevine::License& license) { LOGD("%s: can_play = %s, can_persist = %s, can_renew = %s", label, license.policy().can_play() ? "true" : "false", license.policy().can_persist() ? "true" : "false", license.policy().can_renew() ? "true" : "false"); #define QUOTE_DEFINE(A) #A #define QUOTE(A) QUOTE_DEFINE(A) #define LOG_TIMER(NAME) \ LogTimer(QUOTE(NAME), license.policy().has_##NAME(), license.policy().NAME()) LOG_TIMER(rental_duration_seconds); LOG_TIMER(playback_duration_seconds); LOG_TIMER(license_duration_seconds); LOG_TIMER(renewal_recovery_duration_seconds); LOG_TIMER(renewal_delay_seconds); LOG_TIMER(renewal_retry_interval_seconds); LOGD("renewal_server_url: %s", license.policy().renewal_server_url().c_str()); LOGD("renew_with_usage: %s", license.policy().renew_with_usage() ? "true" : "false"); LOG_TIMER(play_start_grace_period_seconds); } } // namespace std::ofstream MessageDumper::license_file; std::ofstream MessageDumper::renewal_file; std::ofstream MessageDumper::provision_file; void MessageDumper::SetUp() { LOGD("Creating golden data files for ODK golden data tests."); license_file.open("license_data.cpp"); if (!license_file) LOGE("Could not open dump file license_data.cpp"); renewal_file.open("renewal_data.cpp"); if (!renewal_file) LOGE("Could not open dump file renewal_data.cpp"); provision_file.open("provision_data.cpp"); if (!provision_file) LOGE("Could not open dump file provision_data.cpp"); } void MessageDumper::TearDown() { LOGD("Closing golden data files."); license_file.close(); renewal_file.close(); provision_file.close(); } void MessageDumper::PrintLicenseRequest(const CdmKeyRequest& request) { SignedMessage signed_message; ASSERT_TRUE(signed_message.ParseFromString(request.message)); if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { ASSERT_TRUE(signed_message.has_oemcrypto_core_message()); const std::string& core_request = signed_message.oemcrypto_core_message(); oemcrypto_core_message::ODK_LicenseRequest core_request_data; ASSERT_TRUE( oemcrypto_core_message::deserialize::CoreLicenseRequestFromMessage( core_request, &core_request_data)); // Print core message information for the license request. std::cout << std::endl << "License Request Information:" << std::endl; std::cout << " License Request Core Version = " << core_request_data.api_major_version << "." << core_request_data.api_minor_version << std::endl; std::cout << " Nonce = " << core_request_data.nonce << std::endl; std::cout << " Session ID = " << core_request_data.session_id << std::endl; } } void MessageDumper::PrintLicenseResponse(const std::string& response) { SignedMessage signed_response; ASSERT_TRUE(signed_response.ParseFromString(response)); if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { ASSERT_TRUE(signed_response.has_oemcrypto_core_message()); video_widevine::License license; ASSERT_TRUE(license.ParseFromString(signed_response.msg())); std::string message = signed_response.oemcrypto_core_message() + signed_response.msg(); ODK_Message odk_msg = ODK_Message_Create( reinterpret_cast(const_cast(message.c_str())), message.length()); ODK_Message_SetSize(&odk_msg, signed_response.oemcrypto_core_message().length()); ODK_ParsedLicense odk_parsed_license = {}; ODK_LicenseResponse odk_license_response = {}; odk_license_response.parsed_license = &odk_parsed_license; Unpack_ODK_LicenseResponse(&odk_msg, &odk_license_response); ASSERT_EQ(ODK_Message_GetStatus(&odk_msg), MESSAGE_STATUS_OK); // Print core message information for the license response. std::cout << std::endl << "License Response Information:" << std::endl; std::cout << " License Response Core Version = " << odk_license_response.core_message.nonce_values.api_major_version << "." << odk_license_response.core_message.nonce_values.api_minor_version << std::endl; std::cout << " Nonce = " << odk_license_response.core_message.nonce_values.nonce << std::endl; std::cout << " Session ID = " << odk_license_response.core_message.nonce_values.session_id << std::endl; } } void MessageDumper::DumpLicenseRequest(const CdmKeyRequest& request) { SignedMessage signed_message; DumpHeader(&license_file, "License"); EXPECT_TRUE(signed_message.ParseFromString(request.message)); if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { EXPECT_TRUE(signed_message.has_oemcrypto_core_message()); DumpHex(&license_file, "core_request", signed_message.oemcrypto_core_message()); } // Since this is run within a test, we can also verify that the // request is valid. video_widevine::LicenseRequest license_request; EXPECT_TRUE(license_request.ParseFromString(signed_message.msg())); // TODO(fredgc): figure out if we can build tests with full protobufs // instead of proto lite, so that we can use TextFormat. PrintRequestProto("License Request", license_request); } void MessageDumper::DumpLicense(const std::string& response) { SignedMessage signed_response; EXPECT_TRUE(signed_response.ParseFromString(response)); if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { EXPECT_TRUE(signed_response.has_oemcrypto_core_message()); DumpHex(&license_file, "core_response", signed_response.oemcrypto_core_message()); } video_widevine::License license; EXPECT_TRUE(license.ParseFromString(signed_response.msg())); PrintLicenseProto("License", license); DumpHex(&license_file, "serialized_license", signed_response.msg()); std::string message = signed_response.oemcrypto_core_message() + signed_response.msg(); if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { ODK_Message odk_msg = ODK_Message_Create( reinterpret_cast(const_cast(message.c_str())), message.length()); ODK_Message_SetSize(&odk_msg, signed_response.oemcrypto_core_message().length()); ODK_ParsedLicense odk_parsed_license = {}; ODK_LicenseResponse odk_license_response = {}; odk_license_response.parsed_license = &odk_parsed_license; Unpack_ODK_LicenseResponse(&odk_msg, &odk_license_response); EXPECT_EQ(ODK_Message_GetStatus(&odk_msg), MESSAGE_STATUS_OK); // Valid hash is only needed for v16 messages. std::string hash(ODK_SHA256_HASH_SIZE, ' '); DumpHex(&license_file, "core_request_sha256", hash); license_file << " nonce_required_ = " << (odk_parsed_license.nonce_required ? "true" : "false") << ";\n"; } license_file << " RunTest();\n"; license_file << "}\n\n"; } void MessageDumper::DumpRenewalRequest(const CdmKeyRequest& request) { DumpHeader(&renewal_file, "Renewal"); SignedMessage signed_message; EXPECT_TRUE(signed_message.ParseFromString(request.message)); if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { EXPECT_TRUE(signed_message.has_oemcrypto_core_message()); DumpHex(&renewal_file, "core_request", signed_message.oemcrypto_core_message()); } video_widevine::LicenseRequest renewal_request; EXPECT_TRUE(renewal_request.ParseFromString(signed_message.msg())); PrintRequestProto("Renewal Request", renewal_request); } void MessageDumper::DumpRenewal(const std::string& response) { SignedMessage signed_response; EXPECT_TRUE(signed_response.ParseFromString(response)) << "Response = " << wvutil::b2a_hex(response); if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { EXPECT_TRUE(signed_response.has_oemcrypto_core_message()); DumpHex(&renewal_file, "core_response", signed_response.oemcrypto_core_message()); } video_widevine::License renewal; EXPECT_TRUE(renewal.ParseFromString(signed_response.msg())); PrintLicenseProto("Renewal", renewal); DumpHex(&renewal_file, "renewal", signed_response.msg()); if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { std::string message = signed_response.oemcrypto_core_message() + signed_response.msg(); ODK_Message odk_msg = ODK_Message_Create( reinterpret_cast(const_cast(message.c_str())), message.length()); ODK_Message_SetSize(&odk_msg, signed_response.oemcrypto_core_message().length()); ODK_RenewalResponse odk_renewal_response = {}; Unpack_ODK_RenewalResponse(&odk_msg, &odk_renewal_response); EXPECT_EQ(ODK_Message_GetStatus(&odk_msg), MESSAGE_STATUS_OK); renewal_file << " renewal_duration_seconds_ = " << odk_renewal_response.renewal_duration_seconds << ";\n"; } renewal_file << " RunTest();\n"; renewal_file << "}\n\n"; } void MessageDumper::PrintProvisioningRequest( const CdmProvisioningRequest& request) { SignedProvisioningMessage signed_message; ASSERT_TRUE(signed_message.ParseFromString(request)); if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { ASSERT_TRUE(signed_message.has_oemcrypto_core_message()); const std::string& core_request = signed_message.oemcrypto_core_message(); oemcrypto_core_message::ODK_ProvisioningRequest core_request_data; ASSERT_TRUE( oemcrypto_core_message::deserialize::CoreProvisioningRequestFromMessage( core_request, &core_request_data)); // Print core message information for the provisioning request. if (wvoec::global_features.derive_key_method == wvoec::DeviceFeatures::TEST_PROVISION_40) { std::cout << std::endl << "Provisioning 4.0 Request Information:" << std::endl; std::cout << " Provisioning 4.0 Request Core Version = " << core_request_data.api_major_version << "." << core_request_data.api_minor_version << std::endl; } else { std::cout << std::endl << "Provisioning Request Information:" << std::endl; std::cout << " Provisioning Request Core Version = " << core_request_data.api_major_version << "." << core_request_data.api_minor_version << std::endl; } std::cout << " Nonce = " << core_request_data.nonce << std::endl; std::cout << " Session ID = " << core_request_data.session_id << std::endl; } } void MessageDumper::PrintProvisioningResponse( const CdmProvisioningResponse& response) { if (wvoec::global_features.derive_key_method == wvoec::DeviceFeatures::TEST_PROVISION_40) { std::cout << " Provisioning 4.0 does not have a response. " << std::endl; return; } SignedProvisioningMessage signed_response; if (!signed_response.ParseFromString(response)) { // A binary provisioning response is buried within a json structure. std::string extracted_message; ASSERT_TRUE(CertificateProvisioning::ExtractAndDecodeSignedMessage( response, &extracted_message)); ASSERT_TRUE(signed_response.ParseFromString(extracted_message)); } if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { ASSERT_TRUE(signed_response.has_oemcrypto_core_message()); std::string message = signed_response.oemcrypto_core_message() + signed_response.message(); ODK_Message odk_msg = ODK_Message_Create( reinterpret_cast(const_cast(message.c_str())), message.length()); ODK_Message_SetSize(&odk_msg, signed_response.oemcrypto_core_message().length()); ODK_ParsedProvisioning odk_parsed_provisioning; // For v17 and earlier, we use a special v16/v17 provisioning response type. // So need to separate the way we unpack the provisioning response into // v16/v17 and v18+. if (wvoec::global_features.api_version > 17) { ODK_ProvisioningResponse odk_provisioning_response; odk_provisioning_response.parsed_provisioning = &odk_parsed_provisioning; Unpack_ODK_ProvisioningResponse(&odk_msg, &odk_provisioning_response); EXPECT_EQ(ODK_Message_GetStatus(&odk_msg), MESSAGE_STATUS_OK); // Print core message information for the provisioning response. std::cout << std::endl << "Provisioning Response Information:" << std::endl; std::cout << " Provisioning Response Core Version = " << odk_provisioning_response.core_message.nonce_values .api_major_version << "." << odk_provisioning_response.core_message.nonce_values .api_minor_version << std::endl; std::cout << " Nonce = " << odk_provisioning_response.core_message.nonce_values.nonce << std::endl; std::cout << " Session ID = " << odk_provisioning_response.core_message.nonce_values.session_id << std::endl; std::cout << " Key Type = " << ((odk_parsed_provisioning.key_type == OEMCrypto_RSA_Private_Key) ? "OEMCrypto_RSA_Private_Key" : "OEMCrypto_ECC_Private_Key") << std::endl; } else { // ODK_ParsedProvisioning odk_parsed_provisioning; ODK_ProvisioningResponseV16 odk_provisioning_response; odk_provisioning_response.parsed_provisioning = &odk_parsed_provisioning; Unpack_ODK_ProvisioningResponseV16(&odk_msg, &odk_provisioning_response); EXPECT_EQ(ODK_Message_GetStatus(&odk_msg), MESSAGE_STATUS_OK); // Print core message information for the provisioning response. std::cout << std::endl << "Provisioning Response Information:" << std::endl; std::cout << " Provisioning Response Core Version = " << odk_provisioning_response.request.core_message.nonce_values .api_major_version << "." << odk_provisioning_response.request.core_message.nonce_values .api_minor_version << std::endl; std::cout << " Nonce = " << odk_provisioning_response.request.core_message.nonce_values.nonce << std::endl; std::cout << " Session ID = " << odk_provisioning_response.request.core_message.nonce_values .session_id << std::endl; std::cout << " Key Type = " << ((odk_parsed_provisioning.key_type == OEMCrypto_RSA_Private_Key) ? "OEMCrypto_RSA_Private_Key" : "OEMCrypto_ECC_Private_Key") << std::endl; } } } void MessageDumper::DumpProvisioningRequest( const CdmProvisioningRequest& request) { if (wvoec::global_features.derive_key_method == wvoec::DeviceFeatures::TEST_PROVISION_40) { // The ODKGoldenProvision40V19 test will have its own class for now since // we are only testing the request. DumpHeader(&provision_file, "Provision40"); } else { DumpHeader(&provision_file, "Provision"); } SignedProvisioningMessage signed_message; EXPECT_TRUE(signed_message.ParseFromString(request)) << "Request = " << wvutil::b2a_hex(request); if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { EXPECT_TRUE(signed_message.has_oemcrypto_core_message()); DumpHex(&provision_file, "core_request", signed_message.oemcrypto_core_message()); } } void MessageDumper::DumpProvisioning(const CdmProvisioningResponse& response) { if (wvoec::global_features.derive_key_method == wvoec::DeviceFeatures::TEST_PROVISION_40) { LOGD( "Provisioning 4.0 does not have a v17, v18 or v19 core message in " "the " "response."); provision_file << " RunTest();\n"; provision_file << "}\n\n"; } else { SignedProvisioningMessage signed_response; if (!signed_response.ParseFromString(response)) { // A binary provisioning response is buried within a json structure. std::string extracted_message; EXPECT_TRUE(CertificateProvisioning::ExtractAndDecodeSignedMessage( response, &extracted_message)); EXPECT_TRUE(signed_response.ParseFromString(extracted_message)); } if (wvoec::global_features.api_version >= wvoec::kCoreMessagesAPI) { EXPECT_TRUE(signed_response.has_oemcrypto_core_message()); DumpHex(&provision_file, "core_response", signed_response.oemcrypto_core_message()); } DumpHex(&provision_file, "provisioning_response", signed_response.message()); // The choice of ECC or RSA key is decided at the server, based on // information in the DCSL. We can only reproduce this by looking // at the current response. std::string message = signed_response.oemcrypto_core_message() + signed_response.message(); ODK_Message odk_msg = ODK_Message_Create( reinterpret_cast(const_cast(message.c_str())), message.length()); ODK_Message_SetSize(&odk_msg, signed_response.oemcrypto_core_message().length()); ODK_ParsedProvisioning odk_parsed_provisioning; ODK_ProvisioningResponse odk_provisioning_response; odk_provisioning_response.parsed_provisioning = &odk_parsed_provisioning; Unpack_ODK_ProvisioningResponse(&odk_msg, &odk_provisioning_response); EXPECT_EQ(ODK_Message_GetStatus(&odk_msg), MESSAGE_STATUS_OK); provision_file << " device_key_type_ = " << ((odk_parsed_provisioning.key_type == OEMCrypto_RSA_Private_Key) ? "OEMCrypto_RSA_Private_Key;\n" : "OEMCrypto_ECC_Private_Key;\n"); provision_file << " RunTest();\n"; provision_file << "}\n\n"; } } } // namespace wvcdm