From b0fa978058362cf20b123b7fc30dd1dfb6bb6e61 Mon Sep 17 00:00:00 2001 From: Alex Dale Date: Mon, 21 Apr 2025 16:43:37 -0700 Subject: [PATCH] Added state to CertificateProvisioning. [ Cherry-pick of v19 http://go/wvgerrit/219310 ] [ Merge of http://go/wvgerrit/219453 ] To enable the CDM to determine between OEM vs DRM responses, a state variable was needed in CertificateProvisioning. Previously, the presence/absence of the OEM certificate in the file system was used; however, if two apps (or single app with multiple origins) attempts provisioning simultaneously, the later response would trigger unexpected failures. The main functional changes this provides is that a more informative error will be returned to the app if they provide a provisioning response without ever creating a provisioning request; and that if multiple clients attempted first-stage provisioning simultaneously, fewer errors will occur. Bug: 391469176 Test: run_prov40_tests Change-Id: I51a118ce73aa809bad6ecee640139a92d8518575 --- .../core/include/certificate_provisioning.h | 25 ++++ .../cdm/core/include/wv_cdm_types.h | 1 + .../cdm/core/src/certificate_provisioning.cpp | 108 ++++++++++++++---- libwvdrmengine/cdm/core/src/wv_cdm_types.cpp | 2 + .../certificate_provisioning_unittest.cpp | 10 ++ libwvdrmengine/include/mapErrors-inl.h | 3 + 6 files changed, 128 insertions(+), 21 deletions(-) diff --git a/libwvdrmengine/cdm/core/include/certificate_provisioning.h b/libwvdrmengine/cdm/core/include/certificate_provisioning.h index 52d35fd6..36793ab4 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,6 +144,9 @@ 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_; diff --git a/libwvdrmengine/cdm/core/include/wv_cdm_types.h b/libwvdrmengine/cdm/core/include/wv_cdm_types.h index e4847271..8cef4cae 100644 --- a/libwvdrmengine/cdm/core/include/wv_cdm_types.h +++ b/libwvdrmengine/cdm/core/include/wv_cdm_types.h @@ -465,6 +465,7 @@ 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, // 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/certificate_provisioning.cpp b/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp index b40c3825..e8875307 100644 --- a/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp +++ b/libwvdrmengine/cdm/core/src/certificate_provisioning.cpp @@ -98,6 +98,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 +133,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 +229,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 +329,7 @@ CdmResponseType CertificateProvisioning::GetProvisioningRequestInternal( } else { *request = std::move(serialized_request); } + state_ = kDrmRequestSent; return CdmResponseType(NO_ERROR); } @@ -341,6 +372,7 @@ 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. @@ -363,7 +395,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 +409,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. @@ -490,6 +522,8 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal( *request = std::move(serialized_request); } request_ = std::move(serialized_message); + + state_ = is_oem_prov_request ? kOemRequestSent : kDrmRequestSent; return CdmResponseType(NO_ERROR); } @@ -552,6 +586,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(); @@ -593,6 +639,7 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response( *cert = device_certificate; *wrapped_key = cast_cert_private_key.key(); + state_ = is_oem_prov_response ? kOemResponseReceived : kDrmResponseReceived; return CdmResponseType(NO_ERROR); } @@ -607,28 +654,36 @@ 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"); + state_ = kOemResponseReceived; + 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)) { 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); - } + state_ = kOemResponseReceived; + 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, + private_key)) { + LOGE("Failed to store provisioning 4 DRM certificate"); + return CdmResponseType(PROVISIONING_4_FAILED_TO_STORE_DRM_CERTIFICATE); + } + state_ = kDrmResponseReceived; return CdmResponseType(NO_ERROR); } @@ -678,6 +733,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 +817,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 +864,7 @@ CdmResponseType CertificateProvisioning::HandleProvisioningResponse( return CdmResponseType(CERT_PROVISIONING_RESPONSE_ERROR_8); } + state_ = kDrmResponseReceived; 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 28b3969d..7e79c143 100644 --- a/libwvdrmengine/cdm/core/src/wv_cdm_types.cpp +++ b/libwvdrmengine/cdm/core/src/wv_cdm_types.cpp @@ -879,6 +879,8 @@ 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"; } 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/include/mapErrors-inl.h b/libwvdrmengine/include/mapErrors-inl.h index 05b9fbb8..127bd9c2 100644 --- a/libwvdrmengine/include/mapErrors-inl.h +++ b/libwvdrmengine/include/mapErrors-inl.h @@ -299,6 +299,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: