diff --git a/libwvdrmengine/cdm/core/include/certificate_provisioning.h b/libwvdrmengine/cdm/core/include/certificate_provisioning.h index 36793ab4..be2d7d0d 100644 --- a/libwvdrmengine/cdm/core/include/certificate_provisioning.h +++ b/libwvdrmengine/cdm/core/include/certificate_provisioning.h @@ -151,18 +151,24 @@ class CertificateProvisioning { 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/wv_cdm_types.h b/libwvdrmengine/cdm/core/include/wv_cdm_types.h index 8cef4cae..605ec668 100644 --- a/libwvdrmengine/cdm/core/include/wv_cdm_types.h +++ b/libwvdrmengine/cdm/core/include/wv_cdm_types.h @@ -466,6 +466,8 @@ enum CdmResponseEnum : int32_t { 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 cb189cfb..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 diff --git a/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp b/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp index e8875307..14602c33 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; @@ -355,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()) { @@ -376,6 +502,7 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal( // 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); @@ -448,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); @@ -522,6 +648,13 @@ 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, 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_ = public_key; state_ = is_oem_prov_request ? kOemRequestSent : kDrmRequestSent; return CdmResponseType(NO_ERROR); @@ -606,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; @@ -627,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; @@ -640,11 +771,78 @@ 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)) { @@ -659,17 +857,23 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response( // 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); } + 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. @@ -679,11 +883,14 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response( return CdmResponseType(PROVISIONING_4_FAILED_TO_INITIALIZE_DEVICE_FILES_3); } if (!per_origin_file_handle.StoreCertificate(device_certificate, - private_key)) { + 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); } diff --git a/libwvdrmengine/cdm/core/src/wv_cdm_types.cpp b/libwvdrmengine/cdm/core/src/wv_cdm_types.cpp index 7e79c143..73968616 100644 --- a/libwvdrmengine/cdm/core/src/wv_cdm_types.cpp +++ b/libwvdrmengine/cdm/core/src/wv_cdm_types.cpp @@ -881,6 +881,10 @@ const char* CdmResponseEnumToString(CdmResponseEnum cdm_response_enum) { 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/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/include/mapErrors-inl.h b/libwvdrmengine/include/mapErrors-inl.h index 127bd9c2..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: @@ -403,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: