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
This commit is contained in:
Fred Gylys-Colwell
2018-08-03 17:09:47 -07:00
parent e635d4d384
commit 790799ceaa
3 changed files with 574 additions and 276 deletions

View File

@@ -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<uint8_t>* 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<uint8_t> key,
const std::vector<uint8_t>& clear,
const std::vector<uint8_t> iv) {
std::vector<uint8_t> encrypted(clear.size());
std::vector<uint8_t> 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<uint8_t> key,
const std::vector<uint8_t>& clear,
const std::vector<uint8_t> iv) {
std::vector<uint8_t> encrypted(clear.size());
std::vector<uint8_t> 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<uint8_t>& key) {
uint8_t signature[SHA256_DIGEST_LENGTH];
unsigned int md_len = SHA256_DIGEST_LENGTH;
HMAC(EVP_sha256(), &key[0], key.size(),
reinterpret_cast<const uint8_t*>(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<uint8_t>(
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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t>& 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<uint8_t> 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<uint8_t> block_v(
reinterpret_cast<const uint8_t*>(&block),
reinterpret_cast<const uint8_t*>(&block) + sizeof(block));
std::vector<uint8_t> 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<uint8_t> mac_key_context(context.begin(), context.end());
GenerateEncryptContext(license_request_data_, &context);
std::vector<uint8_t> enc_key_context(context.begin(), context.end());
ASSERT_TRUE(
DeriveKey(session_key_, mac_key_context, 1, &derived_mac_key_server_));
std::vector<uint8_t> 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<uint8_t> enc_key;
ASSERT_TRUE(DeriveKey(session_key_, enc_key_context, 1, &enc_key_));
}
bool TestLicenseHolder::DeriveKey(const std::vector<uint8_t>& key,
const std::vector<uint8_t>& context,
int counter, std::vector<uint8_t>* 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<uint8_t> 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