Combine provisioning code for tests and improve logging

Merge from Widevine repo of http://go/wvgerrit/169018

This CL adds a provisioning holder that attempts to
provision and logs the request and response for
failures. The server team can replay the request to debug
problems on their end.

Bug: 276464340
Test: ran cast and ota tests
Change-Id: I6eed117e504ae3287f2ba16c3c507cfdc7456f8d
This commit is contained in:
Fred Gylys-Colwell
2023-05-04 21:22:53 -07:00
committed by Kyle Zhang
parent 65d52908af
commit 836b1a30a6
6 changed files with 254 additions and 245 deletions

View File

@@ -10,6 +10,7 @@
#include "create_test_file_system.h"
#include "crypto_session.h"
#include "properties.h"
#include "provisioning_holder.h"
#include "test_base.h"
#include "test_printers.h"
#include "test_sleep.h"
@@ -43,47 +44,11 @@ class CdmOtaKeyboxTest : public ::testing::Test {
void Provision(TestCdmEngine* cdm_engine) {
ConfigTestEnv config = *WvCdmTestBase::default_config_;
ProvisioningHolder provisioner(cdm_engine, config.provisioning_server(),
config.provisioning_service_certificate());
CdmCertificateType cert_type = kCertificateWidevine;
std::string cert_authority;
CdmProvisioningRequest prov_request;
std::string provisioning_server_url;
CdmResponseType result(CERT_PROVISIONING_NONCE_GENERATION_ERROR);
// Get a provisioning request. We might need one retry if there is a nonce
// flood failure.
for (int i = 0; i < 2; i++) {
result = cdm_engine->GetProvisioningRequest(
cert_type, cert_authority, config.provisioning_service_certificate(),
kLevelDefault, &prov_request, &provisioning_server_url);
if (result == CERT_PROVISIONING_NONCE_GENERATION_ERROR) {
wvutil::TestSleep::Sleep(2);
continue;
}
break;
}
ASSERT_EQ(NO_ERROR, result);
LOGV("Provisioning request: 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());
// Make request.
UrlRequest url_request(provisioning_server_url);
if (!url_request.is_connected()) {
LOGE("Failed to connect to provisioning server: url = %s",
provisioning_server_url.c_str());
}
url_request.PostCertRequestInQueryString(prov_request);
// Receive and parse response.
std::string http_message;
ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponse(&http_message))
<< "Keybox OTA provisioning request failed.";
LOGV("http_message: \n%s\n", http_message.c_str());
std::string cert, wrapped_key;
ASSERT_EQ(NO_ERROR, cdm_engine->HandleProvisioningResponse(
http_message, kLevelDefault, &cert, &wrapped_key));
constexpr bool binary_provisioning = false;
provisioner.Provision(cert_type, binary_provisioning);
}
};

View File

@@ -0,0 +1,193 @@
// 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 "provisioning_holder.h"
#include <gtest/gtest.h>
#include "cdm_engine.h"
#include "config_test_env.h"
#include "log.h"
#include "oec_device_features.h"
#include "test_printers.h"
#include "test_sleep.h"
#include "url_request.h"
namespace wvcdm {
void ProvisioningHolder::Provision(CdmCertificateType cert_type,
bool binary_provisioning) {
CdmProvisioningRequest request;
std::string provisioning_server_url;
std::string cert_authority;
CdmSessionId session_id;
if (cert_type == kCertificateX509) {
cert_authority = "cast.google.com";
}
LOGV("Provision with type %s.", CdmCertificateTypeToString(cert_type));
CdmResponseType result(CERT_PROVISIONING_NONCE_GENERATION_ERROR);
// Get a provisioning request. We might need one retry if there is a nonce
// flood failure.
for (int i = 0; i < 2 && result == CERT_PROVISIONING_NONCE_GENERATION_ERROR;
i++) {
result = cdm_engine_->GetProvisioningRequest(
cert_type, cert_authority, provisioning_service_certificate_,
kLevelDefault, &request, &provisioning_server_url);
if (result == CERT_PROVISIONING_NONCE_GENERATION_ERROR) {
wvutil::TestSleep::Sleep(2);
}
}
ASSERT_EQ(NO_ERROR, result);
LOGV("cert_authority = %s", cert_authority.c_str());
if (binary_provisioning) {
request = wvutil::Base64SafeEncodeNoPad(request);
}
LOGV("Provisioning request: req = %s", request.c_str());
// Ignore URL provided by CdmEngine. Use ours, as configured
// for test vs. production server.
provisioning_server_url.assign(provisioning_server_url_);
// Make request.
UrlRequest url_request(provisioning_server_url);
if (!url_request.is_connected()) {
LOGE("Failed to connect to provisioning server: url = %s",
provisioning_server_url.c_str());
}
url_request.PostCertRequestInQueryString(request);
// Receive and parse response.
std::string response;
ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponse(&response))
<< "Failed to fetch provisioning response. "
<< DumpProvAttempt(request, response, cert_type);
if (binary_provisioning) {
// extract provisioning response from received message
// Extracts signed response from JSON string, result is serialized
// protobuf.
std::string protobuf_response;
const bool extract_ok = ExtractSignedMessage(response, &protobuf_response);
ASSERT_TRUE(extract_ok) << "Failed to extract signed serialized "
"response from JSON response";
LOGV("Extracted response message: \n%s\n", protobuf_response.c_str());
ASSERT_FALSE(protobuf_response.empty())
<< "Protobuf response is unexpectedly empty";
// base64 decode response to yield binary protobuf
const std::vector<uint8_t> response_vec(
wvutil::Base64SafeDecode(protobuf_response));
ASSERT_FALSE(response_vec.empty())
<< "Failed to decode base64 of response: response = "
<< protobuf_response;
const std::string binary_protobuf_response(response_vec.begin(),
response_vec.end());
ASSERT_EQ(NO_ERROR, cdm_engine_->HandleProvisioningResponse(
binary_protobuf_response, kLevelDefault,
&certificate_, &wrapped_key_))
<< "Binary provisioning failed. "
<< DumpProvAttempt(request, response, cert_type);
} else {
ASSERT_EQ(NO_ERROR,
cdm_engine_->HandleProvisioningResponse(
response, kLevelDefault, &certificate_, &wrapped_key_))
<< "Non-binary provisioning failed. "
<< DumpProvAttempt(request, response, cert_type);
}
}
bool ProvisioningHolder::ExtractSignedMessage(const std::string& response,
std::string* result) {
static const std::string kMessageStart = "\"signedResponse\": \"";
static const std::string kMessageEnd = "\"";
std::string response_string;
size_t start = response.find(kMessageStart);
if (start == response.npos) {
// Assume serialized protobuf message.
result->assign(response);
} else {
// Assume JSON-wrapped protobuf.
size_t end = response.find(kMessageEnd, start + kMessageStart.length());
if (end == response.npos) {
LOGE("ExtractSignedMessage cannot locate end substring");
result->clear();
return false;
}
size_t result_string_size = end - start - kMessageStart.length();
result->assign(response, start + kMessageStart.length(),
result_string_size);
}
if (result->empty()) {
LOGE("ExtractSignedMessage: Response message is empty");
return false;
}
return true;
}
std::string ProvisioningHolder::DumpProvAttempt(const std::string& request,
const std::string& response,
CdmCertificateType cert_type) {
std::stringstream info;
info << "Cert Type: ";
PrintTo(cert_type, &info);
info << "\n";
info << "Provisioning url: " << provisioning_server_url_ << "\n";
info << "Provisioning Request: " << request << "\n\n";
info << "Provisioning Response: " << response << "\n\n";
std::string system_id;
cdm_engine_->QueryStatus(kLevelDefault, QUERY_KEY_SYSTEM_ID, &system_id);
info << "system id: " << system_id << "\n";
if (wvoec::global_features.derive_key_method ==
wvoec::DeviceFeatures::TEST_PROVISION_30) {
std::vector<uint8_t> cert;
size_t cert_length = 0;
OEMCryptoResult result = OEMCrypto_GetOEMPublicCertificate(
cert.data(), &cert_length, kLevelDefault);
if (result == OEMCrypto_ERROR_SHORT_BUFFER) {
cert.resize(cert_length);
result = OEMCrypto_GetOEMPublicCertificate(cert.data(), &cert_length,
kLevelDefault);
}
if (result != OEMCrypto_SUCCESS) {
info << "--- ERROR GETTING CERT. result=" << result;
} else {
info << "OEM Cert = (len=" << cert_length << ") "
<< wvutil::unlimited_b2a_hex(cert);
}
}
if (wvoec::global_features.derive_key_method ==
wvoec::DeviceFeatures::TEST_PROVISION_40) {
std::vector<uint8_t> bcc;
size_t bcc_length = 0;
std::vector<uint8_t> signature;
size_t signature_length = 0;
OEMCryptoResult result = OEMCrypto_GetBootCertificateChain(
bcc.data(), &bcc_length, signature.data(), &signature_length);
if (result == OEMCrypto_ERROR_SHORT_BUFFER) {
bcc.resize(bcc_length);
signature.resize(signature_length);
result = OEMCrypto_GetBootCertificateChain(
bcc.data(), &bcc_length, signature.data(), &signature_length);
}
if (result != OEMCrypto_SUCCESS) {
info << "--- ERROR GETTING BCC. result=" << result;
} else {
info << "BCC = (len=" << bcc_length << ") "
<< wvutil::unlimited_b2a_hex(bcc) << "\n"
<< "Additional Sig = (len=" << signature_length << ") "
<< wvutil::unlimited_b2a_hex(signature) << "\n";
}
}
return info.str();
}
} // namespace wvcdm

View File

@@ -0,0 +1,46 @@
// Copyright 2023 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_PROVISIONING_HOLDER_H_
#define WVCDM_CORE_TEST_PROVISIONING_HOLDER_H_
#include "test_base.h"
#include <string>
namespace wvcdm {
class ProvisioningHolder {
public:
ProvisioningHolder(TestCdmEngine* cdm_engine,
const std::string& provisioning_server_url,
const std::string& provisioning_service_certificate)
: cdm_engine_(cdm_engine),
provisioning_server_url_(provisioning_server_url),
provisioning_service_certificate_(provisioning_service_certificate) {}
void Provision(CdmCertificateType cert_type, bool binary_provisioning);
protected:
TestCdmEngine* cdm_engine_;
std::string provisioning_server_url_;
std::string provisioning_service_certificate_;
std::string certificate_;
std::string wrapped_key_;
// Locate the portion of the server's provisioning 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, std::string* result);
// Dump request and response information for use in a debug or failure log.
std::string DumpProvAttempt(const std::string& request,
const std::string& response,
CdmCertificateType cert_type);
};
} // namespace wvcdm
#endif // WVCDM_CORE_TEST_PROVISIONING_HOLDER_H_

View File

@@ -26,6 +26,7 @@
#include "oec_test_data.h"
#include "platform.h"
#include "properties.h"
#include "provisioning_holder.h"
#include "test_printers.h"
#include "test_sleep.h"
#include "url_request.h"
@@ -143,101 +144,6 @@ void show_menu(const char* prog_name, const std::string& extra_help_text) {
std::cout << extra_help_text << 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;
}
// TODO(b/242744857): This extra debugging may not be needed in all cases. When
// provisioning fails, this dumps the cert and other information.
std::string DumpProvAttempt(const std::string& url, const std::string& request,
const std::string& http_message,
CdmEngine* cdm_engine) {
std::stringstream info;
info << "Provisioning url: " << url << "\n";
info << "Request: " << wvutil::unlimited_b2a_hex(request) << "\n";
info << "http_message: " << wvutil::unlimited_b2a_hex(http_message) << "\n";
std::string system_id;
cdm_engine->QueryStatus(kLevelDefault, QUERY_KEY_SYSTEM_ID, &system_id);
info << "system id: " << system_id << "\n";
if (wvoec::global_features.derive_key_method ==
wvoec::DeviceFeatures::TEST_PROVISION_30) {
std::vector<uint8_t> cert;
size_t cert_length = 0;
OEMCryptoResult result = OEMCrypto_GetOEMPublicCertificate(
cert.data(), &cert_length, kLevelDefault);
if (result == OEMCrypto_ERROR_SHORT_BUFFER) {
cert.resize(cert_length);
result = OEMCrypto_GetOEMPublicCertificate(cert.data(), &cert_length,
kLevelDefault);
}
if (result != OEMCrypto_SUCCESS) {
info << "--- ERROR GETTING CERT. result=" << result;
} else {
info << "OEM Cert = (len=" << cert_length << ") "
<< wvutil::unlimited_b2a_hex(cert);
}
}
if (wvoec::global_features.derive_key_method ==
wvoec::DeviceFeatures::TEST_PROVISION_40) {
std::vector<uint8_t> bcc;
size_t bcc_length = 0;
std::vector<uint8_t> signature;
size_t signature_length = 0;
OEMCryptoResult result = OEMCrypto_GetBootCertificateChain(
bcc.data(), &bcc_length, signature.data(), &signature_length);
if (result == OEMCrypto_ERROR_SHORT_BUFFER) {
bcc.resize(bcc_length);
signature.resize(signature_length);
result = OEMCrypto_GetBootCertificateChain(
bcc.data(), &bcc_length, signature.data(), &signature_length);
}
if (result != OEMCrypto_SUCCESS) {
info << "--- ERROR GETTING BCC. result=" << result;
} else {
info << "BCC = (len=" << bcc_length << ") "
<< wvutil::unlimited_b2a_hex(bcc) << "\n"
<< "Additional Sig = (len=" << signature_length << ") "
<< wvutil::unlimited_b2a_hex(signature) << "\n";
}
}
return info.str();
}
} // namespace
std::unique_ptr<ConfigTestEnv> WvCdmTestBase::default_config_;
@@ -412,117 +318,15 @@ WvCdmTestBase::WvCdmTestBase()
}
void WvCdmTestBase::Provision() {
CdmProvisioningRequest prov_request;
std::string provisioning_server_url;
CdmCertificateType cert_type = kCertificateWidevine;
std::string cert_authority;
std::string cert, wrapped_key;
CdmSessionId session_id;
const CdmCertificateType cert_type = kCertificateWidevine;
std::unique_ptr<wvutil::FileSystem> file_system(CreateTestFileSystem());
if (config_.provisioning_server() == "fake") {
LOGD("Using fake provisioning server.");
TestCdmEngine cdm_engine(file_system.get(),
std::shared_ptr<EngineMetrics>(new EngineMetrics));
FakeProvisioningServer server;
CdmResponseType result = cdm_engine.GetProvisioningRequest(
cert_type, cert_authority, server.service_certificate(), kLevelDefault,
&prov_request, &provisioning_server_url);
ASSERT_EQ(NO_ERROR, result);
if (!binary_provisioning_) {
std::vector<uint8_t> prov_request_v =
wvutil::Base64SafeDecode(prov_request);
prov_request = std::string(prov_request_v.begin(), prov_request_v.end());
}
std::string response;
ASSERT_TRUE(server.MakeResponse(prov_request, &response))
<< "Fake provisioning server could not provision";
result = cdm_engine.HandleProvisioningResponse(response, kLevelDefault,
&cert, &wrapped_key);
EXPECT_EQ(NO_ERROR, result);
} else {
// TODO(fredgc): provision for different SPOIDs.
TestCdmEngine cdm_engine(file_system.get(),
std::shared_ptr<EngineMetrics>(new EngineMetrics));
CdmResponseType result = cdm_engine.GetProvisioningRequest(
cert_type, cert_authority, config_.provisioning_service_certificate(),
kLevelDefault, &prov_request, &provisioning_server_url);
ASSERT_EQ(NO_ERROR, result);
if (binary_provisioning_) {
prov_request = wvutil::Base64SafeEncodeNoPad(prov_request);
}
LOGV("Provisioning request: 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());
// Make request.
UrlRequest url_request(provisioning_server_url);
if (!url_request.is_connected()) {
LOGE("Failed to connect to provisioning server: url = %s",
provisioning_server_url.c_str());
}
url_request.PostCertRequestInQueryString(prov_request);
// Receive and parse response.
std::string http_message;
ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponse(&http_message))
<< "Failed to fetch provisioning response. "
<< DumpProvAttempt(provisioning_server_url, prov_request, http_message,
&cdm_engine);
if (binary_provisioning_) {
// extract provisioning response from received message
// Extracts signed response from JSON string, result is serialized
// protobuf.
static const std::string kMessageStart = "\"signedResponse\": \"";
static const std::string kMessageEnd = "\"";
std::string protobuf_response;
const bool extract_ok = ExtractSignedMessage(
http_message, kMessageStart, kMessageEnd, &protobuf_response);
ASSERT_TRUE(extract_ok) << "Failed to extract signed serialized "
"response from JSON response";
LOGV("Extracted response message: \n%s\n", protobuf_response.c_str());
ASSERT_FALSE(protobuf_response.empty())
<< "Protobuf response is unexpectedly empty";
// base64 decode response to yield binary protobuf
const std::vector<uint8_t> response_vec(
wvutil::Base64SafeDecode(protobuf_response));
ASSERT_FALSE(response_vec.empty())
<< "Failed to decode base64 of response: response = "
<< protobuf_response;
const std::string binary_protobuf_response(response_vec.begin(),
response_vec.end());
ASSERT_EQ(NO_ERROR, cdm_engine.HandleProvisioningResponse(
binary_protobuf_response, kLevelDefault, &cert,
&wrapped_key))
<< "Binary provisioning failed. "
<< DumpProvAttempt(provisioning_server_url, prov_request,
http_message, &cdm_engine);
} else {
ASSERT_EQ(NO_ERROR, cdm_engine.HandleProvisioningResponse(
http_message, kLevelDefault, &cert, &wrapped_key))
<< "Non-binary provisioning failed. "
<< DumpProvAttempt(provisioning_server_url, prov_request,
http_message, &cdm_engine);
}
}
TestCdmEngine cdm_engine(file_system.get(),
std::shared_ptr<EngineMetrics>(new EngineMetrics));
ProvisioningHolder provisioner(&cdm_engine, config_.provisioning_server(),
config_.provisioning_service_certificate());
provisioner.Provision(cert_type, binary_provisioning_);
}
// 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;
std::unique_ptr<wvutil::FileSystem> file_system(CreateTestFileSystem());

View File

@@ -217,8 +217,8 @@ bool UrlRequest::PostRequestWithPath(const std::string& path,
const int ret = socket_.WriteAndLogErrors(
request.c_str(), static_cast<int>(request.size()), kWriteTimeoutMs);
LOGV("HTTP request: (%zu): %s", request.size(),
wvutil::b2a_hex(request).c_str());
LOGV("HTTP request: (%zu): %s", request.size(), request.c_str());
LOGV("HTTP request hex: %s", wvutil::b2a_hex(request).c_str());
return ret != -1;
}