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/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/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: