From 790799ceaa55d15b130428ae1efeec3ede1b7b36 Mon Sep 17 00:00:00 2001 From: Fred Gylys-Colwell Date: Fri, 3 Aug 2018 17:09:47 -0700 Subject: [PATCH] Refactor provisioning tests Merge from Widevine repo of http://go/wvgerrit/56522 This CL moves provisioning from core/test/cdm_engine_test.cpp to test_base.cpp because other tests should also only be run when the device has been provisioned. It also adds a fake license server. The license holder helps a test create a license request and then generates a bare-bones license, without actually sending anything to a real license server. Test: more unit tests pass than before. Bug: 72354901 Fix Generic Crypto tests. Change-Id: Iec067a6a1fb91fa8fd7b904fdf36e90981e293a3 --- .../cdm/core/test/cdm_engine_test.cpp | 352 ++++----------- libwvdrmengine/cdm/core/test/test_base.cpp | 406 ++++++++++++++++++ libwvdrmengine/cdm/core/test/test_base.h | 92 +++- 3 files changed, 574 insertions(+), 276 deletions(-) diff --git a/libwvdrmengine/cdm/core/test/cdm_engine_test.cpp b/libwvdrmengine/cdm/core/test/cdm_engine_test.cpp index 9989687e..ef77df4f 100644 --- a/libwvdrmengine/cdm/core/test/cdm_engine_test.cpp +++ b/libwvdrmengine/cdm/core/test/cdm_engine_test.cpp @@ -31,97 +31,23 @@ namespace wvcdm { -using drm_metrics::WvCdmMetrics; using drm_metrics::DistributionMetric; +using drm_metrics::WvCdmMetrics; namespace { // Http OK response code. const int kHttpOk = 200; -// TODO(fredgc): Move these to test_base.cpp in next CL. -// Default license server, can be configured using --server command line option -// Default key id (pssh), can be configured using --keyid command line option -std::string g_client_auth; -KeyId g_key_id_pssh; -KeyId g_key_id_unwrapped; -CdmKeySystem g_key_system; -std::string g_license_server; -std::string g_provisioning_server; -std::string g_provisioning_service_certificate; -std::string g_license_service_certificate; - const std::string kCencMimeType = "video/mp4"; const std::string kWebmMimeType = "video/webm"; -static void CommonSetup(ServerConfigurationId which, - bool bin_prov = false) { - - Properties::set_provisioning_messages_are_binary(bin_prov); - Properties::Init(); - - // NOTE: Select configuration - ConfigTestEnv config(which); - - g_client_auth.assign(config.client_auth()); - g_key_system.assign(config.key_system()); - g_license_server.assign(config.license_server()); - g_key_id_pssh.assign(a2bs_hex(config.key_id())); - g_provisioning_service_certificate.assign( - config.provisioning_service_certificate()); - g_license_service_certificate.assign(config.license_service_certificate()); - g_provisioning_server.assign(config.provisioning_server()); - - // Extract the key ID from the PSSH box. - InitializationData extractor(CENC_INIT_DATA_FORMAT, g_key_id_pssh); - g_key_id_unwrapped = extractor.data(); -} - -/* - * Locate the portion of the server's response message that is between - * the strings jason_start_substr and json_end_substr. Returns the string - * through *result. If the start substring match fails, assume the entire - * string represents a serialized protobuf mesaage and return true with - * the entire string. If the end_substring match fails, return false with - * an empty *result. - */ -bool ExtractSignedMessage(const std::string& response, - const std::string& json_start_substr, - const std::string& json_end_substr, - std::string* result) { - std::string response_string; - size_t start = response.find(json_start_substr); - - if (start == response.npos) { - // Assume serialized protobuf message. - result->assign(response); - } else { - // Assume JSON-wrapped protobuf. - size_t end = response.find(json_end_substr, - start + json_start_substr.length()); - if (end == response.npos) { - LOGE("ExtractSignedMessage cannot locate end substring"); - result->clear(); - return false; - } - size_t result_string_size = end - start - json_start_substr.length(); - result->assign(response, start + json_start_substr.length(), - result_string_size); - } - - if (result->empty()) { - LOGE("ExtractSignedMessage: Response message is empty"); - return false; - } - return true; -} - } // namespace class WvCdmEnginePreProvTest : public WvCdmTestBase { public: - WvCdmEnginePreProvTest() : cdm_engine_(&file_system_), - session_opened_(false) {} + WvCdmEnginePreProvTest() + : cdm_engine_(&file_system_), session_opened_(false) {} virtual ~WvCdmEnginePreProvTest() {} @@ -132,10 +58,11 @@ class WvCdmEnginePreProvTest : public WvCdmTestBase { virtual void OpenSession() { CdmResponseType status = - cdm_engine_.OpenSession(g_key_system, NULL, NULL, &session_id_); + cdm_engine_.OpenSession(config_.key_system(), NULL, NULL, &session_id_); if (status == NEED_PROVISIONING) { Provision(); - status = cdm_engine_.OpenSession(g_key_system, NULL, NULL, &session_id_); + status = cdm_engine_.OpenSession(config_.key_system(), NULL, NULL, + &session_id_); } ASSERT_EQ(status, NO_ERROR); ASSERT_NE("", session_id_) << "Could not open CDM session."; @@ -157,13 +84,12 @@ class WvCdmEnginePreProvTest : public WvCdmTestBase { } protected: - // Trade request for response via the license server. virtual bool LicenseServerRequestResponse(const std::string& request, std::string* response) { LOGV("LicenseServerRequestResponse: server url: %s", - g_license_server.c_str()); - UrlRequest url_request(g_license_server + g_client_auth); + config_.license_server().c_str()); + UrlRequest url_request(config_.license_server() + config_.client_auth()); url_request.PostRequest(request); std::string http_response; @@ -178,55 +104,12 @@ class WvCdmEnginePreProvTest : public WvCdmTestBase { license_request.GetDrmMessage(http_response, *response); LOGV("response: size=%u, string:\n%s\n", response->size(), - Base64SafeEncode(std::vector(response->begin(), - response->end())).c_str()); + Base64SafeEncode( + std::vector(response->begin(), response->end())) + .c_str()); return true; } - virtual void Provision() { - LOGV("WvCdmEnginePreProvTest::Provision: url=%s", - g_provisioning_server.c_str()); - CdmProvisioningRequest prov_request; - std::string provisioning_server_url; - CdmCertificateType cert_type = kCertificateWidevine; - std::string cert_authority; - std::string cert, wrapped_key; - - CdmResponseType result = NO_ERROR; - for(int i = 0; i < 2; ++i) { // Retry once if there is a nonce problem. - result = cdm_engine_.GetProvisioningRequest( - cert_type, cert_authority, g_provisioning_service_certificate, - &prov_request, &provisioning_server_url); - if (result == CERT_PROVISIONING_NONCE_GENERATION_ERROR) { - LOGW("Woops. Nonce problem. Try again?"); - sleep(1); - } else { - break; - } - } - ASSERT_EQ(NO_ERROR, result); - - LOGV("WvCdmEnginePreProvTest::Provision: req=%s", prov_request.c_str()); - - // Ignore URL provided by CdmEngine. Use ours, as configured - // for test vs. production server. - provisioning_server_url.assign(g_provisioning_server); - UrlRequest url_request(provisioning_server_url); - EXPECT_TRUE(url_request.is_connected()); - url_request.PostCertRequestInQueryString(prov_request); - std::string http_message; - bool ok = url_request.GetResponse(&http_message); - EXPECT_TRUE(ok) << http_message; - - LOGV("WvCdmEnginePreProvTest::Provision: http_message: \n%s\n", - http_message.c_str()); - - ASSERT_EQ(NO_ERROR, - cdm_engine_.HandleProvisioningResponse(http_message, - &cert, &wrapped_key)) - << "message = " << http_message; - } - FileSystem file_system_; CdmEngine cdm_engine_; bool session_opened_; @@ -236,125 +119,44 @@ class WvCdmEnginePreProvTest : public WvCdmTestBase { class WvCdmEnginePreProvTestStaging : public WvCdmEnginePreProvTest { public: - WvCdmEnginePreProvTestStaging() {} - - virtual ~WvCdmEnginePreProvTestStaging() {} - - static void SetUpTestCase() { - // NOTE: Select server configuration - CommonSetup(kContentProtectionStagingServer); + WvCdmEnginePreProvTestStaging() { + config_ = ConfigTestEnv(kContentProtectionStagingServer); } }; class WvCdmEnginePreProvTestProd : public WvCdmEnginePreProvTest { public: - WvCdmEnginePreProvTestProd() {} - - virtual ~WvCdmEnginePreProvTestProd() {} - - static void SetUpTestCase() { - // NOTE: Select server configuration - CommonSetup(kContentProtectionProductionServer); + WvCdmEnginePreProvTestProd() { + config_ = ConfigTestEnv(kContentProtectionProductionServer); } }; class WvCdmEnginePreProvTestUat : public WvCdmEnginePreProvTest { public: - WvCdmEnginePreProvTestUat() {} - - virtual ~WvCdmEnginePreProvTestUat() {} - - static void SetUpTestCase() { - // NOTE: Select server configuration - CommonSetup(kContentProtectionUatServer); + WvCdmEnginePreProvTestUat() { + config_ = ConfigTestEnv(kContentProtectionUatServer); } }; class WvCdmEnginePreProvTestUatBinary : public WvCdmEnginePreProvTest { public: - WvCdmEnginePreProvTestUatBinary() {} - - virtual ~WvCdmEnginePreProvTestUatBinary() {} - - static void SetUpTestCase() { - // NOTE: Select server configuration + WvCdmEnginePreProvTestUatBinary() { + config_ = ConfigTestEnv(kContentProtectionUatServer); // Override default setting of provisioning_messages_are_binary property - CommonSetup(kContentProtectionUatServer, true); + binary_provisioning_ = true; } - - protected: - - virtual void Provision() { - LOGV("WvCdmEnginePreProvTestProv30Binary::Provision: url=%s", - g_provisioning_server.c_str()); - CdmProvisioningRequest binary_prov_request; - std::string provisioning_server_url; - CdmCertificateType cert_type = kCertificateWidevine; - std::string cert_authority; - std::string cert, wrapped_key; - ASSERT_EQ(NO_ERROR, cdm_engine_.GetProvisioningRequest( - cert_type, cert_authority, g_provisioning_service_certificate, - &binary_prov_request, &provisioning_server_url)); - - // prov_request is binary - base64 encode it - std::string prov_request(Base64SafeEncodeNoPad( - std::vector(binary_prov_request.begin(), - binary_prov_request.end()))); - - LOGV("WvCdmEnginePreProvTest::Provision: req=%s", prov_request.c_str()); - - // Ignore URL provided by CdmEngine. Use ours, as configured - // for test vs. production server. - provisioning_server_url.assign(g_provisioning_server); - UrlRequest url_request(provisioning_server_url); - EXPECT_TRUE(url_request.is_connected()); - url_request.PostCertRequestInQueryString(prov_request); - std::string http_message; - bool ok = url_request.GetResponse(&http_message); - EXPECT_TRUE(ok); - - LOGV("WvCdmEnginePreProvTest::Provision: http_message: \n%s\n", - http_message.c_str()); - - // extract provisioning response from received message - // Extracts signed response from JSON string, result is serialized protobuf. - const std::string kMessageStart = "\"signedResponse\": \""; - const std::string kMessageEnd = "\""; - std::string protobuf_response; - EXPECT_TRUE (ExtractSignedMessage(http_message, kMessageStart, kMessageEnd, - &protobuf_response)) << - "Failed to extract signed serialized response from JSON response"; - - LOGV("WvCdmEnginePreProvTest::Provision: extracted response " - "message: \n%s\n", protobuf_response.c_str()); - - // base64 decode response to yield binary protobuf - std::vector response_vec(Base64SafeDecode( - std::string(protobuf_response.begin(), protobuf_response.end()))); - std::string binary_protobuf_response(response_vec.begin(), - response_vec.end()); - ASSERT_EQ(NO_ERROR, - cdm_engine_.HandleProvisioningResponse(binary_protobuf_response, - &cert, &wrapped_key)); - } - }; class WvCdmEngineTest : public WvCdmEnginePreProvTest { public: WvCdmEngineTest() {} - static void SetUpTestCase() { - // NOTE: Select server configuration - CommonSetup(kContentProtectionUatServer); - } - virtual void SetUp() { CdmResponseType status = - cdm_engine_.OpenSession(g_key_system, NULL, NULL, &session_id_); + cdm_engine_.OpenSession(config_.key_system(), NULL, NULL, &session_id_); if (status == NEED_PROVISIONING) { Provision(); - status = cdm_engine_.OpenSession(g_key_system, NULL, NULL, &session_id_); + status = cdm_engine_.OpenSession(config_.key_system(), NULL, NULL, &session_id_); } ASSERT_EQ(NO_ERROR, status); ASSERT_NE("", session_id_) << "Could not open CDM session."; @@ -362,7 +164,6 @@ class WvCdmEngineTest : public WvCdmEnginePreProvTest { } protected: - void GenerateKeyRequest(const std::string& key_id, const std::string& init_data_type_string) { CdmAppParameterMap app_parameters; @@ -421,8 +222,8 @@ class WvCdmEngineTest : public WvCdmEnginePreProvTest { EXPECT_TRUE(ok); int status_code = url_request.GetStatusCode(response); - if (expect_success) EXPECT_EQ(kHttpOk, status_code) - << "Error response: " << response; + if (expect_success) + EXPECT_EQ(kHttpOk, status_code) << "Error response: " << response; if (status_code != kHttpOk) { return ""; @@ -432,7 +233,8 @@ class WvCdmEngineTest : public WvCdmEnginePreProvTest { lic_request.GetDrmMessage(response, drm_msg); LOGV("drm msg: %u bytes\r\n%s", drm_msg.size(), HexEncode(reinterpret_cast(drm_msg.data()), - drm_msg.size()).c_str()); + drm_msg.size()) + .c_str()); return drm_msg; } } @@ -441,8 +243,7 @@ class WvCdmEngineTest : public WvCdmEnginePreProvTest { const std::string& client_auth) { std::string resp = GetKeyRequestResponse(server_url, client_auth); CdmKeySetId key_set_id; - EXPECT_EQ(KEY_ADDED, - cdm_engine_.AddKey(session_id_, resp, &key_set_id)); + EXPECT_EQ(KEY_ADDED, cdm_engine_.AddKey(session_id_, resp, &key_set_id)); VerifyLicenseRequestLatency(kKeyRequestTypeInitial, *cdm_engine_.GetMetrics()); } @@ -469,8 +270,9 @@ class WvCdmEngineTest : public WvCdmEnginePreProvTest { j++) { DistributionMetric latency_distribution = session_metrics.cdm_session_license_request_latency_ms(j); - if (latency_distribution.attributes().key_request_type() - == key_request_type && latency_distribution.operation_count() > 0) { + if (latency_distribution.attributes().key_request_type() == + key_request_type && + latency_distribution.operation_count() > 0) { has_request_type = true; } } @@ -487,88 +289,88 @@ class WvCdmEngineTest : public WvCdmEnginePreProvTest { // Tests to validate service certificate TEST_F(WvCdmEnginePreProvTestUat, ProvisioningServiceCertificateValidTest) { - ASSERT_EQ( - cdm_engine_.ValidateServiceCertificate( - g_provisioning_service_certificate), - NO_ERROR); + ASSERT_EQ(cdm_engine_.ValidateServiceCertificate( + config_.provisioning_service_certificate()), + NO_ERROR); }; TEST_F(WvCdmEnginePreProvTestUat, ProvisioningServiceCertificateInvalidTest) { - std::string certificate = g_provisioning_service_certificate; + std::string certificate = config_.provisioning_service_certificate(); // Add four nulls to the beginning of the cert to invalidate it certificate.insert(0, 4, '\0'); ASSERT_NE(cdm_engine_.ValidateServiceCertificate(certificate), NO_ERROR); }; -// Test that provisioning works, even if device is already provisioned. -TEST_F(WvCdmEnginePreProvTestStaging, DISABLED_ProvisioningTest) { - uint32_t nonce = 0; - uint8_t buffer[1]; - size_t size = 0; - - int result = OEMCrypto_RewrapDeviceRSAKey( - 0, buffer, 0, buffer, 0, &nonce, buffer, 0, buffer, buffer, &size); - int result30 = OEMCrypto_RewrapDeviceRSAKey30( - 0, &nonce, buffer, 0, buffer, 0, buffer, buffer, &size); - int method = OEMCrypto_GetProvisioningMethod(kLevelDefault); - - if (result == OEMCrypto_ERROR_NOT_IMPLEMENTED && - result30 == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - LOGW("WARNING: Skipping ProvisioningTest because the device does not " - "support provisioning. If you are using a baked-in certificate, " - "this is expected. Otherwise, something is wrong."); - ASSERT_EQ(method, OEMCrypto_DrmCertificate); - } else { - if (result == OEMCrypto_ERROR_NOT_IMPLEMENTED) { - ASSERT_EQ(method, OEMCrypto_OEMCertificate); - } else { - ASSERT_EQ(method, OEMCrypto_Keybox); - } - } - - Provision(); -} +TEST_F(WvCdmEnginePreProvTestStaging, ProvisioningTest) { Provision(); } +// TODO(b/112046733): This test is broken. It should be fixed. TEST_F(WvCdmEnginePreProvTestUatBinary, DISABLED_ProvisioningTest) { Provision(); } -// Test that provisioning works, even if device is already provisioned. -TEST_F(WvCdmEngineTest, DISABLED_ProvisioningTest) { +// Test that provisioning works. +TEST_F(WvCdmEngineTest, ProvisioningTest) { + Provision(); +} +// Test that provisioning works, even if device is already provisioned. +TEST_F(WvCdmEngineTest, ReprovisioningTest) { + // Provision once. + Provision(); + // Verify that we can provision a second time, even though we already + // provisioned once. Provision(); } TEST_F(WvCdmEngineTest, BaseIsoBmffMessageTest) { - GenerateKeyRequest(g_key_id_pssh, kCencMimeType); - GetKeyRequestResponse(g_license_server, g_client_auth); + GenerateKeyRequest(binary_key_id(), kCencMimeType); + GetKeyRequestResponse(config_.license_server(), config_.client_auth()); } // TODO(juce): Set up with correct test data. TEST_F(WvCdmEngineTest, DISABLED_BaseWebmMessageTest) { - GenerateKeyRequest(g_key_id_unwrapped, kWebmMimeType); - GetKeyRequestResponse(g_license_server, g_client_auth); + // Extract the key ID from the PSSH box. + InitializationData extractor(CENC_INIT_DATA_FORMAT, binary_key_id()); + KeyId key_id_unwrapped = extractor.data(); + GenerateKeyRequest(key_id_unwrapped, kWebmMimeType); + GetKeyRequestResponse(config_.license_server(), config_.client_auth()); } TEST_F(WvCdmEngineTest, NormalDecryptionIsoBmff) { - GenerateKeyRequest(g_key_id_pssh, kCencMimeType); - VerifyNewKeyResponse(g_license_server, g_client_auth); + GenerateKeyRequest(binary_key_id(), kCencMimeType); + VerifyNewKeyResponse(config_.license_server(), config_.client_auth()); } // TODO(juce): Set up with correct test data. TEST_F(WvCdmEngineTest, DISABLED_NormalDecryptionWebm) { - GenerateKeyRequest(g_key_id_unwrapped, kWebmMimeType); - VerifyNewKeyResponse(g_license_server, g_client_auth); + // Extract the key ID from the PSSH box. + InitializationData extractor(CENC_INIT_DATA_FORMAT, binary_key_id()); + KeyId key_id_unwrapped = extractor.data(); + GenerateKeyRequest(key_id_unwrapped, kWebmMimeType); + VerifyNewKeyResponse(config_.license_server(), config_.client_auth()); +} + +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(KEY_SIZE, '1'); + wvoec::KeyControlBlock block = {}; + holder.AddKey("key_one", key_data, block); + holder.SignAndLoadLicense(); } TEST_F(WvCdmEngineTest, LicenseRenewal) { - GenerateKeyRequest(g_key_id_pssh, kCencMimeType); - VerifyNewKeyResponse(g_license_server, g_client_auth); + GenerateKeyRequest(binary_key_id(), kCencMimeType); + VerifyNewKeyResponse(config_.license_server(), config_.client_auth()); GenerateRenewalRequest(); - VerifyRenewalKeyResponse(server_url_.empty() ? g_license_server : server_url_, - g_client_auth); + VerifyRenewalKeyResponse( + server_url_.empty() ? config_.license_server() : server_url_, + config_.client_auth()); } } // namespace wvcdm diff --git a/libwvdrmengine/cdm/core/test/test_base.cpp b/libwvdrmengine/cdm/core/test/test_base.cpp index 48545f5c..decabfee 100644 --- a/libwvdrmengine/cdm/core/test/test_base.cpp +++ b/libwvdrmengine/cdm/core/test/test_base.cpp @@ -16,6 +16,7 @@ #include "cdm_engine.h" #include "crypto_session.h" #include "file_store.h" +#include "license.h" #include "log.h" #include "oec_device_features.h" #include "oec_test_data.h" @@ -89,10 +90,92 @@ void show_menu(char* prog_name) { << std::endl; } +/* + * Locate the portion of the server's response message that is between + * the strings jason_start_substr and json_end_substr. Returns the string + * through *result. If the start substring match fails, assume the entire + * string represents a serialized protobuf mesaage and return true with + * the entire string. If the end_substring match fails, return false with + * an empty *result. + */ +bool ExtractSignedMessage(const std::string& response, + const std::string& json_start_substr, + const std::string& json_end_substr, + std::string* result) { + std::string response_string; + size_t start = response.find(json_start_substr); + + if (start == response.npos) { + // Assume serialized protobuf message. + result->assign(response); + } else { + // Assume JSON-wrapped protobuf. + size_t end = + response.find(json_end_substr, start + json_start_substr.length()); + if (end == response.npos) { + LOGE("ExtractSignedMessage cannot locate end substring"); + result->clear(); + return false; + } + size_t result_string_size = end - start - json_start_substr.length(); + result->assign(response, start + json_start_substr.length(), + result_string_size); + } + + if (result->empty()) { + LOGE("ExtractSignedMessage: Response message is empty"); + return false; + } + return true; +} + } // namespace ConfigTestEnv WvCdmTestBase::default_config_(kContentProtectionUatServer); +void WvCdmTestBase::StripeBuffer(std::vector* buffer, size_t size, + uint8_t init) { + buffer->assign(size, 0); + for (size_t i = 0; i < size; i++) { + (*buffer)[i] = init + i % 250; + } +} + +std::string WvCdmTestBase::Aes128CbcEncrypt(std::vector key, + const std::vector& clear, + const std::vector iv) { + std::vector encrypted(clear.size()); + std::vector iv_mod(iv.begin(), iv.end()); + AES_KEY aes_key; + AES_set_encrypt_key(&key[0], 128, &aes_key); + AES_cbc_encrypt(&clear[0], &encrypted[0], clear.size(), &aes_key, &iv_mod[0], + AES_ENCRYPT); + return std::string(encrypted.begin(), encrypted.end()); +} + +std::string WvCdmTestBase::Aes128CbcDecrypt(std::vector key, + const std::vector& clear, + const std::vector iv) { + std::vector encrypted(clear.size()); + std::vector iv_mod(iv.begin(), iv.end()); + AES_KEY aes_key; + AES_set_decrypt_key(&key[0], 128, &aes_key); + AES_cbc_encrypt(&clear[0], &encrypted[0], clear.size(), &aes_key, &iv_mod[0], + AES_DECRYPT); + return std::string(encrypted.begin(), encrypted.end()); +} + +std::string WvCdmTestBase::SignHMAC(const std::string& message, + const std::vector& key) { + uint8_t signature[SHA256_DIGEST_LENGTH]; + unsigned int md_len = SHA256_DIGEST_LENGTH; + HMAC(EVP_sha256(), &key[0], key.size(), + reinterpret_cast(message.data()), message.size(), + signature, &md_len); + std::string result(signature, signature + SHA256_DIGEST_LENGTH); + return result; +} + TestCryptoSession::TestCryptoSession(metrics::CryptoMetrics* crypto_metrics) : CryptoSession(crypto_metrics) { // The first CryptoSession should have initialized OEMCrypto. This is right @@ -119,6 +202,7 @@ class TestCryptoSessionFactory : public CryptoSessionFactory { void WvCdmTestBase::SetUp() { ::testing::Test::SetUp(); + Properties::set_provisioning_messages_are_binary(binary_provisioning_); Properties::Init(); const ::testing::TestInfo* const test_info = ::testing::UnitTest::GetInstance()->current_test_info(); @@ -147,6 +231,93 @@ void WvCdmTestBase::InstallTestRootOfTrust() { } } +void WvCdmTestBase::Provision() { + CdmProvisioningRequest prov_request; + CdmProvisioningRequest binary_prov_request; + std::string provisioning_server_url; + CdmCertificateType cert_type = kCertificateWidevine; + std::string cert_authority; + std::string cert, wrapped_key; + + CdmSessionId session_id; + FileSystem file_system; + // TODO(fredgc): provision for different SPOIDs. + CdmEngine cdm_engine(&file_system); + + CdmResponseType result = cdm_engine.GetProvisioningRequest( + cert_type, cert_authority, config_.provisioning_service_certificate(), + &prov_request, &provisioning_server_url); + ASSERT_EQ(NO_ERROR, result); + + if (binary_provisioning_) { + binary_prov_request = prov_request; + prov_request = std::string(Base64SafeEncodeNoPad(std::vector( + binary_prov_request.begin(), binary_prov_request.end()))); + } + + LOGV("WvCdmTestBase::Provision: req=%s", prov_request.c_str()); + + // Ignore URL provided by CdmEngine. Use ours, as configured + // for test vs. production server. + provisioning_server_url.assign(config_.provisioning_server()); + UrlRequest url_request(provisioning_server_url); + EXPECT_TRUE(url_request.is_connected()); + url_request.PostCertRequestInQueryString(prov_request); + std::string http_message; + bool ok = url_request.GetResponse(&http_message); + EXPECT_TRUE(ok) << http_message; + + LOGV("WvCdmTestBase::Provision: http_message: \n%s\n", http_message.c_str()); + + if (binary_provisioning_) { + // extract provisioning response from received message + // Extracts signed response from JSON string, result is serialized protobuf. + const std::string kMessageStart = "\"signedResponse\": \""; + const std::string kMessageEnd = "\""; + std::string protobuf_response; + EXPECT_TRUE(ExtractSignedMessage(http_message, kMessageStart, kMessageEnd, + &protobuf_response)) + << "Failed to extract signed serialized response from JSON response"; + + LOGV("WvCdmEnginePreProvTest::Provision: extracted response message: \n" + "%s\n", protobuf_response.c_str()); + + // base64 decode response to yield binary protobuf + std::vector response_vec(Base64SafeDecode( + std::string(protobuf_response.begin(), protobuf_response.end()))); + std::string binary_protobuf_response(response_vec.begin(), + response_vec.end()); + ASSERT_EQ(NO_ERROR, cdm_engine.HandleProvisioningResponse( + binary_protobuf_response, &cert, &wrapped_key)) + << "message = " << http_message; + } else { + ASSERT_EQ(NO_ERROR, cdm_engine.HandleProvisioningResponse( + http_message, &cert, &wrapped_key)) + << "message = " << http_message; + } +} + +// TODO(fredgc): Replace this with a pre-defined DRM certificate. We could do +// that because either the device is using a known test keybox with a known +// device key, or the device is using an OEM certificate, and we can extract +// that certificate from the provisioning request. +void WvCdmTestBase::EnsureProvisioned() { + CdmSessionId session_id; + FileSystem file_system; + CdmEngine cdm_engine(&file_system); + CdmResponseType status = + cdm_engine.OpenSession(config_.key_system(), NULL, NULL, &session_id); + if (status == NEED_PROVISIONING) { + Provision(); + status = + cdm_engine.OpenSession(config_.key_system(), NULL, NULL, &session_id); + } + ASSERT_EQ(NO_ERROR, status); + ASSERT_NE("", session_id) << "Could not open CDM session."; + ASSERT_TRUE(cdm_engine.IsOpenSession(session_id)); + ASSERT_EQ(NO_ERROR, cdm_engine.CloseSession(session_id)); +} + bool WvCdmTestBase::Initialize(int argc, char **argv) { Properties::Init(); bool is_cast_receiver = false; @@ -256,4 +427,239 @@ bool WvCdmTestBase::Initialize(int argc, char **argv) { return true; } +TestLicenseHolder::TestLicenseHolder(CdmEngine* cdm_engine) + : cdm_engine_(cdm_engine), + session_opened_(false), + // Keys are initialized with simple values, and the correct size: + derived_mac_key_server_(MAC_KEY_SIZE, 'a'), + derived_mac_key_client_(MAC_KEY_SIZE, 'b'), + mac_key_server_(MAC_KEY_SIZE, 'c'), + mac_key_client_(MAC_KEY_SIZE, 'd'), + enc_key_(KEY_SIZE, 'e'), + session_key_(KEY_SIZE, 'f') {} + +TestLicenseHolder::~TestLicenseHolder() { + CloseSession(); +} + +void TestLicenseHolder::OpenSession(const std::string& key_system) { + CdmResponseType status = + cdm_engine_->OpenSession(key_system, NULL, NULL, &session_id_); + ASSERT_EQ(status, NO_ERROR); + ASSERT_NE("", session_id_) << "Could not open CDM session."; + ASSERT_TRUE(cdm_engine_->IsOpenSession(session_id_)); + session_opened_ = true; +} + +void TestLicenseHolder::CloseSession() { + if (session_opened_) { + cdm_engine_->CloseSession(session_id_); + session_opened_ = false; + } +} + +void TestLicenseHolder::GenerateKeyRequest( + const std::string& key_id, const std::string& init_data_type_string) { + ASSERT_TRUE(session_opened_); + CdmAppParameterMap app_parameters; + CdmKeySetId key_set_id; + InitializationData init_data(init_data_type_string, key_id); + CdmKeyRequest key_request; + CdmResponseType result = cdm_engine_->GenerateKeyRequest( + session_id_, key_set_id, init_data, kLicenseTypeStreaming, app_parameters, + &key_request); + EXPECT_EQ(KEY_MESSAGE, result); + signed_license_request_data_ = key_request.message; + EXPECT_EQ(kKeyRequestTypeInitial, key_request.type); +} + +void TestLicenseHolder::CreateDefaultLicense() { + video_widevine::SignedMessage signed_message; + EXPECT_TRUE(signed_message.ParseFromString(signed_license_request_data_)); + license_request_data_ = signed_message.msg(); + video_widevine::LicenseRequest license_request; + EXPECT_TRUE(license_request.ParseFromString(license_request_data_)); + video_widevine::ClientIdentification client_id = license_request.client_id(); + + EXPECT_EQ( + video_widevine::ClientIdentification_TokenType_DRM_DEVICE_CERTIFICATE, + client_id.type()); + + // Extract the RSA key from the DRM certificate. + std::string token = client_id.token(); + video_widevine::SignedDrmDeviceCertificate signed_drm_cert; + EXPECT_TRUE(signed_drm_cert.ParseFromString(token)); + video_widevine::DrmDeviceCertificate drm_cert; + EXPECT_TRUE(drm_cert.ParseFromString(signed_drm_cert.drm_certificate())); + EXPECT_TRUE(rsa_key_.Init(drm_cert.public_key())); + EXPECT_TRUE(rsa_key_.VerifySignature(signed_message.msg(), + signed_message.signature())); + + DeriveKeysFromSessionKey(); + + video_widevine::LicenseIdentification* license_id = license()->mutable_id(); + license_id->set_request_id("TestCase"); + license_id->set_session_id(session_id_); + license_id->set_type(video_widevine::STREAMING); + license_id->set_version(0); + + ::video_widevine::License_Policy* policy = license()->mutable_policy(); + policy->set_can_play(true); + policy->set_can_persist(false); + policy->set_can_renew(false); + policy->set_playback_duration_seconds(0); + policy->set_license_duration_seconds(0); + + AddMacKey(); +} + +void TestLicenseHolder::AddMacKey() { + video_widevine::License_KeyContainer* key_container = license()->add_key(); + std::vector iv(KEY_SIZE, 'v'); + std::string iv_s(iv.begin(), iv.end()); + key_container->set_iv(iv_s); + key_container->set_type(video_widevine::License_KeyContainer_KeyType_SIGNING); + + // Combine server and client mac keys. + std::vector keys(mac_key_server_); + keys.insert(keys.end(), mac_key_client_.begin(), mac_key_client_.end()); + std::string encrypted_keys = + WvCdmTestBase::Aes128CbcEncrypt(enc_key_, keys, iv); + key_container->set_key(encrypted_keys); +} + +video_widevine::License_KeyContainer* TestLicenseHolder::AddKey( + const KeyId& key_id, const std::vector& key_data, + const wvoec::KeyControlBlock& block_in) { + video_widevine::License_KeyContainer* key_container = license()->add_key(); + wvoec::KeyControlBlock block = block_in; + if (block.verification[0] == 0) { + block.verification[0] = 'k'; + block.verification[1] = 'c'; + block.verification[2] = '1'; + // This will work until oemcrypto api 20. + block.verification[3] = '0' + wvoec::global_features.api_version - 10; + } + key_container->set_id(key_id); + key_container->set_type(video_widevine::License_KeyContainer_KeyType_CONTENT); + key_container->set_level( + video_widevine::License_KeyContainer_SecurityLevel_SW_SECURE_CRYPTO); + + std::vector iv(KEY_SIZE, 'v'); + std::string iv_s(iv.begin(), iv.end()); + key_container->set_iv(iv_s); + + std::string encrypted_key_data = + WvCdmTestBase::Aes128CbcEncrypt(enc_key_, key_data, iv); + // TODO(b/111069024): remove this! + std::string padding(KEY_SIZE, '-'); + key_container->set_key(encrypted_key_data + padding); + + std::vector block_v( + reinterpret_cast(&block), + reinterpret_cast(&block) + sizeof(block)); + std::vector block_iv(KEY_SIZE, 'w'); + std::string block_iv_s(block_iv.begin(), block_iv.end()); + std::string encrypted_block = + WvCdmTestBase::Aes128CbcEncrypt(key_data, block_v, block_iv); + key_container->mutable_key_control()->set_iv(block_iv_s); + key_container->mutable_key_control()->set_key_control_block(encrypted_block); + return key_container; +} + +void TestLicenseHolder::SignAndLoadLicense() { +#if 0 // Need to turn off protobuf_lite to use this. + LOGV("License = %s\n", license_.DebugString().c_str()); +#endif + std::string license_data; + license_.SerializeToString(&license_data); + + std::string signature = + WvCdmTestBase::SignHMAC(license_data, derived_mac_key_server_); + + std::string session_key_s(session_key_.begin(), session_key_.end()); + std::string encrypted_session_key; + EXPECT_TRUE(rsa_key_.Encrypt(session_key_s, &encrypted_session_key)); + + video_widevine::SignedMessage signed_response; + signed_response.set_msg(license_data); + signed_response.set_type(video_widevine::SignedMessage_MessageType_LICENSE); + signed_response.set_session_key(encrypted_session_key); + signed_response.set_signature(signature); + + std::string response_data; + signed_response.SerializeToString(&response_data); + + CdmKeySetId key_set_id; + EXPECT_EQ(KEY_ADDED, + cdm_engine_->AddKey(session_id_, response_data, &key_set_id)); +} + +void TestLicenseHolder::DeriveKeysFromSessionKey() { + std::string context; + GenerateMacContext(license_request_data_, &context); + std::vector mac_key_context(context.begin(), context.end()); + GenerateEncryptContext(license_request_data_, &context); + std::vector enc_key_context(context.begin(), context.end()); + + ASSERT_TRUE( + DeriveKey(session_key_, mac_key_context, 1, &derived_mac_key_server_)); + std::vector mac_key_part2; + ASSERT_TRUE(DeriveKey(session_key_, mac_key_context, 2, &mac_key_part2)); + derived_mac_key_server_.insert(derived_mac_key_server_.end(), + mac_key_part2.begin(), mac_key_part2.end()); + + ASSERT_TRUE( + DeriveKey(session_key_, mac_key_context, 3, &derived_mac_key_client_)); + ASSERT_TRUE(DeriveKey(session_key_, mac_key_context, 4, &mac_key_part2)); + derived_mac_key_client_.insert(derived_mac_key_client_.end(), + mac_key_part2.begin(), mac_key_part2.end()); + + std::vector enc_key; + ASSERT_TRUE(DeriveKey(session_key_, enc_key_context, 1, &enc_key_)); +} + +bool TestLicenseHolder::DeriveKey(const std::vector& key, + const std::vector& context, + int counter, std::vector* out) { + if (key.empty() || counter > 4 || context.empty() || out == NULL) { + LOGE("DeriveKey(): bad context"); + return false; + } + const EVP_CIPHER* cipher = EVP_aes_128_cbc(); + CMAC_CTX* cmac_ctx = CMAC_CTX_new(); + + if (!cmac_ctx) { + LOGE("DeriveKey(): cmac failure"); + return false; + } + + if (!CMAC_Init(cmac_ctx, &key[0], key.size(), cipher, 0)) { + LOGE("DeriveKey(): CMAC_Init"); + CMAC_CTX_free(cmac_ctx); + return false; + } + + std::vector message; + message.push_back(counter); + message.insert(message.end(), context.begin(), context.end()); + + if (!CMAC_Update(cmac_ctx, &message[0], message.size())) { + LOGE("DeriveKey(): CMAC_Update"); + CMAC_CTX_free(cmac_ctx); + return false; + } + + size_t reslen; + uint8_t res[128]; + if (!CMAC_Final(cmac_ctx, res, &reslen)) { + LOGE("DeriveKey(): CMAC_Final"); + CMAC_CTX_free(cmac_ctx); + return false; + } + out->assign(res, res + reslen); + CMAC_CTX_free(cmac_ctx); + return true; +} + } // namespace wvcdm diff --git a/libwvdrmengine/cdm/core/test/test_base.h b/libwvdrmengine/cdm/core/test/test_base.h index b19af94e..478487c2 100644 --- a/libwvdrmengine/cdm/core/test/test_base.h +++ b/libwvdrmengine/cdm/core/test/test_base.h @@ -7,9 +7,11 @@ #include +#include "cdm_engine.h" #include "config_test_env.h" #include "crypto_session.h" #include "metrics_collections.h" +#include "oec_session_util.h" #include "string_conversions.h" namespace wvcdm { @@ -17,7 +19,7 @@ namespace wvcdm { // to configure OEMCrypto to use a test keybox. class WvCdmTestBase : public ::testing::Test { public: - WvCdmTestBase() : config_(default_config_) {} + WvCdmTestBase() : config_(default_config_), binary_provisioning_(false) {} virtual ~WvCdmTestBase() {} virtual void SetUp(); virtual std::string binary_key_id() const { return a2bs_hex(config_.key_id()); } @@ -29,6 +31,28 @@ class WvCdmTestBase : public ::testing::Test { // Install a test keybox, if appropriate. static void InstallTestRootOfTrust(); + // Send provisioning request to the server and handle response. + virtual void Provision(); + // Calls Provision() if not already provisioned. + virtual void EnsureProvisioned(); + + // Fill a buffer with some nonconstant data of the given size. The first byte + // will be set to to help you find the buffer when debugging. + static void StripeBuffer(std::vector* buffer, size_t size, + uint8_t init); + + // Helper method for doing cryptography. + static std::string Aes128CbcEncrypt(std::vector key, + const std::vector& clear, + const std::vector iv); + // Helper method for doing cryptography. + static std::string Aes128CbcDecrypt(std::vector key, + const std::vector& clear, + const std::vector iv); + // Helper method for doing cryptography. + static std::string SignHMAC(const std::string& message, + const std::vector& key); + // The default test configuration. This is influenced by command line // arguments before any tests are created. static ConfigTestEnv default_config_; @@ -36,6 +60,10 @@ class WvCdmTestBase : public ::testing::Test { // Configuration for an individual test. This is initialized to be the // default configuration, but can be modified by the test itself. ConfigTestEnv config_; + + // This should be set by test subclasses BEFORE calling SetUp -- i.e. in the + // tests's constructor. + bool binary_provisioning_; }; class TestCryptoSession : public CryptoSession { @@ -46,6 +74,68 @@ class TestCryptoSession : public CryptoSession { bool GenerateNonce(uint32_t* nonce); }; +// A holder for a license. Users of this class will first open a session with +// OpenSession, then generate a key request with GenerateKeyRequest, and then +// call CreateDefaultLicense to create a bare-bones license with no keys in it. +// The user may then access the license to adjust the policy, or use AddKey to +// add keys to the license. The license is then loaded via SignAndLoadLicense. +class TestLicenseHolder { + public: + // cdm_engine must exist and outlive the TestLicenseHolder. + TestLicenseHolder(CdmEngine *cdm_engine); + ~TestLicenseHolder(); + // Caller must ensure device already provisioned. + void OpenSession(const std::string& key_system); + void CloseSession(); + // Use the cdm_engine to generate a key request in the session. This should + // be called after OpenSession. This saves the signed license request, so + // that the DRM certificate can be extracted in CreateDefaultLicense. + void GenerateKeyRequest(const std::string& key_id, + const std::string& init_data_type_string); + // Create a bare-bones license from the license request. After this, the user + // may access and modify the license using license() below. + void CreateDefaultLicense(); + // Sign the license using the DRM certificate's RSA key. Then the license is + // passed to the cdm_engine using AddKey. After this, the license is loaded + // and the keys may be used. + void SignAndLoadLicense(); + + // The session id. This is only valid after a call to OpenSession. + const std::string& session_id() { return session_id_; } + // The license protobuf. This is only valid after CreateDefaultLicense. + video_widevine::License* license() { return &license_; }; + // Add a key with the given key control block and key data. + // If the block's verification is empty, it will be set to a valid value. + // The key data is encrypted correctly. + video_widevine::License_KeyContainer* AddKey( + const KeyId& key_id, const std::vector& key_data, + const wvoec::KeyControlBlock& block); + + private: + // Helper method to generate mac keys and encryption keys for the license. + void DeriveKeysFromSessionKey(); + // Derive a single mac key or encryption key using CMAC. + bool DeriveKey(const std::vector& key, + const std::vector& context, int counter, + std::vector* out); + // Add the mac keys to the license. + void AddMacKey(); + + CdmEngine* cdm_engine_; + std::string signed_license_request_data_; + std::string license_request_data_; + std::string session_id_; + bool session_opened_; + RsaPublicKey rsa_key_; // From the DRM Certificate. + video_widevine::License license_; + std::vector derived_mac_key_server_; + std::vector derived_mac_key_client_; + std::vector mac_key_server_; + std::vector mac_key_client_; + std::vector enc_key_; + std::vector session_key_; +}; + } // namespace wvcdm #endif // WVCDM_CORE_TEST_BASE_H_