Make change and version bump to AV1A.250512.001
Snap for 13481725 from aa656ab3c0 to vic-widevine-partner-release
Change-Id: Id5e57a7d11fdfdb1de7b2673e99f66ecc827a019
This commit is contained in:
@@ -99,6 +99,7 @@ cc_defaults {
|
|||||||
relative_install_path: "hw",
|
relative_install_path: "hw",
|
||||||
include_dirs: [
|
include_dirs: [
|
||||||
"vendor/widevine/libwvdrmengine/cdm/core/include/",
|
"vendor/widevine/libwvdrmengine/cdm/core/include/",
|
||||||
|
"vendor/widevine/libwvdrmengine/cdm/util/include/",
|
||||||
"vendor/widevine/libwvdrmengine/include",
|
"vendor/widevine/libwvdrmengine/include",
|
||||||
"vendor/widevine/libwvdrmengine/mediadrm/include",
|
"vendor/widevine/libwvdrmengine/mediadrm/include",
|
||||||
"vendor/widevine/libwvdrmengine/oemcrypto/include",
|
"vendor/widevine/libwvdrmengine/oemcrypto/include",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ readonly -a ARCHS=(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
function die() { format=$1; shift; printf >&2 "$format\n" $@; exit 1; }
|
function die() { format=$1; shift; printf >&2 "$format\n" "$@"; exit 1; }
|
||||||
|
|
||||||
|
|
||||||
# $@: additional build targets
|
# $@: additional build targets
|
||||||
@@ -76,8 +76,9 @@ function build_bundle() {
|
|||||||
"${base_zip}" \
|
"${base_zip}" \
|
||||||
"${zips[@]}"
|
"${zips[@]}"
|
||||||
|
|
||||||
|
# Use prebuilt bundletool JAR
|
||||||
# build bundle
|
# build bundle
|
||||||
${BUNDLETOOL} build-bundle \
|
"${JAVA_EXE}" -jar "${BUNDLETOOL_PREBUILT_JAR}" build-bundle \
|
||||||
--overwrite \
|
--overwrite \
|
||||||
--modules "${base_zip}" \
|
--modules "${base_zip}" \
|
||||||
--config "${TMPDIR}/${apex}/bundle_config.json" \
|
--config "${TMPDIR}/${apex}/bundle_config.json" \
|
||||||
@@ -88,6 +89,8 @@ function build_bundle() {
|
|||||||
# inputs: ${DIST_DIR}/${arch}/${apex}-base.zip
|
# inputs: ${DIST_DIR}/${arch}/${apex}-base.zip
|
||||||
# outputs: ${DIST_DIR}/${apex}.aab
|
# outputs: ${DIST_DIR}/${apex}.aab
|
||||||
function build_bundles() {
|
function build_bundles() {
|
||||||
|
# Assume TARGET_BUILD_APPS is set in the environment
|
||||||
|
local app
|
||||||
for app in ${TARGET_BUILD_APPS}; do
|
for app in ${TARGET_BUILD_APPS}; do
|
||||||
build_bundle "${app}"
|
build_bundle "${app}"
|
||||||
done
|
done
|
||||||
@@ -98,7 +101,7 @@ function build_bundles() {
|
|||||||
# outputs: ${DIST_DIR}/dev_keys_signed/${apex}/${apex}.apks
|
# outputs: ${DIST_DIR}/dev_keys_signed/${apex}/${apex}.apks
|
||||||
function sign_bundles() {
|
function sign_bundles() {
|
||||||
${DEV_SIGN_BUNDLE} \
|
${DEV_SIGN_BUNDLE} \
|
||||||
--java_binary_path "${ANDROID_JAVA_HOME}/bin/java" \
|
--java_binary_path "${JAVA_EXE}" \
|
||||||
--input_dir "${DIST_DIR}" \
|
--input_dir "${DIST_DIR}" \
|
||||||
--output_dir "${DIST_DIR}/dev_keys_signed"
|
--output_dir "${DIST_DIR}/dev_keys_signed"
|
||||||
}
|
}
|
||||||
@@ -108,10 +111,16 @@ function configure_build() {
|
|||||||
# Assign to a variable and eval that, since bash ignores any error status from
|
# Assign to a variable and eval that, since bash ignores any error status from
|
||||||
# the command substitution if it's directly on the eval line.
|
# the command substitution if it's directly on the eval line.
|
||||||
local -r vars="$(TARGET_PRODUCT='' build/soong/soong_ui.bash --dumpvars-mode \
|
local -r vars="$(TARGET_PRODUCT='' build/soong/soong_ui.bash --dumpvars-mode \
|
||||||
--vars="ANDROID_JAVA_HOME TMPDIR OUT_DIR")"
|
--vars="ANDROID_JAVA_HOME TMPDIR OUT_DIR TARGET_BUILD_APPS DIST_DIR")"
|
||||||
eval "${vars}"
|
eval "${vars}"
|
||||||
|
|
||||||
declare -gr BUNDLETOOL=${OUT_DIR}/host/linux-x86/bin/bundletool
|
# Define prebuilt bundletool path and JAVA_EXE
|
||||||
|
# Assume exactly one JAR matches
|
||||||
|
local -a jar_matches=(prebuilts/bundletool/bundletool-all-*.jar)
|
||||||
|
declare -gr BUNDLETOOL_PREBUILT_JAR="${jar_matches[0]}"
|
||||||
|
declare -gr JAVA_EXE="${ANDROID_JAVA_HOME}/bin/java"
|
||||||
|
|
||||||
|
# Keep declarations for built tools
|
||||||
declare -gr MERGE_ZIPS=${OUT_DIR}/host/linux-x86/bin/merge_zips
|
declare -gr MERGE_ZIPS=${OUT_DIR}/host/linux-x86/bin/merge_zips
|
||||||
declare -gr DEV_SIGN_BUNDLE=${OUT_DIR}/host/linux-x86/bin/dev_sign_bundle
|
declare -gr DEV_SIGN_BUNDLE=${OUT_DIR}/host/linux-x86/bin/dev_sign_bundle
|
||||||
}
|
}
|
||||||
@@ -120,10 +129,11 @@ function configure_build() {
|
|||||||
function main() {
|
function main() {
|
||||||
[[ -e "build/make/core/Makefile" ]] || die "$0 must be run from the top of the Android source tree."
|
[[ -e "build/make/core/Makefile" ]] || die "$0 must be run from the top of the Android source tree."
|
||||||
configure_build
|
configure_build
|
||||||
build_modules bundletool merge_zips dev_sign_bundle
|
|
||||||
build_bundles # use bundletool & merge_zips
|
build_modules merge_zips dev_sign_bundle
|
||||||
sign_bundles # use dev_sign_bundle
|
build_bundles # use prebuilt bundletool & merge_zips
|
||||||
|
sign_bundles # use built dev_sign_bundle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
main
|
main
|
||||||
@@ -212,6 +212,15 @@ class CdmEngine {
|
|||||||
// system. This will force the device to reprovision itself.
|
// system. This will force the device to reprovision itself.
|
||||||
virtual CdmResponseType Unprovision(CdmSecurityLevel security_level);
|
virtual CdmResponseType Unprovision(CdmSecurityLevel security_level);
|
||||||
|
|
||||||
|
// Remove the system's REE-side OEM certificate for the specified
|
||||||
|
// |security_level|.
|
||||||
|
// Only effects two-stage provisioning devices which have an OEM cert
|
||||||
|
// in the REE side file system.
|
||||||
|
// Removing the OEM certificate will cause all DRM certificates tied to
|
||||||
|
// the OEM certificate to be invalidated and unloadable to future
|
||||||
|
// sessions.
|
||||||
|
virtual CdmResponseType UnprovisionOemCert(CdmSecurityLevel security_level);
|
||||||
|
|
||||||
// Return the list of key_set_ids stored on the current (origin-specific)
|
// Return the list of key_set_ids stored on the current (origin-specific)
|
||||||
// file system.
|
// file system.
|
||||||
virtual CdmResponseType ListStoredLicenses(
|
virtual CdmResponseType ListStoredLicenses(
|
||||||
|
|||||||
@@ -72,6 +72,28 @@ class CertificateProvisioning {
|
|||||||
// |default_url| by GetProvisioningRequest().
|
// |default_url| by GetProvisioningRequest().
|
||||||
static void GetProvisioningServerUrl(std::string* default_url);
|
static void GetProvisioningServerUrl(std::string* default_url);
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
// Freshly created, not yet initialized.
|
||||||
|
kUninitialized,
|
||||||
|
// A successful call to Init() has been made.
|
||||||
|
kInitialized,
|
||||||
|
// Has generated a DRM request; apps are allowed generate
|
||||||
|
// another one even if a response has not been received.
|
||||||
|
kDrmRequestSent,
|
||||||
|
// Has received (and successfully loaded) a DRM response.
|
||||||
|
kDrmResponseReceived,
|
||||||
|
// Has generated an OEM (Prov 4.0) request; apps are allowed
|
||||||
|
// generate another one even if a response has not been
|
||||||
|
// received.
|
||||||
|
kOemRequestSent,
|
||||||
|
// Has received (and successfully loaded) an OEM response.
|
||||||
|
kOemResponseReceived,
|
||||||
|
};
|
||||||
|
static const char* StateToString(State state);
|
||||||
|
|
||||||
|
// State setter for testing only.
|
||||||
|
void SetStateForTesting(State state) { state_ = state; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
#if defined(UNIT_TEST)
|
#if defined(UNIT_TEST)
|
||||||
friend class CertificateProvisioningTest;
|
friend class CertificateProvisioningTest;
|
||||||
@@ -122,22 +144,31 @@ class CertificateProvisioning {
|
|||||||
CdmResponseType CloseSessionOnError(CdmResponseType status);
|
CdmResponseType CloseSessionOnError(CdmResponseType status);
|
||||||
void CloseSession();
|
void CloseSession();
|
||||||
|
|
||||||
|
// Tracks the state of CertificateProvisioning.
|
||||||
|
State state_ = kUninitialized;
|
||||||
|
|
||||||
std::unique_ptr<CryptoSession> crypto_session_;
|
std::unique_ptr<CryptoSession> crypto_session_;
|
||||||
CdmCertificateType cert_type_;
|
CdmCertificateType cert_type_;
|
||||||
std::unique_ptr<ServiceCertificate> service_certificate_;
|
std::unique_ptr<ServiceCertificate> service_certificate_;
|
||||||
std::string request_;
|
std::string request_;
|
||||||
|
|
||||||
|
// == Provisioning 4.0 Variables ==
|
||||||
// The wrapped private key in provisioning 4 generated by calling
|
// The wrapped private key in provisioning 4 generated by calling
|
||||||
// GenerateCertificateKeyPair. It will be saved to file system if a valid
|
// GenerateCertificateKeyPair. It will be saved to file system if a valid
|
||||||
// response is received.
|
// response is received.
|
||||||
std::string provisioning_40_wrapped_private_key_;
|
CryptoWrappedKey prov40_wrapped_private_key_;
|
||||||
// Key type of the generated key pair in provisioning 4.
|
// Cache of the most recently sent OEM/DRM public key sent. Used
|
||||||
CryptoWrappedKey::Type provisioning_40_key_type_;
|
// to match the response with the request.
|
||||||
// Store the last provisioning request message
|
// This MUST be matched with the current |prov40_wrapped_private_key_|.
|
||||||
std::string provisioning_request_message_;
|
std::string prov40_public_key_;
|
||||||
|
|
||||||
|
// Store the last provisioning request message.
|
||||||
|
// This is the serialized ProvisioningRequest.
|
||||||
|
// Used for X.509 responses which require the original
|
||||||
|
// request to verify the signature of the response.
|
||||||
|
std::string prov40_request_;
|
||||||
|
|
||||||
CORE_DISALLOW_COPY_AND_ASSIGN(CertificateProvisioning);
|
CORE_DISALLOW_COPY_AND_ASSIGN(CertificateProvisioning);
|
||||||
};
|
}; // class CertificateProvisioning
|
||||||
|
|
||||||
} // namespace wvcdm
|
} // namespace wvcdm
|
||||||
|
|
||||||
#endif // WVCDM_CORE_CERTIFICATE_PROVISIONING_H_
|
#endif // WVCDM_CORE_CERTIFICATE_PROVISIONING_H_
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#define WVCDM_CORE_CRYPTO_WRAPPED_KEY_H_
|
#define WVCDM_CORE_CRYPTO_WRAPPED_KEY_H_
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace wvcdm {
|
namespace wvcdm {
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ class CryptoWrappedKey {
|
|||||||
CryptoWrappedKey() {}
|
CryptoWrappedKey() {}
|
||||||
CryptoWrappedKey(Type type, const std::string& key)
|
CryptoWrappedKey(Type type, const std::string& key)
|
||||||
: type_(type), key_(key) {}
|
: type_(type), key_(key) {}
|
||||||
|
CryptoWrappedKey(Type type, std::string&& key)
|
||||||
|
: type_(type), key_(std::move(key)) {}
|
||||||
|
|
||||||
Type type() const { return type_; }
|
Type type() const { return type_; }
|
||||||
void set_type(Type type) { type_ = type; }
|
void set_type(Type type) { type_ = type; }
|
||||||
@@ -26,6 +29,7 @@ class CryptoWrappedKey {
|
|||||||
// Mutable reference getter for passing to OMECrypto.
|
// Mutable reference getter for passing to OMECrypto.
|
||||||
std::string& key() { return key_; }
|
std::string& key() { return key_; }
|
||||||
void set_key(const std::string& key) { key_ = key; }
|
void set_key(const std::string& key) { key_ = key; }
|
||||||
|
void set_key(std::string&& key) { key_ = std::move(key); }
|
||||||
|
|
||||||
void Clear() {
|
void Clear() {
|
||||||
type_ = kUninitialized;
|
type_ = kUninitialized;
|
||||||
|
|||||||
@@ -465,6 +465,9 @@ enum CdmResponseEnum : int32_t {
|
|||||||
GET_DEVICE_INFORMATION_ERROR = 398,
|
GET_DEVICE_INFORMATION_ERROR = 398,
|
||||||
GET_DEVICE_SIGNED_CSR_PAYLOAD_ERROR = 399,
|
GET_DEVICE_SIGNED_CSR_PAYLOAD_ERROR = 399,
|
||||||
GET_TOKEN_FROM_EMBEDDED_CERT_ERROR = 400,
|
GET_TOKEN_FROM_EMBEDDED_CERT_ERROR = 400,
|
||||||
|
PROVISIONING_UNEXPECTED_RESPONSE_ERROR = 402,
|
||||||
|
PROVISIONING_4_STALE_RESPONSE = 403,
|
||||||
|
PROVISIONING_4_FAILED_TO_VERIFY_CERT_KEY = 404,
|
||||||
// Don't forget to add new values to
|
// Don't forget to add new values to
|
||||||
// * core/src/wv_cdm_types.cpp
|
// * core/src/wv_cdm_types.cpp
|
||||||
// * android/include/mapErrors-inl.h
|
// * android/include/mapErrors-inl.h
|
||||||
|
|||||||
@@ -1286,6 +1286,13 @@ CdmResponseType CdmEngine::HandleProvisioningResponse(
|
|||||||
LOGE("Device has been revoked, cannot provision: status = %s",
|
LOGE("Device has been revoked, cannot provision: status = %s",
|
||||||
ret.ToString().c_str());
|
ret.ToString().c_str());
|
||||||
cert_provisioning_.reset();
|
cert_provisioning_.reset();
|
||||||
|
} else if (ret == PROVISIONING_4_STALE_RESPONSE) {
|
||||||
|
// The response is considered "stale" (likely from generating multiple
|
||||||
|
// requests, and providing out of order responses).
|
||||||
|
// Drop message without returning error or resetting
|
||||||
|
// provisioning context.
|
||||||
|
LOGW("Stale response, app may try again");
|
||||||
|
return CdmResponseType(NO_ERROR);
|
||||||
} else {
|
} else {
|
||||||
// It is possible that a provisioning attempt was made after this one was
|
// It is possible that a provisioning attempt was made after this one was
|
||||||
// requested but before the response was received, which will cause this
|
// requested but before the response was received, which will cause this
|
||||||
@@ -1332,8 +1339,7 @@ CdmProvisioningStatus CdmEngine::GetProvisioningStatus(
|
|||||||
return kUnknownProvisionStatus;
|
return kUnknownProvisionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
UsagePropertySet property_set;
|
if (handle.HasCertificate(/* atsc_mode_enabled = */ false)) {
|
||||||
if (handle.HasCertificate(property_set.use_atsc_mode())) {
|
|
||||||
return kProvisioned;
|
return kProvisioned;
|
||||||
}
|
}
|
||||||
if (crypto_session->GetPreProvisionTokenType() == kClientTokenBootCertChain) {
|
if (crypto_session->GetPreProvisionTokenType() == kClientTokenBootCertChain) {
|
||||||
@@ -1356,8 +1362,8 @@ CdmResponseType CdmEngine::Unprovision(CdmSecurityLevel security_level) {
|
|||||||
LOGD("OKP fallback to L3");
|
LOGD("OKP fallback to L3");
|
||||||
security_level = kSecurityLevelL3;
|
security_level = kSecurityLevelL3;
|
||||||
}
|
}
|
||||||
// Devices with baked-in DRM certs cannot be reprovisioned and therefore must
|
// Devices with baked-in DRM certs cannot be reprovisioned
|
||||||
// not be unprovisioned.
|
// and therefore must not be unprovisioned.
|
||||||
std::unique_ptr<CryptoSession> crypto_session(
|
std::unique_ptr<CryptoSession> crypto_session(
|
||||||
CryptoSession::MakeCryptoSession(metrics_->GetCryptoMetrics()));
|
CryptoSession::MakeCryptoSession(metrics_->GetCryptoMetrics()));
|
||||||
CdmClientTokenType token_type = kClientTokenUninitialized;
|
CdmClientTokenType token_type = kClientTokenUninitialized;
|
||||||
@@ -1376,18 +1382,78 @@ CdmResponseType CdmEngine::Unprovision(CdmSecurityLevel security_level) {
|
|||||||
LOGE("Unable to initialize device files");
|
LOGE("Unable to initialize device files");
|
||||||
return CdmResponseType(UNPROVISION_ERROR_1);
|
return CdmResponseType(UNPROVISION_ERROR_1);
|
||||||
}
|
}
|
||||||
|
// This if statement is misleading. There is no consistent
|
||||||
// TODO(b/141705730): Remove usage entries during unprovisioning.
|
// concept of "global" vs "per-app/origin" storage in the
|
||||||
if (!file_system_->IsGlobal()) {
|
// core library. Android vs CE CDM behave very different.
|
||||||
if (!handle.RemoveCertificate() || !handle.RemoveOemCertificate()) {
|
// On CE device:
|
||||||
LOGE("Unable to delete certificate");
|
// file_system_->IsGlobal() is always true, even if app/origin
|
||||||
|
// specific.
|
||||||
|
// On Android:
|
||||||
|
// file_system_->IsGlobal() is always false, except for some C++
|
||||||
|
// test code.
|
||||||
|
// TODO(b/142280599): Refactor this once CE CDM SPOIDs are supported
|
||||||
|
// by the file system. May require moving platform-dependent behavior
|
||||||
|
// to the platform-dependent layer. Only have this remove the
|
||||||
|
// certificate and nothing else.
|
||||||
|
if (!file_system_->IsGlobal()) { // AKA is Android
|
||||||
|
// TODO(b/141705730): Remove usage entries during unprovisioning.
|
||||||
|
// Not considered an error if no certificate exists.
|
||||||
|
if (handle.HasCertificate(/* atsc_mode_enabled = */ false) &&
|
||||||
|
!handle.RemoveCertificate()) {
|
||||||
|
LOGE("Unable to delete DRM certificate");
|
||||||
return CdmResponseType(UNPROVISION_ERROR_2);
|
return CdmResponseType(UNPROVISION_ERROR_2);
|
||||||
}
|
}
|
||||||
|
// Maintaining old behavior expected by Android.
|
||||||
|
const CdmResponseType oem_cert_status = UnprovisionOemCert(security_level);
|
||||||
|
if (oem_cert_status != NO_ERROR) return oem_cert_status;
|
||||||
|
} else { // AKA is CE CDM (or some Android tests)
|
||||||
|
// On CE CDM, deleting all files only deletes the app/origin
|
||||||
|
// specific files.
|
||||||
|
// On Android, this will delete all files (only possible
|
||||||
|
// during testing).
|
||||||
|
if (!handle.DeleteAllFiles()) {
|
||||||
|
LOGE("Unable to delete files");
|
||||||
|
return CdmResponseType(UNPROVISION_ERROR_3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CdmResponseType(NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
CdmResponseType CdmEngine::UnprovisionOemCert(CdmSecurityLevel security_level) {
|
||||||
|
LOGI("security_level = %s", CdmSecurityLevelToString(security_level));
|
||||||
|
if (security_level == kSecurityLevelL1 && OkpIsInFallbackMode()) {
|
||||||
|
LOGD("OKP fallback to L3");
|
||||||
|
security_level = kSecurityLevelL3;
|
||||||
|
}
|
||||||
|
// Only BCC-based system have an OEM certificate that can
|
||||||
|
// unprovisioned.
|
||||||
|
// Prov 3.0 system's OEM certs are built into the TEE.
|
||||||
|
std::unique_ptr<CryptoSession> crypto_session(
|
||||||
|
CryptoSession::MakeCryptoSession(metrics_->GetCryptoMetrics()));
|
||||||
|
CdmClientTokenType token_type = kClientTokenUninitialized;
|
||||||
|
const CdmResponseType res = crypto_session->GetProvisioningMethod(
|
||||||
|
security_level == kSecurityLevelL3 ? kLevel3 : kLevelDefault,
|
||||||
|
&token_type);
|
||||||
|
if (res != NO_ERROR) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
if (token_type != kClientTokenBootCertChain) {
|
||||||
|
LOGD("Device does not support OEM certificate unprovisioning");
|
||||||
return CdmResponseType(NO_ERROR);
|
return CdmResponseType(NO_ERROR);
|
||||||
}
|
}
|
||||||
if (!handle.DeleteAllFiles()) {
|
// For Prov 4.0 devices, this will cause every app/origin client
|
||||||
LOGE("Unable to delete files");
|
// to lose their offline content for the same TEE security level.
|
||||||
return CdmResponseType(UNPROVISION_ERROR_3);
|
wvutil::FileSystem global_file_system;
|
||||||
|
DeviceFiles global_handle(&global_file_system);
|
||||||
|
if (!global_handle.Init(security_level)) {
|
||||||
|
LOGE("Unable to initialize global device files");
|
||||||
|
return CdmResponseType(UNPROVISION_ERROR_1);
|
||||||
|
}
|
||||||
|
// Not considered an error if no certificate exists.
|
||||||
|
if (global_handle.HasOemCertificate() &&
|
||||||
|
!global_handle.RemoveOemCertificate()) {
|
||||||
|
LOGE("Unable to delete OEM certificate");
|
||||||
|
return CdmResponseType(UNPROVISION_ERROR_2);
|
||||||
}
|
}
|
||||||
return CdmResponseType(NO_ERROR);
|
return CdmResponseType(NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
|
// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
|
||||||
// source code may only be used and distributed under the Widevine License
|
// source code may only be used and distributed under the Widevine License
|
||||||
// Agreement.
|
// Agreement.
|
||||||
|
|
||||||
#include "certificate_provisioning.h"
|
#include "certificate_provisioning.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "client_identification.h"
|
#include "client_identification.h"
|
||||||
#include "crypto_wrapped_key.h"
|
#include "crypto_wrapped_key.h"
|
||||||
#include "device_files.h"
|
#include "device_files.h"
|
||||||
@@ -87,6 +88,127 @@ bool RetrieveOemCertificateAndLoadPrivateKey(CryptoSession& crypto_session,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks if any instances of |needle| sequences found in the |haystack|.
|
||||||
|
//
|
||||||
|
// Special cases:
|
||||||
|
// - An empty |needle| is always present, even if |haystack| is empty.
|
||||||
|
// Note: This is a convention used by many string utility
|
||||||
|
// libraries.
|
||||||
|
bool StringContains(const std::string& haystack, const std::string& needle) {
|
||||||
|
if (needle.empty()) return true;
|
||||||
|
if (haystack.size() < needle.size()) return false;
|
||||||
|
return haystack.find(needle) != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the |needle| sequences found at the end of |haystack|.
|
||||||
|
//
|
||||||
|
// Special cases:
|
||||||
|
// - An empty |needle| is always present, even if |haystack| is empty.
|
||||||
|
// Note: This is a convention used by many string utility
|
||||||
|
// libraries.
|
||||||
|
bool StringEndsWith(const std::string& haystack, const std::string& needle) {
|
||||||
|
if (haystack.size() < needle.size()) return false;
|
||||||
|
return std::equal(haystack.rbegin(), haystack.rbegin() + needle.size(),
|
||||||
|
needle.rbegin(), needle.rend());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks the actual length of an ASN.1 DER encoded message
|
||||||
|
// roughly matches the expected length from within the message.
|
||||||
|
// Technically, the DER message may contain some trailing
|
||||||
|
// end-of-contents bytes (at most 2).
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// |actual_length| - The real length of the DER message
|
||||||
|
// |expected_length| - The reported length of the DER message plus
|
||||||
|
// the header bytes parsed.
|
||||||
|
bool IsAsn1ExpectedLength(size_t actual_length, size_t expected_length) {
|
||||||
|
return actual_length >= expected_length &&
|
||||||
|
actual_length <= (expected_length + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the provided |message| resembles ASN.1 DER encoded
|
||||||
|
// message.
|
||||||
|
// This is a light check, it verifies the type (SEQUENCE) and that
|
||||||
|
// the encoded length matches the total message length.
|
||||||
|
bool IsAsn1DerSequenceLike(const std::string& message) {
|
||||||
|
// Anything less than 3 bytes will not be an ASN.1 sequence.
|
||||||
|
if (message.size() < 3) return false;
|
||||||
|
// Verify type header
|
||||||
|
// class = universal(0) - bits 6-7
|
||||||
|
// p/c = constructed(1) - bit 5
|
||||||
|
// tag = sequence(0x10) - bits 0-4
|
||||||
|
static constexpr uint8_t kUniversal = (0 << 6);
|
||||||
|
static constexpr uint8_t kConstructBit = (1 << 5);
|
||||||
|
static constexpr uint8_t kSequenceTag = 0x10;
|
||||||
|
static constexpr uint8_t kSequenceHeader =
|
||||||
|
kUniversal | kConstructBit | kSequenceTag;
|
||||||
|
const uint8_t type_header = static_cast<uint8_t>(message.front());
|
||||||
|
if (type_header != kSequenceHeader) return false;
|
||||||
|
|
||||||
|
// Verify length.
|
||||||
|
const uint8_t length_header = static_cast<uint8_t>(message[1]);
|
||||||
|
// A reserved length is never used. If |length_header| is
|
||||||
|
// reserved length, then this is not an ASN.1 message.
|
||||||
|
static constexpr uint8_t kReservedLength = 0xff;
|
||||||
|
if (length_header == kReservedLength) return false;
|
||||||
|
|
||||||
|
static constexpr uint8_t kIndefiniteLength = 0x80;
|
||||||
|
if (length_header == kIndefiniteLength) {
|
||||||
|
// If length is indefinite, then search for two "end of contents"
|
||||||
|
// octets at the end.
|
||||||
|
static constexpr uint8_t kAsnEndOfContents = 0x00;
|
||||||
|
const std::string kDoubleEoc(2, kAsnEndOfContents);
|
||||||
|
return StringEndsWith(message, kDoubleEoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Definite lengths may be long or short (most likely long for our case).
|
||||||
|
static constexpr uint8_t kLongLengthBit = 0x80;
|
||||||
|
|
||||||
|
if ((length_header & kLongLengthBit) != kLongLengthBit) {
|
||||||
|
// Short length (unlikely, but check anyways).
|
||||||
|
// For short lengths, the value component of the length
|
||||||
|
// header is the payload length.
|
||||||
|
static constexpr uint8_t kShortLengthMask = 0x7f;
|
||||||
|
const size_t payload_length =
|
||||||
|
static_cast<size_t>(length_header & kShortLengthMask);
|
||||||
|
|
||||||
|
// The total message is: type header + length header + payload.
|
||||||
|
const size_t total_length = 2 + payload_length;
|
||||||
|
return IsAsn1ExpectedLength(message.size(), total_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Long length.
|
||||||
|
// |length_header| contains the number of bytes following the
|
||||||
|
// length header containing the payload length.
|
||||||
|
static constexpr uint8_t kLengthSizeMask = 0x7f;
|
||||||
|
const size_t length_length =
|
||||||
|
static_cast<size_t>(length_header & kLengthSizeMask);
|
||||||
|
// For long-lengths, the first two bytes were type header and
|
||||||
|
// length header.
|
||||||
|
static constexpr size_t kPayloadLengthOffset = 2;
|
||||||
|
// If the message is smaller than needed to obtain the length,
|
||||||
|
// it is either not ASN.1 (or an incomplete message, which is still
|
||||||
|
// invalid).
|
||||||
|
if ((message.size()) < (length_length + kPayloadLengthOffset)) return false;
|
||||||
|
// DER encoding should use the minimum number of bytes necessary
|
||||||
|
// to encode the length, and if the number of bytes to encode the
|
||||||
|
// length is more than 3 (payload is larged than 16 MB) which is much
|
||||||
|
// larger than any expected certificate chain.
|
||||||
|
if (length_length > 3) return false;
|
||||||
|
|
||||||
|
// Decode the length as big-endian.
|
||||||
|
size_t payload_length = 0;
|
||||||
|
for (size_t i = 0; i < length_length; i++) {
|
||||||
|
// Casting from char to uint8_t to size_t is necessary.
|
||||||
|
const uint8_t length_byte =
|
||||||
|
static_cast<uint8_t>(message[kPayloadLengthOffset + i]);
|
||||||
|
payload_length = (payload_length << 8) + static_cast<size_t>(length_byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total message is: type header + length header + payload length + payload.
|
||||||
|
const size_t total_length = 2 + length_length + payload_length;
|
||||||
|
return IsAsn1ExpectedLength(message.size(), total_length);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
// Protobuf generated classes.
|
// Protobuf generated classes.
|
||||||
using video_widevine::DrmCertificate;
|
using video_widevine::DrmCertificate;
|
||||||
@@ -98,6 +220,25 @@ using video_widevine::PublicKeyToCertify;
|
|||||||
using video_widevine::SignedDrmCertificate;
|
using video_widevine::SignedDrmCertificate;
|
||||||
using video_widevine::SignedProvisioningMessage;
|
using video_widevine::SignedProvisioningMessage;
|
||||||
|
|
||||||
|
// static
|
||||||
|
const char* CertificateProvisioning::StateToString(State state) {
|
||||||
|
switch (state) {
|
||||||
|
case kUninitialized:
|
||||||
|
return "Uninitialized";
|
||||||
|
case kInitialized:
|
||||||
|
return "Initialized";
|
||||||
|
case kDrmRequestSent:
|
||||||
|
return "DrmRequestSent";
|
||||||
|
case kDrmResponseReceived:
|
||||||
|
return "DrmResponseReceived";
|
||||||
|
case kOemRequestSent:
|
||||||
|
return "OemRequestSent";
|
||||||
|
case kOemResponseReceived:
|
||||||
|
return "OemResponseReceived";
|
||||||
|
}
|
||||||
|
return "<unknown>";
|
||||||
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
void CertificateProvisioning::GetProvisioningServerUrl(
|
void CertificateProvisioning::GetProvisioningServerUrl(
|
||||||
std::string* default_url) {
|
std::string* default_url) {
|
||||||
@@ -114,7 +255,11 @@ CdmResponseType CertificateProvisioning::Init(
|
|||||||
service_certificate.empty()
|
service_certificate.empty()
|
||||||
? wvutil::a2bs_hex(kCpProductionServiceCertificate)
|
? wvutil::a2bs_hex(kCpProductionServiceCertificate)
|
||||||
: service_certificate;
|
: service_certificate;
|
||||||
return service_certificate_->Init(certificate);
|
const CdmResponseType result = service_certificate_->Init(certificate);
|
||||||
|
if (result == NO_ERROR) {
|
||||||
|
state_ = kInitialized;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill in the appropriate SPOID (Stable Per-Origin IDentifier) option.
|
// Fill in the appropriate SPOID (Stable Per-Origin IDentifier) option.
|
||||||
@@ -206,11 +351,18 @@ CdmResponseType CertificateProvisioning::GetProvisioningRequestInternal(
|
|||||||
|
|
||||||
default_url->assign(kProvisioningServerUrl);
|
default_url->assign(kProvisioningServerUrl);
|
||||||
|
|
||||||
|
if (state_ != kInitialized) {
|
||||||
|
LOGD("Overriding old request: state = %s", StateToString(state_));
|
||||||
|
// Once the previous session is closed, there is no way to complete
|
||||||
|
// an in-flight request.
|
||||||
|
state_ = kInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
CloseSession();
|
CloseSession();
|
||||||
CdmResponseType status = crypto_session_->Open(requested_security_level);
|
CdmResponseType status = crypto_session_->Open(requested_security_level);
|
||||||
if (NO_ERROR != status) {
|
if (NO_ERROR != status) {
|
||||||
LOGE("Failed to create a crypto session: status = %d",
|
LOGE("Failed to create a crypto session: status = %s",
|
||||||
static_cast<int>(status));
|
status.ToString().c_str());
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +451,7 @@ CdmResponseType CertificateProvisioning::GetProvisioningRequestInternal(
|
|||||||
} else {
|
} else {
|
||||||
*request = std::move(serialized_request);
|
*request = std::move(serialized_request);
|
||||||
}
|
}
|
||||||
|
state_ = kDrmRequestSent;
|
||||||
return CdmResponseType(NO_ERROR);
|
return CdmResponseType(NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +477,11 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal(
|
|||||||
return CdmResponseType(PROVISIONING_4_FAILED_TO_INITIALIZE_DEVICE_FILES);
|
return CdmResponseType(PROVISIONING_4_FAILED_TO_INITIALIZE_DEVICE_FILES);
|
||||||
}
|
}
|
||||||
|
|
||||||
ProvisioningRequest provisioning_request;
|
if (!service_certificate_) {
|
||||||
|
LOGE("Service certificate not set");
|
||||||
|
return CdmResponseType(CERT_PROVISIONING_EMPTY_SERVICE_CERTIFICATE);
|
||||||
|
}
|
||||||
|
|
||||||
// Determine the current stage by checking if OEM cert exists.
|
// Determine the current stage by checking if OEM cert exists.
|
||||||
std::string stored_oem_cert;
|
std::string stored_oem_cert;
|
||||||
if (global_file_handle.HasOemCertificate()) {
|
if (global_file_handle.HasOemCertificate()) {
|
||||||
@@ -341,9 +498,11 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const bool is_oem_prov_request = stored_oem_cert.empty();
|
||||||
|
|
||||||
// Retrieve the Spoid, but put it to the client identification instead, so it
|
// Retrieve the Spoid, but put it to the client identification instead, so it
|
||||||
// is encrypted.
|
// is encrypted.
|
||||||
|
ProvisioningRequest provisioning_request;
|
||||||
CdmAppParameterMap additional_parameter;
|
CdmAppParameterMap additional_parameter;
|
||||||
CdmResponseType status =
|
CdmResponseType status =
|
||||||
SetSpoidParameter(origin, spoid, &provisioning_request);
|
SetSpoidParameter(origin, spoid, &provisioning_request);
|
||||||
@@ -363,7 +522,7 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal(
|
|||||||
provisioning_request.clear_stable_id();
|
provisioning_request.clear_stable_id();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stored_oem_cert.empty()) {
|
if (is_oem_prov_request) {
|
||||||
// This is the first stage provisioning.
|
// This is the first stage provisioning.
|
||||||
default_url->assign(std::string(kProvisioningServerUrl) +
|
default_url->assign(std::string(kProvisioningServerUrl) +
|
||||||
kProv40FirstStageServerUrlSuffix);
|
kProv40FirstStageServerUrlSuffix);
|
||||||
@@ -377,8 +536,8 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal(
|
|||||||
|
|
||||||
// Since |stored_oem_cert| is empty, the client identification token will be
|
// Since |stored_oem_cert| is empty, the client identification token will be
|
||||||
// retrieved from OEMCrypto, which is the BCC in this case.
|
// retrieved from OEMCrypto, which is the BCC in this case.
|
||||||
status = FillEncryptedClientId(stored_oem_cert, provisioning_request,
|
status = FillEncryptedClientId(/* client_token = */ std::string(),
|
||||||
wv_service_cert);
|
provisioning_request, wv_service_cert);
|
||||||
if (status != NO_ERROR) return status;
|
if (status != NO_ERROR) return status;
|
||||||
} else {
|
} else {
|
||||||
// This is the second stage provisioning.
|
// This is the second stage provisioning.
|
||||||
@@ -416,25 +575,24 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal(
|
|||||||
|
|
||||||
std::string public_key;
|
std::string public_key;
|
||||||
std::string public_key_signature;
|
std::string public_key_signature;
|
||||||
provisioning_40_wrapped_private_key_.clear();
|
std::string wrapped_private_key;
|
||||||
provisioning_40_key_type_ = CryptoWrappedKey::kUninitialized;
|
CryptoWrappedKey::Type private_key_type = CryptoWrappedKey::kUninitialized;
|
||||||
status = crypto_session_->GenerateCertificateKeyPair(
|
status = crypto_session_->GenerateCertificateKeyPair(
|
||||||
&public_key, &public_key_signature, &provisioning_40_wrapped_private_key_,
|
&public_key, &public_key_signature, &wrapped_private_key,
|
||||||
&provisioning_40_key_type_);
|
&private_key_type);
|
||||||
if (status != NO_ERROR) return status;
|
if (status != NO_ERROR) return status;
|
||||||
|
|
||||||
PublicKeyToCertify* key_to_certify =
|
PublicKeyToCertify* key_to_certify =
|
||||||
provisioning_request.mutable_certificate_public_key();
|
provisioning_request.mutable_certificate_public_key();
|
||||||
key_to_certify->set_public_key(public_key);
|
key_to_certify->set_public_key(public_key);
|
||||||
key_to_certify->set_signature(public_key_signature);
|
key_to_certify->set_signature(public_key_signature);
|
||||||
key_to_certify->set_key_type(provisioning_40_key_type_ ==
|
key_to_certify->set_key_type(private_key_type == CryptoWrappedKey::kRsa
|
||||||
CryptoWrappedKey::kRsa
|
|
||||||
? PublicKeyToCertify::RSA
|
? PublicKeyToCertify::RSA
|
||||||
: PublicKeyToCertify::ECC);
|
: PublicKeyToCertify::ECC);
|
||||||
|
|
||||||
std::string serialized_message;
|
std::string serialized_message;
|
||||||
provisioning_request.SerializeToString(&serialized_message);
|
provisioning_request.SerializeToString(&serialized_message);
|
||||||
provisioning_request_message_ = serialized_message;
|
prov40_request_ = serialized_message;
|
||||||
|
|
||||||
SignedProvisioningMessage signed_provisioning_msg;
|
SignedProvisioningMessage signed_provisioning_msg;
|
||||||
signed_provisioning_msg.set_message(serialized_message);
|
signed_provisioning_msg.set_message(serialized_message);
|
||||||
@@ -490,6 +648,15 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal(
|
|||||||
*request = std::move(serialized_request);
|
*request = std::move(serialized_request);
|
||||||
}
|
}
|
||||||
request_ = std::move(serialized_message);
|
request_ = std::move(serialized_message);
|
||||||
|
// Need the wrapped Prov 4.0 private key to store once the response
|
||||||
|
// is received. The wrapped key is not available in the response.
|
||||||
|
prov40_wrapped_private_key_ =
|
||||||
|
CryptoWrappedKey(private_key_type, std::move(wrapped_private_key));
|
||||||
|
// Store the public key from the request. This is used to match
|
||||||
|
// up the response with the most recently generated request.
|
||||||
|
prov40_public_key_ = std::move(public_key);
|
||||||
|
|
||||||
|
state_ = is_oem_prov_request ? kOemRequestSent : kDrmRequestSent;
|
||||||
return CdmResponseType(NO_ERROR);
|
return CdmResponseType(NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,6 +719,18 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response(
|
|||||||
return CdmResponseType(PROVISIONING_4_RESPONSE_HAS_ERROR_STATUS);
|
return CdmResponseType(PROVISIONING_4_RESPONSE_HAS_ERROR_STATUS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (state_ == kOemResponseReceived || state_ == kDrmResponseReceived) {
|
||||||
|
// A response has already been received (successfully), this
|
||||||
|
// response can be silently dropped.
|
||||||
|
LOGW("Response already received: state = %s", StateToString(state_));
|
||||||
|
return CdmResponseType(NO_ERROR);
|
||||||
|
}
|
||||||
|
if (state_ != kOemRequestSent && state_ != kDrmRequestSent) {
|
||||||
|
LOGE("Not expecting a response: state = %s", StateToString(state_));
|
||||||
|
return CdmResponseType(PROVISIONING_UNEXPECTED_RESPONSE_ERROR);
|
||||||
|
}
|
||||||
|
LOGD("Handling response: state = %s", StateToString(state_));
|
||||||
|
const bool is_oem_prov_response = (state_ == kOemRequestSent);
|
||||||
|
|
||||||
const std::string& device_certificate =
|
const std::string& device_certificate =
|
||||||
provisioning_response.device_certificate();
|
provisioning_response.device_certificate();
|
||||||
@@ -560,17 +739,16 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response(
|
|||||||
return CdmResponseType(PROVISIONING_4_RESPONSE_HAS_NO_CERTIFICATE);
|
return CdmResponseType(PROVISIONING_4_RESPONSE_HAS_NO_CERTIFICATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provisioning_40_wrapped_private_key_.empty()) {
|
if (!prov40_wrapped_private_key_.IsValid() || prov40_public_key_.empty()) {
|
||||||
LOGE("No private key was generated");
|
LOGE("No %s key was generated",
|
||||||
|
!prov40_wrapped_private_key_.IsValid() ? "private" : "public");
|
||||||
return CdmResponseType(PROVISIONING_4_NO_PRIVATE_KEY);
|
return CdmResponseType(PROVISIONING_4_NO_PRIVATE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CryptoWrappedKey private_key(provisioning_40_key_type_,
|
|
||||||
provisioning_40_wrapped_private_key_);
|
|
||||||
|
|
||||||
if (cert_type_ == kCertificateX509) {
|
if (cert_type_ == kCertificateX509) {
|
||||||
// Load csr private key to decrypt session key
|
// Load csr private key to decrypt session key
|
||||||
auto status = crypto_session_->LoadCertificatePrivateKey(private_key);
|
auto status =
|
||||||
|
crypto_session_->LoadCertificatePrivateKey(prov40_wrapped_private_key_);
|
||||||
if (status != NO_ERROR) {
|
if (status != NO_ERROR) {
|
||||||
LOGE("Failed to load x509 certificate.");
|
LOGE("Failed to load x509 certificate.");
|
||||||
return status;
|
return status;
|
||||||
@@ -581,9 +759,8 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response(
|
|||||||
const std::string& signature = signed_response.signature();
|
const std::string& signature = signed_response.signature();
|
||||||
const std::string& core_message = signed_response.oemcrypto_core_message();
|
const std::string& core_message = signed_response.oemcrypto_core_message();
|
||||||
status = crypto_session_->LoadProvisioningCast(
|
status = crypto_session_->LoadProvisioningCast(
|
||||||
signed_response.session_key(), provisioning_request_message_,
|
signed_response.session_key(), prov40_request_, response_message,
|
||||||
response_message, core_message, signature,
|
core_message, signature, &cast_cert_private_key.key());
|
||||||
&cast_cert_private_key.key());
|
|
||||||
if (status != NO_ERROR) {
|
if (status != NO_ERROR) {
|
||||||
LOGE("Failed to generate wrapped key for cast cert.");
|
LOGE("Failed to generate wrapped key for cast cert.");
|
||||||
return status;
|
return status;
|
||||||
@@ -593,11 +770,79 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response(
|
|||||||
|
|
||||||
*cert = device_certificate;
|
*cert = device_certificate;
|
||||||
*wrapped_key = cast_cert_private_key.key();
|
*wrapped_key = cast_cert_private_key.key();
|
||||||
|
state_ = is_oem_prov_response ? kOemResponseReceived : kDrmResponseReceived;
|
||||||
|
prov40_wrapped_private_key_.Clear();
|
||||||
|
prov40_public_key_.clear();
|
||||||
return CdmResponseType(NO_ERROR);
|
return CdmResponseType(NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify that the response contains the same key as the request.
|
||||||
|
// It is possible that multiple requests were generated, the CDM
|
||||||
|
// can only accept the response from the most recently generated
|
||||||
|
// one.
|
||||||
|
//
|
||||||
|
// Check the first few bytes to determine the type of message.
|
||||||
|
// OEM responses:
|
||||||
|
// ASN.1 DER encoded ContentInfo (containing an X.509 certificate).
|
||||||
|
// DRM responses:
|
||||||
|
// Protobuf SignedDrmCertificate
|
||||||
|
if (is_oem_prov_response) {
|
||||||
|
// Here |device_certificate| (haystack) is an X.509 cert chain, and
|
||||||
|
// |prov40_public_key_| (needle) is a SubjectPublicKeyInfo.
|
||||||
|
// The cert chain should contain a byte-for-byte copy of the
|
||||||
|
// public key.
|
||||||
|
// TODO(b/391469176): Use RSA/ECC key loading to detected mismatched
|
||||||
|
// keys.
|
||||||
|
if (!StringContains(/* haystack = */ device_certificate,
|
||||||
|
/* needle */ prov40_public_key_)) {
|
||||||
|
LOGD("OEM response is stale");
|
||||||
|
return CdmResponseType(PROVISIONING_4_STALE_RESPONSE);
|
||||||
|
}
|
||||||
|
} else { // Is DRM response
|
||||||
|
video_widevine::SignedDrmCertificate signed_certificate;
|
||||||
|
if (!signed_certificate.ParseFromString(device_certificate)) {
|
||||||
|
// Check if ASN.1 like.
|
||||||
|
if (IsAsn1DerSequenceLike(device_certificate)) {
|
||||||
|
// This might be a late OEM certificate response
|
||||||
|
// generated from before the DRM response was received.
|
||||||
|
LOGD("Received late OEM certificate response");
|
||||||
|
return CdmResponseType(PROVISIONING_4_STALE_RESPONSE);
|
||||||
|
}
|
||||||
|
LOGE("Unable to parse Signed DRM certificate");
|
||||||
|
return CdmResponseType(PROVISIONING_4_FAILED_TO_VERIFY_CERT_KEY);
|
||||||
|
}
|
||||||
|
video_widevine::DrmCertificate drm_certificate;
|
||||||
|
if (!drm_certificate.ParseFromString(
|
||||||
|
signed_certificate.drm_certificate())) {
|
||||||
|
LOGE("Unable to parse DRM certificate");
|
||||||
|
return CdmResponseType(PROVISIONING_4_FAILED_TO_VERIFY_CERT_KEY);
|
||||||
|
}
|
||||||
|
// The sent public key is of the format SubjectPublicKeyInfo;
|
||||||
|
// however, the received format is RSAPublicKey (RSA only) or
|
||||||
|
// SubjectPublicKeyInfo (ECC, and future RSA).
|
||||||
|
// Here |prov40_public_key_| (haystack) is SubjectPublicKeyInfo,
|
||||||
|
// and |drm_certificate.public_key()| (needle) may be
|
||||||
|
// SubjectPublicKeyInfo or RSAPublicKey.
|
||||||
|
// If the DRM cert's public key is in SubjectPublicKeyInfo format
|
||||||
|
// it should be a byte-for-byte copy. If the DRM cert's public key
|
||||||
|
// is RSAPublicKey format then hopefully a byte-for-byte copy is
|
||||||
|
// found within the SubjectPublicKeyInfo. Note: SubjectPublicKeyInfo
|
||||||
|
// containing an RSA public key uses RSAPublicKey to store the
|
||||||
|
// key fields.
|
||||||
|
// TODO(b/391469176): Use RSA/ECC key loading to detected mismatched
|
||||||
|
// keys.
|
||||||
|
if (!StringContains(/* haystack = */ prov40_public_key_,
|
||||||
|
/* needle = */ drm_certificate.public_key())) {
|
||||||
|
// This might be a response from a previously generated DRM
|
||||||
|
// certificate response.
|
||||||
|
LOGD("DRM response is stale");
|
||||||
|
return CdmResponseType(PROVISIONING_4_STALE_RESPONSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Can clear the |prov40_public_key_| after validating.
|
||||||
|
prov40_public_key_.clear();
|
||||||
|
|
||||||
const CdmSecurityLevel security_level = crypto_session_->GetSecurityLevel();
|
const CdmSecurityLevel security_level = crypto_session_->GetSecurityLevel();
|
||||||
CloseSession();
|
|
||||||
wvutil::FileSystem global_file_system;
|
wvutil::FileSystem global_file_system;
|
||||||
DeviceFiles global_file_handle(&global_file_system);
|
DeviceFiles global_file_handle(&global_file_system);
|
||||||
if (!global_file_handle.Init(security_level)) {
|
if (!global_file_handle.Init(security_level)) {
|
||||||
@@ -607,28 +852,45 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response(
|
|||||||
|
|
||||||
// Check the stage of the provisioning by checking if an OEM cert is already
|
// Check the stage of the provisioning by checking if an OEM cert is already
|
||||||
// stored in the file system.
|
// stored in the file system.
|
||||||
if (!global_file_handle.HasOemCertificate()) {
|
if (is_oem_prov_response) {
|
||||||
|
if (global_file_handle.HasOemCertificate()) {
|
||||||
|
// Possible that concurrent apps were generated provisioning
|
||||||
|
// requests, and this one arrived after an other one.
|
||||||
|
LOGI("CDM has already received an OEM certificate");
|
||||||
|
CloseSession();
|
||||||
|
state_ = kOemResponseReceived;
|
||||||
|
prov40_wrapped_private_key_.Clear();
|
||||||
|
prov40_public_key_.clear();
|
||||||
|
return CdmResponseType(NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
// No OEM cert already stored => the response is expected to be an OEM cert.
|
// No OEM cert already stored => the response is expected to be an OEM cert.
|
||||||
if (!global_file_handle.StoreOemCertificate(device_certificate,
|
if (!global_file_handle.StoreOemCertificate(device_certificate,
|
||||||
private_key)) {
|
prov40_wrapped_private_key_)) {
|
||||||
LOGE("Failed to store provisioning 4 OEM certificate");
|
LOGE("Failed to store provisioning 4 OEM certificate");
|
||||||
return CdmResponseType(PROVISIONING_4_FAILED_TO_STORE_OEM_CERTIFICATE);
|
return CdmResponseType(PROVISIONING_4_FAILED_TO_STORE_OEM_CERTIFICATE);
|
||||||
}
|
}
|
||||||
} else {
|
CloseSession();
|
||||||
// The response is assumed to be an DRM cert.
|
state_ = kOemResponseReceived;
|
||||||
DeviceFiles per_origin_file_handle(file_system);
|
prov40_wrapped_private_key_.Clear();
|
||||||
if (!per_origin_file_handle.Init(security_level)) {
|
prov40_public_key_.clear();
|
||||||
LOGE("Failed to initialize per-origin DeviceFiles");
|
return CdmResponseType(NO_ERROR);
|
||||||
return CdmResponseType(
|
|
||||||
PROVISIONING_4_FAILED_TO_INITIALIZE_DEVICE_FILES_3);
|
|
||||||
}
|
|
||||||
if (!per_origin_file_handle.StoreCertificate(device_certificate,
|
|
||||||
private_key)) {
|
|
||||||
LOGE("Failed to store provisioning 4 DRM certificate");
|
|
||||||
return CdmResponseType(PROVISIONING_4_FAILED_TO_STORE_DRM_CERTIFICATE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// The response is assumed to be a DRM cert.
|
||||||
|
DeviceFiles per_origin_file_handle(file_system);
|
||||||
|
if (!per_origin_file_handle.Init(security_level)) {
|
||||||
|
LOGE("Failed to initialize per-origin DeviceFiles");
|
||||||
|
return CdmResponseType(PROVISIONING_4_FAILED_TO_INITIALIZE_DEVICE_FILES_3);
|
||||||
|
}
|
||||||
|
if (!per_origin_file_handle.StoreCertificate(device_certificate,
|
||||||
|
prov40_wrapped_private_key_)) {
|
||||||
|
LOGE("Failed to store provisioning 4 DRM certificate");
|
||||||
|
return CdmResponseType(PROVISIONING_4_FAILED_TO_STORE_DRM_CERTIFICATE);
|
||||||
|
}
|
||||||
|
CloseSession();
|
||||||
|
state_ = kDrmResponseReceived;
|
||||||
|
prov40_wrapped_private_key_.Clear();
|
||||||
|
prov40_public_key_.clear();
|
||||||
return CdmResponseType(NO_ERROR);
|
return CdmResponseType(NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,6 +940,15 @@ CdmResponseType CertificateProvisioning::HandleProvisioningResponse(
|
|||||||
wrapped_key);
|
wrapped_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state_ == kDrmResponseReceived) {
|
||||||
|
LOGD("Response already received");
|
||||||
|
return CdmResponseType(NO_ERROR);
|
||||||
|
}
|
||||||
|
if (state_ != kDrmRequestSent) {
|
||||||
|
LOGE("Not expecting a response: state = %s", StateToString(state_));
|
||||||
|
return CdmResponseType(PROVISIONING_UNEXPECTED_RESPONSE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
bool error = false;
|
bool error = false;
|
||||||
if (!signed_response.has_signature()) {
|
if (!signed_response.has_signature()) {
|
||||||
LOGE("Signed response does not have signature");
|
LOGE("Signed response does not have signature");
|
||||||
@@ -753,6 +1024,7 @@ CdmResponseType CertificateProvisioning::HandleProvisioningResponse(
|
|||||||
if (cert_type_ == kCertificateX509) {
|
if (cert_type_ == kCertificateX509) {
|
||||||
*cert = device_cert_data;
|
*cert = device_cert_data;
|
||||||
*wrapped_key = private_key.key();
|
*wrapped_key = private_key.key();
|
||||||
|
state_ = kDrmResponseReceived;
|
||||||
return CdmResponseType(NO_ERROR);
|
return CdmResponseType(NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,6 +1071,7 @@ CdmResponseType CertificateProvisioning::HandleProvisioningResponse(
|
|||||||
return CdmResponseType(CERT_PROVISIONING_RESPONSE_ERROR_8);
|
return CdmResponseType(CERT_PROVISIONING_RESPONSE_ERROR_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state_ = kDrmResponseReceived;
|
||||||
return CdmResponseType(NO_ERROR);
|
return CdmResponseType(NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -709,17 +709,26 @@ bool DeviceFiles::RemoveCertificate() {
|
|||||||
RETURN_FALSE_IF_UNINITIALIZED()
|
RETURN_FALSE_IF_UNINITIALIZED()
|
||||||
|
|
||||||
std::string certificate_file_name;
|
std::string certificate_file_name;
|
||||||
if (GetCertificateFileName(kCertificateLegacy, &certificate_file_name))
|
// Return true so long as at least one certificate was removed.
|
||||||
RemoveFile(certificate_file_name);
|
// This is to compliment the behavior of HasCertificate() which
|
||||||
if (GetCertificateFileName(kCertificateDefault, &certificate_file_name))
|
// returns true if at least one certificate exists.
|
||||||
return RemoveFile(certificate_file_name);
|
bool result = false;
|
||||||
return true;
|
if (GetCertificateFileName(kCertificateLegacy, &certificate_file_name)) {
|
||||||
|
LOGI("Removing legacy DRM cert");
|
||||||
|
result |= RemoveFile(certificate_file_name);
|
||||||
|
}
|
||||||
|
if (GetCertificateFileName(kCertificateDefault, &certificate_file_name)) {
|
||||||
|
LOGI("Removing DRM cert");
|
||||||
|
result |= RemoveFile(certificate_file_name);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeviceFiles::RemoveOemCertificate() {
|
bool DeviceFiles::RemoveOemCertificate() {
|
||||||
RETURN_FALSE_IF_UNINITIALIZED()
|
RETURN_FALSE_IF_UNINITIALIZED()
|
||||||
std::string certificate_file_name;
|
std::string certificate_file_name;
|
||||||
if (GetOemCertificateFileName(&certificate_file_name)) {
|
if (GetOemCertificateFileName(&certificate_file_name)) {
|
||||||
|
LOGI("Removing OEM certificate");
|
||||||
return RemoveFile(certificate_file_name);
|
return RemoveFile(certificate_file_name);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -879,6 +879,12 @@ const char* CdmResponseEnumToString(CdmResponseEnum cdm_response_enum) {
|
|||||||
return "SESSION_NOT_FOUND_GENERIC_CRYPTO";
|
return "SESSION_NOT_FOUND_GENERIC_CRYPTO";
|
||||||
case SESSION_NOT_FOUND_24:
|
case SESSION_NOT_FOUND_24:
|
||||||
return "SESSION_NOT_FOUND_24";
|
return "SESSION_NOT_FOUND_24";
|
||||||
|
case PROVISIONING_UNEXPECTED_RESPONSE_ERROR:
|
||||||
|
return "PROVISIONING_UNEXPECTED_RESPONSE_ERROR";
|
||||||
|
case PROVISIONING_4_STALE_RESPONSE:
|
||||||
|
return "PROVISIONING_4_STALE_RESPONSE";
|
||||||
|
case PROVISIONING_4_FAILED_TO_VERIFY_CERT_KEY:
|
||||||
|
return "PROVISIONING_4_FAILED_TO_VERIFY_CERT_KEY";
|
||||||
}
|
}
|
||||||
return UnknownValueRep(cdm_response_enum);
|
return UnknownValueRep(cdm_response_enum);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -413,6 +413,9 @@ TEST_P(CertificateProvisioningTest, ProvisioningRequestFailsEmptySignature) {
|
|||||||
TEST_P(CertificateProvisioningTest,
|
TEST_P(CertificateProvisioningTest,
|
||||||
ProvisioningResponseFailsWithEmptyResponse) {
|
ProvisioningResponseFailsWithEmptyResponse) {
|
||||||
certificate_provisioning_->Init("");
|
certificate_provisioning_->Init("");
|
||||||
|
// Must set state if not generating request.
|
||||||
|
certificate_provisioning_->SetStateForTesting(
|
||||||
|
CertificateProvisioning::kDrmRequestSent);
|
||||||
|
|
||||||
MockFileSystem file_system;
|
MockFileSystem file_system;
|
||||||
std::string certificate;
|
std::string certificate;
|
||||||
@@ -425,6 +428,9 @@ TEST_P(CertificateProvisioningTest,
|
|||||||
TEST_P(CertificateProvisioningTest,
|
TEST_P(CertificateProvisioningTest,
|
||||||
ProvisioningResponseFailsIfDeviceIsRevoked) {
|
ProvisioningResponseFailsIfDeviceIsRevoked) {
|
||||||
certificate_provisioning_->Init("");
|
certificate_provisioning_->Init("");
|
||||||
|
// Must set state if not generating request.
|
||||||
|
certificate_provisioning_->SetStateForTesting(
|
||||||
|
CertificateProvisioning::kDrmRequestSent);
|
||||||
|
|
||||||
MockFileSystem file_system;
|
MockFileSystem file_system;
|
||||||
std::string response_certificate;
|
std::string response_certificate;
|
||||||
@@ -445,6 +451,10 @@ TEST_P(CertificateProvisioningTest,
|
|||||||
|
|
||||||
TEST_P(CertificateProvisioningTest, ProvisioningResponseSuccess) {
|
TEST_P(CertificateProvisioningTest, ProvisioningResponseSuccess) {
|
||||||
certificate_provisioning_->Init("");
|
certificate_provisioning_->Init("");
|
||||||
|
// Must set state if not generating request.
|
||||||
|
certificate_provisioning_->SetStateForTesting(
|
||||||
|
CertificateProvisioning::kDrmRequestSent);
|
||||||
|
|
||||||
std::string expected_certificate;
|
std::string expected_certificate;
|
||||||
std::string response;
|
std::string response;
|
||||||
ASSERT_TRUE(MakeSignedDrmCertificate(kFakePublicKey, kSerialNumber, kSystemId,
|
ASSERT_TRUE(MakeSignedDrmCertificate(kFakePublicKey, kSerialNumber, kSystemId,
|
||||||
|
|||||||
@@ -279,4 +279,539 @@ TEST_F(CoreIntegrationTest, NeedKeyBeforeLicenseLoad) {
|
|||||||
EXPECT_EQ(NEED_KEY, holder.Decrypt(key_id));
|
EXPECT_EQ(NEED_KEY, holder.Decrypt(key_id));
|
||||||
ASSERT_NO_FATAL_FAILURE(holder.CloseSession());
|
ASSERT_NO_FATAL_FAILURE(holder.CloseSession());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Prov40IntegrationTest : public WvCdmTestBaseWithEngine {
|
||||||
|
public:
|
||||||
|
void SetUp() override {
|
||||||
|
WvCdmTestBaseWithEngine::SetUp();
|
||||||
|
// Ensure CDM is operating using Provisioning 4.0.
|
||||||
|
std::string prov_model;
|
||||||
|
CdmResponseType status = cdm_engine_.QueryStatus(
|
||||||
|
kLevelDefault, QUERY_KEY_PROVISIONING_MODEL, &prov_model);
|
||||||
|
ASSERT_EQ(status, NO_ERROR) << "Failed to determine provisioning model";
|
||||||
|
if (prov_model != QUERY_VALUE_BOOT_CERTIFICATE_CHAIN) {
|
||||||
|
GTEST_SKIP() << "Test is for Prov4.0 only";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ensure CDM is not provisioned.
|
||||||
|
if (IsProvisioned()) {
|
||||||
|
status = cdm_engine_.Unprovision(kSecurityLevelL1);
|
||||||
|
ASSERT_EQ(status, NO_ERROR) << "Failed to unprovision DRM cert";
|
||||||
|
status = cdm_engine_.UnprovisionOemCert(kSecurityLevelL1);
|
||||||
|
ASSERT_EQ(status, NO_ERROR) << "Failed to unprovision OEM cert";
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsOemCertProvisioning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CdmProvisioningStatus GetProvisioningStatus() {
|
||||||
|
return cdm_engine_.GetProvisioningStatus(kSecurityLevelL1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsProvisioned() { return cdm_engine_.IsProvisioned(kSecurityLevelL1); }
|
||||||
|
|
||||||
|
void PreDrmProvisioningCheck() {
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsOemCertProvisioning)
|
||||||
|
<< "Not in valid state for pre DRM provisioning check";
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
// OEM provisioning.
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_))
|
||||||
|
<< "OEM Certificate provisioning attempt failed";
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning)
|
||||||
|
<< "OEM Certificate provisioning was not completed";
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostIncompleteOemProvisioningCheck() {
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsOemCertProvisioning)
|
||||||
|
<< "Not in valid state for post incomplete OEM provisioning check";
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
// OEM provisioning.
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_))
|
||||||
|
<< "OEM Certificate provisioning attempt failed";
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning)
|
||||||
|
<< "OEM Certificate provisioning was not completed";
|
||||||
|
// DRM provisioning.
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_))
|
||||||
|
<< "DRM Certificate provisioning attempt failed";
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kProvisioned)
|
||||||
|
<< "DRM Certificate provisioning was not completed";
|
||||||
|
// Remaining is the same as post DRM provisioning.
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostDrmProvisioningCheck())
|
||||||
|
<< "Failed post incomplete OEM provisioning check after DRM "
|
||||||
|
"provisioning";
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostOemProvisioningCheck() {
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning)
|
||||||
|
<< "Not in valid state for post OEM provisioning check";
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_))
|
||||||
|
<< "DRM Certificate provisioning failed";
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kProvisioned)
|
||||||
|
<< "DRM Certificate provisioning was not completed";
|
||||||
|
// Remaining is the same as post DRM provisioning.
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostDrmProvisioningCheck())
|
||||||
|
<< "Failed post OEM provisioning check after DRM provisioning";
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostIncompleteDrmProvisioningCheck() {
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning)
|
||||||
|
<< "Not in valid state for post incomplete DRM provisioning check";
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_))
|
||||||
|
<< "DRM Certificate provisioning failed";
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kProvisioned)
|
||||||
|
<< "DRM Certificate provisioning was not completed";
|
||||||
|
// Remaining is the same as post DRM provisioning.
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostDrmProvisioningCheck())
|
||||||
|
<< "Failed post incomplete DRM provisioning check after DRM "
|
||||||
|
"provisioning";
|
||||||
|
}
|
||||||
|
|
||||||
|
void PostDrmProvisioningCheck() {
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kProvisioned)
|
||||||
|
<< "Not in valid state for post DRM provisioning check";
|
||||||
|
LicenseHolder holder("CDM_Streaming", &cdm_engine_, config_);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(holder.OpenSession());
|
||||||
|
ASSERT_NO_FATAL_FAILURE(holder.FetchLicense());
|
||||||
|
ASSERT_NO_FATAL_FAILURE(holder.LoadLicense());
|
||||||
|
ASSERT_NO_FATAL_FAILURE(holder.CloseSession());
|
||||||
|
}
|
||||||
|
}; // class Prov40IntegrationTest
|
||||||
|
|
||||||
|
// Expected flow of an app; 1 OEM request-response, 1 DRM request-response.
|
||||||
|
//
|
||||||
|
// Case: OemReq1, OemResp1, DrmReq1, DrmResp1
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// This is Widevine's expected behavior by an app.
|
||||||
|
//
|
||||||
|
// Post-Case: Load license
|
||||||
|
TEST_F(Prov40IntegrationTest, UsualOrder_LoadOem1_LoadDrm1) {
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsOemCertProvisioning);
|
||||||
|
|
||||||
|
// Round 1 - OEM provisioning (OemReq1, OemResp1).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_))
|
||||||
|
<< "OEM Certificate provisioning failed";
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning);
|
||||||
|
|
||||||
|
// Round 2 - DRM provisioning (DrmReq1, DrmResp1).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_))
|
||||||
|
<< "DRM Certificate provisioning failed";
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kProvisioned);
|
||||||
|
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostDrmProvisioningCheck());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case: OemReq1, OemReq2, OemResp1 (OemResp2 is never acquired)
|
||||||
|
// Expectation:
|
||||||
|
// CDM handles OemResp1, but does not complete OEM
|
||||||
|
// provisioning.
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// This is undesirable behavior by the app, but can be partially
|
||||||
|
// handle by the CDM.
|
||||||
|
// Apps that encounter this situation are likely generating many
|
||||||
|
// provisioning requests and loading them in whatever order they
|
||||||
|
// arrive.
|
||||||
|
//
|
||||||
|
// Post-Case: OEM provisioning, DRM provisioning, load license
|
||||||
|
TEST_F(Prov40IntegrationTest, UnusualOrder_DropOem2_LoadOem1) {
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
|
||||||
|
// OEM provisioning.
|
||||||
|
// Generate first request (OemReq1).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
const std::string oem_request1 = provisioner.request();
|
||||||
|
|
||||||
|
// Generate second request (OemReq2).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
// Never send for the second request.
|
||||||
|
|
||||||
|
// Use first request for fetching/loading response (OemResp1).
|
||||||
|
// CDM may or may not return an error, but OEM provisioning is still
|
||||||
|
// needed.
|
||||||
|
provisioner.set_request(oem_request1);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.FetchResponse());
|
||||||
|
// Do not enforce any particular error (including NO_ERROR).
|
||||||
|
provisioner.LoadResponseReturnStatus(binary_provisioning_);
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsOemCertProvisioning);
|
||||||
|
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostIncompleteOemProvisioningCheck());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case: OemReq1, OemReq2, OemResp2 (OemResp1 is never acquired)
|
||||||
|
// Expectation:
|
||||||
|
// CDM handles OemReq2 (NO_ERROR), and OEM provisioning is
|
||||||
|
// completed.
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// This is OK behavior by the app.
|
||||||
|
// Only the OEM response from the most recent OEM request will
|
||||||
|
// complete provisioning.
|
||||||
|
//
|
||||||
|
// Post-Case: OEM provisioning, DRM provisioning, load license
|
||||||
|
TEST_F(Prov40IntegrationTest, UnusualOrder_DropOem1_LoadOem2) {
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
|
||||||
|
// OEM provisioning.
|
||||||
|
// Generate first request (OemReq1).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
// Never send for the first request.
|
||||||
|
|
||||||
|
// Generate, fetch and load second request (OemReq2, OemResp2).
|
||||||
|
// This should complete OEM provisioning.
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_))
|
||||||
|
<< "OEM Certificate provisioning failed";
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning);
|
||||||
|
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostOemProvisioningCheck());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case: OemReq1, OemReq2, OemResp1, OemResp2
|
||||||
|
// Expectation:
|
||||||
|
// OemResp1 is handled by the CDM, but does not complete
|
||||||
|
// provisioning. OemResp2 is accepted by the CDM
|
||||||
|
// and completes provisioning.
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// This is undesirable behavior by the app, but can be partially
|
||||||
|
// handle by the CDM.
|
||||||
|
// Only the OEM response from the most recent OEM request will
|
||||||
|
// complete provisioning.
|
||||||
|
//
|
||||||
|
// Post-Case: DRM provisioning, load license
|
||||||
|
TEST_F(Prov40IntegrationTest, UnusualOrder_LoadOem1_LoadOem2) {
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
|
||||||
|
// OEM provisioning.
|
||||||
|
// Generate first request, store it for later (OemReq1).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
const std::string oem_request1 = provisioner.request();
|
||||||
|
|
||||||
|
// Generate second request, store it for later (OemReq2).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
const std::string oem_request2 = provisioner.request();
|
||||||
|
|
||||||
|
// Use first request for fetching/loading response (OemResp1).
|
||||||
|
// CDM may or may not return an error, but OEM provisioning is still
|
||||||
|
// needed.
|
||||||
|
provisioner.set_request(oem_request1);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.FetchResponse());
|
||||||
|
// Do not enforce any particular error (including NO_ERROR).
|
||||||
|
provisioner.LoadResponseReturnStatus(binary_provisioning_);
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsOemCertProvisioning);
|
||||||
|
|
||||||
|
// Use second request for fetching/loading response (OemResp2).
|
||||||
|
// CDM should accept the second response as valid (so long as
|
||||||
|
// a third was not generated).
|
||||||
|
provisioner.set_request(oem_request2);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.FetchResponse());
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.LoadResponse(binary_provisioning_))
|
||||||
|
<< "OEM Certificate provisioning failed";
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning);
|
||||||
|
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostOemProvisioningCheck());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case: OemReq1, OemReq2, OemResp2, OemResp1
|
||||||
|
// Expectation:
|
||||||
|
// OemResp2 is accepted by the CDM and comletes OEM provisioning.
|
||||||
|
// OemResp1 does not cause the CDM to be corrupted.
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// This is undesirable behavior by the app, cannot be handle
|
||||||
|
// by the CDM.
|
||||||
|
// In single-staged provisioning, the CDM silently drops
|
||||||
|
// any additional provisioning responses; but in two-stage
|
||||||
|
// this cannot easily by determine that the response is a
|
||||||
|
// late OEM response.
|
||||||
|
//
|
||||||
|
// Post-Case: DRM provisioning, load license
|
||||||
|
TEST_F(Prov40IntegrationTest, UnusualOrder_LoadOem2_LoadOem1) {
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
|
||||||
|
// OEM provisioning.
|
||||||
|
// Generate first request, store it for later (OemReq1).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
const std::string oem_request1 = provisioner.request();
|
||||||
|
|
||||||
|
// Generate, fetch and load second request (OemReq2, OemResp2).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_))
|
||||||
|
<< "OEM Certificate provisioning failed";
|
||||||
|
// Provisioning should be complete.
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning);
|
||||||
|
|
||||||
|
// Use first request for fetching/loading response (OemResp1).
|
||||||
|
// CDM may or may not return an error, but DRM provisioning
|
||||||
|
// should still be allowed after.
|
||||||
|
provisioner.set_request(oem_request1);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.FetchResponse());
|
||||||
|
// Do not enforce any particular error (including NO_ERROR).
|
||||||
|
provisioner.LoadResponseReturnStatus(binary_provisioning_);
|
||||||
|
// Should not effect existing provisioning state.
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning)
|
||||||
|
<< "Late OEM Certificate response invalidated original response";
|
||||||
|
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostOemProvisioningCheck());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case: DrmReq1, DrmReq2, DrmResp1, (DrmResp2 is never acquired)
|
||||||
|
// Expectation:
|
||||||
|
// DrmResp1 is handled by the CDM, but does not complete
|
||||||
|
// provisioning.
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// This is undesirable behavior by the app, but can be partially
|
||||||
|
// handle by the CDM.
|
||||||
|
// Apps that encounter this situation are likely generating many
|
||||||
|
// provisioning requests and loading them in whatever order they
|
||||||
|
// arrive.
|
||||||
|
// For single-stage, this situation usually returns a signature
|
||||||
|
// failure.
|
||||||
|
//
|
||||||
|
// Pre-Case: OEM provisioning
|
||||||
|
// Post-Case: DRM provisioning, load license
|
||||||
|
TEST_F(Prov40IntegrationTest, UnusualOrder_DropDrm2_LoadDrm1) {
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PreDrmProvisioningCheck());
|
||||||
|
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
// DRM provisioning.
|
||||||
|
// Generate first request, store it for later (DrmReq1).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
const std::string drm_request1 = provisioner.request();
|
||||||
|
|
||||||
|
// Generate second request (DrmReq2).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
// Never send for the second request.
|
||||||
|
|
||||||
|
// Use first request for fetching/loading response (DrmResp1).
|
||||||
|
// CDM may or may not return an error, but DRM provisioning is still
|
||||||
|
// needed.
|
||||||
|
provisioner.set_request(drm_request1);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.FetchResponse());
|
||||||
|
// Do not enforce any particular error (including NO_ERROR).
|
||||||
|
provisioner.LoadResponseReturnStatus(binary_provisioning_);
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning);
|
||||||
|
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostIncompleteDrmProvisioningCheck());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case: DrmReq1, DrmReq2, DrmResp2 (DrmResp1 is never acquired)
|
||||||
|
// Expectation:
|
||||||
|
// CDM accepts DrmReq2 (NO_ERROR), and DRM provisioning is
|
||||||
|
// completed.
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// This is OK behavior by the app.
|
||||||
|
// Only the DRM response from the most recent DRM request will
|
||||||
|
// complete provisioning.
|
||||||
|
//
|
||||||
|
// Pre-Case: OEM provisioning
|
||||||
|
// Post-Case: Load license
|
||||||
|
TEST_F(Prov40IntegrationTest, UnusualOrder_DropDrm1_LoadDrm2) {
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PreDrmProvisioningCheck());
|
||||||
|
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
// DRM provisioning.
|
||||||
|
// Generate first request (DrmReq1).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
// Never send for the first request.
|
||||||
|
|
||||||
|
// Generate, fetch and load second request (DrmReq2, DrmResp2).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_))
|
||||||
|
<< "DRM Certificate provisioning failed";
|
||||||
|
ASSERT_TRUE(IsProvisioned());
|
||||||
|
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostDrmProvisioningCheck());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case: DrmReq1, DrmReq2, DrmResp1, DrmResp2
|
||||||
|
// Expectation:
|
||||||
|
// DrmResp1 is handled by the CDM, but does not complete
|
||||||
|
// provisioning. DrmResp2 is accepted by the CDM and
|
||||||
|
// completes provisioning.
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// This is undesirable behavior by the app, but can be partially
|
||||||
|
// handle by the CDM.
|
||||||
|
// Only the DRM response from the most recent DRM request will
|
||||||
|
// complete provisioning.
|
||||||
|
//
|
||||||
|
// Pre-Case: OEM provisioning
|
||||||
|
// Post-Case: Load license
|
||||||
|
TEST_F(Prov40IntegrationTest, UnusualOrder_LoadDrm1_LoadDrm2) {
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PreDrmProvisioningCheck());
|
||||||
|
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
// DRM provisioning.
|
||||||
|
// Generate first request, store it for later (DrmReq1).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
const std::string drm_request1 = provisioner.request();
|
||||||
|
|
||||||
|
// Generate second request, store it for later (DrmReq2).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
const std::string drm_request2 = provisioner.request();
|
||||||
|
|
||||||
|
// Use first request for fetching/loading response (DrmResp1).
|
||||||
|
// CDM may or may not return an error, but DRM provisioning is still
|
||||||
|
// needed.
|
||||||
|
provisioner.set_request(drm_request1);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.FetchResponse());
|
||||||
|
// Do not enforce any particular error (including NO_ERROR).
|
||||||
|
provisioner.LoadResponseReturnStatus(binary_provisioning_);
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning);
|
||||||
|
|
||||||
|
// Use second request for fetching/loading response (DrmResp2).
|
||||||
|
// CDM should accept the second response as valid (so long as
|
||||||
|
// a third was not generated).
|
||||||
|
provisioner.set_request(drm_request2);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.FetchResponse());
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.LoadResponse(binary_provisioning_))
|
||||||
|
<< "DRM Certificate provisioning failed";
|
||||||
|
ASSERT_TRUE(IsProvisioned());
|
||||||
|
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostDrmProvisioningCheck());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case: DrmReq1, DrmReq2, DrmResp2, DrmResp1
|
||||||
|
// Expectation:
|
||||||
|
// DrmResp2 is accepted by the CDM (NO_ERROR) and completes
|
||||||
|
// provisioning. DrmResp1 is handled by the CDM, but is dropped
|
||||||
|
// without causing issues with existing certificates.
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// This is undesirable behavior by the app, but can be partially
|
||||||
|
// handle by the CDM.
|
||||||
|
//
|
||||||
|
// Pre-Case: OEM provisioning
|
||||||
|
// Post-Case: Load license
|
||||||
|
TEST_F(Prov40IntegrationTest, UnusualOrder_LoadDrm2_LoadDrm1) {
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PreDrmProvisioningCheck());
|
||||||
|
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
// DRM provisioning.
|
||||||
|
// Generate first request, store it for later (DrmReq1).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
const std::string drm_request1 = provisioner.request();
|
||||||
|
|
||||||
|
// Generate, fetch and load second request (DrmReq2, DrmResp2).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_))
|
||||||
|
<< "DRM Certificate provisioning failed";
|
||||||
|
ASSERT_TRUE(IsProvisioned());
|
||||||
|
|
||||||
|
// Use first request for fetching/loading response (DrmResp1).
|
||||||
|
// CDM may or may not return an error, and the CDM should still
|
||||||
|
// be considered provisioned.
|
||||||
|
provisioner.set_request(drm_request1);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.FetchResponse());
|
||||||
|
// Do not enforce any particular error (including NO_ERROR).
|
||||||
|
provisioner.LoadResponseReturnStatus(binary_provisioning_);
|
||||||
|
// Should not effect existing provisioning state.
|
||||||
|
ASSERT_TRUE(IsProvisioned())
|
||||||
|
<< "Late DRM Certificate response invalidated original response";
|
||||||
|
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostDrmProvisioningCheck());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case: OemReq1, OemReq2, OemResp2, DrmReq1, OemResp1, DrmResp1
|
||||||
|
// Expectation:
|
||||||
|
// OemResp2 will complete OEM provisioning, allowing the
|
||||||
|
// creation of DrmReq1.
|
||||||
|
// OemResp1 (being received after OEM provisioning is completed,
|
||||||
|
// and DRM provisioning initiated) is handled by the CDM
|
||||||
|
// and does not prevent the completion of DRM provisioning.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// This is undesirable behavior by the app, but can be partially
|
||||||
|
// handle by the CDM.
|
||||||
|
// Stale OEM responses should not interrupt DRM provisioning in
|
||||||
|
// progress.
|
||||||
|
//
|
||||||
|
// Post-Case: Load license
|
||||||
|
TEST_F(Prov40IntegrationTest, UnusualOrder_LoadOem2_LoadDrm1_LoadOem1AsDrm) {
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
|
||||||
|
// Round 1 - OEM provisioning.
|
||||||
|
// Generated and stored first OEM request (OemReq1)
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
const std::string oem_request1 = provisioner.request();
|
||||||
|
|
||||||
|
// Complete provisioning on the second attempt (OemReq2, OemResp2).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_));
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning);
|
||||||
|
|
||||||
|
// Round 2 - DRM provisioning.
|
||||||
|
// Generate DRM certificate request (DrmReq1).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
const std::string drm_request1 = provisioner.request();
|
||||||
|
|
||||||
|
// Use OEM request 1 to get an OEM response (OemResp1).
|
||||||
|
// CDM should detect that the OEM response is no longer needed
|
||||||
|
// and should drop the response with or without errors.
|
||||||
|
provisioner.set_request(oem_request1);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.FetchResponse());
|
||||||
|
// Do not enforce any particular error (including NO_ERROR).
|
||||||
|
provisioner.LoadResponseReturnStatus(binary_provisioning_);
|
||||||
|
// Should not effect existing provisioning state.
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning);
|
||||||
|
|
||||||
|
// Use DRM request 1 to get a DRM response (DrmResp1).
|
||||||
|
provisioner.set_request(drm_request1);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.FetchResponse());
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.LoadResponse(binary_provisioning_))
|
||||||
|
<< "Real DRM Certificate provisioning failed";
|
||||||
|
ASSERT_TRUE(IsProvisioned());
|
||||||
|
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostDrmProvisioningCheck());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case: OemReq1, OemReq2, OemResp2, DrmReq1, DrmResp1, OemResp1
|
||||||
|
// Expectation:
|
||||||
|
// OemResp2 will complete OEM provisioning, allowing the
|
||||||
|
// creation of DrmReq1.
|
||||||
|
// DrmResp1 will complete DRM provisioning.
|
||||||
|
// OemResp1 (being received after OEM provisioning is completed,
|
||||||
|
// and after DRM provisioning is complete) is handled by the CDM
|
||||||
|
// and does not cause any other issue.
|
||||||
|
//
|
||||||
|
// Notes:
|
||||||
|
// This is undesirable behavior by the app, but can be partially
|
||||||
|
// handle by the CDM.
|
||||||
|
// Any provisioning response received after DRM provisioning
|
||||||
|
// is completed is ignored.
|
||||||
|
//
|
||||||
|
// Post-Case: Load license
|
||||||
|
TEST_F(Prov40IntegrationTest, UnusualOrder_LoadOem2_LoadOem1AsDrm_LoadDrm1) {
|
||||||
|
ProvisioningHolder provisioner(&cdm_engine_, config_);
|
||||||
|
|
||||||
|
// Round 1 - OEM provisioning.
|
||||||
|
// Generated and stored first OEM request (OemReq1)
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.GenerateRequest(binary_provisioning_));
|
||||||
|
const std::string oem_request1 = provisioner.request();
|
||||||
|
|
||||||
|
// Complete provisioning on the second attempt (OemReq2, OemResp2).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_));
|
||||||
|
ASSERT_EQ(GetProvisioningStatus(), kNeedsDrmCertProvisioning);
|
||||||
|
|
||||||
|
// Round 2 - DRM provisioning (DrmReq1, DrmReq2).
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.Provision(binary_provisioning_));
|
||||||
|
ASSERT_TRUE(IsProvisioned());
|
||||||
|
|
||||||
|
// Use OEM request 1 to get an OEM response (OemResp2).
|
||||||
|
// CDM should detect that CDM is fully provisioned and should drop
|
||||||
|
// the response with or without errors.
|
||||||
|
provisioner.set_request(oem_request1);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(provisioner.FetchResponse());
|
||||||
|
// Do not enforce any particular error (including NO_ERROR).
|
||||||
|
provisioner.LoadResponseReturnStatus(binary_provisioning_);
|
||||||
|
// Should not effect existing provisioning state.
|
||||||
|
ASSERT_TRUE(IsProvisioned())
|
||||||
|
<< "Late OEM Certificate response invalidated DRM certificate";
|
||||||
|
;
|
||||||
|
|
||||||
|
ASSERT_NO_FATAL_FAILURE(PostDrmProvisioningCheck());
|
||||||
|
}
|
||||||
} // namespace wvcdm
|
} // namespace wvcdm
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ void LicenseHolder::GenerateAndPostRenewalRequest(
|
|||||||
void LicenseHolder::FetchRenewal() {
|
void LicenseHolder::FetchRenewal() {
|
||||||
ASSERT_NE(request_in_flight_, nullptr) << "Failed for " << content_id();
|
ASSERT_NE(request_in_flight_, nullptr) << "Failed for " << content_id();
|
||||||
ASSERT_NO_FATAL_FAILURE(
|
ASSERT_NO_FATAL_FAILURE(
|
||||||
request_in_flight_->AssertOkResponse(&request_response_))
|
request_in_flight_->AssertOkResponseWithRetry(&request_response_))
|
||||||
<< "Renewal failed for " << content_id();
|
<< "Renewal failed for " << content_id();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ void LicenseHolder::GenerateAndPostReleaseRequest(
|
|||||||
void LicenseHolder::FetchRelease() {
|
void LicenseHolder::FetchRelease() {
|
||||||
ASSERT_NE(request_in_flight_, nullptr) << "Failed for " << content_id();
|
ASSERT_NE(request_in_flight_, nullptr) << "Failed for " << content_id();
|
||||||
ASSERT_NO_FATAL_FAILURE(
|
ASSERT_NO_FATAL_FAILURE(
|
||||||
request_in_flight_->AssertOkResponse(&request_response_))
|
request_in_flight_->AssertOkResponseWithRetry(&request_response_))
|
||||||
<< "Renewal failed for " << content_id();
|
<< "Renewal failed for " << content_id();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ void LicenseHolder::GetKeyResponse(const CdmKeyRequest& key_request) {
|
|||||||
|
|
||||||
std::string http_response;
|
std::string http_response;
|
||||||
url_request.PostRequest(key_request.message);
|
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();
|
<< "Failed for " << content_id();
|
||||||
LicenseRequest license_request;
|
LicenseRequest license_request;
|
||||||
license_request.GetDrmMessage(http_response, key_response_);
|
license_request.GetDrmMessage(http_response, key_response_);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include "cdm_engine.h"
|
#include "cdm_engine.h"
|
||||||
#include "config_test_env.h"
|
|
||||||
|
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "message_dumper.h"
|
#include "message_dumper.h"
|
||||||
@@ -20,93 +19,159 @@ namespace wvcdm {
|
|||||||
|
|
||||||
void ProvisioningHolder::Provision(CdmCertificateType cert_type,
|
void ProvisioningHolder::Provision(CdmCertificateType cert_type,
|
||||||
bool binary_provisioning) {
|
bool binary_provisioning) {
|
||||||
CdmProvisioningRequest request;
|
ASSERT_NO_FATAL_FAILURE(GenerateRequest(cert_type, binary_provisioning))
|
||||||
std::string provisioning_server_url;
|
<< "Failed to generate request";
|
||||||
|
ASSERT_NO_FATAL_FAILURE(FetchResponse()) << "Failed to fetch response";
|
||||||
|
ASSERT_NO_FATAL_FAILURE(LoadResponse(binary_provisioning))
|
||||||
|
<< "Failed to load response";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProvisioningHolder::GenerateRequest(CdmCertificateType cert_type,
|
||||||
|
bool binary_provisioning) {
|
||||||
|
// Preconditions.
|
||||||
|
ASSERT_NE(cdm_engine_, nullptr) << "CdmEngine instance not set";
|
||||||
|
|
||||||
|
// Set cert authority if using X.509.
|
||||||
std::string cert_authority;
|
std::string cert_authority;
|
||||||
|
|
||||||
CdmSessionId session_id;
|
|
||||||
|
|
||||||
if (cert_type == kCertificateX509) {
|
if (cert_type == kCertificateX509) {
|
||||||
cert_authority = "cast.google.com";
|
cert_authority = "cast.google.com";
|
||||||
}
|
}
|
||||||
LOGV("Provision with type %s.", CdmCertificateTypeToString(cert_type));
|
LOGV("Provision with type %s.", CdmCertificateTypeToString(cert_type));
|
||||||
CdmResponseType result(CERT_PROVISIONING_NONCE_GENERATION_ERROR);
|
LOGV("cert_authority = %s", cert_authority.c_str());
|
||||||
|
|
||||||
|
CdmResponseType status(CERT_PROVISIONING_NONCE_GENERATION_ERROR);
|
||||||
|
CdmProvisioningRequest cdm_prov_request;
|
||||||
|
std::string provisioning_server_url_unused;
|
||||||
// Get a provisioning request. We might need one retry if there is a nonce
|
// Get a provisioning request. We might need one retry if there is a nonce
|
||||||
// flood failure.
|
// flood failure.
|
||||||
for (int i = 0; i < 2 && result == CERT_PROVISIONING_NONCE_GENERATION_ERROR;
|
for (int i = 0; i < 2 && status == CERT_PROVISIONING_NONCE_GENERATION_ERROR;
|
||||||
i++) {
|
i++) {
|
||||||
result = cdm_engine_->GetProvisioningRequest(
|
status = cdm_engine_->GetProvisioningRequest(
|
||||||
cert_type, cert_authority, provisioning_service_certificate_,
|
cert_type, cert_authority, provisioning_service_certificate_,
|
||||||
kLevelDefault, &request, &provisioning_server_url);
|
kLevelDefault, &cdm_prov_request, &provisioning_server_url_unused);
|
||||||
if (result == CERT_PROVISIONING_NONCE_GENERATION_ERROR) {
|
if (status == CERT_PROVISIONING_NONCE_GENERATION_ERROR) {
|
||||||
wvutil::TestSleep::Sleep(2);
|
wvutil::TestSleep::Sleep(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ASSERT_EQ(NO_ERROR, result);
|
ASSERT_EQ(status, NO_ERROR) << "Failed to generate provisioning request";
|
||||||
LOGV("cert_authority = %s", cert_authority.c_str());
|
|
||||||
|
|
||||||
|
// |binary_provisioning| implies the CdmEngine is using binary
|
||||||
|
// provisioning messages; however, ProvisioningHolder can only
|
||||||
|
// operate using Base64 requests.
|
||||||
if (binary_provisioning) {
|
if (binary_provisioning) {
|
||||||
request = wvutil::Base64SafeEncodeNoPad(request);
|
request_ = wvutil::Base64SafeEncodeNoPad(cdm_prov_request);
|
||||||
|
} else {
|
||||||
|
request_ = std::move(cdm_prov_request);
|
||||||
}
|
}
|
||||||
|
cert_type_ = cert_type;
|
||||||
|
|
||||||
if (config_.dump_golden_data()) {
|
if (config_.dump_golden_data()) {
|
||||||
std::vector<uint8_t> binary_request = wvutil::Base64SafeDecode(request);
|
const std::vector<uint8_t> binary_request =
|
||||||
CdmProvisioningRequest binary_request_string(binary_request.begin(),
|
wvutil::Base64SafeDecode(request_);
|
||||||
binary_request.end());
|
const CdmProvisioningRequest binary_request_string(binary_request.begin(),
|
||||||
|
binary_request.end());
|
||||||
MessageDumper::DumpProvisioningRequest(binary_request_string);
|
MessageDumper::DumpProvisioningRequest(binary_request_string);
|
||||||
}
|
}
|
||||||
LOGV("Provisioning request: req = %s", request.c_str());
|
}
|
||||||
|
|
||||||
// Ignore URL provided by CdmEngine. Use ours, as configured
|
void ProvisioningHolder::FetchResponse() {
|
||||||
// for test vs. production server.
|
// Preconditions.
|
||||||
provisioning_server_url.assign(provisioning_server_url_);
|
ASSERT_NE(cdm_engine_, nullptr) << "CdmEngine instance not set";
|
||||||
|
ASSERT_FALSE(request_.empty()) << "No request was set";
|
||||||
|
ASSERT_FALSE(provisioning_server_url_.empty())
|
||||||
|
<< "Test config is missing provisioning URL";
|
||||||
|
|
||||||
// Make request.
|
UrlRequest url_request(provisioning_server_url_);
|
||||||
UrlRequest url_request(provisioning_server_url);
|
ASSERT_TRUE(url_request.is_connected())
|
||||||
if (!url_request.is_connected()) {
|
<< "Failed to connect to provisoining server: "
|
||||||
LOGE("Failed to connect to provisioning server: url = %s",
|
<< provisioning_server_url_;
|
||||||
provisioning_server_url.c_str());
|
ASSERT_TRUE(url_request.PostCertRequestInQueryString(request_));
|
||||||
}
|
std::string response;
|
||||||
url_request.PostCertRequestInQueryString(request);
|
ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponseWithRetry(&response))
|
||||||
|
|
||||||
// Receive and parse response.
|
|
||||||
ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponse(&response_))
|
|
||||||
<< "Failed to fetch provisioning response. "
|
<< "Failed to fetch provisioning response. "
|
||||||
<< DumpProvAttempt(request, response_, cert_type);
|
<< DumpProvAttempt(request_, response, cert_type_);
|
||||||
|
ASSERT_FALSE(response.empty()) << "Missing response";
|
||||||
|
response_ = std::move(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProvisioningHolder::LoadResponse(bool binary_provisioning) {
|
||||||
|
// Preconditions.
|
||||||
|
ASSERT_FALSE(response_.empty()) << "No response was fetched";
|
||||||
|
|
||||||
|
std::string cdm_prov_response;
|
||||||
if (binary_provisioning) {
|
if (binary_provisioning) {
|
||||||
// extract provisioning response from received message
|
// CDM is expecting the response to be in binary form, response
|
||||||
// Extracts signed response from JSON string, result is serialized
|
// must be extracted and decoded.
|
||||||
// protobuf.
|
std::string base_64_response;
|
||||||
std::string protobuf_response;
|
ASSERT_TRUE(ExtractSignedMessage(response_, &base_64_response))
|
||||||
const bool extract_ok = ExtractSignedMessage(response_, &protobuf_response);
|
<< "Failed to extract signed serialized response from JSON response";
|
||||||
ASSERT_TRUE(extract_ok) << "Failed to extract signed serialized "
|
ASSERT_FALSE(base_64_response.empty())
|
||||||
"response from JSON response";
|
<< "Base64 encoded provisioning response is unexpectedly empty";
|
||||||
LOGV("Extracted response message: \n%s\n", protobuf_response.c_str());
|
LOGV("Extracted response message: \n%s\n", base_64_response.c_str());
|
||||||
|
|
||||||
ASSERT_FALSE(protobuf_response.empty())
|
const std::vector<uint8_t> response_vec =
|
||||||
<< "Protobuf response is unexpectedly empty";
|
wvutil::Base64SafeDecode(base_64_response);
|
||||||
|
|
||||||
// base64 decode response to yield binary protobuf
|
|
||||||
const std::vector<uint8_t> response_vec(
|
|
||||||
wvutil::Base64SafeDecode(protobuf_response));
|
|
||||||
ASSERT_FALSE(response_vec.empty())
|
ASSERT_FALSE(response_vec.empty())
|
||||||
<< "Failed to decode base64 of response: response = "
|
<< "Failed to decode base64 response: " << base_64_response;
|
||||||
<< protobuf_response;
|
cdm_prov_response.assign(response_vec.begin(), response_vec.end());
|
||||||
|
} else {
|
||||||
response_.assign(response_vec.begin(), response_vec.end());
|
cdm_prov_response = response_;
|
||||||
}
|
}
|
||||||
|
|
||||||
ASSERT_EQ(NO_ERROR,
|
const CdmResponseType status = cdm_engine_->HandleProvisioningResponse(
|
||||||
cdm_engine_->HandleProvisioningResponse(
|
cdm_prov_response, kLevelDefault, &certificate_, &wrapped_key_);
|
||||||
response_, kLevelDefault, &certificate_, &wrapped_key_))
|
ASSERT_EQ(status, NO_ERROR)
|
||||||
<< (binary_provisioning ? "Binary provisioning failed. "
|
<< (binary_provisioning ? "Binary provisioning failed. "
|
||||||
: "Non-binary provisioning failed. ")
|
: "Non-binary provisioning failed. ")
|
||||||
<< DumpProvAttempt(request, response_, cert_type);
|
<< DumpProvAttempt(request_, response_, cert_type_);
|
||||||
if (config_.dump_golden_data()) {
|
if (config_.dump_golden_data()) {
|
||||||
MessageDumper::DumpProvisioning(response_);
|
MessageDumper::DumpProvisioning(response_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CdmResponseType ProvisioningHolder::LoadResponseReturnStatus(
|
||||||
|
bool binary_provisioning) {
|
||||||
|
// Preconditions.
|
||||||
|
if (response_.empty()) {
|
||||||
|
ADD_FAILURE() << "No response was fetched";
|
||||||
|
return CdmResponseType(UNKNOWN_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cdm_prov_response;
|
||||||
|
if (binary_provisioning) {
|
||||||
|
// CDM is expecting the response to be in binary form, response
|
||||||
|
// must be extracted and decoded.
|
||||||
|
std::string base_64_response;
|
||||||
|
if (!ExtractSignedMessage(response_, &base_64_response)) {
|
||||||
|
ADD_FAILURE()
|
||||||
|
<< "Failed to extract signed serialized response from JSON response";
|
||||||
|
return CdmResponseType(UNKNOWN_ERROR);
|
||||||
|
}
|
||||||
|
if (base_64_response.empty()) {
|
||||||
|
ADD_FAILURE()
|
||||||
|
<< "Base64 encoded provisioning response is unexpectedly empty";
|
||||||
|
return CdmResponseType(UNKNOWN_ERROR);
|
||||||
|
}
|
||||||
|
LOGV("Extracted response message: \n%s\n", base_64_response.c_str());
|
||||||
|
|
||||||
|
const std::vector<uint8_t> response_vec =
|
||||||
|
wvutil::Base64SafeDecode(base_64_response);
|
||||||
|
if (response_vec.empty()) {
|
||||||
|
ADD_FAILURE() << "Failed to decode base64 response: " << base_64_response;
|
||||||
|
return CdmResponseType(UNKNOWN_ERROR);
|
||||||
|
}
|
||||||
|
cdm_prov_response.assign(response_vec.begin(), response_vec.end());
|
||||||
|
} else {
|
||||||
|
cdm_prov_response = response_;
|
||||||
|
}
|
||||||
|
// HandleProvisioningResponse() may or may not succeed,
|
||||||
|
// left to caller to determine if this is considered a
|
||||||
|
// test failure.
|
||||||
|
const CdmResponseType status = cdm_engine_->HandleProvisioningResponse(
|
||||||
|
cdm_prov_response, kLevelDefault, &certificate_, &wrapped_key_);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
bool ProvisioningHolder::ExtractSignedMessage(const std::string& response,
|
bool ProvisioningHolder::ExtractSignedMessage(const std::string& response,
|
||||||
std::string* result) {
|
std::string* result) {
|
||||||
static const std::string kMessageStart = "\"signedResponse\": \"";
|
static const std::string kMessageStart = "\"signedResponse\": \"";
|
||||||
@@ -137,9 +202,9 @@ bool ProvisioningHolder::ExtractSignedMessage(const std::string& response,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ProvisioningHolder::DumpProvAttempt(const std::string& request,
|
std::string ProvisioningHolder::DumpProvAttempt(
|
||||||
const std::string& response,
|
const std::string& request, const std::string& response,
|
||||||
CdmCertificateType cert_type) {
|
CdmCertificateType cert_type) const {
|
||||||
std::stringstream info;
|
std::stringstream info;
|
||||||
info << "Cert Type: ";
|
info << "Cert Type: ";
|
||||||
PrintTo(cert_type, &info);
|
PrintTo(cert_type, &info);
|
||||||
|
|||||||
@@ -19,20 +19,62 @@ class ProvisioningHolder {
|
|||||||
provisioning_server_url_(config.provisioning_server()),
|
provisioning_server_url_(config.provisioning_server()),
|
||||||
provisioning_service_certificate_(
|
provisioning_service_certificate_(
|
||||||
config.provisioning_service_certificate()) {}
|
config.provisioning_service_certificate()) {}
|
||||||
|
|
||||||
|
// Generates requests, fetches response, and loads response.
|
||||||
void Provision(CdmCertificateType cert_type, bool binary_provisioning);
|
void Provision(CdmCertificateType cert_type, bool binary_provisioning);
|
||||||
void Provision(bool binary_provisioning) {
|
void Provision(bool binary_provisioning) {
|
||||||
Provision(kCertificateWidevine, binary_provisioning);
|
Provision(kCertificateWidevine, binary_provisioning);
|
||||||
}
|
}
|
||||||
std::string response() const { return response_; }
|
|
||||||
std::string certificate() const { return certificate_; }
|
// Generates a provisioning request from the |cdm_engine_|.
|
||||||
std::string wrapped_key() const { return wrapped_key_; }
|
// If successful, the request the stored in |request_|.
|
||||||
|
// The result of |request_| should always be the URL-Safe-Base64
|
||||||
|
// encoded value of the request.
|
||||||
|
void GenerateRequest(CdmCertificateType cert_type, bool binary_provisioning);
|
||||||
|
void GenerateRequest(bool binary_provisioning) {
|
||||||
|
GenerateRequest(kCertificateWidevine, binary_provisioning);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the provisioning response from the server.
|
||||||
|
// Uses |request_| as the source of the request, and stores
|
||||||
|
// JSON message of the response to |response_|.
|
||||||
|
void FetchResponse();
|
||||||
|
|
||||||
|
// Loads the response into the |cdm_engine_|, expecting success.
|
||||||
|
void LoadResponse(bool binary_provisioning);
|
||||||
|
// Loads the response into the |cdm_engine_|, returning the
|
||||||
|
// result from CDM.
|
||||||
|
CdmResponseType LoadResponseReturnStatus(bool binary_provisioning);
|
||||||
|
|
||||||
|
const std::string& request() const { return request_; }
|
||||||
|
// Sets the request to be used on next call to FetchResponse().
|
||||||
|
// The provided |request| must be a URL-Safe-Base64 encoding
|
||||||
|
// of the provisioning request.
|
||||||
|
void set_request(const std::string& request) { request_ = request; }
|
||||||
|
const std::string& response() const { return response_; }
|
||||||
|
const std::string& certificate() const { return certificate_; }
|
||||||
|
const std::string& wrapped_key() const { return wrapped_key_; }
|
||||||
|
|
||||||
|
void ClearProvisioningData() {
|
||||||
|
cert_type_ = kCertificateWidevine;
|
||||||
|
request_.clear();
|
||||||
|
response_.clear();
|
||||||
|
certificate_.clear();
|
||||||
|
wrapped_key_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
TestCdmEngine* cdm_engine_;
|
TestCdmEngine* cdm_engine_;
|
||||||
|
// Config variables.
|
||||||
const ConfigTestEnv& config_;
|
const ConfigTestEnv& config_;
|
||||||
std::string provisioning_server_url_;
|
std::string provisioning_server_url_;
|
||||||
std::string provisioning_service_certificate_;
|
std::string provisioning_service_certificate_;
|
||||||
std::string response_;
|
// Request variables.
|
||||||
|
CdmCertificateType cert_type_;
|
||||||
|
std::string request_; // URL-Safe-Base64 encoding of request.
|
||||||
|
// Response variables.
|
||||||
|
std::string response_; // JSON message containing response.
|
||||||
|
// Post-provisioning variables.
|
||||||
std::string certificate_;
|
std::string certificate_;
|
||||||
std::string wrapped_key_;
|
std::string wrapped_key_;
|
||||||
|
|
||||||
@@ -42,11 +84,12 @@ class ProvisioningHolder {
|
|||||||
// entire string represents a serialized protobuf mesaage and return true with
|
// entire string represents a serialized protobuf mesaage and return true with
|
||||||
// the entire string. If the end_substring match fails, return false with an
|
// the entire string. If the end_substring match fails, return false with an
|
||||||
// empty *result.
|
// empty *result.
|
||||||
bool ExtractSignedMessage(const std::string& response, std::string* result);
|
static bool ExtractSignedMessage(const std::string& response,
|
||||||
|
std::string* result);
|
||||||
// Dump request and response information for use in a debug or failure log.
|
// Dump request and response information for use in a debug or failure log.
|
||||||
std::string DumpProvAttempt(const std::string& request,
|
std::string DumpProvAttempt(const std::string& request,
|
||||||
const std::string& response,
|
const std::string& response,
|
||||||
CdmCertificateType cert_type);
|
CdmCertificateType cert_type) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace wvcdm
|
} // namespace wvcdm
|
||||||
|
|||||||
@@ -5,9 +5,12 @@
|
|||||||
#include "url_request.h"
|
#include "url_request.h"
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
#include "http_socket.h"
|
#include "http_socket.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
@@ -23,11 +26,15 @@ const int kConnectTimeoutMs = 15000;
|
|||||||
const int kWriteTimeoutMs = 12000;
|
const int kWriteTimeoutMs = 12000;
|
||||||
const int kReadTimeoutMs = 12000;
|
const int kReadTimeoutMs = 12000;
|
||||||
constexpr int kHttpOk = 200;
|
constexpr int kHttpOk = 200;
|
||||||
|
const std::vector<int> kRetryCodes = {502, 504};
|
||||||
|
|
||||||
const std::string kGoogleHeaderUpper("X-Google");
|
const std::string kGoogleHeaderUpper("X-Google");
|
||||||
const std::string kGoogleHeaderLower("x-google");
|
const std::string kGoogleHeaderLower("x-google");
|
||||||
const std::string kCrLf("\r\n");
|
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
|
// Concatenate all chunks into one blob and returns the response with
|
||||||
// header information.
|
// header information.
|
||||||
void ConcatenateChunkedResponse(const std::string http_response,
|
void ConcatenateChunkedResponse(const std::string http_response,
|
||||||
@@ -126,13 +133,34 @@ bool UrlRequest::GetResponse(std::string* message) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UrlRequest::AssertOkResponse(std::string* message) {
|
void UrlRequest::AssertOkResponseWithRetry(std::string* message) {
|
||||||
ASSERT_TRUE(message);
|
ASSERT_TRUE(message);
|
||||||
ASSERT_TRUE(GetResponse(message));
|
int status_code = 0;
|
||||||
const int status_code = GetStatusCode(*message);
|
for (unsigned i = 0; i < kRetryCount; i++) {
|
||||||
ASSERT_EQ(kHttpOk, status_code) << "HTTP response from " << socket_.url()
|
*message = "";
|
||||||
<< ": (" << message->size() << ") :\n"
|
ASSERT_TRUE(GetResponse(message)) << "For attempt " << (i + 1);
|
||||||
<< *message;
|
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
|
// static
|
||||||
@@ -189,36 +217,35 @@ bool UrlRequest::GetDebugHeaderFields(
|
|||||||
|
|
||||||
bool UrlRequest::PostRequestWithPath(const std::string& path,
|
bool UrlRequest::PostRequestWithPath(const std::string& path,
|
||||||
const std::string& data) {
|
const std::string& data) {
|
||||||
std::string request;
|
request_.clear();
|
||||||
|
|
||||||
request.append("POST ");
|
request_.append("POST ");
|
||||||
request.append(path);
|
request_.append(path);
|
||||||
request.append(" HTTP/1.1\r\n");
|
request_.append(" HTTP/1.1\r\n");
|
||||||
|
|
||||||
request.append("Host: ");
|
request_.append("Host: ");
|
||||||
request.append(socket_.domain_name());
|
request_.append(socket_.domain_name());
|
||||||
request.append("\r\n");
|
request_.append("\r\n");
|
||||||
|
|
||||||
request.append("Connection: close\r\n");
|
request_.append("Connection: close\r\n");
|
||||||
request.append("User-Agent: Widevine CDM v1.0\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("X-Return-Encrypted-Headers: request_and_response\r\n");
|
||||||
|
|
||||||
// buffer to store length of data as a string
|
request_.append("Content-Length: ");
|
||||||
char data_size_buffer[32] = {0};
|
request_.append(std::to_string(data.size()));
|
||||||
snprintf(data_size_buffer, sizeof(data_size_buffer), "%zu", data.size());
|
request_.append("\r\n");
|
||||||
|
|
||||||
request.append("Content-Length: ");
|
request_.append("\r\n"); // empty line to terminate headers
|
||||||
request.append(data_size_buffer); // appends size of data
|
|
||||||
request.append("\r\n");
|
|
||||||
|
|
||||||
request.append("\r\n"); // empty line to terminate headers
|
request_.append(data);
|
||||||
|
return SendRequestOnce();
|
||||||
request.append(data);
|
}
|
||||||
|
|
||||||
|
bool UrlRequest::SendRequestOnce() {
|
||||||
const int ret = socket_.WriteAndLogErrors(
|
const int ret = socket_.WriteAndLogErrors(
|
||||||
request.c_str(), static_cast<int>(request.size()), kWriteTimeoutMs);
|
request_.c_str(), static_cast<int>(request_.size()), kWriteTimeoutMs);
|
||||||
LOGV("HTTP request: (%zu): %s", request.size(), request.c_str());
|
LOGV("HTTP request: (%zu): %s", request_.size(), request_.c_str());
|
||||||
LOGV("HTTP request hex: %s", wvutil::b2a_hex(request).c_str());
|
LOGV("HTTP request hex: %s", wvutil::b2a_hex(request_).c_str());
|
||||||
return ret != -1;
|
return ret != -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ class UrlRequest {
|
|||||||
bool GetResponse(std::string* message);
|
bool GetResponse(std::string* message);
|
||||||
static int GetStatusCode(const std::string& response);
|
static int GetStatusCode(const std::string& response);
|
||||||
// Get the response, and expect the status is OK.
|
// 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(
|
static bool GetDebugHeaderFields(
|
||||||
const std::string& response,
|
const std::string& response,
|
||||||
@@ -37,9 +38,11 @@ class UrlRequest {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
bool PostRequestWithPath(const std::string& path, const std::string& data);
|
bool PostRequestWithPath(const std::string& path, const std::string& data);
|
||||||
|
bool SendRequestOnce();
|
||||||
|
|
||||||
bool is_connected_;
|
bool is_connected_;
|
||||||
HttpSocket socket_;
|
HttpSocket socket_;
|
||||||
|
std::string request_;
|
||||||
|
|
||||||
CORE_DISALLOW_COPY_AND_ASSIGN(UrlRequest);
|
CORE_DISALLOW_COPY_AND_ASSIGN(UrlRequest);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -346,6 +346,8 @@ CdmResponseType WvContentDecryptionModule::Unprovision(
|
|||||||
// Enable immediate OEMCrypto termination and re-initalization on
|
// Enable immediate OEMCrypto termination and re-initalization on
|
||||||
// unprovisioning.
|
// unprovisioning.
|
||||||
CryptoSession::DisableDelayedTermination();
|
CryptoSession::DisableDelayedTermination();
|
||||||
|
// Android unprovisioning has historically allowed for both
|
||||||
|
// DRM (app/origin-specific) and OEM (global) unprovisioning.
|
||||||
return cdm_engine->Unprovision(level);
|
return cdm_engine->Unprovision(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ inline ::ndk::ScopedAStatus toNdkScopedAStatus(
|
|||||||
return toNdkScopedAStatus(::wvdrm::WvStatus(status), msg);
|
return toNdkScopedAStatus(::wvdrm::WvStatus(status), msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool checkIfEnableMultiThreadBinder();
|
||||||
|
|
||||||
} // namespace wvdrm
|
} // namespace wvdrm
|
||||||
|
|
||||||
#endif // WV_UTILS_H_
|
#endif // WV_UTILS_H_
|
||||||
|
|||||||
@@ -270,6 +270,8 @@ static inline WvStatus mapCdmResponseType(wvcdm::CdmResponseType res) {
|
|||||||
case wvcdm::USAGE_INVALID_PARAMETERS_2:
|
case wvcdm::USAGE_INVALID_PARAMETERS_2:
|
||||||
case wvcdm::USAGE_STORE_ENTRY_RETRIEVE_INVALID_STORAGE_TYPE:
|
case wvcdm::USAGE_STORE_ENTRY_RETRIEVE_INVALID_STORAGE_TYPE:
|
||||||
case wvcdm::CLIENT_TOKEN_NOT_SET:
|
case wvcdm::CLIENT_TOKEN_NOT_SET:
|
||||||
|
// Stale responses should have been caught by the CDM engine.
|
||||||
|
case wvcdm::PROVISIONING_4_STALE_RESPONSE:
|
||||||
err = Status::GENERAL_PLUGIN_ERROR;
|
err = Status::GENERAL_PLUGIN_ERROR;
|
||||||
break;
|
break;
|
||||||
case wvcdm::CLIENT_ID_GENERATE_RANDOM_ERROR:
|
case wvcdm::CLIENT_ID_GENERATE_RANDOM_ERROR:
|
||||||
@@ -299,6 +301,9 @@ static inline WvStatus mapCdmResponseType(wvcdm::CdmResponseType res) {
|
|||||||
case wvcdm::INVALID_QUERY_KEY:
|
case wvcdm::INVALID_QUERY_KEY:
|
||||||
case wvcdm::KEY_NOT_FOUND_1:
|
case wvcdm::KEY_NOT_FOUND_1:
|
||||||
case wvcdm::SAMPLE_AND_SUBSAMPLE_SIZE_MISMATCH:
|
case wvcdm::SAMPLE_AND_SUBSAMPLE_SIZE_MISMATCH:
|
||||||
|
// Client provided a provisioning response without
|
||||||
|
// generating a provisioning request.
|
||||||
|
case wvcdm::PROVISIONING_UNEXPECTED_RESPONSE_ERROR:
|
||||||
err = Status::BAD_VALUE;
|
err = Status::BAD_VALUE;
|
||||||
break;
|
break;
|
||||||
case wvcdm::KEY_NOT_FOUND_3:
|
case wvcdm::KEY_NOT_FOUND_3:
|
||||||
@@ -400,6 +405,9 @@ static inline WvStatus mapCdmResponseType(wvcdm::CdmResponseType res) {
|
|||||||
case wvcdm::CERT_PROVISIONING_RESPONSE_ERROR_4:
|
case wvcdm::CERT_PROVISIONING_RESPONSE_ERROR_4:
|
||||||
case wvcdm::CERT_PROVISIONING_RESPONSE_ERROR_9:
|
case wvcdm::CERT_PROVISIONING_RESPONSE_ERROR_9:
|
||||||
case wvcdm::LOAD_PROVISIONING_ERROR:
|
case wvcdm::LOAD_PROVISIONING_ERROR:
|
||||||
|
// Failure to verify provisioning cert key is always
|
||||||
|
// due to a malformed response.
|
||||||
|
case wvcdm::PROVISIONING_4_FAILED_TO_VERIFY_CERT_KEY:
|
||||||
err = Status::PROVISIONING_PARSE_ERROR;
|
err = Status::PROVISIONING_PARSE_ERROR;
|
||||||
break;
|
break;
|
||||||
case wvcdm::CERT_PROVISIONING_RESPONSE_ERROR_10:
|
case wvcdm::CERT_PROVISIONING_RESPONSE_ERROR_10:
|
||||||
|
|||||||
@@ -2,17 +2,79 @@
|
|||||||
// source code may only be used and distributed under the Widevine
|
// source code may only be used and distributed under the Widevine
|
||||||
// License Agreement.
|
// License Agreement.
|
||||||
//
|
//
|
||||||
|
|
||||||
#include "oemcrypto_basic_test.h"
|
#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 "clock.h"
|
||||||
#include "jsmn.h"
|
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "oemcrypto_corpus_generator_helper.h"
|
#include "oemcrypto_corpus_generator_helper.h"
|
||||||
#include "oemcrypto_resource_test.h"
|
#include "oemcrypto_resource_test.h"
|
||||||
#include "test_sleep.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 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() {
|
void OEMCryptoClientTest::SetUp() {
|
||||||
::testing::Test::SetUp();
|
::testing::Test::SetUp();
|
||||||
wvutil::TestSleep::SyncFakeClock();
|
wvutil::TestSleep::SyncFakeClock();
|
||||||
@@ -316,29 +378,167 @@ 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) {
|
||||||
|
if (wvoec::global_features.api_version < 17) {
|
||||||
|
GTEST_SKIP() << "Test for versions 17 and up only.";
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
// Try again using the size they provided, ensuring that it
|
||||||
|
// is successful.
|
||||||
|
const size_t initial_estimate_length = build_info_length;
|
||||||
|
build_info.assign(build_info_length, kNullChar);
|
||||||
|
result = OEMCrypto_BuildInformation(&build_info[0], &build_info_length);
|
||||||
|
ASSERT_EQ(result, OEMCrypto_SUCCESS)
|
||||||
|
<< "initial_estimate_length = " << initial_estimate_length
|
||||||
|
<< ", build_info_length (output) = " << build_info_length;
|
||||||
|
ASSERT_GT(build_info_length, kZero) << "Build info cannot be empty";
|
||||||
|
// Ensure the real length is within the size originally specified.
|
||||||
|
// OK if final length is smaller than estimated length.
|
||||||
|
ASSERT_LE(build_info_length, initial_estimate_length);
|
||||||
|
const size_t expected_length = build_info_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 short_length = (expected_length >= 2) ? expected_length / 2 : 1;
|
||||||
|
build_info.assign(short_length, kNullChar);
|
||||||
|
build_info_length = build_info.size();
|
||||||
|
|
||||||
|
result = OEMCrypto_BuildInformation(&build_info[0], &build_info_length);
|
||||||
|
ASSERT_EQ(result, OEMCrypto_ERROR_SHORT_BUFFER)
|
||||||
|
<< "short_length = " << short_length
|
||||||
|
<< ", expected_length = " << expected_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, short_length);
|
||||||
|
|
||||||
|
// Final attempt with a buffer large enough buffer, padding to
|
||||||
|
// ensure the caller truncates.
|
||||||
|
constexpr size_t kBufferPadSize = 42;
|
||||||
|
const size_t oversize_length = expected_length + kBufferPadSize;
|
||||||
|
build_info.assign(oversize_length, kNullChar);
|
||||||
|
build_info_length = build_info.size();
|
||||||
|
|
||||||
|
result = OEMCrypto_BuildInformation(&build_info[0], &build_info_length);
|
||||||
|
|
||||||
|
ASSERT_EQ(result, OEMCrypto_SUCCESS)
|
||||||
|
<< "oversize_length = " << oversize_length
|
||||||
|
<< ", expected_length = " << expected_length
|
||||||
|
<< ", build_info_length (output) = " << 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, oversize_length)
|
||||||
|
<< "Should have truncated from oversized buffer: expected_length = "
|
||||||
|
<< expected_length;
|
||||||
|
// Ensure that length is equal to the length of the previous
|
||||||
|
// successful call.
|
||||||
|
ASSERT_EQ(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) {
|
||||||
|
if (wvoec::global_features.api_version < 17) {
|
||||||
|
GTEST_SKIP() << "Test for versions 17 and up only.";
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
TEST_F(OEMCryptoClientTest, CheckJsonBuildInformationAPI18) {
|
||||||
if (wvoec::global_features.api_version < 18) {
|
if (wvoec::global_features.api_version < 18) {
|
||||||
GTEST_SKIP() << "Test for versions 18 and up only.";
|
GTEST_SKIP() << "Test for versions 18 and up only.";
|
||||||
}
|
}
|
||||||
std::string build_info;
|
constexpr char kNullChar = '\0';
|
||||||
OEMCryptoResult sts = OEMCrypto_BuildInformation(&build_info[0], nullptr);
|
constexpr size_t kZero = 0;
|
||||||
ASSERT_EQ(OEMCrypto_ERROR_INVALID_CONTEXT, sts);
|
|
||||||
size_t buf_length = 0;
|
// Step 1: Get Build Info
|
||||||
|
size_t buffer_length = 0;
|
||||||
// OEMCrypto must allow |buffer| to be null so long as |buffer_length|
|
// OEMCrypto must allow |buffer| to be null so long as |buffer_length|
|
||||||
// is provided and initially set to zero.
|
// is provided and initially set to zero.
|
||||||
sts = OEMCrypto_BuildInformation(nullptr, &buf_length);
|
OEMCryptoResult result = OEMCrypto_BuildInformation(nullptr, &buffer_length);
|
||||||
ASSERT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, sts);
|
ASSERT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, result);
|
||||||
build_info.resize(buf_length);
|
ASSERT_GT(buffer_length, kZero);
|
||||||
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);
|
|
||||||
|
|
||||||
|
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_parser p;
|
||||||
jsmn_init(&p);
|
jsmn_init(&p);
|
||||||
std::vector<jsmntok_t> tokens;
|
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);
|
jsmn_parse(&p, build_info.c_str(), build_info.size(), nullptr, 0);
|
||||||
EXPECT_GT(num_tokens, 0)
|
EXPECT_GT(num_tokens, 0)
|
||||||
<< "Failed to parse BuildInformation as JSON, parse returned "
|
<< "Failed to parse BuildInformation as JSON, parse returned "
|
||||||
@@ -346,45 +546,186 @@ TEST_F(OEMCryptoClientTest, CheckJsonBuildInformationAPI18) {
|
|||||||
|
|
||||||
tokens.resize(num_tokens);
|
tokens.resize(num_tokens);
|
||||||
jsmn_init(&p);
|
jsmn_init(&p);
|
||||||
int32_t jsmn_result = jsmn_parse(&p, build_info.c_str(), build_info.size(),
|
const int32_t jsmn_result = jsmn_parse(
|
||||||
tokens.data(), num_tokens);
|
&p, build_info.c_str(), build_info.size(), tokens.data(), num_tokens);
|
||||||
EXPECT_GE(jsmn_result, 0)
|
EXPECT_GE(jsmn_result, 0)
|
||||||
<< "Failed to parse BuildInformation as JSON, parse returned "
|
<< "Failed to parse BuildInformation as JSON, parse returned "
|
||||||
<< jsmn_result << "for following build info: " << build_info;
|
<< jsmn_result << "for following build info: " << build_info;
|
||||||
|
|
||||||
std::map<std::string, jsmntype_t> expected;
|
// Step 3a: Ensure info is a single JSON object.
|
||||||
expected["soc_vendor"] = JSMN_STRING;
|
const jsmntok_t& object_token = tokens[0];
|
||||||
expected["soc_model"] = JSMN_STRING;
|
ASSERT_EQ(object_token.type, JSMN_OBJECT)
|
||||||
expected["ta_ver"] = JSMN_STRING;
|
<< "Build info is not a JSON object: " << build_info;
|
||||||
expected["uses_opk"] = JSMN_PRIMITIVE;
|
|
||||||
expected["tee_os"] = JSMN_STRING;
|
|
||||||
expected["tee_os_ver"] = JSMN_STRING;
|
|
||||||
|
|
||||||
// for values in token
|
// Step 3b: Verify schema of defined fields.
|
||||||
// build string from start,end
|
|
||||||
// check for existence in map
|
// Required fields must be present in the build information,
|
||||||
// check if value matches expectation
|
// and be of the correct type.
|
||||||
// remove from map
|
const std::map<std::string, jsmntype_t> kRequiredFields = {
|
||||||
for (int32_t i = 0; i < jsmn_result; i++) {
|
// SOC manufacturer name
|
||||||
jsmntok_t token = tokens[i];
|
{"soc_vendor", JSMN_STRING},
|
||||||
std::string key = build_info.substr(token.start, token.end - token.start);
|
// SOC model name
|
||||||
if (expected.find(key) != expected.end()) {
|
{"soc_model", JSMN_STRING},
|
||||||
EXPECT_EQ(expected.find(key)->second, tokens[i + 1].type)
|
// TA version in string format eg "1.12.3+tag", "2.0"
|
||||||
<< "Type is incorrect for key " << key;
|
{"ta_ver", JSMN_STRING},
|
||||||
expected.erase(key);
|
// [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
|
// Step 3c: Ensure all required fields were found.
|
||||||
if (expected.size() > 0) {
|
if (found_required_fields.size() != kRequiredFields.size()) {
|
||||||
std::string missing;
|
// Generate a list of all the missing fields.
|
||||||
for (const auto& e : expected) {
|
std::string missing_fields;
|
||||||
missing.append(e.first);
|
for (const auto& required_field : kRequiredFields) {
|
||||||
missing.append(" ");
|
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) {
|
TEST_F(OEMCryptoClientTest, CheckMaxNumberOfSessionsAPI10) {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include "WVCDMSingleton.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
#include <json/json.h>
|
#include <json/json.h>
|
||||||
|
|
||||||
@@ -163,4 +165,97 @@ CryptoSessionApi getCryptoSessionMethodEnum(const std::string& method) {
|
|||||||
err, writer.write(jsonMsg).c_str());
|
err, writer.write(jsonMsg).c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr const char* kJsonKeySocVendor = "soc_vendor";
|
||||||
|
constexpr const char* kJsonKeySocModel = "soc_model";
|
||||||
|
constexpr const char* kJsonKeyFormFactor = "form_factor";
|
||||||
|
|
||||||
|
struct DeviceInfo {
|
||||||
|
std::string soc_vendor;
|
||||||
|
std::string soc_model;
|
||||||
|
std::string form_factor;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::vector<DeviceInfo> const kMultiThreadBinderEnabledDevices = {
|
||||||
|
{"MediaTek", "mt*_multi_thread", "TV"},
|
||||||
|
};
|
||||||
|
|
||||||
|
bool matchesSocModelPattern(const std::string& socModel) {
|
||||||
|
const std::string prefix = "mt";
|
||||||
|
const std::string suffix = "_multi_thread";
|
||||||
|
const size_t minLength = prefix.length() + suffix.length();
|
||||||
|
|
||||||
|
if (socModel.length() < minLength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socModel.compare(0, prefix.length(), prefix) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socModel.compare(socModel.length() - suffix.length(), suffix.length(), suffix) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkIfEnableMultiThreadBinder() {
|
||||||
|
android::sp<wvcdm::WvContentDecryptionModule> cdm = wvdrm::getCDM();
|
||||||
|
if (cdm == nullptr) {
|
||||||
|
LOGW("Failed to get CDM when checking if multi-thread binder is enabled.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string buildInfoValue;
|
||||||
|
cdm->QueryStatus(wvcdm::RequestedSecurityLevel::kLevelDefault,
|
||||||
|
wvcdm::QUERY_KEY_OEMCRYPTO_BUILD_INFORMATION, &buildInfoValue);
|
||||||
|
|
||||||
|
Json::Reader reader;
|
||||||
|
Json::Value buildInfoJson;
|
||||||
|
if (!reader.parse(buildInfoValue, buildInfoJson)) {
|
||||||
|
LOGW("Failed to parse build info to JSON: %s", buildInfoValue.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buildInfoJson.isMember(kJsonKeySocVendor) ||
|
||||||
|
!buildInfoJson.isMember(kJsonKeySocModel) ||
|
||||||
|
!buildInfoJson.isMember(kJsonKeyFormFactor)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string currentSocVendor = buildInfoJson[kJsonKeySocVendor].asString();
|
||||||
|
std::string currentSocModel = buildInfoJson[kJsonKeySocModel].asString();
|
||||||
|
std::string currentFormFactor = buildInfoJson[kJsonKeyFormFactor].asString();
|
||||||
|
|
||||||
|
for (const auto& allowedDevice : kMultiThreadBinderEnabledDevices) {
|
||||||
|
if (allowedDevice.soc_vendor == currentSocVendor &&
|
||||||
|
allowedDevice.form_factor == currentFormFactor) {
|
||||||
|
|
||||||
|
if (allowedDevice.soc_vendor == "MediaTek" &&
|
||||||
|
allowedDevice.soc_model == "mt*_multi_thread" &&
|
||||||
|
allowedDevice.form_factor == "TV")
|
||||||
|
{
|
||||||
|
if (matchesSocModelPattern(currentSocModel)) {
|
||||||
|
LOGI("Multi-thread binder enabled for device via MediaTek TV pattern: "
|
||||||
|
"Model=%s matches pattern=%s",
|
||||||
|
currentSocModel.c_str(),
|
||||||
|
allowedDevice.soc_model.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For all other rules, perform an exact match on soc_model
|
||||||
|
if (allowedDevice.soc_model == currentSocModel) {
|
||||||
|
LOGI("Multi-thread binder enabled for device via exact match rule: "
|
||||||
|
"Vendor=%s, Model=%s, FormFactor=%s",
|
||||||
|
allowedDevice.soc_vendor.c_str(),
|
||||||
|
allowedDevice.soc_model.c_str(),
|
||||||
|
allowedDevice.form_factor.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace wvdrm
|
} // namespace wvdrm
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
#include "WVCreatePluginFactories.h"
|
#include "WVCreatePluginFactories.h"
|
||||||
#include "WVDrmFactory.h"
|
#include "WVDrmFactory.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
|
||||||
using ::wvdrm::hardware::drm::widevine::createDrmFactory;
|
using ::wvdrm::hardware::drm::widevine::createDrmFactory;
|
||||||
using ::wvdrm::hardware::drm::widevine::WVDrmFactory;
|
using ::wvdrm::hardware::drm::widevine::WVDrmFactory;
|
||||||
@@ -29,6 +30,9 @@ using ::wvdrm::hardware::drm::widevine::WVDrmFactory;
|
|||||||
int main(int /* argc */, char** /* argv */) {
|
int main(int /* argc */, char** /* argv */) {
|
||||||
ABinderProcess_setThreadPoolMaxThreadCount(8);
|
ABinderProcess_setThreadPoolMaxThreadCount(8);
|
||||||
|
|
||||||
|
if (wvdrm::checkIfEnableMultiThreadBinder()) {
|
||||||
|
ABinderProcess_startThreadPool();
|
||||||
|
}
|
||||||
std::shared_ptr<WVDrmFactory> drmFactory = createDrmFactory();
|
std::shared_ptr<WVDrmFactory> drmFactory = createDrmFactory();
|
||||||
|
|
||||||
const std::string drmInstance =
|
const std::string drmInstance =
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
#include "WVCreatePluginFactories.h"
|
#include "WVCreatePluginFactories.h"
|
||||||
#include "WVDrmFactory.h"
|
#include "WVDrmFactory.h"
|
||||||
|
#include "Utils.h"
|
||||||
|
|
||||||
using ::wvdrm::hardware::drm::widevine::createDrmFactory;
|
using ::wvdrm::hardware::drm::widevine::createDrmFactory;
|
||||||
using ::wvdrm::hardware::drm::widevine::WVDrmFactory;
|
using ::wvdrm::hardware::drm::widevine::WVDrmFactory;
|
||||||
@@ -29,6 +30,9 @@ using ::wvdrm::hardware::drm::widevine::WVDrmFactory;
|
|||||||
int main(int /* argc */, char** /* argv */) {
|
int main(int /* argc */, char** /* argv */) {
|
||||||
ABinderProcess_setThreadPoolMaxThreadCount(8);
|
ABinderProcess_setThreadPoolMaxThreadCount(8);
|
||||||
|
|
||||||
|
if (wvdrm::checkIfEnableMultiThreadBinder()) {
|
||||||
|
ABinderProcess_startThreadPool();
|
||||||
|
}
|
||||||
std::shared_ptr<WVDrmFactory> drmFactory = createDrmFactory();
|
std::shared_ptr<WVDrmFactory> drmFactory = createDrmFactory();
|
||||||
|
|
||||||
const std::string drmInstance =
|
const std::string drmInstance =
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
AV1A.250207.001
|
AV1A.250512.001
|
||||||
|
|||||||
Reference in New Issue
Block a user