diff --git a/libwvdrmengine/Android.bp b/libwvdrmengine/Android.bp index 4e21fc5f..1b7fb375 100644 --- a/libwvdrmengine/Android.bp +++ b/libwvdrmengine/Android.bp @@ -99,6 +99,7 @@ cc_defaults { relative_install_path: "hw", include_dirs: [ "vendor/widevine/libwvdrmengine/cdm/core/include/", + "vendor/widevine/libwvdrmengine/cdm/util/include/", "vendor/widevine/libwvdrmengine/include", "vendor/widevine/libwvdrmengine/mediadrm/include", "vendor/widevine/libwvdrmengine/oemcrypto/include", diff --git a/libwvdrmengine/apex/device/build.sh b/libwvdrmengine/apex/device/build.sh index b58d3f22..e6b45bb5 100755 --- a/libwvdrmengine/apex/device/build.sh +++ b/libwvdrmengine/apex/device/build.sh @@ -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 @@ -76,8 +76,9 @@ function build_bundle() { "${base_zip}" \ "${zips[@]}" + # Use prebuilt bundletool JAR # build bundle - ${BUNDLETOOL} build-bundle \ + "${JAVA_EXE}" -jar "${BUNDLETOOL_PREBUILT_JAR}" build-bundle \ --overwrite \ --modules "${base_zip}" \ --config "${TMPDIR}/${apex}/bundle_config.json" \ @@ -88,6 +89,8 @@ function build_bundle() { # inputs: ${DIST_DIR}/${arch}/${apex}-base.zip # outputs: ${DIST_DIR}/${apex}.aab function build_bundles() { + # Assume TARGET_BUILD_APPS is set in the environment + local app for app in ${TARGET_BUILD_APPS}; do build_bundle "${app}" done @@ -98,7 +101,7 @@ function build_bundles() { # outputs: ${DIST_DIR}/dev_keys_signed/${apex}/${apex}.apks function sign_bundles() { ${DEV_SIGN_BUNDLE} \ - --java_binary_path "${ANDROID_JAVA_HOME}/bin/java" \ + --java_binary_path "${JAVA_EXE}" \ --input_dir "${DIST_DIR}" \ --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 # the command substitution if it's directly on the eval line. 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}" - 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 DEV_SIGN_BUNDLE=${OUT_DIR}/host/linux-x86/bin/dev_sign_bundle } @@ -120,10 +129,11 @@ function configure_build() { function main() { [[ -e "build/make/core/Makefile" ]] || die "$0 must be run from the top of the Android source tree." configure_build - build_modules bundletool merge_zips dev_sign_bundle - build_bundles # use bundletool & merge_zips - sign_bundles # use dev_sign_bundle + + build_modules merge_zips dev_sign_bundle + build_bundles # use prebuilt bundletool & merge_zips + sign_bundles # use built dev_sign_bundle } -main +main \ No newline at end of file diff --git a/libwvdrmengine/cdm/core/include/cdm_engine.h b/libwvdrmengine/cdm/core/include/cdm_engine.h index 90a74b8c..a29c46b9 100644 --- a/libwvdrmengine/cdm/core/include/cdm_engine.h +++ b/libwvdrmengine/cdm/core/include/cdm_engine.h @@ -212,6 +212,15 @@ class CdmEngine { // system. This will force the device to reprovision itself. 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) // file system. virtual CdmResponseType ListStoredLicenses( diff --git a/libwvdrmengine/cdm/core/include/certificate_provisioning.h b/libwvdrmengine/cdm/core/include/certificate_provisioning.h index 52d35fd6..be2d7d0d 100644 --- a/libwvdrmengine/cdm/core/include/certificate_provisioning.h +++ b/libwvdrmengine/cdm/core/include/certificate_provisioning.h @@ -72,6 +72,28 @@ class CertificateProvisioning { // |default_url| by GetProvisioningRequest(). 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: #if defined(UNIT_TEST) friend class CertificateProvisioningTest; @@ -122,22 +144,31 @@ class CertificateProvisioning { CdmResponseType CloseSessionOnError(CdmResponseType status); void CloseSession(); + // Tracks the state of CertificateProvisioning. + State state_ = kUninitialized; + std::unique_ptr crypto_session_; CdmCertificateType cert_type_; std::unique_ptr service_certificate_; std::string request_; + + // == Provisioning 4.0 Variables == // The wrapped private key in provisioning 4 generated by calling // GenerateCertificateKeyPair. It will be saved to file system if a valid // response is received. - std::string provisioning_40_wrapped_private_key_; - // Key type of the generated key pair in provisioning 4. - CryptoWrappedKey::Type provisioning_40_key_type_; - // Store the last provisioning request message - std::string provisioning_request_message_; + CryptoWrappedKey prov40_wrapped_private_key_; + // Cache of the most recently sent OEM/DRM public key sent. Used + // to match the response with the request. + // This MUST be matched with the current |prov40_wrapped_private_key_|. + 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); -}; - +}; // class CertificateProvisioning } // namespace wvcdm - #endif // WVCDM_CORE_CERTIFICATE_PROVISIONING_H_ diff --git a/libwvdrmengine/cdm/core/include/crypto_wrapped_key.h b/libwvdrmengine/cdm/core/include/crypto_wrapped_key.h index bf947bb9..3dfe045f 100644 --- a/libwvdrmengine/cdm/core/include/crypto_wrapped_key.h +++ b/libwvdrmengine/cdm/core/include/crypto_wrapped_key.h @@ -5,6 +5,7 @@ #define WVCDM_CORE_CRYPTO_WRAPPED_KEY_H_ #include +#include namespace wvcdm { @@ -18,6 +19,8 @@ class CryptoWrappedKey { CryptoWrappedKey() {} CryptoWrappedKey(Type type, const std::string& key) : type_(type), key_(key) {} + CryptoWrappedKey(Type type, std::string&& key) + : type_(type), key_(std::move(key)) {} Type type() const { return type_; } void set_type(Type type) { type_ = type; } @@ -26,6 +29,7 @@ class CryptoWrappedKey { // Mutable reference getter for passing to OMECrypto. std::string& key() { return key_; } void set_key(const std::string& key) { key_ = key; } + void set_key(std::string&& key) { key_ = std::move(key); } void Clear() { type_ = kUninitialized; diff --git a/libwvdrmengine/cdm/core/include/wv_cdm_types.h b/libwvdrmengine/cdm/core/include/wv_cdm_types.h index e4847271..605ec668 100644 --- a/libwvdrmengine/cdm/core/include/wv_cdm_types.h +++ b/libwvdrmengine/cdm/core/include/wv_cdm_types.h @@ -465,6 +465,9 @@ enum CdmResponseEnum : int32_t { GET_DEVICE_INFORMATION_ERROR = 398, GET_DEVICE_SIGNED_CSR_PAYLOAD_ERROR = 399, 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 // * core/src/wv_cdm_types.cpp // * android/include/mapErrors-inl.h diff --git a/libwvdrmengine/cdm/core/src/cdm_engine.cpp b/libwvdrmengine/cdm/core/src/cdm_engine.cpp index 3d8ed126..343377f9 100644 --- a/libwvdrmengine/cdm/core/src/cdm_engine.cpp +++ b/libwvdrmengine/cdm/core/src/cdm_engine.cpp @@ -1286,6 +1286,13 @@ CdmResponseType CdmEngine::HandleProvisioningResponse( LOGE("Device has been revoked, cannot provision: status = %s", ret.ToString().c_str()); 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 { // It is possible that a provisioning attempt was made after this one was // requested but before the response was received, which will cause this @@ -1332,8 +1339,7 @@ CdmProvisioningStatus CdmEngine::GetProvisioningStatus( return kUnknownProvisionStatus; } - UsagePropertySet property_set; - if (handle.HasCertificate(property_set.use_atsc_mode())) { + if (handle.HasCertificate(/* atsc_mode_enabled = */ false)) { return kProvisioned; } if (crypto_session->GetPreProvisionTokenType() == kClientTokenBootCertChain) { @@ -1356,8 +1362,8 @@ CdmResponseType CdmEngine::Unprovision(CdmSecurityLevel security_level) { LOGD("OKP fallback to L3"); security_level = kSecurityLevelL3; } - // Devices with baked-in DRM certs cannot be reprovisioned and therefore must - // not be unprovisioned. + // Devices with baked-in DRM certs cannot be reprovisioned + // and therefore must not be unprovisioned. std::unique_ptr crypto_session( CryptoSession::MakeCryptoSession(metrics_->GetCryptoMetrics())); CdmClientTokenType token_type = kClientTokenUninitialized; @@ -1376,18 +1382,78 @@ CdmResponseType CdmEngine::Unprovision(CdmSecurityLevel security_level) { LOGE("Unable to initialize device files"); return CdmResponseType(UNPROVISION_ERROR_1); } - - // TODO(b/141705730): Remove usage entries during unprovisioning. - if (!file_system_->IsGlobal()) { - if (!handle.RemoveCertificate() || !handle.RemoveOemCertificate()) { - LOGE("Unable to delete certificate"); + // This if statement is misleading. There is no consistent + // concept of "global" vs "per-app/origin" storage in the + // core library. Android vs CE CDM behave very different. + // On CE device: + // 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); } + // 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 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); } - if (!handle.DeleteAllFiles()) { - LOGE("Unable to delete files"); - return CdmResponseType(UNPROVISION_ERROR_3); + // For Prov 4.0 devices, this will cause every app/origin client + // to lose their offline content for the same TEE security level. + 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); } diff --git a/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp b/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp index b40c3825..bf853b66 100644 --- a/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp +++ b/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp @@ -1,9 +1,10 @@ // Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary // source code may only be used and distributed under the Widevine License // Agreement. - #include "certificate_provisioning.h" +#include + #include "client_identification.h" #include "crypto_wrapped_key.h" #include "device_files.h" @@ -87,6 +88,127 @@ bool RetrieveOemCertificateAndLoadPrivateKey(CryptoSession& crypto_session, 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(message.front()); + if (type_header != kSequenceHeader) return false; + + // Verify length. + const uint8_t length_header = static_cast(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(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(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(message[kPayloadLengthOffset + i]); + payload_length = (payload_length << 8) + static_cast(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 // Protobuf generated classes. using video_widevine::DrmCertificate; @@ -98,6 +220,25 @@ using video_widevine::PublicKeyToCertify; using video_widevine::SignedDrmCertificate; 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 ""; +} + // static void CertificateProvisioning::GetProvisioningServerUrl( std::string* default_url) { @@ -114,7 +255,11 @@ CdmResponseType CertificateProvisioning::Init( service_certificate.empty() ? wvutil::a2bs_hex(kCpProductionServiceCertificate) : 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. @@ -206,11 +351,18 @@ CdmResponseType CertificateProvisioning::GetProvisioningRequestInternal( 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(); CdmResponseType status = crypto_session_->Open(requested_security_level); if (NO_ERROR != status) { - LOGE("Failed to create a crypto session: status = %d", - static_cast(status)); + LOGE("Failed to create a crypto session: status = %s", + status.ToString().c_str()); return status; } @@ -299,6 +451,7 @@ CdmResponseType CertificateProvisioning::GetProvisioningRequestInternal( } else { *request = std::move(serialized_request); } + state_ = kDrmRequestSent; return CdmResponseType(NO_ERROR); } @@ -324,7 +477,11 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal( 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. std::string stored_oem_cert; 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 // is encrypted. + ProvisioningRequest provisioning_request; CdmAppParameterMap additional_parameter; CdmResponseType status = SetSpoidParameter(origin, spoid, &provisioning_request); @@ -363,7 +522,7 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal( provisioning_request.clear_stable_id(); } - if (stored_oem_cert.empty()) { + if (is_oem_prov_request) { // This is the first stage provisioning. default_url->assign(std::string(kProvisioningServerUrl) + kProv40FirstStageServerUrlSuffix); @@ -377,8 +536,8 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal( // Since |stored_oem_cert| is empty, the client identification token will be // retrieved from OEMCrypto, which is the BCC in this case. - status = FillEncryptedClientId(stored_oem_cert, provisioning_request, - wv_service_cert); + status = FillEncryptedClientId(/* client_token = */ std::string(), + provisioning_request, wv_service_cert); if (status != NO_ERROR) return status; } else { // This is the second stage provisioning. @@ -416,25 +575,24 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal( std::string public_key; std::string public_key_signature; - provisioning_40_wrapped_private_key_.clear(); - provisioning_40_key_type_ = CryptoWrappedKey::kUninitialized; + std::string wrapped_private_key; + CryptoWrappedKey::Type private_key_type = CryptoWrappedKey::kUninitialized; status = crypto_session_->GenerateCertificateKeyPair( - &public_key, &public_key_signature, &provisioning_40_wrapped_private_key_, - &provisioning_40_key_type_); + &public_key, &public_key_signature, &wrapped_private_key, + &private_key_type); if (status != NO_ERROR) return status; PublicKeyToCertify* key_to_certify = provisioning_request.mutable_certificate_public_key(); key_to_certify->set_public_key(public_key); key_to_certify->set_signature(public_key_signature); - key_to_certify->set_key_type(provisioning_40_key_type_ == - CryptoWrappedKey::kRsa + key_to_certify->set_key_type(private_key_type == CryptoWrappedKey::kRsa ? PublicKeyToCertify::RSA : PublicKeyToCertify::ECC); std::string serialized_message; provisioning_request.SerializeToString(&serialized_message); - provisioning_request_message_ = serialized_message; + prov40_request_ = serialized_message; SignedProvisioningMessage signed_provisioning_msg; signed_provisioning_msg.set_message(serialized_message); @@ -490,6 +648,15 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal( *request = std::move(serialized_request); } 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); } @@ -552,6 +719,18 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response( 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 = provisioning_response.device_certificate(); @@ -560,17 +739,16 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response( return CdmResponseType(PROVISIONING_4_RESPONSE_HAS_NO_CERTIFICATE); } - if (provisioning_40_wrapped_private_key_.empty()) { - LOGE("No private key was generated"); + if (!prov40_wrapped_private_key_.IsValid() || prov40_public_key_.empty()) { + LOGE("No %s key was generated", + !prov40_wrapped_private_key_.IsValid() ? "private" : "public"); return CdmResponseType(PROVISIONING_4_NO_PRIVATE_KEY); } - const CryptoWrappedKey private_key(provisioning_40_key_type_, - provisioning_40_wrapped_private_key_); - if (cert_type_ == kCertificateX509) { // 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) { LOGE("Failed to load x509 certificate."); return status; @@ -581,9 +759,8 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response( const std::string& signature = signed_response.signature(); const std::string& core_message = signed_response.oemcrypto_core_message(); status = crypto_session_->LoadProvisioningCast( - signed_response.session_key(), provisioning_request_message_, - response_message, core_message, signature, - &cast_cert_private_key.key()); + signed_response.session_key(), prov40_request_, response_message, + core_message, signature, &cast_cert_private_key.key()); if (status != NO_ERROR) { LOGE("Failed to generate wrapped key for cast cert."); return status; @@ -593,11 +770,79 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response( *cert = device_certificate; *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); } + // 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(); - CloseSession(); wvutil::FileSystem global_file_system; DeviceFiles global_file_handle(&global_file_system); 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 // 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. if (!global_file_handle.StoreOemCertificate(device_certificate, - private_key)) { + prov40_wrapped_private_key_)) { LOGE("Failed to store provisioning 4 OEM certificate"); return CdmResponseType(PROVISIONING_4_FAILED_TO_STORE_OEM_CERTIFICATE); } - } else { - // The response is assumed to be an 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, - private_key)) { - LOGE("Failed to store provisioning 4 DRM certificate"); - return CdmResponseType(PROVISIONING_4_FAILED_TO_STORE_DRM_CERTIFICATE); - } + CloseSession(); + state_ = kOemResponseReceived; + prov40_wrapped_private_key_.Clear(); + prov40_public_key_.clear(); + return CdmResponseType(NO_ERROR); } - + // 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); } @@ -678,6 +940,15 @@ CdmResponseType CertificateProvisioning::HandleProvisioningResponse( 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; if (!signed_response.has_signature()) { LOGE("Signed response does not have signature"); @@ -753,6 +1024,7 @@ CdmResponseType CertificateProvisioning::HandleProvisioningResponse( if (cert_type_ == kCertificateX509) { *cert = device_cert_data; *wrapped_key = private_key.key(); + state_ = kDrmResponseReceived; return CdmResponseType(NO_ERROR); } @@ -799,6 +1071,7 @@ CdmResponseType CertificateProvisioning::HandleProvisioningResponse( return CdmResponseType(CERT_PROVISIONING_RESPONSE_ERROR_8); } + state_ = kDrmResponseReceived; return CdmResponseType(NO_ERROR); } diff --git a/libwvdrmengine/cdm/core/src/device_files.cpp b/libwvdrmengine/cdm/core/src/device_files.cpp index c3ca252a..efe61c8e 100644 --- a/libwvdrmengine/cdm/core/src/device_files.cpp +++ b/libwvdrmengine/cdm/core/src/device_files.cpp @@ -709,17 +709,26 @@ bool DeviceFiles::RemoveCertificate() { RETURN_FALSE_IF_UNINITIALIZED() std::string certificate_file_name; - if (GetCertificateFileName(kCertificateLegacy, &certificate_file_name)) - RemoveFile(certificate_file_name); - if (GetCertificateFileName(kCertificateDefault, &certificate_file_name)) - return RemoveFile(certificate_file_name); - return true; + // Return true so long as at least one certificate was removed. + // This is to compliment the behavior of HasCertificate() which + // returns true if at least one certificate exists. + bool result = false; + 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() { RETURN_FALSE_IF_UNINITIALIZED() std::string certificate_file_name; if (GetOemCertificateFileName(&certificate_file_name)) { + LOGI("Removing OEM certificate"); return RemoveFile(certificate_file_name); } return true; diff --git a/libwvdrmengine/cdm/core/src/wv_cdm_types.cpp b/libwvdrmengine/cdm/core/src/wv_cdm_types.cpp index 28b3969d..73968616 100644 --- a/libwvdrmengine/cdm/core/src/wv_cdm_types.cpp +++ b/libwvdrmengine/cdm/core/src/wv_cdm_types.cpp @@ -879,6 +879,12 @@ const char* CdmResponseEnumToString(CdmResponseEnum cdm_response_enum) { return "SESSION_NOT_FOUND_GENERIC_CRYPTO"; case 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); } diff --git a/libwvdrmengine/cdm/core/test/certificate_provisioning_unittest.cpp b/libwvdrmengine/cdm/core/test/certificate_provisioning_unittest.cpp index fcc62117..73ace83a 100644 --- a/libwvdrmengine/cdm/core/test/certificate_provisioning_unittest.cpp +++ b/libwvdrmengine/cdm/core/test/certificate_provisioning_unittest.cpp @@ -413,6 +413,9 @@ TEST_P(CertificateProvisioningTest, ProvisioningRequestFailsEmptySignature) { TEST_P(CertificateProvisioningTest, ProvisioningResponseFailsWithEmptyResponse) { certificate_provisioning_->Init(""); + // Must set state if not generating request. + certificate_provisioning_->SetStateForTesting( + CertificateProvisioning::kDrmRequestSent); MockFileSystem file_system; std::string certificate; @@ -425,6 +428,9 @@ TEST_P(CertificateProvisioningTest, TEST_P(CertificateProvisioningTest, ProvisioningResponseFailsIfDeviceIsRevoked) { certificate_provisioning_->Init(""); + // Must set state if not generating request. + certificate_provisioning_->SetStateForTesting( + CertificateProvisioning::kDrmRequestSent); MockFileSystem file_system; std::string response_certificate; @@ -445,6 +451,10 @@ TEST_P(CertificateProvisioningTest, TEST_P(CertificateProvisioningTest, ProvisioningResponseSuccess) { certificate_provisioning_->Init(""); + // Must set state if not generating request. + certificate_provisioning_->SetStateForTesting( + CertificateProvisioning::kDrmRequestSent); + std::string expected_certificate; std::string response; ASSERT_TRUE(MakeSignedDrmCertificate(kFakePublicKey, kSerialNumber, kSystemId, diff --git a/libwvdrmengine/cdm/core/test/core_integration_test.cpp b/libwvdrmengine/cdm/core/test/core_integration_test.cpp index 03bd37f2..f1a3b1b1 100644 --- a/libwvdrmengine/cdm/core/test/core_integration_test.cpp +++ b/libwvdrmengine/cdm/core/test/core_integration_test.cpp @@ -279,4 +279,539 @@ TEST_F(CoreIntegrationTest, NeedKeyBeforeLicenseLoad) { EXPECT_EQ(NEED_KEY, holder.Decrypt(key_id)); 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 diff --git a/libwvdrmengine/cdm/core/test/license_holder.cpp b/libwvdrmengine/cdm/core/test/license_holder.cpp index de131c62..d3748c88 100644 --- a/libwvdrmengine/cdm/core/test/license_holder.cpp +++ b/libwvdrmengine/cdm/core/test/license_holder.cpp @@ -103,7 +103,7 @@ void LicenseHolder::GenerateAndPostRenewalRequest( void LicenseHolder::FetchRenewal() { ASSERT_NE(request_in_flight_, nullptr) << "Failed for " << content_id(); ASSERT_NO_FATAL_FAILURE( - request_in_flight_->AssertOkResponse(&request_response_)) + request_in_flight_->AssertOkResponseWithRetry(&request_response_)) << "Renewal failed for " << content_id(); } @@ -162,7 +162,7 @@ void LicenseHolder::GenerateAndPostReleaseRequest( void LicenseHolder::FetchRelease() { ASSERT_NE(request_in_flight_, nullptr) << "Failed for " << content_id(); ASSERT_NO_FATAL_FAILURE( - request_in_flight_->AssertOkResponse(&request_response_)) + request_in_flight_->AssertOkResponseWithRetry(&request_response_)) << "Renewal failed for " << content_id(); } @@ -310,7 +310,7 @@ void LicenseHolder::GetKeyResponse(const CdmKeyRequest& key_request) { std::string http_response; url_request.PostRequest(key_request.message); - ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponse(&http_response)) + ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponseWithRetry(&http_response)) << "Failed for " << content_id(); LicenseRequest license_request; license_request.GetDrmMessage(http_response, key_response_); diff --git a/libwvdrmengine/cdm/core/test/provisioning_holder.cpp b/libwvdrmengine/cdm/core/test/provisioning_holder.cpp index 697beb97..80a7ae54 100644 --- a/libwvdrmengine/cdm/core/test/provisioning_holder.cpp +++ b/libwvdrmengine/cdm/core/test/provisioning_holder.cpp @@ -7,7 +7,6 @@ #include #include "cdm_engine.h" -#include "config_test_env.h" #include "log.h" #include "message_dumper.h" @@ -20,93 +19,159 @@ namespace wvcdm { void ProvisioningHolder::Provision(CdmCertificateType cert_type, bool binary_provisioning) { - CdmProvisioningRequest request; - std::string provisioning_server_url; + ASSERT_NO_FATAL_FAILURE(GenerateRequest(cert_type, binary_provisioning)) + << "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; - - CdmSessionId session_id; - if (cert_type == kCertificateX509) { cert_authority = "cast.google.com"; } LOGV("Provision with type %s.", CdmCertificateTypeToString(cert_type)); - CdmResponseType result(CERT_PROVISIONING_NONCE_GENERATION_ERROR); + 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 // 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++) { - result = cdm_engine_->GetProvisioningRequest( + status = cdm_engine_->GetProvisioningRequest( cert_type, cert_authority, provisioning_service_certificate_, - kLevelDefault, &request, &provisioning_server_url); - if (result == CERT_PROVISIONING_NONCE_GENERATION_ERROR) { + kLevelDefault, &cdm_prov_request, &provisioning_server_url_unused); + if (status == CERT_PROVISIONING_NONCE_GENERATION_ERROR) { wvutil::TestSleep::Sleep(2); } } - ASSERT_EQ(NO_ERROR, result); - LOGV("cert_authority = %s", cert_authority.c_str()); + ASSERT_EQ(status, NO_ERROR) << "Failed to generate provisioning request"; + // |binary_provisioning| implies the CdmEngine is using binary + // provisioning messages; however, ProvisioningHolder can only + // operate using Base64 requests. 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()) { - std::vector binary_request = wvutil::Base64SafeDecode(request); - CdmProvisioningRequest binary_request_string(binary_request.begin(), - binary_request.end()); + const std::vector binary_request = + wvutil::Base64SafeDecode(request_); + const CdmProvisioningRequest binary_request_string(binary_request.begin(), + binary_request.end()); MessageDumper::DumpProvisioningRequest(binary_request_string); } - LOGV("Provisioning request: req = %s", request.c_str()); +} - // Ignore URL provided by CdmEngine. Use ours, as configured - // for test vs. production server. - provisioning_server_url.assign(provisioning_server_url_); +void ProvisioningHolder::FetchResponse() { + // Preconditions. + 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); - if (!url_request.is_connected()) { - LOGE("Failed to connect to provisioning server: url = %s", - provisioning_server_url.c_str()); - } - url_request.PostCertRequestInQueryString(request); - - // Receive and parse response. - ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponse(&response_)) + UrlRequest url_request(provisioning_server_url_); + ASSERT_TRUE(url_request.is_connected()) + << "Failed to connect to provisoining server: " + << provisioning_server_url_; + ASSERT_TRUE(url_request.PostCertRequestInQueryString(request_)); + std::string response; + ASSERT_NO_FATAL_FAILURE(url_request.AssertOkResponseWithRetry(&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) { - // extract provisioning response from received message - // Extracts signed response from JSON string, result is serialized - // protobuf. - std::string protobuf_response; - const bool extract_ok = ExtractSignedMessage(response_, &protobuf_response); - ASSERT_TRUE(extract_ok) << "Failed to extract signed serialized " - "response from JSON response"; - LOGV("Extracted response message: \n%s\n", protobuf_response.c_str()); + // CDM is expecting the response to be in binary form, response + // must be extracted and decoded. + std::string base_64_response; + ASSERT_TRUE(ExtractSignedMessage(response_, &base_64_response)) + << "Failed to extract signed serialized response from JSON response"; + ASSERT_FALSE(base_64_response.empty()) + << "Base64 encoded provisioning response is unexpectedly empty"; + LOGV("Extracted response message: \n%s\n", base_64_response.c_str()); - ASSERT_FALSE(protobuf_response.empty()) - << "Protobuf response is unexpectedly empty"; - - // base64 decode response to yield binary protobuf - const std::vector response_vec( - wvutil::Base64SafeDecode(protobuf_response)); + const std::vector response_vec = + wvutil::Base64SafeDecode(base_64_response); ASSERT_FALSE(response_vec.empty()) - << "Failed to decode base64 of response: response = " - << protobuf_response; - - response_.assign(response_vec.begin(), response_vec.end()); + << "Failed to decode base64 response: " << base_64_response; + cdm_prov_response.assign(response_vec.begin(), response_vec.end()); + } else { + cdm_prov_response = response_; } - ASSERT_EQ(NO_ERROR, - cdm_engine_->HandleProvisioningResponse( - response_, kLevelDefault, &certificate_, &wrapped_key_)) + const CdmResponseType status = cdm_engine_->HandleProvisioningResponse( + cdm_prov_response, kLevelDefault, &certificate_, &wrapped_key_); + ASSERT_EQ(status, NO_ERROR) << (binary_provisioning ? "Binary provisioning failed. " : "Non-binary provisioning failed. ") - << DumpProvAttempt(request, response_, cert_type); + << DumpProvAttempt(request_, response_, cert_type_); if (config_.dump_golden_data()) { 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 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, std::string* result) { static const std::string kMessageStart = "\"signedResponse\": \""; @@ -137,9 +202,9 @@ bool ProvisioningHolder::ExtractSignedMessage(const std::string& response, return true; } -std::string ProvisioningHolder::DumpProvAttempt(const std::string& request, - const std::string& response, - CdmCertificateType cert_type) { +std::string ProvisioningHolder::DumpProvAttempt( + const std::string& request, const std::string& response, + CdmCertificateType cert_type) const { std::stringstream info; info << "Cert Type: "; PrintTo(cert_type, &info); diff --git a/libwvdrmengine/cdm/core/test/provisioning_holder.h b/libwvdrmengine/cdm/core/test/provisioning_holder.h index 1b68c9cc..ccc10e59 100644 --- a/libwvdrmengine/cdm/core/test/provisioning_holder.h +++ b/libwvdrmengine/cdm/core/test/provisioning_holder.h @@ -19,20 +19,62 @@ class ProvisioningHolder { provisioning_server_url_(config.provisioning_server()), provisioning_service_certificate_( config.provisioning_service_certificate()) {} + + // Generates requests, fetches response, and loads response. void Provision(CdmCertificateType cert_type, bool binary_provisioning); void Provision(bool binary_provisioning) { Provision(kCertificateWidevine, binary_provisioning); } - std::string response() const { return response_; } - std::string certificate() const { return certificate_; } - std::string wrapped_key() const { return wrapped_key_; } + + // Generates a provisioning request from the |cdm_engine_|. + // 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: TestCdmEngine* cdm_engine_; + // Config variables. const ConfigTestEnv& config_; std::string provisioning_server_url_; 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 wrapped_key_; @@ -42,11 +84,12 @@ class ProvisioningHolder { // entire string represents a serialized protobuf mesaage and return true with // the entire string. If the end_substring match fails, return false with an // empty *result. - bool ExtractSignedMessage(const std::string& response, std::string* result); + static bool ExtractSignedMessage(const std::string& response, + std::string* result); // Dump request and response information for use in a debug or failure log. std::string DumpProvAttempt(const std::string& request, const std::string& response, - CdmCertificateType cert_type); + CdmCertificateType cert_type) const; }; } // namespace wvcdm diff --git a/libwvdrmengine/cdm/core/test/url_request.cpp b/libwvdrmengine/cdm/core/test/url_request.cpp index b7f4a7c1..0c37e3b7 100644 --- a/libwvdrmengine/cdm/core/test/url_request.cpp +++ b/libwvdrmengine/cdm/core/test/url_request.cpp @@ -5,9 +5,12 @@ #include "url_request.h" #include +#include + +#include +#include #include -#include #include "http_socket.h" #include "log.h" @@ -23,11 +26,15 @@ const int kConnectTimeoutMs = 15000; const int kWriteTimeoutMs = 12000; const int kReadTimeoutMs = 12000; constexpr int kHttpOk = 200; +const std::vector kRetryCodes = {502, 504}; const std::string kGoogleHeaderUpper("X-Google"); const std::string kGoogleHeaderLower("x-google"); const std::string kCrLf("\r\n"); +constexpr unsigned kRetryCount = 3; +constexpr unsigned kRetryIntervalSeconds = 1; + // Concatenate all chunks into one blob and returns the response with // header information. void ConcatenateChunkedResponse(const std::string http_response, @@ -126,13 +133,34 @@ bool UrlRequest::GetResponse(std::string* message) { return true; } -void UrlRequest::AssertOkResponse(std::string* message) { +void UrlRequest::AssertOkResponseWithRetry(std::string* message) { ASSERT_TRUE(message); - ASSERT_TRUE(GetResponse(message)); - const int status_code = GetStatusCode(*message); - ASSERT_EQ(kHttpOk, status_code) << "HTTP response from " << socket_.url() - << ": (" << message->size() << ") :\n" - << *message; + int status_code = 0; + for (unsigned i = 0; i < kRetryCount; i++) { + *message = ""; + ASSERT_TRUE(GetResponse(message)) << "For attempt " << (i + 1); + status_code = GetStatusCode(*message); + // If we didn't get a retry status, then we're done. + if (std::find(kRetryCodes.begin(), kRetryCodes.end(), status_code) == + kRetryCodes.end()) { + ASSERT_EQ(kHttpOk, status_code) << "HTTP response from " << socket_.url() + << ": (" << message->size() << ") :\n" + << *message; + return; + } + std::cerr << "Temporary failure HTTP response from " << socket_.url() + << ": (" << message->size() << ") :\n" + << *message << "\n" + << "Attempt " << (i + 1) << "\n"; + socket_.CloseSocket(); + is_connected_ = false; + sleep(kRetryIntervalSeconds << i); + Reconnect(); + SendRequestOnce(); + } + GTEST_FAIL() << "HTTP response from " << socket_.url() << ": (" + << message->size() << ") :\n" + << *message; } // static @@ -189,36 +217,35 @@ bool UrlRequest::GetDebugHeaderFields( bool UrlRequest::PostRequestWithPath(const std::string& path, const std::string& data) { - std::string request; + request_.clear(); - request.append("POST "); - request.append(path); - request.append(" HTTP/1.1\r\n"); + request_.append("POST "); + request_.append(path); + request_.append(" HTTP/1.1\r\n"); - request.append("Host: "); - request.append(socket_.domain_name()); - request.append("\r\n"); + request_.append("Host: "); + request_.append(socket_.domain_name()); + request_.append("\r\n"); - request.append("Connection: close\r\n"); - request.append("User-Agent: Widevine CDM v1.0\r\n"); - request.append("X-Return-Encrypted-Headers: request_and_response\r\n"); + request_.append("Connection: close\r\n"); + request_.append("User-Agent: Widevine CDM v1.0\r\n"); + request_.append("X-Return-Encrypted-Headers: request_and_response\r\n"); - // buffer to store length of data as a string - char data_size_buffer[32] = {0}; - snprintf(data_size_buffer, sizeof(data_size_buffer), "%zu", data.size()); + request_.append("Content-Length: "); + request_.append(std::to_string(data.size())); + request_.append("\r\n"); - request.append("Content-Length: "); - request.append(data_size_buffer); // appends size of data - request.append("\r\n"); + request_.append("\r\n"); // empty line to terminate headers - request.append("\r\n"); // empty line to terminate headers - - request.append(data); + request_.append(data); + return SendRequestOnce(); +} +bool UrlRequest::SendRequestOnce() { const int ret = socket_.WriteAndLogErrors( - request.c_str(), static_cast(request.size()), kWriteTimeoutMs); - LOGV("HTTP request: (%zu): %s", request.size(), request.c_str()); - LOGV("HTTP request hex: %s", wvutil::b2a_hex(request).c_str()); + request_.c_str(), static_cast(request_.size()), kWriteTimeoutMs); + LOGV("HTTP request: (%zu): %s", request_.size(), request_.c_str()); + LOGV("HTTP request hex: %s", wvutil::b2a_hex(request_).c_str()); return ret != -1; } diff --git a/libwvdrmengine/cdm/core/test/url_request.h b/libwvdrmengine/cdm/core/test/url_request.h index c2d0d35e..68dc593e 100644 --- a/libwvdrmengine/cdm/core/test/url_request.h +++ b/libwvdrmengine/cdm/core/test/url_request.h @@ -29,7 +29,8 @@ class UrlRequest { bool GetResponse(std::string* message); static int GetStatusCode(const std::string& response); // Get the response, and expect the status is OK. - void AssertOkResponse(std::string* message); + // It will retry if the response code is in the 500 range. + void AssertOkResponseWithRetry(std::string* message); static bool GetDebugHeaderFields( const std::string& response, @@ -37,9 +38,11 @@ class UrlRequest { private: bool PostRequestWithPath(const std::string& path, const std::string& data); + bool SendRequestOnce(); bool is_connected_; HttpSocket socket_; + std::string request_; CORE_DISALLOW_COPY_AND_ASSIGN(UrlRequest); }; diff --git a/libwvdrmengine/cdm/src/wv_content_decryption_module.cpp b/libwvdrmengine/cdm/src/wv_content_decryption_module.cpp index b21a075c..11e62770 100644 --- a/libwvdrmengine/cdm/src/wv_content_decryption_module.cpp +++ b/libwvdrmengine/cdm/src/wv_content_decryption_module.cpp @@ -346,6 +346,8 @@ CdmResponseType WvContentDecryptionModule::Unprovision( // Enable immediate OEMCrypto termination and re-initalization on // unprovisioning. CryptoSession::DisableDelayedTermination(); + // Android unprovisioning has historically allowed for both + // DRM (app/origin-specific) and OEM (global) unprovisioning. return cdm_engine->Unprovision(level); } diff --git a/libwvdrmengine/include/Utils.h b/libwvdrmengine/include/Utils.h index c18f2365..0c1a2b6b 100644 --- a/libwvdrmengine/include/Utils.h +++ b/libwvdrmengine/include/Utils.h @@ -42,6 +42,8 @@ inline ::ndk::ScopedAStatus toNdkScopedAStatus( return toNdkScopedAStatus(::wvdrm::WvStatus(status), msg); } +bool checkIfEnableMultiThreadBinder(); + } // namespace wvdrm #endif // WV_UTILS_H_ diff --git a/libwvdrmengine/include/mapErrors-inl.h b/libwvdrmengine/include/mapErrors-inl.h index 05b9fbb8..5f78cecd 100644 --- a/libwvdrmengine/include/mapErrors-inl.h +++ b/libwvdrmengine/include/mapErrors-inl.h @@ -270,6 +270,8 @@ static inline WvStatus mapCdmResponseType(wvcdm::CdmResponseType res) { case wvcdm::USAGE_INVALID_PARAMETERS_2: case wvcdm::USAGE_STORE_ENTRY_RETRIEVE_INVALID_STORAGE_TYPE: 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; break; 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::KEY_NOT_FOUND_1: 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; break; 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_9: 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; break; case wvcdm::CERT_PROVISIONING_RESPONSE_ERROR_10: diff --git a/libwvdrmengine/oemcrypto/test/oemcrypto_basic_test.cpp b/libwvdrmengine/oemcrypto/test/oemcrypto_basic_test.cpp index 0d669262..2bf8fa56 100644 --- a/libwvdrmengine/oemcrypto/test/oemcrypto_basic_test.cpp +++ b/libwvdrmengine/oemcrypto/test/oemcrypto_basic_test.cpp @@ -2,17 +2,79 @@ // source code may only be used and distributed under the Widevine // License Agreement. // - #include "oemcrypto_basic_test.h" +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "OEMCryptoCENC.h" #include "clock.h" -#include "jsmn.h" #include "log.h" #include "oemcrypto_corpus_generator_helper.h" #include "oemcrypto_resource_test.h" #include "test_sleep.h" +void PrintTo(const jsmntype_t& type, std::ostream* out) { + switch (type) { + case JSMN_UNDEFINED: + *out << "Undefined"; + return; + case JSMN_OBJECT: + *out << "Object"; + return; + case JSMN_ARRAY: + *out << "Array"; + return; + case JSMN_STRING: + *out << "String"; + return; + case JSMN_PRIMITIVE: + *out << "Primitive"; + return; + } + *out << "Unknown(" << static_cast(type) << ')'; +} + namespace wvoec { +namespace { +// Counts the number of ancestor tokens of the provided |root_index| token. +// The result does not count the root itself. +// +// JSMN tokens specify the count of immediate ancessor tokens, but +// not the total. +// - Primitives never have children +// - Strings have 0 if they are a value, and 1 if they are the +// name of an object member +// - Objects have the count of members (each key-value pair is 1, +// regardless of the value's children elements) +// - Arrays have the count of elements (regardless of the values members) +// +int32_t JsmnAncestorCount(const std::vector& tokens, + int32_t root_index) { + if (root_index >= static_cast(tokens.size())) return 0; + int32_t count = 0; + int32_t iter = root_index; + int32_t remainder = 1; + while (remainder > 0 && iter < static_cast(tokens.size())) { + const int32_t child_count = tokens[iter].size; + remainder += child_count; + count += child_count; + iter++; + remainder--; + } + return count; +} +} // namespace + void OEMCryptoClientTest::SetUp() { ::testing::Test::SetUp(); wvutil::TestSleep::SyncFakeClock(); @@ -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) { if (wvoec::global_features.api_version < 18) { GTEST_SKIP() << "Test for versions 18 and up only."; } - std::string build_info; - OEMCryptoResult sts = OEMCrypto_BuildInformation(&build_info[0], nullptr); - ASSERT_EQ(OEMCrypto_ERROR_INVALID_CONTEXT, sts); - size_t buf_length = 0; + constexpr char kNullChar = '\0'; + constexpr size_t kZero = 0; + + // Step 1: Get Build Info + size_t buffer_length = 0; // OEMCrypto must allow |buffer| to be null so long as |buffer_length| // is provided and initially set to zero. - sts = OEMCrypto_BuildInformation(nullptr, &buf_length); - ASSERT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, sts); - build_info.resize(buf_length); - const size_t max_final_size = buf_length; - sts = OEMCrypto_BuildInformation(&build_info[0], &buf_length); - ASSERT_EQ(OEMCrypto_SUCCESS, sts); - ASSERT_LE(buf_length, max_final_size); - build_info.resize(buf_length); + OEMCryptoResult result = OEMCrypto_BuildInformation(nullptr, &buffer_length); + ASSERT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, result); + ASSERT_GT(buffer_length, kZero); + std::string build_info(buffer_length, kNullChar); + const size_t max_final_size = buffer_length; + result = OEMCrypto_BuildInformation(&build_info[0], &buffer_length); + ASSERT_EQ(OEMCrypto_SUCCESS, result); + ASSERT_LE(buffer_length, max_final_size); + build_info.resize(buffer_length); + + // Step 2: Parse as JSON jsmn_parser p; jsmn_init(&p); std::vector tokens; - int32_t num_tokens = + const int32_t num_tokens = jsmn_parse(&p, build_info.c_str(), build_info.size(), nullptr, 0); EXPECT_GT(num_tokens, 0) << "Failed to parse BuildInformation as JSON, parse returned " @@ -346,45 +546,186 @@ TEST_F(OEMCryptoClientTest, CheckJsonBuildInformationAPI18) { tokens.resize(num_tokens); jsmn_init(&p); - int32_t jsmn_result = jsmn_parse(&p, build_info.c_str(), build_info.size(), - tokens.data(), num_tokens); + const int32_t jsmn_result = jsmn_parse( + &p, build_info.c_str(), build_info.size(), tokens.data(), num_tokens); EXPECT_GE(jsmn_result, 0) << "Failed to parse BuildInformation as JSON, parse returned " << jsmn_result << "for following build info: " << build_info; - std::map expected; - expected["soc_vendor"] = JSMN_STRING; - expected["soc_model"] = JSMN_STRING; - expected["ta_ver"] = JSMN_STRING; - expected["uses_opk"] = JSMN_PRIMITIVE; - expected["tee_os"] = JSMN_STRING; - expected["tee_os_ver"] = JSMN_STRING; + // Step 3a: Ensure info is a single JSON object. + const jsmntok_t& object_token = tokens[0]; + ASSERT_EQ(object_token.type, JSMN_OBJECT) + << "Build info is not a JSON object: " << build_info; - // for values in token - // build string from start,end - // check for existence in map - // check if value matches expectation - // remove from map - for (int32_t i = 0; i < jsmn_result; i++) { - jsmntok_t token = tokens[i]; - std::string key = build_info.substr(token.start, token.end - token.start); - if (expected.find(key) != expected.end()) { - EXPECT_EQ(expected.find(key)->second, tokens[i + 1].type) - << "Type is incorrect for key " << key; - expected.erase(key); + // Step 3b: Verify schema of defined fields. + + // Required fields must be present in the build information, + // and be of the correct type. + const std::map kRequiredFields = { + // SOC manufacturer name + {"soc_vendor", JSMN_STRING}, + // SOC model name + {"soc_model", JSMN_STRING}, + // TA version in string format eg "1.12.3+tag", "2.0" + {"ta_ver", JSMN_STRING}, + // [bool] Whether TA was built with Widevine's OPK + {"uses_opk", JSMN_PRIMITIVE}, + // Trusted OS intended to run the TA, eg "Trusty", "QSEE", "OP-TEE" + {"tee_os", JSMN_STRING}, + // Version of Trusted OS intended to run the TA + {"tee_os_ver", JSMN_STRING}, + // [bool] Whether this is a debug build of the TA + // Not forcing behavior until implementations fix + // them self + // {"is_debug", JSMN_PRIMITIVE}, + }; + + const std::string kSpecialCaseReeKey = "ree"; + + // Optional fields may be present in the build information; + // if they are, then the must be the correct type. + const std::map kOptionalFields = { + // Name of company or entity that provides OEMCrypto. + {"implementor", JSMN_STRING}, + // Git commit hash of the code repository. + {"git_commit", JSMN_STRING}, + // ISO 8601 formatted timestamp of the time the TA was compiled + {"build_timestamp", JSMN_STRING}, + // Whether this was built with FACTORY_MODE_ONLY defined + {"is_factory_mode", JSMN_PRIMITIVE}, + // ... provide information about liboemcrypto.so + // Special case, see kOptionalReeFields for details. + {kSpecialCaseReeKey, JSMN_OBJECT}, + // Technically required, but several implementations + // do not implement this fields. + {"is_debug", JSMN_PRIMITIVE}, + }; + + // A set of the required fields found when examining the + // build information, use to verify all fields are present. + std::set found_required_fields; + // Stores the tokens of the "ree" field, if set, used to + // validate its content. + std::vector ree_tokens; + bool has_ree_info = false; + + // Start: first object key token + // Condition: key-value pair (2 tokens) + // Iter: next key-value pair (2 tokens) + for (int32_t i = 1; (i + 1) < jsmn_result; i += 2) { + // JSMN objects consist of pairs of key-value pairs (keys are always + // JSMN_STRING). + const jsmntok_t& key_token = tokens[i]; + ASSERT_EQ(key_token.type, JSMN_STRING) + << "Bad object key: i = " << i << ", build_info = " << build_info; + const jsmntok_t& value_token = tokens[i + 1]; + + const std::string key = + build_info.substr(key_token.start, key_token.end - key_token.start); + if (kRequiredFields.find(key) != kRequiredFields.end()) { + ASSERT_EQ(value_token.type, kRequiredFields.at(key)) + << "Unexpected required field type: field = " << key + << ", build_info = " << build_info; + found_required_fields.insert(key); + } else if (kOptionalFields.find(key) != kOptionalFields.end()) { + ASSERT_EQ(value_token.type, kOptionalFields.at(key)) + << "Unexpected optional field type: field = " << key + << ", build_info = " << build_info; + } // Do not validate vendor fields. + + if (key == kSpecialCaseReeKey) { + // Store the tokens of the "ree" field for additional validation. + const int32_t first_ree_field_index = i + 2; + const int32_t ree_token_count = JsmnAncestorCount(tokens, i + 1); + const auto first_ree_field_iter = tokens.begin() + first_ree_field_index; + ree_tokens.assign(first_ree_field_iter, + first_ree_field_iter + ree_token_count); + has_ree_info = true; } + + // Skip potential nested tokens. + i += JsmnAncestorCount(tokens, i + 1); } - // if map is not empty, return false - if (expected.size() > 0) { - std::string missing; - for (const auto& e : expected) { - missing.append(e.first); - missing.append(" "); + // Step 3c: Ensure all required fields were found. + if (found_required_fields.size() != kRequiredFields.size()) { + // Generate a list of all the missing fields. + std::string missing_fields; + for (const auto& required_field : kRequiredFields) { + if (found_required_fields.find(required_field.first) != + found_required_fields.end()) + continue; + if (!missing_fields.empty()) { + missing_fields.append(", "); + } + missing_fields.push_back('"'); + missing_fields.append(required_field.first); + missing_fields.push_back('"'); } - FAIL() << "JSON does not contain all required keys. Missing keys: [" - << missing << "] in string " << build_info; + + FAIL() << "Build info JSON object does not contain all required keys; " + << "missing_fields = [" << missing_fields + << "], build_info = " << build_info; + return; } + + // If no "ree" field tokens, then end here. + if (!has_ree_info) return; + // Step 4a: Verify "ree" object scheme. + ASSERT_FALSE(ree_tokens.empty()) + << "REE field was specified, but contents were empty: build_info = " + << build_info; + + // The optional field "ree", if present, must follow the required + // format. + const std::map kReeRequiredFields = { + // liboemcrypto.so version in string format eg "2.15.0+tag" + {"liboemcrypto_ver", JSMN_STRING}, + // git hash of code that compiled liboemcrypto.so + {"git_commit", JSMN_STRING}, + // ISO 8601 timestamp for when liboemcrypto.so was built + {"build_timestamp", JSMN_STRING}}; + + found_required_fields.clear(); + for (int32_t i = 0; (i + 1) < static_cast(ree_tokens.size()); + i += 2) { + const jsmntok_t& key_token = ree_tokens[i]; + ASSERT_EQ(key_token.type, JSMN_STRING) + << "Bad REE object key: i = " << i << ", build_info = " << build_info; + const jsmntok_t& value_token = ree_tokens[i + 1]; + + const std::string key = + build_info.substr(key_token.start, key_token.end - key_token.start); + if (kReeRequiredFields.find(key) != kReeRequiredFields.end()) { + ASSERT_EQ(value_token.type, kReeRequiredFields.at(key)) + << "Unexpected optional REE field type: ree_field = " << key + << ", build_info = " << build_info; + found_required_fields.insert(key); + } // Do not validate vendor fields. + + // Skip potential nested tokens. + i += JsmnAncestorCount(ree_tokens, i + 1); + } + + // Step 4b: Ensure all required fields of the "ree" object were found. + if (found_required_fields.size() == kReeRequiredFields.size()) return; + // Generate a list of all the missing REE fields. + std::string missing_ree_fields; + for (const auto& required_field : kReeRequiredFields) { + if (found_required_fields.find(required_field.first) != + found_required_fields.end()) + continue; + if (!missing_ree_fields.empty()) { + missing_ree_fields.append(", "); + } + missing_ree_fields.push_back('"'); + missing_ree_fields.append(required_field.first); + missing_ree_fields.push_back('"'); + } + + FAIL() << "REE info JSON object does not contain all required keys; " + << "missing_ree_fields = [" << missing_ree_fields + << "], build_info = " << build_info; } TEST_F(OEMCryptoClientTest, CheckMaxNumberOfSessionsAPI10) { diff --git a/libwvdrmengine/src/Utils.cpp b/libwvdrmengine/src/Utils.cpp index 754f802e..f517518e 100644 --- a/libwvdrmengine/src/Utils.cpp +++ b/libwvdrmengine/src/Utils.cpp @@ -7,6 +7,8 @@ #include "Utils.h" #include +#include "WVCDMSingleton.h" +#include "log.h" #include @@ -163,4 +165,97 @@ CryptoSessionApi getCryptoSessionMethodEnum(const std::string& method) { 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 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 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 diff --git a/libwvdrmengine/src/service.cpp b/libwvdrmengine/src/service.cpp index d30ad182..e3b2af6a 100644 --- a/libwvdrmengine/src/service.cpp +++ b/libwvdrmengine/src/service.cpp @@ -22,6 +22,7 @@ #include "WVCreatePluginFactories.h" #include "WVDrmFactory.h" +#include "Utils.h" using ::wvdrm::hardware::drm::widevine::createDrmFactory; using ::wvdrm::hardware::drm::widevine::WVDrmFactory; @@ -29,6 +30,9 @@ using ::wvdrm::hardware::drm::widevine::WVDrmFactory; int main(int /* argc */, char** /* argv */) { ABinderProcess_setThreadPoolMaxThreadCount(8); + if (wvdrm::checkIfEnableMultiThreadBinder()) { + ABinderProcess_startThreadPool(); + } std::shared_ptr drmFactory = createDrmFactory(); const std::string drmInstance = diff --git a/libwvdrmengine/src/serviceLazy.cpp b/libwvdrmengine/src/serviceLazy.cpp index 53312dab..c511314d 100644 --- a/libwvdrmengine/src/serviceLazy.cpp +++ b/libwvdrmengine/src/serviceLazy.cpp @@ -22,6 +22,7 @@ #include "WVCreatePluginFactories.h" #include "WVDrmFactory.h" +#include "Utils.h" using ::wvdrm::hardware::drm::widevine::createDrmFactory; using ::wvdrm::hardware::drm::widevine::WVDrmFactory; @@ -29,6 +30,9 @@ using ::wvdrm::hardware::drm::widevine::WVDrmFactory; int main(int /* argc */, char** /* argv */) { ABinderProcess_setThreadPoolMaxThreadCount(8); + if (wvdrm::checkIfEnableMultiThreadBinder()) { + ABinderProcess_startThreadPool(); + } std::shared_ptr drmFactory = createDrmFactory(); const std::string drmInstance = diff --git a/libwvdrmengine/version.txt b/libwvdrmengine/version.txt index 7bbb2184..f91fce18 100644 --- a/libwvdrmengine/version.txt +++ b/libwvdrmengine/version.txt @@ -1 +1 @@ -AV1A.250207.001 +AV1A.250512.001