Code Drop Two (Update One)
This is the second code drop for the white-box api reference implementation and tests. This corrects the errors in the license white-box reference implementation and implements the remaining test cases. It should be noted that there is one test case missing, the test case for handling ChromeOS's unique policy settings. In order to make the tests easier to create and read, a license builder class was created and golden content and keys were wrapped in their own classes. How key errors are communicated was changed in the API. WB_RESULT_NO_SUCH_KEY and WB_RESULT_WRONG_KEY_TYPE were merged into WB_RESULT_KEY_UNAVAILABLE.
This commit is contained in:
@@ -26,15 +26,17 @@
|
||||
#include "third_party/boringssl/src/include/openssl/sha.h"
|
||||
|
||||
namespace {
|
||||
using KeyContainer = video_widevine::License_KeyContainer;
|
||||
using RsaPrivateKey = widevine::RsaPrivateKey;
|
||||
using AesCbcDecryptor = widevine::AesCbcDecryptor;
|
||||
using AesCtrDecryptor = widevine::AesCtrEncryptor;
|
||||
using SecurityLevel = video_widevine::License_KeyContainer_SecurityLevel;
|
||||
using KeyContainer = video_widevine::License_KeyContainer;
|
||||
using RsaPrivateKey = widevine::RsaPrivateKey;
|
||||
|
||||
struct ContentKey {
|
||||
// Minimum security level that this key requires in order to be used.
|
||||
SecurityLevel level;
|
||||
// When we store a key, we create our own little policy for the key saying
|
||||
// what functions may use it. This allows us to "blacklist" a key by setting
|
||||
// all "allow_*" to false.
|
||||
bool allow_decrypt;
|
||||
bool allow_masked_decrypt;
|
||||
|
||||
// Key used to decrypt content.
|
||||
std::vector<uint8_t> key;
|
||||
@@ -50,10 +52,6 @@ struct WB_License_Whitebox {
|
||||
std::string server_signing_key;
|
||||
std::string client_signing_key;
|
||||
|
||||
// We use two data structures to track keys. We track all the key ids seen
|
||||
// when loading the license and the content keys. This allows us to tell the
|
||||
// difference between a key missing and a key being misused.
|
||||
std::set<std::string> all_key_ids;
|
||||
std::map<std::string, ContentKey> content_keys;
|
||||
};
|
||||
|
||||
@@ -88,36 +86,15 @@ void ApplyPattern(const uint8_t* input_buffer,
|
||||
}
|
||||
}
|
||||
|
||||
WB_Result FindKey(const WB_License_Whitebox* whitebox,
|
||||
const uint8_t* id,
|
||||
size_t id_size,
|
||||
ContentKey* key) {
|
||||
const ContentKey* FindKey(const WB_License_Whitebox* whitebox,
|
||||
const uint8_t* id,
|
||||
size_t id_size) {
|
||||
DCHECK(whitebox);
|
||||
DCHECK(id);
|
||||
DCHECK_GT(id_size, 0u);
|
||||
DCHECK(key);
|
||||
|
||||
const std::string key_id = AsString(id, id_size);
|
||||
|
||||
if (whitebox->all_key_ids.empty()) {
|
||||
DVLOG(1) << "Invalid state: no keys have been loaded.";
|
||||
return WB_RESULT_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (whitebox->all_key_ids.find(key_id) == whitebox->all_key_ids.end()) {
|
||||
DVLOG(1) << "No such key: " << base::HexEncode(id, id_size) << ".";
|
||||
return WB_RESULT_NO_SUCH_KEY;
|
||||
}
|
||||
|
||||
const auto found = whitebox->content_keys.find(key_id);
|
||||
|
||||
if (found == whitebox->content_keys.end()) {
|
||||
DVLOG(1) << "Wrong key type: " << base::HexEncode(id, id_size) << ".";
|
||||
return WB_RESULT_WRONG_KEY_TYPE;
|
||||
}
|
||||
|
||||
*key = found->second;
|
||||
return WB_RESULT_OK;
|
||||
return found == whitebox->content_keys.end() ? nullptr : &found->second;
|
||||
}
|
||||
|
||||
WB_Result DecryptBuffer(WB_CipherMode mode,
|
||||
@@ -246,6 +223,7 @@ WB_Result WB_License_SignLicenseRequest(const WB_License_Whitebox* whitebox,
|
||||
return WB_RESULT_BUFFER_TOO_SMALL;
|
||||
}
|
||||
|
||||
*signature_size = result.size();
|
||||
return WB_RESULT_OK;
|
||||
}
|
||||
|
||||
@@ -258,16 +236,33 @@ WB_Result WB_License_ProcessLicenseResponse(WB_License_Whitebox* whitebox,
|
||||
size_t session_key_size,
|
||||
const uint8_t* license_request,
|
||||
size_t license_request_size) {
|
||||
const size_t kSigningKeySizeBytes =
|
||||
widevine::crypto_util::kSigningKeySizeBytes;
|
||||
|
||||
if (!whitebox || !message || !signature || !session_key || !license_request) {
|
||||
DVLOG(1) << "Invalid parameter: null pointer.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (message_size == 0 || license_request_size == 0) {
|
||||
// If we have already loaded a license, ProcessLicenseResponse() may not be
|
||||
// called again. The white-box needs to be destroyed and a new one needs
|
||||
// to be created.
|
||||
if (whitebox->content_keys.size() > 0) {
|
||||
DVLOG(1) << "Invalid state: already loaded a license.";
|
||||
return WB_RESULT_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (message_size == 0 || session_key_size == 0 || license_request_size == 0) {
|
||||
DVLOG(1) << "Invalid parameter: array size 0.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
// Because we use SHA256, the hash will be 32 bytes (256 bits).
|
||||
if (signature_size != 32) {
|
||||
DVLOG(1) << "Invalid parameter: invalid signature size.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
std::string decrypted_session_key;
|
||||
if (!whitebox->key->Decrypt(AsString(session_key, session_key_size),
|
||||
&decrypted_session_key)) {
|
||||
@@ -280,15 +275,15 @@ WB_Result WB_License_ProcessLicenseResponse(WB_License_Whitebox* whitebox,
|
||||
widevine::AsStringView(license_request, license_request_size),
|
||||
widevine::crypto_util::kSigningKeySizeBits * 2);
|
||||
|
||||
if (signing_key_material.size() <
|
||||
widevine::crypto_util::kSigningKeySizeBytes * 2) {
|
||||
if (signing_key_material.size() < kSigningKeySizeBytes * 2) {
|
||||
DVLOG(1) << "Invalid parameter: invalid session key size.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
const std::string server_signing_key =
|
||||
signing_key_material.substr(0, kSigningKeySizeBytes);
|
||||
if (!widevine::crypto_util::VerifySignatureHmacSha256(
|
||||
signing_key_material,
|
||||
widevine::AsStringView(signature, signature_size),
|
||||
server_signing_key, widevine::AsStringView(signature, signature_size),
|
||||
widevine::AsStringView(message, message_size))) {
|
||||
DVLOG(1) << "Failed to verify signed message.";
|
||||
return WB_RESULT_INVALID_SIGNATURE;
|
||||
@@ -314,8 +309,18 @@ WB_Result WB_License_ProcessLicenseResponse(WB_License_Whitebox* whitebox,
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
CHECK(!license.key().empty());
|
||||
std::string server_renewal_key;
|
||||
std::string client_renewal_key;
|
||||
std::map<std::string, ContentKey> content_keys;
|
||||
|
||||
for (const auto& key : license.key()) {
|
||||
// If this is not a key we're interested in, skip it as soon as possible.
|
||||
// Don't even bother unwrapping it.
|
||||
if (key.type() != KeyContainer::SIGNING &&
|
||||
key.type() != KeyContainer::CONTENT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string wrapped_key = key.key();
|
||||
std::vector<uint8_t> unwrapped_key(wrapped_key.size());
|
||||
|
||||
@@ -329,47 +334,64 @@ WB_Result WB_License_ProcessLicenseResponse(WB_License_Whitebox* whitebox,
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
whitebox->all_key_ids.insert(key.id());
|
||||
|
||||
switch (key.type()) {
|
||||
case KeyContainer::SIGNING: {
|
||||
const size_t kSigningKeySizeBytes =
|
||||
widevine::crypto_util::kSigningKeySizeBytes;
|
||||
if (unwrapped_key.size() < kSigningKeySizeBytes * 2) {
|
||||
DVLOG(1) << "Invalid parameter: Invalid signing key.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
const std::string signing_key =
|
||||
AsString(unwrapped_key.data(), unwrapped_key.size());
|
||||
whitebox->server_signing_key =
|
||||
signing_key.substr(0, kSigningKeySizeBytes);
|
||||
whitebox->client_signing_key =
|
||||
signing_key.substr(kSigningKeySizeBytes, kSigningKeySizeBytes);
|
||||
break;
|
||||
if (key.type() == KeyContainer::SIGNING) {
|
||||
if (unwrapped_key.size() < kSigningKeySizeBytes * 2) {
|
||||
DVLOG(1) << "Invalid parameter: Invalid signing key.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
case KeyContainer::CONTENT: {
|
||||
constexpr size_t kContentKeySizeBytes = 16;
|
||||
if (unwrapped_key.size() < kContentKeySizeBytes) {
|
||||
DVLOG(1) << "Invalid parameter: Invalid content key.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
const std::string signing_key =
|
||||
AsString(unwrapped_key.data(), unwrapped_key.size());
|
||||
server_renewal_key = signing_key.substr(0, kSigningKeySizeBytes);
|
||||
client_renewal_key =
|
||||
signing_key.substr(kSigningKeySizeBytes, kSigningKeySizeBytes);
|
||||
} else if (key.type() == KeyContainer::CONTENT) {
|
||||
constexpr size_t kContentKeySizeBytes = 16;
|
||||
|
||||
unwrapped_key.resize(kContentKeySizeBytes);
|
||||
whitebox->content_keys[key.id()] = {key.level(),
|
||||
std::move(unwrapped_key)};
|
||||
break;
|
||||
if (unwrapped_key.size() < kContentKeySizeBytes) {
|
||||
DVLOG(1) << "Invalid parameter: Invalid content key.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
default:
|
||||
// We purposefully ignore the other cases. In the case that any other
|
||||
// key appeared in the license, the key id would have been tracked in
|
||||
// |all_key_ids|.
|
||||
break;
|
||||
unwrapped_key.resize(kContentKeySizeBytes);
|
||||
|
||||
ContentKey content_key;
|
||||
|
||||
switch (key.level()) {
|
||||
case video_widevine::
|
||||
License_KeyContainer_SecurityLevel_SW_SECURE_CRYPTO:
|
||||
content_key.allow_decrypt = true;
|
||||
content_key.allow_masked_decrypt = true;
|
||||
content_key.key = std::move(unwrapped_key);
|
||||
break;
|
||||
case video_widevine::
|
||||
License_KeyContainer_SecurityLevel_SW_SECURE_DECODE:
|
||||
content_key.allow_decrypt = false;
|
||||
content_key.allow_masked_decrypt = true;
|
||||
content_key.key = std::move(unwrapped_key);
|
||||
break;
|
||||
default:
|
||||
content_key.allow_decrypt = false;
|
||||
content_key.allow_masked_decrypt = false;
|
||||
// Don't set key. We don't want to save this key as we should never
|
||||
// be using it. We only have an entry so that we can handle errors
|
||||
// correctly.
|
||||
break;
|
||||
}
|
||||
|
||||
content_keys[key.id()] = content_key;
|
||||
} else {
|
||||
// We should have already skipped over this key.
|
||||
CHECK(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the loaded state over to the white-box instance now that we know we
|
||||
// have a valid state.
|
||||
whitebox->server_signing_key.swap(server_renewal_key);
|
||||
whitebox->client_signing_key.swap(client_renewal_key);
|
||||
whitebox->content_keys.swap(content_keys);
|
||||
|
||||
return WB_RESULT_OK;
|
||||
}
|
||||
|
||||
@@ -388,10 +410,13 @@ WB_Result WB_License_SignRenewalRequest(const WB_License_Whitebox* whitebox,
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
// License request must have been processed at least once before calling this
|
||||
// function.
|
||||
if (whitebox->content_keys.empty()) {
|
||||
DVLOG(1) << "Invalid state: missing license.";
|
||||
return WB_RESULT_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (whitebox->client_signing_key.empty()) {
|
||||
DVLOG(1) << __func__ << " called before license received.";
|
||||
DVLOG(1) << "Invalid state: license does not support renewals.";
|
||||
return WB_RESULT_INVALID_STATE;
|
||||
}
|
||||
|
||||
@@ -408,6 +433,7 @@ WB_Result WB_License_SignRenewalRequest(const WB_License_Whitebox* whitebox,
|
||||
return WB_RESULT_BUFFER_TOO_SMALL;
|
||||
}
|
||||
|
||||
*signature_size = computed_signature.size();
|
||||
return WB_RESULT_OK;
|
||||
}
|
||||
|
||||
@@ -426,10 +452,13 @@ WB_Result WB_License_VerifyRenewalResponse(const WB_License_Whitebox* whitebox,
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
// License request must have been processed at least once before calling this
|
||||
// function.
|
||||
if (whitebox->content_keys.empty()) {
|
||||
DVLOG(1) << "Invalid state: missing license.";
|
||||
return WB_RESULT_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (whitebox->server_signing_key.empty()) {
|
||||
DVLOG(1) << __func__ << " called before license received.";
|
||||
DVLOG(1) << "Invalid state: license does not support renewals.";
|
||||
return WB_RESULT_INVALID_STATE;
|
||||
}
|
||||
|
||||
@@ -438,15 +467,14 @@ WB_Result WB_License_VerifyRenewalResponse(const WB_License_Whitebox* whitebox,
|
||||
whitebox->server_signing_key,
|
||||
widevine::AsStringView(message, message_size));
|
||||
|
||||
if (signature_size < computed_signature.size()) {
|
||||
DVLOG(1) << "Invalid parameters: invalid signature "
|
||||
"size.";
|
||||
if (signature_size != computed_signature.size()) {
|
||||
DVLOG(1) << "Invalid parameters: invalid signature size.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (computed_signature != widevine::AsStringView(signature, signature_size)) {
|
||||
DVLOG(1) << "Data verification error: signatures do not match.";
|
||||
return WB_RESULT_DATA_VERIFICATION_ERROR;
|
||||
return WB_RESULT_INVALID_SIGNATURE;
|
||||
}
|
||||
|
||||
return WB_RESULT_OK;
|
||||
@@ -463,6 +491,11 @@ WB_Result WB_License_GetSecretString(const WB_License_Whitebox* whitebox,
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (whitebox->content_keys.empty()) {
|
||||
DVLOG(1) << "Invalid state: missing license.";
|
||||
return WB_RESULT_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (mode != WB_CIPHER_MODE_CTR && mode != WB_CIPHER_MODE_CBC) {
|
||||
DVLOG(1) << "Invalid parameter: invalid cipher mode.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
@@ -475,11 +508,17 @@ WB_Result WB_License_GetSecretString(const WB_License_Whitebox* whitebox,
|
||||
|
||||
// The secret string can differ between keys, so we need to make sure that
|
||||
// the key id is actually a content key.
|
||||
ContentKey content_key;
|
||||
const WB_Result key_lookup_result =
|
||||
FindKey(whitebox, key_id, key_id_size, &content_key);
|
||||
if (key_lookup_result != WB_RESULT_OK) {
|
||||
return key_lookup_result;
|
||||
const ContentKey* content_key = FindKey(whitebox, key_id, key_id_size);
|
||||
|
||||
if (content_key == nullptr) {
|
||||
DVLOG(1) << "Key unavailable: could not find key.";
|
||||
return WB_RESULT_KEY_UNAVAILABLE;
|
||||
}
|
||||
|
||||
if (!content_key->allow_masked_decrypt) {
|
||||
DVLOG(1) << "Insufficient security level: key policy does not allow use "
|
||||
"with MaskedDecrypt().";
|
||||
return WB_RESULT_INSUFFICIENT_SECURITY_LEVEL;
|
||||
}
|
||||
|
||||
if (!widevine::MemCopy(kSecretStringPattern, kSecretStringPatternSize,
|
||||
@@ -489,6 +528,7 @@ WB_Result WB_License_GetSecretString(const WB_License_Whitebox* whitebox,
|
||||
return WB_RESULT_BUFFER_TOO_SMALL;
|
||||
}
|
||||
|
||||
*secret_string_size = kSecretStringPatternSize;
|
||||
return WB_RESULT_OK;
|
||||
}
|
||||
|
||||
@@ -507,31 +547,31 @@ WB_Result WB_License_Decrypt(const WB_License_Whitebox* whitebox,
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (whitebox->content_keys.empty()) {
|
||||
DVLOG(1) << "Invalid state: missing license.";
|
||||
return WB_RESULT_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (key_id_size == 0) {
|
||||
DVLOG(1) << "Invalid parameter: array size 0.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
ContentKey content_key;
|
||||
const WB_Result key_lookup_result =
|
||||
FindKey(whitebox, key_id, key_id_size, &content_key);
|
||||
if (key_lookup_result != WB_RESULT_OK) {
|
||||
return key_lookup_result;
|
||||
const ContentKey* content_key = FindKey(whitebox, key_id, key_id_size);
|
||||
|
||||
if (content_key == nullptr) {
|
||||
DVLOG(1) << "Key unavailable: could not find key.";
|
||||
return WB_RESULT_KEY_UNAVAILABLE;
|
||||
}
|
||||
|
||||
// Check if decryption is allowed by the policy.
|
||||
switch (content_key.level) {
|
||||
case video_widevine::License_KeyContainer_SecurityLevel_SW_SECURE_CRYPTO:
|
||||
break;
|
||||
case video_widevine::License_KeyContainer_SecurityLevel_SW_SECURE_DECODE:
|
||||
case video_widevine::License_KeyContainer_SecurityLevel_HW_SECURE_CRYPTO:
|
||||
case video_widevine::License_KeyContainer_SecurityLevel_HW_SECURE_DECODE:
|
||||
case video_widevine::License_KeyContainer_SecurityLevel_HW_SECURE_ALL:
|
||||
return WB_RESULT_INSUFFICIENT_SECURITY_LEVEL;
|
||||
if (!content_key->allow_decrypt) {
|
||||
DVLOG(1) << "Insufficient security level: key policy does not allow use "
|
||||
"with Decrypt().";
|
||||
return WB_RESULT_INSUFFICIENT_SECURITY_LEVEL;
|
||||
}
|
||||
|
||||
// DecryptBuffer() will validate the remaining decryption parameters.
|
||||
return DecryptBuffer(mode, content_key.key, input_data, input_data_size, iv,
|
||||
return DecryptBuffer(mode, content_key->key, input_data, input_data_size, iv,
|
||||
iv_size, output_data, output_data_size);
|
||||
}
|
||||
|
||||
@@ -545,32 +585,32 @@ WB_Result WB_License_MaskedDecrypt(const WB_License_Whitebox* whitebox,
|
||||
size_t iv_size,
|
||||
uint8_t* masked_output_data,
|
||||
size_t* masked_output_data_size) {
|
||||
if (!whitebox || !key_id) {
|
||||
if (!whitebox || !key_id || !masked_output_data || !masked_output_data_size) {
|
||||
DVLOG(1) << "Invalid parameter: null pointer.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (whitebox->content_keys.empty()) {
|
||||
DVLOG(1) << "Invalid state: missing license.";
|
||||
return WB_RESULT_INVALID_STATE;
|
||||
}
|
||||
|
||||
if (key_id_size == 0) {
|
||||
DVLOG(1) << "Invalid parameter: array size 0.";
|
||||
return WB_RESULT_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
ContentKey content_key;
|
||||
const WB_Result key_lookup_result =
|
||||
FindKey(whitebox, key_id, key_id_size, &content_key);
|
||||
if (key_lookup_result != WB_RESULT_OK) {
|
||||
return key_lookup_result;
|
||||
const ContentKey* content_key = FindKey(whitebox, key_id, key_id_size);
|
||||
|
||||
if (content_key == nullptr) {
|
||||
DVLOG(1) << "Key unavailable: could not find key.";
|
||||
return WB_RESULT_KEY_UNAVAILABLE;
|
||||
}
|
||||
|
||||
// Check if decryption is allowed by the policy.
|
||||
switch (content_key.level) {
|
||||
case video_widevine::License_KeyContainer_SecurityLevel_SW_SECURE_CRYPTO:
|
||||
case video_widevine::License_KeyContainer_SecurityLevel_SW_SECURE_DECODE:
|
||||
break;
|
||||
case video_widevine::License_KeyContainer_SecurityLevel_HW_SECURE_CRYPTO:
|
||||
case video_widevine::License_KeyContainer_SecurityLevel_HW_SECURE_DECODE:
|
||||
case video_widevine::License_KeyContainer_SecurityLevel_HW_SECURE_ALL:
|
||||
return WB_RESULT_INSUFFICIENT_SECURITY_LEVEL;
|
||||
if (!content_key->allow_masked_decrypt) {
|
||||
DVLOG(1) << "Insufficient security level: key policy does not allow use "
|
||||
"with MaskedDecrypt().";
|
||||
return WB_RESULT_INSUFFICIENT_SECURITY_LEVEL;
|
||||
}
|
||||
|
||||
// DecryptBuffer() will validate all the parameters, so just make sure it is
|
||||
@@ -584,7 +624,7 @@ WB_Result WB_License_MaskedDecrypt(const WB_License_Whitebox* whitebox,
|
||||
// DecryptBuffer() will validate the remaining decryption parameters and set
|
||||
// |masked_output_data_size|.
|
||||
const WB_Result result =
|
||||
DecryptBuffer(mode, content_key.key, input_data, input_data_size, iv,
|
||||
DecryptBuffer(mode, content_key->key, input_data, input_data_size, iv,
|
||||
iv_size, output.data(), masked_output_data_size);
|
||||
|
||||
if (result != WB_RESULT_OK) {
|
||||
|
||||
Reference in New Issue
Block a user