diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fbd7942..b2407403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 2f0dd1aa..f03f5409 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cdm/include/cdm_version.h b/cdm/include/cdm_version.h index 84a40d10..96e12751 100644 --- a/cdm/include/cdm_version.h +++ b/cdm/include/cdm_version.h @@ -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 diff --git a/cdm/test/cdm_test_runner.cpp b/cdm/test/cdm_test_runner.cpp index 02fcc07a..a951dceb 100644 --- a/cdm/test/cdm_test_runner.cpp +++ b/cdm/test/cdm_test_runner.cpp @@ -7,11 +7,14 @@ #include #include #include +#include +#include #include #include #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=\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=\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=\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& 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(data.begin(), data.end())); + } + std::vector new_argv(args.size()); std::transform( std::begin(args), std::end(args), std::begin(new_argv), diff --git a/cdm/test/test_host.cpp b/cdm/test/test_host.cpp index 3a4826a6..7fdcf1af 100644 --- a/cdm/test/test_host.cpp +++ b/cdm/test/test_host.cpp @@ -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(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"); diff --git a/cdm/test/test_host.h b/cdm/test/test_host.h index 29622dc3..5c6b4747 100644 --- a/cdm/test/test_host.h +++ b/cdm/test/test_host.h @@ -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. diff --git a/core/include/system_id_extractor.h b/core/include/system_id_extractor.h index 8b7800e6..e89cd39d 100644 --- a/core/include/system_id_extractor.h +++ b/core/include/system_id_extractor.h @@ -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|. diff --git a/core/src/crypto_session.cpp b/core/src/crypto_session.cpp index 90c1a32c..1b615b73 100644 --- a/core/src/crypto_session.cpp +++ b/core/src/crypto_session.cpp @@ -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 { diff --git a/core/test/core_integration_test.cpp b/core/test/core_integration_test.cpp index ef3657f2..cf695014 100644 --- a/core/test/core_integration_test.cpp +++ b/core/test/core_integration_test.cpp @@ -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, diff --git a/core/test/license_holder.cpp b/core/test/license_holder.cpp index 6bd34468..088d1d3d 100644 --- a/core/test/license_holder.cpp +++ b/core/test/license_holder.cpp @@ -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_); diff --git a/core/test/provisioning_holder.cpp b/core/test/provisioning_holder.cpp index 954b2eff..60265514 100644 --- a/core/test/provisioning_holder.cpp +++ b/core/test/provisioning_holder.cpp @@ -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); diff --git a/core/test/test_base.cpp b/core/test/test_base.cpp index 7db10a25..bd946450 100644 --- a/core/test/test_base.cpp +++ b/core/test/test_base.cpp @@ -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 file_system(CreateTestFileSystem()); // OpenSession will check if a DRM certificate exists, while diff --git a/core/test/url_request.cpp b/core/test/url_request.cpp index e9c8c513..5eab174f 100644 --- a/core/test/url_request.cpp +++ b/core/test/url_request.cpp @@ -5,7 +5,9 @@ #include "url_request.h" #include +#include +#include #include #include @@ -24,11 +26,15 @@ const int kConnectTimeoutMs = 15000; const int kWriteTimeoutMs = 12000; const int kReadTimeoutMs = 12000; constexpr int kHttpOk = 200; +const std::vector 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(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(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; } diff --git a/core/test/url_request.h b/core/test/url_request.h index c2d0d35e..68dc593e 100644 --- a/core/test/url_request.h +++ b/core/test/url_request.h @@ -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); }; diff --git a/factory_upload_tool/ce/wv_factory_extractor.cpp b/factory_upload_tool/ce/wv_factory_extractor.cpp index c306cb0c..1628aba1 100644 --- a/factory_upload_tool/ce/wv_factory_extractor.cpp +++ b/factory_upload_tool/ce/wv_factory_extractor.cpp @@ -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& 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); diff --git a/factory_upload_tool/ce/wv_upload_tool.py b/factory_upload_tool/ce/wv_upload_tool.py old mode 100644 new mode 100755 index 90b24786..e70ce74c --- a/factory_upload_tool/ce/wv_upload_tool.py +++ b/factory_upload_tool/ce/wv_upload_tool.py @@ -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(('' - f' {title}' - f' ' - f'

{message}

' - f' ' - '').encode('utf-8')) + self.wfile.write( + ( + '' + f' {title}' + ' ' + f'

{message}

' + ' ' + '' + ).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__': diff --git a/factory_upload_tool/common/src/WidevineOemcryptoInterface.cpp b/factory_upload_tool/common/src/WidevineOemcryptoInterface.cpp index dee299c6..b35ac733 100644 --- a/factory_upload_tool/common/src/WidevineOemcryptoInterface.cpp +++ b/factory_upload_tool/common/src/WidevineOemcryptoInterface.cpp @@ -46,6 +46,7 @@ std::unique_ptr FileSystem::Open(const std::string&, int) { return std::unique_ptr(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*) { @@ -117,6 +118,9 @@ OEMCryptoResult OEMCryptoInterface::GetBcc(std::vector& 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; } diff --git a/oemcrypto/include/OEMCryptoCENC.h b/oemcrypto/include/OEMCryptoCENC.h index bf771c19..42184e2c 100644 --- a/oemcrypto/include/OEMCryptoCENC.h +++ b/oemcrypto/include/OEMCryptoCENC.h @@ -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 diff --git a/oemcrypto/odk/include/core_message_features.h b/oemcrypto/odk/include/core_message_features.h index 5fd71c39..3779f02e 100644 --- a/oemcrypto/odk/include/core_message_features.h +++ b/oemcrypto/odk/include/core_message_features.h @@ -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 { diff --git a/oemcrypto/odk/include/odk_structs.h b/oemcrypto/odk/include/odk_structs.h index 12f97231..cabe6f01 100644 --- a/oemcrypto/odk/include/odk_structs.h +++ b/oemcrypto/odk/include/odk_structs.h @@ -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 diff --git a/oemcrypto/odk/src/core_message_features.cpp b/oemcrypto/odk/src/core_message_features.cpp index f3125839..e9e075c5 100644 --- a/oemcrypto/odk/src/core_message_features.cpp +++ b/oemcrypto/odk/src/core_message_features.cpp @@ -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; diff --git a/oemcrypto/odk/src/odk_timer.c b/oemcrypto/odk/src/odk_timer.c index 934717f2..b79ac056 100644 --- a/oemcrypto/odk/src/odk_timer.c +++ b/oemcrypto/odk/src/odk_timer.c @@ -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; diff --git a/oemcrypto/odk/test/odk_test.cpp b/oemcrypto/odk/test/odk_test.cpp index 118077e7..fe172bd7 100644 --- a/oemcrypto/odk/test/odk_test.cpp +++ b/oemcrypto/odk/test/odk_test.cpp @@ -1216,7 +1216,7 @@ std::vector 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 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 TestCases() { {0, 18, 4, 18, 4}, {0, 18, 5, 18, 5}, {0, 18, 6, 18, 6}, + {0, 18, 7, 18, 7}, }; return test_cases; } diff --git a/oemcrypto/test/GEN_api_lock_file.c b/oemcrypto/test/GEN_api_lock_file.c index e1d5925e..009338fb 100644 --- a/oemcrypto/test/GEN_api_lock_file.c +++ b/oemcrypto/test/GEN_api_lock_file.c @@ -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); diff --git a/oemcrypto/test/install_prov30_oem_cert_tool.cpp b/oemcrypto/test/install_prov30_oem_cert_tool.cpp index 25eac6d4..f4fd4084 100644 --- a/oemcrypto/test/install_prov30_oem_cert_tool.cpp +++ b/oemcrypto/test/install_prov30_oem_cert_tool.cpp @@ -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. diff --git a/oemcrypto/test/oec_decrypt_fallback_chain.cpp b/oemcrypto/test/oec_decrypt_fallback_chain.cpp index 6cb7aac0..fe303fc6 100644 --- a/oemcrypto/test/oec_decrypt_fallback_chain.cpp +++ b/oemcrypto/test/oec_decrypt_fallback_chain.cpp @@ -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; diff --git a/oemcrypto/test/oec_device_features.cpp b/oemcrypto/test/oec_device_features.cpp index 49f0d8cb..d440159f 100644 --- a/oemcrypto/test/oec_device_features.cpp +++ b/oemcrypto/test/oec_device_features.cpp @@ -10,7 +10,9 @@ #include +#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(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 != diff --git a/oemcrypto/test/oec_device_features.h b/oemcrypto/test/oec_device_features.h index ec0a8fc1..d1e4d7d1 100644 --- a/oemcrypto/test/oec_device_features.h +++ b/oemcrypto/test/oec_device_features.h @@ -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& GetOutputTypes(); + // If the device has a baked in cert, then this is the public key that should + // be used for testing. + const std::vector& rsa_test_key() const { return rsa_test_key_; }; + void set_rsa_test_key(const std::vector& rsa_test_key) { + rsa_test_key_ = rsa_test_key; + } + void set_rsa_test_key(std::vector&& 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 output_types_; bool initialized_ = false; + std::vector rsa_test_key_; }; // There is one global set of features for the version of OEMCrypto being diff --git a/oemcrypto/test/oec_session_util.cpp b/oemcrypto/test/oec_session_util.cpp index e562c3fa..4836d11f 100644 --- a/oemcrypto/test/oec_session_util.cpp +++ b/oemcrypto/test/oec_session_util.cpp @@ -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 public_key(buffer_size); size_t public_key_size = buffer_size; std::vector 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 public_key(buffer_size); size_t public_key_size = buffer_size; std::vector 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, diff --git a/oemcrypto/test/oemcrypto_basic_test.cpp b/oemcrypto/test/oemcrypto_basic_test.cpp index 42f638aa..eae6766b 100644 --- a/oemcrypto/test/oemcrypto_basic_test.cpp +++ b/oemcrypto/test/oemcrypto_basic_test.cpp @@ -2,17 +2,79 @@ // source code may only be used and distributed under the Widevine // License Agreement. // - #include "oemcrypto_basic_test.h" +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#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(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& tokens, + int32_t root_index) { + if (root_index >= static_cast(tokens.size())) return 0; + int32_t count = 0; + int32_t iter = root_index; + int32_t remainder = 1; + while (remainder > 0 && iter < static_cast(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(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 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 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 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 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 found_required_fields; + // Stores the tokens of the "ree" field, if set, used to + // validate its content. + std::vector 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 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(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) { diff --git a/oemcrypto/test/oemcrypto_decrypt_test.cpp b/oemcrypto/test/oemcrypto_decrypt_test.cpp index a61c8933..63e598ac 100644 --- a/oemcrypto/test/oemcrypto_decrypt_test.cpp +++ b/oemcrypto/test/oemcrypto_decrypt_test.cpp @@ -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 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 in_buffer(256); - vector 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()); diff --git a/oemcrypto/test/oemcrypto_session_tests_helper.cpp b/oemcrypto/test/oemcrypto_session_tests_helper.cpp index 2a3d5250..175bb518 100644 --- a/oemcrypto/test/oemcrypto_session_tests_helper.cpp +++ b/oemcrypto/test/oemcrypto_session_tests_helper.cpp @@ -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; diff --git a/oemcrypto/test/oemcrypto_usage_table_test.cpp b/oemcrypto/test/oemcrypto_usage_table_test.cpp index 40491b42..ba62ad30 100644 --- a/oemcrypto/test/oemcrypto_usage_table_test.cpp +++ b/oemcrypto/test/oemcrypto_usage_table_test.cpp @@ -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); diff --git a/platforms/example/no_oemcrypto.cpp b/platforms/example/no_oemcrypto.cpp index 7caa4eac..cb3e0fca 100644 --- a/platforms/example/no_oemcrypto.cpp +++ b/platforms/example/no_oemcrypto.cpp @@ -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;