Source release 18.7.0

This commit is contained in:
John W. Bruce
2024-09-05 07:06:37 +00:00
parent 20c0587dcb
commit 4420a6f812
34 changed files with 979 additions and 200 deletions

View File

@@ -2,16 +2,47 @@
[TOC]
## 18.7.0 (2024-09-04)
This is a minor release with bug fixes and test improvements. However, because
of improvements to the BCC Factory Upload Tool, we recommend that all partners
who use this tool upgrade to version 18.7.0.
### Features
- Added workaround for OEMCrypto implementations with slightly corrupted build
information
- The BCC Factory Upload Tool supports new command-line options for dry runs,
batch checks, version-checking, and verbose output.
### Tests
- Added new tests to better validate the behavior of
`OEMCrypto_BuildInformation()`
- Verifies output length is set correctly
- Verifies content is ASCII JSON without trailing null bytes
- Verifies documented JSON fields: required fields are present, and optional
and required fields are the correct JSON types
### Bug Fixes
- Fixed decrypt failures on devices with low TEE memory caused by sending an
output buffer to decrypt that was much larger than necessary
- Several BCC Factory Upload Tool fixes:
- Added the missing `FileSystem::Exists()` function
- Fixed a bug causing the output to be unnecessarily padded
- Fixed an issue where fields containing JSON were not properly escaped
## 18.6.0 (2024-06-24)
This is a minor release with bug fixes and test improvements.
## Features
### Features
- Added new test data for entitled licenses
- Added new tests for clear lead sample decryption
## Bug Fixes
### Bug Fixes
- Improved error logging for tests
- Small fixes to reduce compiler warning
@@ -29,7 +60,7 @@ CE CDM v18.5.0 includes all changes from CE CDM v17.3.0 and v18.1.0.
address two major bugs in the CE CDM code which could result in lost offline
licenses or app crashes. See _Bug Fixes_ for 18.5.0 and 17.3.0 for details.
## Features
### Features
- Supports up to OEMCrypto v18.5, including new OEMCrypto tests introduced
since OEMCrypto v18.1.

View File

@@ -1,6 +1,6 @@
# Widevine CE CDM 18.6.0
# Widevine CE CDM 18.7.0
Released 2024-06-24
Released 2024-09-04
## Getting Started
@@ -10,21 +10,36 @@ following to learn more about the contents of this project and how to use them:
The [Widevine Developer Site][wv-devsite] documents the CDM API and describes
how to integrate the CDM into a system.
## New in 18.6.0
## New in 18.7.0
This is a minor release with bug fixes and test improvements.
This is a minor release with bug fixes and test improvements. However, because
of improvements to the BCC Factory Upload Tool, we recommend that all partners
who use this tool upgrade to version 18.7.0.
## Features
### Features
- Added new test data for entitled licenses
- Added new tests for clear lead sample decryption
- Added workaround for OEMCrypto implementations with slightly corrupted build
information
- The BCC Factory Upload Tool supports new command-line options for dry runs,
batch checks, version-checking, and verbose output.
## Bug Fixes
### Tests
- Improved error logging for tests
- Small fixes to reduce compiler warning
- Fixed URL error found for tests using different license server SDK
- Skip CAS tests on non-CAS devices
- Added new tests to better validate the behavior of
`OEMCrypto_BuildInformation()`
- Verifies output length is set correctly
- Verifies content is ASCII JSON without trailing null bytes
- Verifies documented JSON fields: required fields are present, and optional
and required fields are the correct JSON types
### Bug Fixes
- Fixed decrypt failures on devices with low TEE memory caused by sending an
output buffer to decrypt that was much larger than necessary
- Several BCC Factory Upload Tool fixes:
- Added the missing `FileSystem::Exists()` function
- Fixed a bug causing the output to be unnecessarily padded
- Fixed an issue where fields containing JSON were not properly escaped
[CHANGELOG.md](./CHANGELOG.md) lists the major changes for each past release.

View File

@@ -10,7 +10,7 @@
# define CDM_VERSION_MAJOR 18
#endif
#ifndef CDM_VERSION_MINOR
# define CDM_VERSION_MINOR 6
# define CDM_VERSION_MINOR 7
#endif
#ifndef CDM_VERSION_PATCH
# define CDM_VERSION_PATCH 0

View File

@@ -7,11 +7,14 @@
#include <string.h>
#include <time.h>
#include <algorithm>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include "cdm.h"
#include "log.h"
#include "oec_device_features.h"
#include "stderr_logger.h"
#include "test_base.h"
#include "test_host.h"
@@ -22,13 +25,56 @@ std::string g_sandbox_id;
namespace widevine {
namespace {
constexpr char kSandboxIdParam[] = "--sandbox_id=";
constexpr char kCertPathParam[] = "--cert_path=";
constexpr char kCertKeyPathParam[] = "--cert_key_path=";
// Following the pattern established by help text in test_base.cpp
constexpr char kExtraHelpText[] =
" --sandbox_id=<sandbox_id>\n"
" Specifies the Sandbox ID that should be sent to OEMCrypto via\n"
" OEMCrypto_SetSandbox(). On most platforms, since Sandbox IDs are not\n"
" in use, this parameter should be omitted.\n";
" in use, this parameter should be omitted.\n"
" --cert_path=<cert_path>\n"
" Path to a preloaded DRM certificate. This may speed up some tests\n"
" by skipping the provisioning step. On most platforms, this parameter\n"
" parameter should be omitted.\n"
" --cert_key_path=<cert_key_path>\n"
" Path to a key in preloaded DRM certificate. This should only be used\n"
" if the device has a baked in cert.\n";
bool ReadFile(const std::string& path, std::string* output) {
output->clear();
std::ifstream fs(path, std::ios::in | std::ios::binary);
if (!fs) {
LOGE("Failed to open %s: %s", path.c_str(), strerror(errno));
return false;
}
std::stringstream buffer;
buffer << fs.rdbuf();
*output = buffer.str();
return true;
}
// Reads a file from the command line arguments.
// The file path is expected to be the first argument after the flag.
// For example, if the flag is "--cert_path=" and the command line is
// cdm_test_runner --cert_path=/path/to/cert
// then the file at /path/to/cert will be read.
// The flag will be removed from the command line arguments.
bool ReadFileFromArg(const char* path_flag, std::vector<std::string>& args,
std::string* data) {
auto path_iter = std::find_if(std::begin(args) + 1, std::end(args),
[path_flag](const std::string& elem) -> bool {
return elem.find(path_flag) == 0;
});
if (path_iter != std::end(args)) {
const std::string path = path_iter->substr(strlen(path_flag));
args.erase(path_iter);
return ReadFile(path, data);
}
return false;
}
} // namespace
int Main(Cdm::IStorage* storage, Cdm::IClock* clock, Cdm::ITimer* timer,
@@ -50,6 +96,15 @@ int Main(Cdm::IStorage* storage, Cdm::IClock* clock, Cdm::ITimer* timer,
(void)status; // status is now used when assertions are turned off.
assert(status == Cdm::kSuccess);
std::string data;
if (ReadFileFromArg(kCertPathParam, args, &data)) {
g_host->set_baked_in_cert(data);
}
if (ReadFileFromArg(kCertKeyPathParam, args, &data)) {
wvoec::global_features.set_rsa_test_key(
std::vector<uint8_t>(data.begin(), data.end()));
}
std::vector<const char*> new_argv(args.size());
std::transform(
std::begin(args), std::end(args), std::begin(new_argv),

View File

@@ -137,6 +137,11 @@ bool TestHost::Storage::SaveToString(std::string* data) const {
}
bool TestHost::Storage::read(const std::string& name, std::string* data) {
if (wvutil::kLegacyCertificateFileName == name &&
!g_host->baked_in_cert().empty()) {
*data = g_host->baked_in_cert();
return true;
}
StorageMap::iterator it = files_.find(name);
bool ok = it != files_.end();
LOGV("read file: %s: %s", name.c_str(), ok ? "ok" : "fail");
@@ -148,12 +153,21 @@ bool TestHost::Storage::read(const std::string& name, std::string* data) {
bool TestHost::Storage::write(const std::string& name,
const std::string& data) {
LOGV("write file: %s", name.c_str());
if (wvutil::kLegacyCertificateFileName == name &&
!g_host->baked_in_cert().empty()) {
return false;
}
if (!CheckFilename(name)) return false;
files_[name] = data;
return true;
}
bool TestHost::Storage::exists(const std::string& name) {
if (wvutil::kLegacyCertificateFileName == name &&
!g_host->baked_in_cert().empty()) {
LOGV("exists? %s: always", name.c_str());
return true;
}
StorageMap::iterator it = files_.find(name);
bool ok = it != files_.end();
LOGV("exists? %s: %s", name.c_str(), ok ? "true" : "false");
@@ -174,6 +188,11 @@ bool TestHost::Storage::remove(const std::string& name) {
}
int32_t TestHost::Storage::size(const std::string& name) {
if (wvutil::kLegacyCertificateFileName == name &&
!g_host->baked_in_cert().empty()) {
LOGV("size? %s: always", name.c_str());
return static_cast<int32_t>(g_host->baked_in_cert().size());
}
StorageMap::iterator it = files_.find(name);
bool ok = (it != files_.end());
LOGV("size? %s: %s", name.c_str(), ok ? "ok" : "fail");

View File

@@ -69,6 +69,13 @@ class TestHost : public widevine::Cdm::IClock,
void setTimeout(int64_t delay_ms, IClient* client, void* context) override;
void cancel(IClient* client) override;
// If this is set, then the storage will return this as a baked in cert.
// Trying to write a new cert will generate an error.
const std::string& baked_in_cert() const { return baked_in_cert_; };
void set_baked_in_cert(const std::string& baked_in_cert) {
baked_in_cert_ = baked_in_cert;
};
private:
struct Timer {
Timer(int64_t expiry_time, IClient* client, void* context)
@@ -95,6 +102,7 @@ class TestHost : public widevine::Cdm::IClock,
Storage global_storage_;
Storage per_origin_storage_;
std::string baked_in_cert_;
};
// Owned and managed by the test runner.

View File

@@ -31,7 +31,7 @@ class SystemIdExtractor {
// |security_level|
// - Requested security level, uses the |crypto_session| handle
// to convert to a concrete security level.
// |crypto_sesssion|
// |crypto_session|
// - Handle into the OEMCrypto platform. If handle is open,
// then the session's real security level should match
// |security_level|.

View File

@@ -151,7 +151,6 @@ void AdvanceDestBuffer(OEMCrypto_DestBufferDesc* dest_buffer, size_t bytes) {
switch (dest_buffer->type) {
case OEMCrypto_BufferType_Clear:
dest_buffer->buffer.clear.clear_buffer += bytes;
dest_buffer->buffer.clear.clear_buffer_length -= bytes;
return;
case OEMCrypto_BufferType_Secure:
@@ -2511,6 +2510,17 @@ bool CryptoSession::GetBuildInformation(RequestedSecurityLevel security_level,
return false;
}
info->resize(info_length);
// Some OEMCrypto implementations may include trailing null
// bytes in the output. Trim them here.
while (!info->empty() && info->back() == '\0') {
info->pop_back();
}
if (info->empty()) {
LOGE("BuildInformation() returned corrupted data: length = %zu",
info_length);
return false;
}
return true;
}
@@ -3219,6 +3229,11 @@ OEMCryptoResult CryptoSession::DecryptSample(
}
fake_sample.buffers.input_data_length = length;
if (fake_sample.buffers.output_descriptor.type ==
OEMCrypto_BufferType_Clear) {
fake_sample.buffers.output_descriptor.buffer.clear
.clear_buffer_length = length;
}
fake_sample.subsamples = &clear_subsample;
fake_sample.subsamples_length = 1;
@@ -3246,6 +3261,11 @@ OEMCryptoResult CryptoSession::DecryptSample(
}
fake_sample.buffers.input_data_length = length;
if (fake_sample.buffers.output_descriptor.type ==
OEMCrypto_BufferType_Clear) {
fake_sample.buffers.output_descriptor.buffer.clear
.clear_buffer_length = length;
}
fake_sample.subsamples = &encrypted_subsample;
fake_sample.subsamples_length = 1;
@@ -3338,6 +3358,10 @@ OEMCryptoResult CryptoSession::LegacyCopyBufferInChunks(
// Calculate the size of the next chunk.
const size_t chunk_size = std::min(remaining_input_data, max_chunk_size);
if (output_descriptor.type == OEMCrypto_BufferType_Clear) {
output_descriptor.buffer.clear.clear_buffer_length = chunk_size;
}
// Re-add "last subsample" flag if this is the last subsample.
if (chunk_size == remaining_input_data) {
subsample_flags |= OEMCrypto_LastSubsample;
@@ -3385,6 +3409,11 @@ OEMCryptoResult CryptoSession::LegacyDecryptInChunks(
// Calculate the size of the next chunk.
const size_t chunk_size = std::min(remaining_input_data, max_chunk_size);
fake_sample.buffers.input_data_length = chunk_size;
if (fake_sample.buffers.output_descriptor.type ==
OEMCrypto_BufferType_Clear) {
fake_sample.buffers.output_descriptor.buffer.clear.clear_buffer_length =
chunk_size;
}
if (is_protected) {
fake_subsample.num_bytes_encrypted = chunk_size;
} else {

View File

@@ -5,6 +5,7 @@
#include "certificate_provisioning.h"
#include "license_holder.h"
#include "log.h"
#include "oec_device_features.h"
#include "provisioning_holder.h"
#include "test_base.h"
#include "wv_cdm_types.h"
@@ -135,6 +136,9 @@ class CoreIntegrationTest : public WvCdmTestBaseWithEngine {
* different apps. Test using two different apps and origins.
*/
TEST_F(CoreIntegrationTest, ProvisioningStableSpoidTest) {
if (wvoec::global_features.provisioning_method == OEMCrypto_DrmCertificate) {
GTEST_SKIP() << "Device does not provision.";
}
std::string level;
ASSERT_EQ(
NO_ERROR,

View File

@@ -94,7 +94,7 @@ void LicenseHolder::GenerateAndPostRenewalRequest(
void LicenseHolder::FetchRenewal() {
ASSERT_NE(renewal_in_flight_, nullptr) << "Failed for " << content_id();
ASSERT_NO_FATAL_FAILURE(
renewal_in_flight_->AssertOkResponse(&renewal_response_))
renewal_in_flight_->AssertOkResponseWithRetry(&renewal_response_))
<< "Renewal failed for " << content_id();
}
@@ -243,7 +243,7 @@ void LicenseHolder::GetKeyResponse(const CdmKeyRequest& key_request) {
std::string http_response;
url_request.PostRequest(key_request.message);
ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponse(&http_response))
ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponseWithRetry(&http_response))
<< "Failed for " << content_id();
LicenseRequest license_request;
license_request.GetDrmMessage(http_response, key_response_);

View File

@@ -69,7 +69,7 @@ void ProvisioningHolder::Provision(CdmCertificateType cert_type,
url_request.PostCertRequestInQueryString(request);
// Receive and parse response.
ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponse(&response_))
ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponseWithRetry(&response_))
<< "Failed to fetch provisioning response. "
<< DumpProvAttempt(request, response_, cert_type);

View File

@@ -329,9 +329,12 @@ void WvCdmTestBase::InstallTestRootOfTrust() {
sizeof(test_keybox)));
break;
case wvoec::DeviceFeatures::LOAD_TEST_RSA_KEY:
// Rare case: used by devices with baked in DRM cert.
// Rare case: used by devices with baked in production DRM cert.
ASSERT_EQ(OEMCrypto_SUCCESS, OEMCrypto_LoadTestRSAKey());
break;
case wvoec::DeviceFeatures::PRELOADED_RSA_KEY:
// Rare case: used by devices with baked in test DRM cert.
break;
case wvoec::DeviceFeatures::TEST_PROVISION_30:
// Can use oem certificate to install test rsa key.
break;
@@ -361,6 +364,10 @@ void WvCdmTestBase::Provision() {
}
void WvCdmTestBase::EnsureProvisioned() {
if (wvoec::global_features.provisioning_method == OEMCrypto_DrmCertificate) {
LOGD("Device is preprovisioned.");
return;
}
CdmSessionId session_id;
std::unique_ptr<wvutil::FileSystem> file_system(CreateTestFileSystem());
// OpenSession will check if a DRM certificate exists, while

View File

@@ -5,7 +5,9 @@
#include "url_request.h"
#include <errno.h>
#include <unistd.h>
#include <iostream>
#include <sstream>
#include <gtest/gtest.h>
@@ -24,11 +26,15 @@ const int kConnectTimeoutMs = 15000;
const int kWriteTimeoutMs = 12000;
const int kReadTimeoutMs = 12000;
constexpr int kHttpOk = 200;
const std::vector<int> kRetryCodes = {502, 504};
const std::string kGoogleHeaderUpper("X-Google");
const std::string kGoogleHeaderLower("x-google");
const std::string kCrLf("\r\n");
constexpr unsigned kRetryCount = 3;
constexpr unsigned kRetryIntervalSeconds = 1;
// Concatenate all chunks into one blob and returns the response with
// header information.
void ConcatenateChunkedResponse(const std::string http_response,
@@ -127,13 +133,34 @@ bool UrlRequest::GetResponse(std::string* message) {
return true;
}
void UrlRequest::AssertOkResponse(std::string* message) {
void UrlRequest::AssertOkResponseWithRetry(std::string* message) {
ASSERT_TRUE(message);
ASSERT_TRUE(GetResponse(message));
const int status_code = GetStatusCode(*message);
ASSERT_EQ(kHttpOk, status_code) << "HTTP response from " << socket_.url()
<< ": (" << message->size() << ") :\n"
<< *message;
int status_code = 0;
for (unsigned i = 0; i < kRetryCount; i++) {
*message = "";
ASSERT_TRUE(GetResponse(message)) << "For attempt " << (i + 1);
status_code = GetStatusCode(*message);
// If we didn't get a retry status, then we're done.
if (std::find(kRetryCodes.begin(), kRetryCodes.end(), status_code) ==
kRetryCodes.end()) {
ASSERT_EQ(kHttpOk, status_code) << "HTTP response from " << socket_.url()
<< ": (" << message->size() << ") :\n"
<< *message;
return;
}
std::cerr << "Temporary failure HTTP response from " << socket_.url()
<< ": (" << message->size() << ") :\n"
<< *message << "\n"
<< "Attempt " << (i + 1) << "\n";
socket_.CloseSocket();
is_connected_ = false;
sleep(kRetryIntervalSeconds << i);
Reconnect();
SendRequestOnce();
}
GTEST_FAIL() << "HTTP response from " << socket_.url() << ": ("
<< message->size() << ") :\n"
<< *message;
}
// static
@@ -190,36 +217,35 @@ bool UrlRequest::GetDebugHeaderFields(
bool UrlRequest::PostRequestWithPath(const std::string& path,
const std::string& data) {
std::string request;
request_.clear();
request.append("POST ");
request.append(path);
request.append(" HTTP/1.1\r\n");
request_.append("POST ");
request_.append(path);
request_.append(" HTTP/1.1\r\n");
request.append("Host: ");
request.append(socket_.domain_name());
request.append("\r\n");
request_.append("Host: ");
request_.append(socket_.domain_name());
request_.append("\r\n");
request.append("Connection: close\r\n");
request.append("User-Agent: Widevine CDM v1.0\r\n");
request.append("X-Return-Encrypted-Headers: request_and_response\r\n");
request_.append("Connection: close\r\n");
request_.append("User-Agent: Widevine CDM v1.0\r\n");
request_.append("X-Return-Encrypted-Headers: request_and_response\r\n");
// buffer to store length of data as a string
char data_size_buffer[32] = {0};
snprintf(data_size_buffer, sizeof(data_size_buffer), "%zu", data.size());
request_.append("Content-Length: ");
request_.append(std::to_string(data.size()));
request_.append("\r\n");
request.append("Content-Length: ");
request.append(data_size_buffer); // appends size of data
request.append("\r\n");
request_.append("\r\n"); // empty line to terminate headers
request.append("\r\n"); // empty line to terminate headers
request.append(data);
request_.append(data);
return SendRequestOnce();
}
bool UrlRequest::SendRequestOnce() {
const int ret = socket_.WriteAndLogErrors(
request.c_str(), static_cast<int>(request.size()), kWriteTimeoutMs);
LOGV("HTTP request: (%zu): %s", request.size(), request.c_str());
LOGV("HTTP request hex: %s", wvutil::b2a_hex(request).c_str());
request_.c_str(), static_cast<int>(request_.size()), kWriteTimeoutMs);
LOGV("HTTP request: (%zu): %s", request_.size(), request_.c_str());
LOGV("HTTP request hex: %s", wvutil::b2a_hex(request_).c_str());
return ret != -1;
}

View File

@@ -29,7 +29,8 @@ class UrlRequest {
bool GetResponse(std::string* message);
static int GetStatusCode(const std::string& response);
// Get the response, and expect the status is OK.
void AssertOkResponse(std::string* message);
// It will retry if the response code is in the 500 range.
void AssertOkResponseWithRetry(std::string* message);
static bool GetDebugHeaderFields(
const std::string& response,
@@ -37,9 +38,11 @@ class UrlRequest {
private:
bool PostRequestWithPath(const std::string& path, const std::string& data);
bool SendRequestOnce();
bool is_connected_;
HttpSocket socket_;
std::string request_;
CORE_DISALLOW_COPY_AND_ASSIGN(UrlRequest);
};

View File

@@ -11,6 +11,39 @@
namespace widevine {
namespace {
std::string EscapeJson(const std::string& input) {
std::string result;
for (std::string::const_iterator c = input.begin(); c != input.end(); ++c) {
switch (*c) {
case '\"':
result += "\\\"";
break;
case '\\':
result += "\\\\";
break;
case '\b':
result += "\\b";
break;
case '\f':
result += "\\f";
break;
case '\n':
result += "\\n";
break;
case '\r':
result += "\\r";
break;
case '\t':
result += "\\t";
break;
default:
result += *c;
break;
}
}
return result;
}
std::string StringMapToJson(
const std::map<std::string, std::string>& string_map) {
std::string json = "{";
@@ -72,7 +105,7 @@ Status WidevineFactoryExtractor::GenerateUploadRequest(std::string& request) {
request_map["model"] = PropertiesCE::GetClientInfo().model_name;
request_map["product"] = PropertiesCE::GetClientInfo().product_name;
request_map["build_info"] = PropertiesCE::GetClientInfo().build_info;
request_map["oemcrypto_build_info"] = oemcrypto_build_info;
request_map["oemcrypto_build_info"] = EscapeJson(oemcrypto_build_info);
request_map["bcc"] = wvutil::Base64Encode(bcc);
std::string request_json = StringMapToJson(request_map);

249
factory_upload_tool/ce/wv_upload_tool.py Normal file → Executable file
View File

@@ -12,6 +12,7 @@ input, with one JSON string per line of input.
"""
import argparse
from http import HTTPStatus
import http.server
import json
import os
@@ -20,16 +21,22 @@ import urllib.parse
import urllib.request
import uuid
import webbrowser
from google.auth.transport import requests
from google.oauth2 import service_account
DEFAULT_BASE = 'https://widevine.googleapis.com/v1beta1'
UPLOAD_PATH = '/uniqueDeviceInfo:batchUpload'
BATCH_CHECK_PATH = '/uniqueDeviceInfo:batchCheck'
TOKEN_CACHE_FILE = os.path.join(
os.path.expanduser('~'), '.device_info_uploader.token')
os.path.expanduser('~'), '.device_info_uploader.token'
)
OAUTH_SERVICE_BASE = 'https://accounts.google.com/o/oauth2'
OAUTH_AUTHN_URL = OAUTH_SERVICE_BASE + '/auth'
OAUTH_TOKEN_URL = OAUTH_SERVICE_BASE + '/token'
OAUTH_SCOPES = ['https://www.googleapis.com/auth/widevine/frontend']
class OAuthHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
"""HTTP Handler used to accept the oauth response when the user logs in."""
@@ -41,17 +48,24 @@ class OAuthHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
params = dict(urllib.parse.parse_qsl(parsed_path.query))
if 'error' in params:
error = params['error']
self.respond(400, error,
f'Error received from the OAuth server: {error}.')
self.respond(
400, error, f'Error received from the OAuth server: {error}.'
)
sys.exit(-1)
elif 'code' not in params:
self.respond(400, 'ERROR',
('Response from OAuth server is missing the authorization '
f'code. Full response: "{self.path}"'))
self.respond(
400,
'ERROR',
(
'Response from OAuth server is missing the authorization '
f'code. Full response: "{self.path}"'
),
)
sys.exit(-1)
else:
self.respond(200, 'Success!',
'Success! You may close this browser window.')
self.respond(
200, 'Success!', 'Success! You may close this browser window.'
)
self.server.code = params['code']
def do_POST(self): # pylint: disable=invalid-name
@@ -70,20 +84,25 @@ class OAuthHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
self.send_response(code)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(('<html>'
f' <title>{title}</title>'
f' <body>'
f' <p style="font-size:24px;">{message}</p>'
f' </body>'
'</html>').encode('utf-8'))
self.wfile.write(
(
'<html>'
f' <title>{title}</title>'
' <body>'
f' <p style="font-size:24px;">{message}</p>'
' </body>'
'</html>'
).encode('utf-8')
)
class LocalOAuthReceiver(http.server.HTTPServer):
"""HTTP server that will wait for an OAuth authorization code."""
def __init__(self):
super(LocalOAuthReceiver, self).__init__(('127.0.0.1', 0),
OAuthHTTPRequestHandler)
super(LocalOAuthReceiver, self).__init__(
('127.0.0.1', 0), OAuthHTTPRequestHandler
)
self.code = None
def port(self):
@@ -111,25 +130,63 @@ def parse_args():
Returns:
An argparse.Namespace object populated with the arguments.
"""
parser = argparse.ArgumentParser(description='Upload device info')
parser = argparse.ArgumentParser(description='Widevine BCC Batch Upload/Check Tool')
parser.add_argument("--version", action="version", version="20240822") #yyyymmdd
parser.add_argument(
'--json-csr',
nargs='+',
required=True,
help='list of files containing JSON output from rkp_factory_extraction_tool'
help='list of files containing JSON output from factory extraction tool',
)
parser.add_argument(
'--credentials', required=True, help='JSON credentials file')
parser.add_argument('--credentials', help='JSON credentials file')
parser.add_argument(
'--endpoint', default=DEFAULT_BASE, help='destination server URL')
'--endpoint', default=DEFAULT_BASE, help='destination server URL'
)
parser.add_argument('--org-name', required=True, help='orgnization name')
parser.add_argument(
'--cache-token',
action='store_true',
help='Use a locally cached a refresh token')
help='Use a locally cached a refresh token',
)
parser.add_argument(
'--service-credentials', help='JSON credentials file for service account'
)
parser.add_argument(
'--die-on-error',
action='store_true',
help='exit on error and stop uploading more CSRs',
)
parser.add_argument(
'--dryrun',
action='store_true',
help=(
'Do not upload anything. Instead print out what actions would have'
' been taken if the --dryrun flag had not been specified.'
),
)
parser.add_argument(
'--check',
action='store_true',
required=False,
help='Perform a batch check on the CSRs.',
)
parser.add_argument(
'--verbose',
action='store_true',
required=False,
help='Print request and response details.',
)
return parser.parse_args()
@@ -146,6 +203,7 @@ def parse_json_csrs(filename, batches):
line_count = 0
for line in open(filename):
line_count = line_count + 1
obj = {}
try:
obj = json.loads(line)
except json.JSONDecodeError as e:
@@ -159,14 +217,17 @@ def parse_json_csrs(filename, batches):
'name': obj['name'],
'model': obj['model'],
'product': obj['product'],
'build_info': obj['build_info']
'build_info': obj['build_info'],
'oemcrypto_build_info': obj['oemcrypto_build_info'],
})
if device_metadata not in batches:
batches[device_metadata] = []
batches[device_metadata].append(bcc)
except KeyError as e:
die(f'Invalid object at {filename}:{line_count}, missing {e}')
if device_metadata not in batches:
batches[device_metadata] = []
batches[device_metadata].append(bcc)
if line_count == 0:
die('Empty BCC file!')
def format_request_body(args, device_metadata, bccs):
@@ -180,6 +241,17 @@ def format_request_body(args, device_metadata, bccs):
return json.dumps(request).encode('utf-8')
def format_check_request_body(args, bccs):
"""Generate a formatted BatchCheck request buffer for the given build and CSRs."""
request = {
'parent': 'orgs/' + args.org_name,
'request_id': uuid.uuid4().hex,
'device_info': bccs,
}
return json.dumps(request).encode('utf-8')
def load_refresh_token():
if not os.path.exists(TOKEN_CACHE_FILE):
return None
@@ -258,7 +330,8 @@ def load_and_validate_creds(credfile):
' The given credentials do not appear to be for a locally installed\n'
' application. Please navigate to the credentials dashboard and\n'
' ensure that the "Type" of your client is "Desktop":\n'
' https://console.cloud.google.com/apis/credentials')
' https://console.cloud.google.com/apis/credentials'
)
if 'installed' not in credmap:
die(not_local_app_creds_error)
@@ -267,10 +340,12 @@ def load_and_validate_creds(credfile):
expected_keys = set(['client_id', 'client_secret', 'redirect_uris'])
if not expected_keys.issubset(creds.keys()):
die(('ERROR: Invalid credential file.\n'
' The given credentials do not appear to be valid. Please\n'
' re-download the client credentials file from the dashboard:\n'
' https://console.cloud.google.com/apis/credentials'))
die((
'ERROR: Invalid credential file.\n'
' The given credentials do not appear to be valid. Please\n'
' re-download the client credentials file from the dashboard:\n'
' https://console.cloud.google.com/apis/credentials'
))
if 'http://localhost' not in creds['redirect_uris']:
die(not_local_app_creds_error)
@@ -279,7 +354,23 @@ def load_and_validate_creds(credfile):
def authenticate_and_fetch_token(args):
"""Authenticate the user and fetch an OAUTH2 access token."""
"""Authenticate and fetch an OAUTH2 access token."""
# Auth for service account
if args.service_credentials:
if not os.path.exists(args.service_credentials):
die('Service account credentials file does not exist.')
svc_account = service_account.Credentials.from_service_account_file(
args.service_credentials,
scopes=OAUTH_SCOPES,
)
svc_account.refresh(requests.Request())
return svc_account.token
# Auth for user account
if args.credentials is None:
die('User credentials is not provided.')
if not os.path.exists(args.credentials):
die('User credentials file does not exist.')
creds = load_and_validate_creds(args.credentials)
access_type = 'online'
@@ -292,10 +383,17 @@ def authenticate_and_fetch_token(args):
httpd = LocalOAuthReceiver()
redirect_uri = f'http://127.0.0.1:{httpd.port()}'
url = (
OAUTH_AUTHN_URL + '?response_type=code' + '&client_id=' +
creds['client_id'] + '&redirect_uri=' + redirect_uri +
'&scope=https://www.googleapis.com/auth/widevine/frontend' +
'&access_type=' + access_type + '&prompt=select_account')
OAUTH_AUTHN_URL
+ '?response_type=code'
+ '&client_id='
+ creds['client_id']
+ '&redirect_uri='
+ redirect_uri
+ '&scope=https://www.googleapis.com/auth/widevine/frontend'
+ '&access_type='
+ access_type
+ '&prompt=select_account'
)
print('Opening your web browser to authenticate...')
if not webbrowser.open(url, new=1, autoraise=True):
print('Error opening the browser. Please open this link in a browser')
@@ -312,15 +410,52 @@ def upload_batch(args, device_metadata, bccs):
device_metadata: The build for which we're uploading CSRs
bccs: a list of BCCs to be uploaded for the given build
"""
print("Uploading {} bcc(s) for build '{}'".format(len(bccs), device_metadata))
print('Uploading {} bcc(s)'.format(len(bccs)))
if args.verbose:
print("Build: '{}'".format(device_metadata))
body = format_request_body(args, device_metadata, bccs)
print(body)
print(args.endpoint + UPLOAD_PATH)
request = urllib.request.Request(args.endpoint + UPLOAD_PATH)
return batch_action_single_attempt(args, UPLOAD_PATH, body)
def check_batch(args, device_metadata, bccs):
"""Batch check all the CSRs.
Args:
args: The parsed command-line arguments
device_metadata: The build for which we're checking CSRs
bccs: a list of BCCs to be checked for the given build
"""
print('Checking {} bcc(s)'.format(len(bccs)))
if args.verbose:
print("Build: '{}'".format(device_metadata))
body = format_check_request_body(args, bccs)
return batch_action_single_attempt(args, BATCH_CHECK_PATH, body)
def batch_action_single_attempt(args, path, body):
"""Batch action (upload or check existence) for all the CSRs in chunks.
Args:
args: The parsed command-line arguments
csrs: a list of CSRs to be uploaded/checked for the given build
path: The endpoint url for the specific action
body: The formatted request body
"""
if args.verbose:
print('Request body:')
print(body)
print('Request target:')
print(args.endpoint + path)
request = urllib.request.Request(args.endpoint + path)
request.add_header('Content-Type', 'application/json')
request.add_header('X-GFE-SSL', 'yes')
request.add_header('Authorization',
'Bearer ' + authenticate_and_fetch_token(args))
request.add_header(
'Authorization', 'Bearer ' + authenticate_and_fetch_token(args)
)
if args.dryrun:
print('dry run: would have reached to ' + request.full_url)
return HTTPStatus.OK
try:
response = urllib.request.urlopen(request, body)
except urllib.error.HTTPError as e:
@@ -329,18 +464,40 @@ def upload_batch(args, device_metadata, bccs):
eprint(line.decode('utf-8').rstrip())
sys.exit(1)
while chunk := response.read(1024):
print(chunk.decode('utf-8'))
response_body = response.read().decode('utf-8')
if args.verbose:
print('Response body:')
print(response_body)
res = json.loads(response_body)
if 'failedDeviceInfo' in res:
eprint('Failed to upload/check some device info! Response body:')
eprint(response_body)
eprint('Failed: {} bcc(s)'.format(len(res['failedDeviceInfo'])))
if args.die_on_error:
sys.exit(1)
elif 'successDeviceInfo' in res:
print('Success: {} bcc(s)'.format(len(res['successDeviceInfo'])))
else:
eprint('Failed with unexpected response:')
eprint(response_body)
def main():
args = parse_args()
if args.dryrun:
print('Dry run mode enabled. Service APIs will not be called.')
batches = {}
for filename in args.json_csr:
parse_json_csrs(filename, batches)
if len(batches) > 1:
print('WARNING: {} different device metadata'.format(len(batches)))
for device_metadata, bccs in batches.items():
upload_batch(args, device_metadata, bccs)
if args.check:
check_batch(args, device_metadata, bccs)
else:
upload_batch(args, device_metadata, bccs)
if __name__ == '__main__':

View File

@@ -46,6 +46,7 @@ std::unique_ptr<File> FileSystem::Open(const std::string&, int) {
return std::unique_ptr<File>(new FileImpl());
}
bool FileSystem::Exists(const std::string&) { return false; }
bool FileSystem::Exists(const std::string&, int*) { return false; }
bool FileSystem::Remove(const std::string&) { return false; }
ssize_t FileSystem::FileSize(const std::string&) { return false; }
bool FileSystem::List(const std::string&, std::vector<std::string>*) {
@@ -117,6 +118,9 @@ OEMCryptoResult OEMCryptoInterface::GetBcc(std::vector<uint8_t>& bcc) {
LOGI("GetBootCertificateChain second attempt result %d", result);
}
if (result == OEMCrypto_SUCCESS) {
bcc.resize(bcc_size);
}
return result;
}
@@ -136,7 +140,9 @@ OEMCryptoResult OEMCryptoInterface::GetOEMCryptoBuildInfo(
result = BuildInformation(&build_info[0], &build_info_size);
LOGI("BuildInformation second attempt result %d", result);
}
if (result == OEMCrypto_SUCCESS) {
build_info.resize(build_info_size);
}
return result;
}

View File

@@ -3,7 +3,7 @@
// License Agreement.
/**
* @mainpage OEMCrypto API v18.6
* @mainpage OEMCrypto API v18.7
*
* OEMCrypto is the low level library implemented by the OEM to provide key and
* content protection, usually in a separate secure memory or process space. The
@@ -721,6 +721,7 @@ typedef enum OEMCrypto_SignatureHashAlgorithm {
#define OEMCrypto_UseSecondaryKey _oecc144
#define OEMCrypto_MarkOfflineSession _oecc153
#define OEMCrypto_WrapClearPrivateKey _oecc154
#define OEMCrypto_SetSessionUsage _oecc155
// clang-format on
/// @addtogroup initcontrol
@@ -1941,6 +1942,33 @@ OEMCryptoResult OEMCrypto_GetOEMKeyToken(OEMCrypto_SESSION key_session,
uint8_t* key_token,
size_t* key_token_length);
/**
* Sets the session's usage information and scrambling mode, allowing the
* descrambler to be set up to decode one or more streams encrypted by the
* Conditional Access System (CAS). This method is currently used exclusively by
* CAS.
*
* @param[in] session: session id.
* @param[in] intent: session usage information. A constant defined by MediaCaS.
* @param[in] mode: scrambling mode. A constant defined by MediaCaS.
*
* @retval OEMCrypto_SUCCESS on success
* @retval OEMCrypto_ERROR_INVALID_SESSION
* @retval OEMCrypto_ERROR_INVALID_CONTEXT
* @retval OEMCrypto_ERROR_NOT_IMPLEMENTED
*
* @threading
* This is a "Session Function" and may be called simultaneously with session
* functions for other sessions but not simultaneously with other functions
* for this session. It is as if the CDM holds a write lock for this session,
* and a read lock on the OEMCrypto system.
*
* @version
* This method is new in API version 19.
*/
OEMCryptoResult OEMCrypto_SetSessionUsage(OEMCrypto_SESSION session,
uint32_t intent, uint32_t mode);
/// @}
/// @addtogroup decryption
@@ -2236,10 +2264,20 @@ OEMCryptoResult OEMCrypto_GetKeyHandle(OEMCrypto_SESSION session,
* usually be non-zero. This mode allows devices to decrypt FMP4 HLS content,
* SAMPLE-AES HLS content, as well as content using the DASH 'cbcs' scheme.
*
* The skip field of OEMCrypto_CENCEncryptPatternDesc may also be zero. If
* the skip field is zero, then patterns are not in use and all crypto blocks
* in the encrypted part of the subsample are encrypted. It is not valid for
* the encrypt field to be zero.
* The skip field of OEMCrypto_CENCEncryptPatternDesc may be zero. If the skip
* field is zero, then patterns are not in use and all crypto blocks in the
* encrypted part of the subsample are encrypted, except for any partial crypto
* blocks at the end. The most common pattern with a skip field of zero is
* (10,0), but all patterns with a skip field of zero are functionally the same.
*
* If the skip field of OEMCrypto_CENCEncryptPatternDesc is zero, the encrypt
* field may also be zero. This pattern sometimes appears in content,
* particularly in audio tracks. This (0,0) pattern should be treated as
* equivalent to the pattern (10,0). e.g. All complete crypto blocks should be
* decrypted.
*
* It is not valid for the encrypt field of OEMCrypto_CENCEncryptPatternDesc to
* be zero if the skip field is non-zero.
*
* The length of a crypto block in AES-128 is 16 bytes. In the 'cbcs' scheme,
* if the encrypted part of a subsample has a length that is not a multiple

View File

@@ -26,9 +26,9 @@ struct CoreMessageFeatures {
// This is the published version of the ODK Core Message library. The default
// behavior is for the server to restrict messages to at most this version
// number. The default is 18.6.
// number. The default is 18.7.
uint32_t maximum_major_version = 18;
uint32_t maximum_minor_version = 6;
uint32_t maximum_minor_version = 7;
bool operator==(const CoreMessageFeatures &other) const;
bool operator!=(const CoreMessageFeatures &other) const {

View File

@@ -16,10 +16,10 @@ extern "C" {
/* The version of this library. */
#define ODK_MAJOR_VERSION 18
#define ODK_MINOR_VERSION 6
#define ODK_MINOR_VERSION 7
/* ODK Version string. Date changed automatically on each release. */
#define ODK_RELEASE_DATE "ODK v18.6 2024-06-04"
#define ODK_RELEASE_DATE "ODK v18.7 2024-09-04"
/* The lowest version number for an ODK message. */
#define ODK_FIRST_VERSION 16

View File

@@ -30,7 +30,7 @@ CoreMessageFeatures CoreMessageFeatures::DefaultFeatures(
features.maximum_minor_version = 2; // 17.2
break;
case 18:
features.maximum_minor_version = 6; // 18.6
features.maximum_minor_version = 7; // 18.7
break;
default:
features.maximum_minor_version = 0;

View File

@@ -274,7 +274,7 @@ OEMCryptoResult ODK_InitializeSessionValues(ODK_TimerLimits* timer_limits,
nonce_values->api_minor_version = 2;
break;
case 18:
nonce_values->api_minor_version = 6;
nonce_values->api_minor_version = 7;
break;
default:
nonce_values->api_minor_version = 0;

View File

@@ -1216,7 +1216,7 @@ std::vector<VersionParameters> TestCases() {
// number.
{16, ODK_MAJOR_VERSION, ODK_MINOR_VERSION, 16, 5},
{17, ODK_MAJOR_VERSION, ODK_MINOR_VERSION, 17, 2},
{18, ODK_MAJOR_VERSION, ODK_MINOR_VERSION, 18, 6},
{18, ODK_MAJOR_VERSION, ODK_MINOR_VERSION, 18, 7},
// Here are some known good versions. Make extra sure they work.
{ODK_MAJOR_VERSION, 16, 3, 16, 3},
{ODK_MAJOR_VERSION, 16, 4, 16, 4},
@@ -1229,6 +1229,7 @@ std::vector<VersionParameters> TestCases() {
{ODK_MAJOR_VERSION, 18, 4, 18, 4},
{ODK_MAJOR_VERSION, 18, 5, 18, 5},
{ODK_MAJOR_VERSION, 18, 6, 18, 6},
{ODK_MAJOR_VERSION, 18, 7, 18, 7},
{0, 16, 3, 16, 3},
{0, 16, 4, 16, 4},
{0, 16, 5, 16, 5},
@@ -1237,6 +1238,7 @@ std::vector<VersionParameters> TestCases() {
{0, 18, 4, 18, 4},
{0, 18, 5, 18, 5},
{0, 18, 6, 18, 6},
{0, 18, 7, 18, 7},
};
return test_cases;
}

View File

@@ -384,3 +384,7 @@ OEMCryptoResult _oecc154(const uint8_t* clear_private_key_bytes,
size_t clear_private_key_length,
uint8_t* wrapped_private_key,
size_t* wrapped_private_key_length);
// OEMCrypto_SetSessionUsage defined in v18.7
OEMCryptoResult _oecc155(OEMCrypto_SESSION session, uint32_t intent,
uint32_t mode);

View File

@@ -26,6 +26,8 @@ format below:
+-----------------------+----------------------+--------------------------+
| Private Key |
+-----------------------+
| (DER-encoded PKCS#8) |
+-----------------------+
|oem_private_key| should be a RSA key in PKCS#8 PrivateKeyInfo format.
|oem_public_cert| should be a DER-encoded PKCS#7 certificate chain.

View File

@@ -17,7 +17,6 @@ void advance_dest_buffer(OEMCrypto_DestBufferDesc* dest_buffer, size_t bytes) {
switch (dest_buffer->type) {
case OEMCrypto_BufferType_Clear:
dest_buffer->buffer.clear.clear_buffer += bytes;
dest_buffer->buffer.clear.clear_buffer_length -= bytes;
break;
case OEMCrypto_BufferType_Secure:
@@ -99,6 +98,11 @@ OEMCryptoResult DecryptFallbackChain::DecryptSample(
const size_t length =
subsample.num_bytes_clear + subsample.num_bytes_encrypted;
fake_sample.buffers.input_data_length = length;
if (fake_sample.buffers.output_descriptor.type ==
OEMCrypto_BufferType_Clear) {
fake_sample.buffers.output_descriptor.buffer.clear.clear_buffer_length =
length;
}
fake_sample.subsamples = &subsample;
fake_sample.subsamples_length = 1;
@@ -144,6 +148,11 @@ OEMCryptoResult DecryptFallbackChain::DecryptSubsample(
if (subsample.num_bytes_clear > 0) {
fake_sample.buffers.input_data_length = subsample.num_bytes_clear;
if (fake_sample.buffers.output_descriptor.type ==
OEMCrypto_BufferType_Clear) {
fake_sample.buffers.output_descriptor.buffer.clear.clear_buffer_length =
subsample.num_bytes_clear;
}
fake_subsample.num_bytes_clear = subsample.num_bytes_clear;
fake_subsample.num_bytes_encrypted = 0;
fake_subsample.block_offset = 0;
@@ -167,6 +176,11 @@ OEMCryptoResult DecryptFallbackChain::DecryptSubsample(
if (subsample.num_bytes_encrypted > 0) {
fake_sample.buffers.input_data_length = subsample.num_bytes_encrypted;
if (fake_sample.buffers.output_descriptor.type ==
OEMCrypto_BufferType_Clear) {
fake_sample.buffers.output_descriptor.buffer.clear.clear_buffer_length =
subsample.num_bytes_encrypted;
}
fake_subsample.num_bytes_clear = 0;
fake_subsample.num_bytes_encrypted = subsample.num_bytes_encrypted;
fake_subsample.block_offset = subsample.block_offset;

View File

@@ -10,7 +10,9 @@
#include <cstring>
#include "log.h"
#include "oec_test_data.h"
#include "string_conversions.h"
#include "test_sleep.h"
namespace wvoec {
@@ -68,6 +70,12 @@ void DeviceFeatures::Initialize() {
provisioning_method == OEMCrypto_BootCertificateChain ||
provisioning_method == OEMCrypto_DrmReprovisioning;
printf("loads_certificate = %s.\n", loads_certificate ? "true" : "false");
if (rsa_test_key().empty()) {
set_rsa_test_key(
std::vector<uint8_t>(kTestRSAPKCS8PrivateKeyInfo2_2048,
kTestRSAPKCS8PrivateKeyInfo2_2048 +
sizeof(kTestRSAPKCS8PrivateKeyInfo2_2048)));
}
generic_crypto =
(OEMCrypto_ERROR_NOT_IMPLEMENTED !=
OEMCrypto_Generic_Encrypt(buffer, 0, buffer, 0, iv,
@@ -129,6 +137,9 @@ void DeviceFeatures::Initialize() {
case LOAD_TEST_RSA_KEY:
printf("LOAD_TEST_RSA_KEY: Call LoadTestRSAKey before deriving keys.\n");
break;
case PRELOADED_RSA_KEY:
printf("PRELOADED_RSA_KEY: Device has test RSA key baked in.\n");
break;
case TEST_PROVISION_30:
printf("TEST_PROVISION_30: Device provisioned with OEM Cert.\n");
break;
@@ -175,9 +186,10 @@ void DeviceFeatures::PickDerivedKey() {
return;
case OEMCrypto_DrmCertificate:
case OEMCrypto_DrmReprovisioning:
if (OEMCrypto_ERROR_NOT_IMPLEMENTED != OEMCrypto_LoadTestRSAKey()) {
derive_key_method = LOAD_TEST_RSA_KEY;
}
derive_key_method =
(OEMCrypto_ERROR_NOT_IMPLEMENTED == OEMCrypto_LoadTestRSAKey())
? PRELOADED_RSA_KEY
: LOAD_TEST_RSA_KEY;
return;
case OEMCrypto_Keybox:
if (OEMCrypto_ERROR_NOT_IMPLEMENTED !=

View File

@@ -38,6 +38,7 @@ class DeviceFeatures {
LOAD_TEST_RSA_KEY, // Call LoadTestRSAKey before deriving keys.
TEST_PROVISION_30, // Device has OEM Certificate installed.
TEST_PROVISION_40, // Device has Boot Certificate Chain installed.
PRELOADED_RSA_KEY, // Device has test RSA key baked in.
};
enum DeriveMethod derive_key_method;
@@ -70,6 +71,16 @@ class DeviceFeatures {
// Get a list of output types that should be tested.
const std::vector<OutputType>& GetOutputTypes();
// If the device has a baked in cert, then this is the public key that should
// be used for testing.
const std::vector<uint8_t>& rsa_test_key() const { return rsa_test_key_; };
void set_rsa_test_key(const std::vector<uint8_t>& rsa_test_key) {
rsa_test_key_ = rsa_test_key;
}
void set_rsa_test_key(std::vector<uint8_t>&& rsa_test_key) {
rsa_test_key_ = std::move(rsa_test_key);
}
private:
// Decide which method should be used to derive session keys, based on
// supported featuers.
@@ -82,6 +93,7 @@ class DeviceFeatures {
// A list of possible output types.
std::vector<OutputType> output_types_;
bool initialized_ = false;
std::vector<uint8_t> rsa_test_key_;
};
// There is one global set of features for the version of OEMCrypto being

View File

@@ -554,7 +554,7 @@ void ProvisioningRoundTrip::VerifyLoadFailed() {
}
void Provisioning40RoundTrip::PrepareSession(bool is_oem_key) {
const size_t buffer_size = 5000; // Make sure it is large enough.
const size_t buffer_size = 10240; // Make sure it is large enough.
std::vector<uint8_t> public_key(buffer_size);
size_t public_key_size = buffer_size;
std::vector<uint8_t> public_key_signature(buffer_size);
@@ -616,7 +616,7 @@ OEMCryptoResult Provisioning40RoundTrip::LoadDRMCertResponse() {
}
void Provisioning40CastRoundTrip::PrepareSession() {
const size_t buffer_size = 5000; // Make sure it is large enough.
const size_t buffer_size = 10240; // Make sure it is large enough.
std::vector<uint8_t> public_key(buffer_size);
size_t public_key_size = buffer_size;
std::vector<uint8_t> public_key_signature(buffer_size);
@@ -1918,10 +1918,9 @@ void Session::LoadOEMCert(bool verify_cert) {
void Session::SetTestRsaPublicKey() {
public_ec_.reset();
public_rsa_ = util::RsaPublicKey::LoadPrivateKeyInfo(
kTestRSAPKCS8PrivateKeyInfo2_2048,
sizeof(kTestRSAPKCS8PrivateKeyInfo2_2048));
ASSERT_TRUE(public_rsa_) << "Could not parse test RSA public key #2";
public_rsa_ =
util::RsaPublicKey::LoadPrivateKeyInfo(global_features.rsa_test_key());
ASSERT_TRUE(public_rsa_) << "Could not parse test RSA public key";
}
void Session::SetPublicKeyFromPrivateKeyInfo(OEMCrypto_PrivateKeyType key_type,

View File

@@ -2,17 +2,79 @@
// source code may only be used and distributed under the Widevine
// License Agreement.
//
#include "oemcrypto_basic_test.h"
#include <ctype.h>
#include <inttypes.h>
#include <algorithm>
#include <map>
#include <ostream>
#include <set>
#include <string>
#include <vector>
#include <jsmn.h>
#include "OEMCryptoCENC.h"
#include "clock.h"
#include "jsmn.h"
#include "log.h"
#include "oemcrypto_corpus_generator_helper.h"
#include "oemcrypto_resource_test.h"
#include "test_sleep.h"
void PrintTo(const jsmntype_t& type, std::ostream* out) {
switch (type) {
case JSMN_UNDEFINED:
*out << "Undefined";
return;
case JSMN_OBJECT:
*out << "Object";
return;
case JSMN_ARRAY:
*out << "Array";
return;
case JSMN_STRING:
*out << "String";
return;
case JSMN_PRIMITIVE:
*out << "Primitive";
return;
}
*out << "Unknown(" << static_cast<int>(type) << ')';
}
namespace wvoec {
namespace {
// Counts the number of ancestor tokens of the provided |root_index| token.
// The result does not count the root itself.
//
// JSMN tokens specify the count of immediate ancessor tokens, but
// not the total.
// - Primitives never have children
// - Strings have 0 if they are a value, and 1 if they are the
// name of an object member
// - Objects have the count of members (each key-value pair is 1,
// regardless of the value's children elements)
// - Arrays have the count of elements (regardless of the values members)
//
int32_t JsmnAncestorCount(const std::vector<jsmntok_t>& tokens,
int32_t root_index) {
if (root_index >= static_cast<int32_t>(tokens.size())) return 0;
int32_t count = 0;
int32_t iter = root_index;
int32_t remainder = 1;
while (remainder > 0 && iter < static_cast<int32_t>(tokens.size())) {
const int32_t child_count = tokens[iter].size;
remainder += child_count;
count += child_count;
iter++;
remainder--;
}
return count;
}
} // namespace
void OEMCryptoClientTest::SetUp() {
::testing::Test::SetUp();
wvutil::TestSleep::SyncFakeClock();
@@ -156,7 +218,7 @@ TEST_F(OEMCryptoClientTest, FreeUnallocatedSecureBufferNoFailure) {
*/
TEST_F(OEMCryptoClientTest, VersionNumber) {
const std::string log_message =
"OEMCrypto unit tests for API 18.6. Tests last updated 2024-06-04";
"OEMCrypto unit tests for API 18.7. Tests last updated 2024-09-04";
cout << " " << log_message << "\n";
cout << " "
<< "These tests are part of Android U."
@@ -165,7 +227,7 @@ TEST_F(OEMCryptoClientTest, VersionNumber) {
// If any of the following fail, then it is time to update the log message
// above.
EXPECT_EQ(ODK_MAJOR_VERSION, 18);
EXPECT_EQ(ODK_MINOR_VERSION, 6);
EXPECT_EQ(ODK_MINOR_VERSION, 7);
EXPECT_EQ(kCurrentAPI, static_cast<unsigned>(ODK_MAJOR_VERSION));
OEMCrypto_Security_Level level = OEMCrypto_SecurityLevel();
EXPECT_GT(level, OEMCrypto_Level_Unknown);
@@ -286,26 +348,143 @@ TEST_F(OEMCryptoClientTest, CheckNullBuildInformationAPI17) {
}
}
// Verifies that OEMCrypto_BuildInformation() is behaving as expected
// by assigning appropriate values to the build info size.
TEST_F(OEMCryptoClientTest, CheckBuildInformation_OutputLengthAPI17) {
constexpr size_t kZero = 0;
constexpr char kNullChar = '\0';
// Allocating single byte to avoid potential null dereference.
std::string build_info(1, kNullChar);
size_t build_info_length = 0;
OEMCryptoResult result =
OEMCrypto_BuildInformation(&build_info[0], &build_info_length);
ASSERT_EQ(result, OEMCrypto_ERROR_SHORT_BUFFER);
ASSERT_GT(build_info_length, kZero)
<< "Signaling ERROR_SHORT_BUFFER should have assigned a length";
// Force a ERROR_SHORT_BUFFER using a non-zero value.
// Note: It is assumed that vendors will provide more than a single
// character of info.
const size_t second_attempt_length =
(build_info_length >= 2) ? build_info_length / 2 : 1;
build_info.assign(second_attempt_length, kNullChar);
build_info_length = build_info.size();
result = OEMCrypto_BuildInformation(&build_info[0], &build_info_length);
ASSERT_EQ(result, OEMCrypto_ERROR_SHORT_BUFFER)
<< "second_attempt_length = " << second_attempt_length
<< ", build_info_length" << build_info_length;
// OEM specified build info length should be larger than the
// original length if returning ERROR_SHORT_BUFFER.
ASSERT_GT(build_info_length, second_attempt_length);
// Final attempt with a buffer large enough buffer, padding to
// ensure the caller truncates.
constexpr size_t kBufferPadSize = 42;
const size_t expected_length = build_info_length;
const size_t final_attempt_length = expected_length + kBufferPadSize;
build_info.assign(final_attempt_length, kNullChar);
build_info_length = build_info.size();
result = OEMCrypto_BuildInformation(&build_info[0], &build_info_length);
ASSERT_EQ(result, OEMCrypto_SUCCESS)
<< "final_attempt_length = " << final_attempt_length
<< ", expected_length = " << expected_length
<< ", build_info_length = " << build_info_length;
// Ensure not empty.
ASSERT_GT(build_info_length, kZero) << "Build info cannot be empty";
// Ensure it was truncated down from the padded length.
ASSERT_LT(build_info_length, final_attempt_length)
<< "Should have truncated from oversized buffer: expected_length = "
<< expected_length;
// Ensure the real length is within the size originally specified.
// OK if final length is smaller than estimated length.
ASSERT_LE(build_info_length, expected_length);
}
// Verifies that OEMCrypto_BuildInformation() is behaving as expected
// by checking the resulting contents.
// Does not validate whether output if valid JSON for v18.
TEST_F(OEMCryptoClientTest, CheckBuildInformation_OutputContentAPI17) {
constexpr size_t kZero = 0;
constexpr char kNullChar = '\0';
// Allocating single byte to avoid potential null dereference.
std::string build_info(1, kNullChar);
size_t build_info_length = 0;
OEMCryptoResult result =
OEMCrypto_BuildInformation(&build_info[0], &build_info_length);
ASSERT_EQ(result, OEMCrypto_ERROR_SHORT_BUFFER);
ASSERT_GT(build_info_length, kZero)
<< "Signaling ERROR_SHORT_BUFFER should have assigned a length";
// Expect successful acquisition of build information.
const size_t expected_length = build_info_length;
build_info.assign(expected_length, kNullChar);
result = OEMCrypto_BuildInformation(&build_info[0], &build_info_length);
ASSERT_EQ(result, OEMCrypto_SUCCESS)
<< "expected_length = " << expected_length
<< ", build_info_length = " << build_info_length;
// Ensure not empty.
ASSERT_GT(build_info_length, kZero) << "Build info cannot be empty";
// Ensure the real length is within the size originally specified.
ASSERT_LE(build_info_length, expected_length)
<< "Cannot specify success if buffer was too small";
build_info.resize(build_info_length);
// Ensure there isn't a trailing null byte.
ASSERT_NE(build_info.back(), kNullChar)
<< "Build info must not contain trailing null byte";
// Ensure all build info characters are printable, or a limited
// set of white space characters (case of JSON build info).
const auto is_valid_build_info_white_space = [](const char& ch) -> bool {
constexpr char kSpace = ' ';
constexpr char kLineFeed = '\n';
constexpr char kTab = '\t';
return ch == kLineFeed || ch == kTab || ch == kSpace;
};
const auto is_valid_build_info_char = [&](const char& ch) -> bool {
return ::isprint(ch) || is_valid_build_info_white_space(ch);
};
ASSERT_TRUE(std::all_of(build_info.begin(), build_info.end(),
is_valid_build_info_char))
<< "Build info is not printable: " << wvutil::b2a_hex(build_info);
// Ensure build info isn't just white space.
ASSERT_FALSE(std::all_of(build_info.begin(), build_info.end(),
is_valid_build_info_white_space))
<< "Build info is just white space: " << wvutil::b2a_hex(build_info);
}
TEST_F(OEMCryptoClientTest, CheckJsonBuildInformationAPI18) {
std::string build_info;
OEMCryptoResult sts = OEMCrypto_BuildInformation(&build_info[0], nullptr);
ASSERT_EQ(OEMCrypto_ERROR_INVALID_CONTEXT, sts);
size_t buf_length = 0;
constexpr char kNullChar = '\0';
constexpr size_t kZero = 0;
// Step 1: Get Build Info
size_t buffer_length = 0;
// OEMCrypto must allow |buffer| to be null so long as |buffer_length|
// is provided and initially set to zero.
sts = OEMCrypto_BuildInformation(nullptr, &buf_length);
ASSERT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, sts);
build_info.resize(buf_length);
const size_t max_final_size = buf_length;
sts = OEMCrypto_BuildInformation(&build_info[0], &buf_length);
ASSERT_EQ(OEMCrypto_SUCCESS, sts);
ASSERT_LE(buf_length, max_final_size);
build_info.resize(buf_length);
OEMCryptoResult result = OEMCrypto_BuildInformation(nullptr, &buffer_length);
ASSERT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, result);
ASSERT_GT(buffer_length, kZero);
std::string build_info(buffer_length, kNullChar);
const size_t max_final_size = buffer_length;
result = OEMCrypto_BuildInformation(&build_info[0], &buffer_length);
ASSERT_EQ(OEMCrypto_SUCCESS, result);
ASSERT_LE(buffer_length, max_final_size);
build_info.resize(buffer_length);
// Step 2: Parse as JSON
jsmn_parser p;
jsmn_init(&p);
std::vector<jsmntok_t> tokens;
int32_t num_tokens =
const int32_t num_tokens =
jsmn_parse(&p, build_info.c_str(), build_info.size(), nullptr, 0);
EXPECT_GT(num_tokens, 0)
<< "Failed to parse BuildInformation as JSON, parse returned "
@@ -313,45 +492,186 @@ TEST_F(OEMCryptoClientTest, CheckJsonBuildInformationAPI18) {
tokens.resize(num_tokens);
jsmn_init(&p);
int32_t jsmn_result = jsmn_parse(&p, build_info.c_str(), build_info.size(),
tokens.data(), num_tokens);
const int32_t jsmn_result = jsmn_parse(
&p, build_info.c_str(), build_info.size(), tokens.data(), num_tokens);
EXPECT_GE(jsmn_result, 0)
<< "Failed to parse BuildInformation as JSON, parse returned "
<< jsmn_result << "for following build info: " << build_info;
std::map<std::string, jsmntype_t> expected;
expected["soc_vendor"] = JSMN_STRING;
expected["soc_model"] = JSMN_STRING;
expected["ta_ver"] = JSMN_STRING;
expected["uses_opk"] = JSMN_PRIMITIVE;
expected["tee_os"] = JSMN_STRING;
expected["tee_os_ver"] = JSMN_STRING;
// Step 3a: Ensure info is a single JSON object.
const jsmntok_t& object_token = tokens[0];
ASSERT_EQ(object_token.type, JSMN_OBJECT)
<< "Build info is not a JSON object: " << build_info;
// for values in token
// build string from start,end
// check for existence in map
// check if value matches expectation
// remove from map
for (int i = 0; i < jsmn_result; i++) {
jsmntok_t token = tokens[i];
std::string key = build_info.substr(token.start, token.end - token.start);
if (expected.find(key) != expected.end()) {
EXPECT_EQ(expected.find(key)->second, tokens[i + 1].type)
<< "Type is incorrect for key " << key;
expected.erase(key);
// Step 3b: Verify schema of defined fields.
// Required fields must be present in the build information,
// and be of the correct type.
const std::map<std::string, jsmntype_t> kRequiredFields = {
// SOC manufacturer name
{"soc_vendor", JSMN_STRING},
// SOC model name
{"soc_model", JSMN_STRING},
// TA version in string format eg "1.12.3+tag", "2.0"
{"ta_ver", JSMN_STRING},
// [bool] Whether TA was built with Widevine's OPK
{"uses_opk", JSMN_PRIMITIVE},
// Trusted OS intended to run the TA, eg "Trusty", "QSEE", "OP-TEE"
{"tee_os", JSMN_STRING},
// Version of Trusted OS intended to run the TA
{"tee_os_ver", JSMN_STRING},
// [bool] Whether this is a debug build of the TA
// Not forcing behavior until implementations fix
// them self
// {"is_debug", JSMN_PRIMITIVE},
};
const std::string kSpecialCaseReeKey = "ree";
// Optional fields may be present in the build information;
// if they are, then the must be the correct type.
const std::map<std::string, jsmntype_t> kOptionalFields = {
// Name of company or entity that provides OEMCrypto.
{"implementor", JSMN_STRING},
// Git commit hash of the code repository.
{"git_commit", JSMN_STRING},
// ISO 8601 formatted timestamp of the time the TA was compiled
{"build_timestamp", JSMN_STRING},
// Whether this was built with FACTORY_MODE_ONLY defined
{"is_factory_mode", JSMN_PRIMITIVE},
// ... provide information about liboemcrypto.so
// Special case, see kOptionalReeFields for details.
{kSpecialCaseReeKey, JSMN_OBJECT},
// Technically required, but several implementations
// do not implement this fields.
{"is_debug", JSMN_PRIMITIVE},
};
// A set of the required fields found when examining the
// build information, use to verify all fields are present.
std::set<std::string> found_required_fields;
// Stores the tokens of the "ree" field, if set, used to
// validate its content.
std::vector<jsmntok_t> ree_tokens;
bool has_ree_info = false;
// Start: first object key token
// Condition: key-value pair (2 tokens)
// Iter: next key-value pair (2 tokens)
for (int32_t i = 1; (i + 1) < jsmn_result; i += 2) {
// JSMN objects consist of pairs of key-value pairs (keys are always
// JSMN_STRING).
const jsmntok_t& key_token = tokens[i];
ASSERT_EQ(key_token.type, JSMN_STRING)
<< "Bad object key: i = " << i << ", build_info = " << build_info;
const jsmntok_t& value_token = tokens[i + 1];
const std::string key =
build_info.substr(key_token.start, key_token.end - key_token.start);
if (kRequiredFields.find(key) != kRequiredFields.end()) {
ASSERT_EQ(value_token.type, kRequiredFields.at(key))
<< "Unexpected required field type: field = " << key
<< ", build_info = " << build_info;
found_required_fields.insert(key);
} else if (kOptionalFields.find(key) != kOptionalFields.end()) {
ASSERT_EQ(value_token.type, kOptionalFields.at(key))
<< "Unexpected optional field type: field = " << key
<< ", build_info = " << build_info;
} // Do not validate vendor fields.
if (key == kSpecialCaseReeKey) {
// Store the tokens of the "ree" field for additional validation.
const int32_t first_ree_field_index = i + 2;
const int32_t ree_token_count = JsmnAncestorCount(tokens, i + 1);
const auto first_ree_field_iter = tokens.begin() + first_ree_field_index;
ree_tokens.assign(first_ree_field_iter,
first_ree_field_iter + ree_token_count);
has_ree_info = true;
}
// Skip potential nested tokens.
i += JsmnAncestorCount(tokens, i + 1);
}
// if map is not empty, return false
if (expected.size() > 0) {
std::string missing;
for (auto e : expected) {
missing.append(e.first);
missing.append(" ");
// Step 3c: Ensure all required fields were found.
if (found_required_fields.size() != kRequiredFields.size()) {
// Generate a list of all the missing fields.
std::string missing_fields;
for (const auto& required_field : kRequiredFields) {
if (found_required_fields.find(required_field.first) !=
found_required_fields.end())
continue;
if (!missing_fields.empty()) {
missing_fields.append(", ");
}
missing_fields.push_back('"');
missing_fields.append(required_field.first);
missing_fields.push_back('"');
}
FAIL() << "JSON does not contain all required keys. Missing keys: ["
<< missing << "] in string " << build_info;
FAIL() << "Build info JSON object does not contain all required keys; "
<< "missing_fields = [" << missing_fields
<< "], build_info = " << build_info;
return;
}
// If no "ree" field tokens, then end here.
if (!has_ree_info) return;
// Step 4a: Verify "ree" object scheme.
ASSERT_FALSE(ree_tokens.empty())
<< "REE field was specified, but contents were empty: build_info = "
<< build_info;
// The optional field "ree", if present, must follow the required
// format.
const std::map<std::string, jsmntype_t> kReeRequiredFields = {
// liboemcrypto.so version in string format eg "2.15.0+tag"
{"liboemcrypto_ver", JSMN_STRING},
// git hash of code that compiled liboemcrypto.so
{"git_commit", JSMN_STRING},
// ISO 8601 timestamp for when liboemcrypto.so was built
{"build_timestamp", JSMN_STRING}};
found_required_fields.clear();
for (int32_t i = 0; (i + 1) < static_cast<int32_t>(ree_tokens.size());
i += 2) {
const jsmntok_t& key_token = ree_tokens[i];
ASSERT_EQ(key_token.type, JSMN_STRING)
<< "Bad REE object key: i = " << i << ", build_info = " << build_info;
const jsmntok_t& value_token = ree_tokens[i + 1];
const std::string key =
build_info.substr(key_token.start, key_token.end - key_token.start);
if (kReeRequiredFields.find(key) != kReeRequiredFields.end()) {
ASSERT_EQ(value_token.type, kReeRequiredFields.at(key))
<< "Unexpected optional REE field type: ree_field = " << key
<< ", build_info = " << build_info;
found_required_fields.insert(key);
} // Do not validate vendor fields.
// Skip potential nested tokens.
i += JsmnAncestorCount(ree_tokens, i + 1);
}
// Step 4b: Ensure all required fields of the "ree" object were found.
if (found_required_fields.size() == kReeRequiredFields.size()) return;
// Generate a list of all the missing REE fields.
std::string missing_ree_fields;
for (const auto& required_field : kReeRequiredFields) {
if (found_required_fields.find(required_field.first) !=
found_required_fields.end())
continue;
if (!missing_ree_fields.empty()) {
missing_ree_fields.append(", ");
}
missing_ree_fields.push_back('"');
missing_ree_fields.append(required_field.first);
missing_ree_fields.push_back('"');
}
FAIL() << "REE info JSON object does not contain all required keys; "
<< "missing_ree_fields = [" << missing_ree_fields
<< "], build_info = " << build_info;
}
TEST_F(OEMCryptoClientTest, CheckMaxNumberOfSessionsAPI10) {

View File

@@ -121,38 +121,6 @@ TEST_P(OEMCryptoLicenseTest, RejectCensAPI16) {
EXPECT_EQ(OEMCrypto_ERROR_INVALID_CONTEXT, sts);
}
// 'cbc1' mode is no longer supported in v16
TEST_P(OEMCryptoLicenseTest, RejectCbc1API16) {
ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest());
ASSERT_NO_FATAL_FAILURE(license_messages_.CreateDefaultResponse());
ASSERT_NO_FATAL_FAILURE(license_messages_.EncryptAndSignResponse());
ASSERT_EQ(OEMCrypto_SUCCESS, license_messages_.LoadResponse());
vector<uint8_t> key_handle;
OEMCryptoResult sts;
sts = GetKeyHandleIntoVector(session_.session_id(),
session_.license().keys[0].key_id,
session_.license().keys[0].key_id_length,
OEMCrypto_CipherMode_CBCS, key_handle);
ASSERT_EQ(OEMCrypto_SUCCESS, sts);
vector<uint8_t> in_buffer(256);
vector<uint8_t> out_buffer(in_buffer.size());
OEMCrypto_SampleDescription sample_description;
OEMCrypto_SubSampleDescription subsample_description;
GenerateSimpleSampleDescription(in_buffer, out_buffer, &sample_description,
&subsample_description);
// Create a zero pattern to indicate this is 'cbc1'
OEMCrypto_CENCEncryptPatternDesc pattern = {0, 0};
// Try to decrypt the data
sts = OEMCrypto_DecryptCENC(key_handle.data(), key_handle.size(),
&sample_description, 1, &pattern);
EXPECT_EQ(OEMCrypto_ERROR_INVALID_CONTEXT, sts);
}
TEST_P(OEMCryptoLicenseTest, RejectCbcsWithBlockOffset) {
ASSERT_NO_FATAL_FAILURE(license_messages_.SignAndVerifyRequest());
ASSERT_NO_FATAL_FAILURE(license_messages_.CreateDefaultResponse());

View File

@@ -70,6 +70,9 @@ void SessionUtil::EnsureTestROT() {
case DeviceFeatures::TEST_PROVISION_30:
// Can use oem certificate to install test rsa key.
break;
case DeviceFeatures::PRELOADED_RSA_KEY:
// There is already a key.
break;
case wvoec::DeviceFeatures::TEST_PROVISION_40:
// OEM certificate is retrieved from the server.
break;

View File

@@ -90,6 +90,9 @@ TEST_F(OEMCryptoSessionTests, Provisioning_IncrementCounterAPI18) {
// Test that successive calls to PrepAndSignLicenseRequest only increase
// the license count in the ODK message
TEST_F(OEMCryptoSessionTests, License_IncrementCounterAPI18) {
if (OEMCrypto_SecurityLevel() == OEMCrypto_Level3) {
GTEST_SKIP() << "L3 does not support license counter.";
}
Session s;
s.open();
LicenseRoundTrip license_messages(&s);
@@ -132,6 +135,9 @@ TEST_F(OEMCryptoSessionTests, MasterGeneration_IncrementCounterAPI18) {
GTEST_SKIP() << "Usage table not supported, so master generation number "
"does not need to be checked.";
}
if (OEMCrypto_SecurityLevel() == OEMCrypto_Level3) {
GTEST_SKIP() << "L3 does not support license counter.";
}
Session s1;
s1.open();
LicenseRoundTrip license_messages(&s1);

View File

@@ -502,6 +502,12 @@ OEMCryptoResult OEMCrypto_GetOEMKeyToken(OEMCrypto_SESSION key_session UNUSED,
return OEMCrypto_ERROR_NOT_IMPLEMENTED;
}
OEMCryptoResult OEMCrypto_SetSessionUsage(OEMCrypto_SESSION session UNUSED,
uint32_t intent UNUSED,
uint32_t mode UNUSED) {
return OEMCrypto_ERROR_NOT_IMPLEMENTED;
}
OEMCryptoResult OEMCrypto_GetDeviceInformation(
uint8_t* device_info UNUSED, size_t* device_info_length UNUSED) {
return OEMCrypto_ERROR_NOT_IMPLEMENTED;