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
This commit is contained in:
Alex Dale
2025-04-21 16:43:37 -07:00
parent 961faf0729
commit b0fa978058
6 changed files with 128 additions and 21 deletions

View File

@@ -72,6 +72,28 @@ class CertificateProvisioning {
// |default_url| by GetProvisioningRequest(). // |default_url| by GetProvisioningRequest().
static void GetProvisioningServerUrl(std::string* default_url); static void GetProvisioningServerUrl(std::string* default_url);
enum State {
// Freshly created, not yet initialized.
kUninitialized,
// A successful call to Init() has been made.
kInitialized,
// Has generated a DRM request; apps are allowed generate
// another one even if a response has not been received.
kDrmRequestSent,
// Has received (and successfully loaded) a DRM response.
kDrmResponseReceived,
// Has generated an OEM (Prov 4.0) request; apps are allowed
// generate another one even if a response has not been
// received.
kOemRequestSent,
// Has received (and successfully loaded) an OEM response.
kOemResponseReceived,
};
static const char* StateToString(State state);
// State setter for testing only.
void SetStateForTesting(State state) { state_ = state; }
private: private:
#if defined(UNIT_TEST) #if defined(UNIT_TEST)
friend class CertificateProvisioningTest; friend class CertificateProvisioningTest;
@@ -122,6 +144,9 @@ class CertificateProvisioning {
CdmResponseType CloseSessionOnError(CdmResponseType status); CdmResponseType CloseSessionOnError(CdmResponseType status);
void CloseSession(); void CloseSession();
// Tracks the state of CertificateProvisioning.
State state_ = kUninitialized;
std::unique_ptr<CryptoSession> crypto_session_; std::unique_ptr<CryptoSession> crypto_session_;
CdmCertificateType cert_type_; CdmCertificateType cert_type_;
std::unique_ptr<ServiceCertificate> service_certificate_; std::unique_ptr<ServiceCertificate> service_certificate_;

View File

@@ -465,6 +465,7 @@ enum CdmResponseEnum : int32_t {
GET_DEVICE_INFORMATION_ERROR = 398, GET_DEVICE_INFORMATION_ERROR = 398,
GET_DEVICE_SIGNED_CSR_PAYLOAD_ERROR = 399, GET_DEVICE_SIGNED_CSR_PAYLOAD_ERROR = 399,
GET_TOKEN_FROM_EMBEDDED_CERT_ERROR = 400, GET_TOKEN_FROM_EMBEDDED_CERT_ERROR = 400,
PROVISIONING_UNEXPECTED_RESPONSE_ERROR = 402,
// Don't forget to add new values to // Don't forget to add new values to
// * core/src/wv_cdm_types.cpp // * core/src/wv_cdm_types.cpp
// * android/include/mapErrors-inl.h // * android/include/mapErrors-inl.h

View File

@@ -98,6 +98,25 @@ using video_widevine::PublicKeyToCertify;
using video_widevine::SignedDrmCertificate; using video_widevine::SignedDrmCertificate;
using video_widevine::SignedProvisioningMessage; using video_widevine::SignedProvisioningMessage;
// static
const char* CertificateProvisioning::StateToString(State state) {
switch (state) {
case kUninitialized:
return "Uninitialized";
case kInitialized:
return "Initialized";
case kDrmRequestSent:
return "DrmRequestSent";
case kDrmResponseReceived:
return "DrmResponseReceived";
case kOemRequestSent:
return "OemRequestSent";
case kOemResponseReceived:
return "OemResponseReceived";
}
return "<unknown>";
}
// static // static
void CertificateProvisioning::GetProvisioningServerUrl( void CertificateProvisioning::GetProvisioningServerUrl(
std::string* default_url) { std::string* default_url) {
@@ -114,7 +133,11 @@ CdmResponseType CertificateProvisioning::Init(
service_certificate.empty() service_certificate.empty()
? wvutil::a2bs_hex(kCpProductionServiceCertificate) ? wvutil::a2bs_hex(kCpProductionServiceCertificate)
: service_certificate; : service_certificate;
return service_certificate_->Init(certificate); const CdmResponseType result = service_certificate_->Init(certificate);
if (result == NO_ERROR) {
state_ = kInitialized;
}
return result;
} }
// Fill in the appropriate SPOID (Stable Per-Origin IDentifier) option. // Fill in the appropriate SPOID (Stable Per-Origin IDentifier) option.
@@ -206,11 +229,18 @@ CdmResponseType CertificateProvisioning::GetProvisioningRequestInternal(
default_url->assign(kProvisioningServerUrl); default_url->assign(kProvisioningServerUrl);
if (state_ != kInitialized) {
LOGD("Overriding old request: state = %s", StateToString(state_));
// Once the previous session is closed, there is no way to complete
// an in-flight request.
state_ = kInitialized;
}
CloseSession(); CloseSession();
CdmResponseType status = crypto_session_->Open(requested_security_level); CdmResponseType status = crypto_session_->Open(requested_security_level);
if (NO_ERROR != status) { if (NO_ERROR != status) {
LOGE("Failed to create a crypto session: status = %d", LOGE("Failed to create a crypto session: status = %s",
static_cast<int>(status)); status.ToString().c_str());
return status; return status;
} }
@@ -299,6 +329,7 @@ CdmResponseType CertificateProvisioning::GetProvisioningRequestInternal(
} else { } else {
*request = std::move(serialized_request); *request = std::move(serialized_request);
} }
state_ = kDrmRequestSent;
return CdmResponseType(NO_ERROR); return CdmResponseType(NO_ERROR);
} }
@@ -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 // Retrieve the Spoid, but put it to the client identification instead, so it
// is encrypted. // is encrypted.
@@ -363,7 +395,7 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal(
provisioning_request.clear_stable_id(); provisioning_request.clear_stable_id();
} }
if (stored_oem_cert.empty()) { if (is_oem_prov_request) {
// This is the first stage provisioning. // This is the first stage provisioning.
default_url->assign(std::string(kProvisioningServerUrl) + default_url->assign(std::string(kProvisioningServerUrl) +
kProv40FirstStageServerUrlSuffix); kProv40FirstStageServerUrlSuffix);
@@ -377,8 +409,8 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal(
// Since |stored_oem_cert| is empty, the client identification token will be // Since |stored_oem_cert| is empty, the client identification token will be
// retrieved from OEMCrypto, which is the BCC in this case. // retrieved from OEMCrypto, which is the BCC in this case.
status = FillEncryptedClientId(stored_oem_cert, provisioning_request, status = FillEncryptedClientId(/* client_token = */ std::string(),
wv_service_cert); provisioning_request, wv_service_cert);
if (status != NO_ERROR) return status; if (status != NO_ERROR) return status;
} else { } else {
// This is the second stage provisioning. // This is the second stage provisioning.
@@ -490,6 +522,8 @@ CdmResponseType CertificateProvisioning::GetProvisioning40RequestInternal(
*request = std::move(serialized_request); *request = std::move(serialized_request);
} }
request_ = std::move(serialized_message); request_ = std::move(serialized_message);
state_ = is_oem_prov_request ? kOemRequestSent : kDrmRequestSent;
return CdmResponseType(NO_ERROR); return CdmResponseType(NO_ERROR);
} }
@@ -552,6 +586,18 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response(
return CdmResponseType(PROVISIONING_4_RESPONSE_HAS_ERROR_STATUS); return CdmResponseType(PROVISIONING_4_RESPONSE_HAS_ERROR_STATUS);
} }
} }
if (state_ == kOemResponseReceived || state_ == kDrmResponseReceived) {
// A response has already been received (successfully), this
// response can be silently dropped.
LOGW("Response already received: state = %s", StateToString(state_));
return CdmResponseType(NO_ERROR);
}
if (state_ != kOemRequestSent && state_ != kDrmRequestSent) {
LOGE("Not expecting a response: state = %s", StateToString(state_));
return CdmResponseType(PROVISIONING_UNEXPECTED_RESPONSE_ERROR);
}
LOGD("Handling response: state = %s", StateToString(state_));
const bool is_oem_prov_response = (state_ == kOemRequestSent);
const std::string& device_certificate = const std::string& device_certificate =
provisioning_response.device_certificate(); provisioning_response.device_certificate();
@@ -593,6 +639,7 @@ CdmResponseType CertificateProvisioning::HandleProvisioning40Response(
*cert = device_certificate; *cert = device_certificate;
*wrapped_key = cast_cert_private_key.key(); *wrapped_key = cast_cert_private_key.key();
state_ = is_oem_prov_response ? kOemResponseReceived : kDrmResponseReceived;
return CdmResponseType(NO_ERROR); 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 // Check the stage of the provisioning by checking if an OEM cert is already
// stored in the file system. // stored in the file system.
if (!global_file_handle.HasOemCertificate()) { if (is_oem_prov_response) {
if (global_file_handle.HasOemCertificate()) {
// Possible that concurrent apps were generated provisioning
// requests, and this one arrived after an other one.
LOGI("CDM has already received an OEM certificate");
state_ = kOemResponseReceived;
return CdmResponseType(NO_ERROR);
}
// No OEM cert already stored => the response is expected to be an OEM cert. // No OEM cert already stored => the response is expected to be an OEM cert.
if (!global_file_handle.StoreOemCertificate(device_certificate, if (!global_file_handle.StoreOemCertificate(device_certificate,
private_key)) { private_key)) {
LOGE("Failed to store provisioning 4 OEM certificate"); LOGE("Failed to store provisioning 4 OEM certificate");
return CdmResponseType(PROVISIONING_4_FAILED_TO_STORE_OEM_CERTIFICATE); return CdmResponseType(PROVISIONING_4_FAILED_TO_STORE_OEM_CERTIFICATE);
} }
} else { state_ = kOemResponseReceived;
// The response is assumed to be an DRM cert. return CdmResponseType(NO_ERROR);
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);
}
} }
// 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); return CdmResponseType(NO_ERROR);
} }
@@ -678,6 +733,15 @@ CdmResponseType CertificateProvisioning::HandleProvisioningResponse(
wrapped_key); wrapped_key);
} }
if (state_ == kDrmResponseReceived) {
LOGD("Response already received");
return CdmResponseType(NO_ERROR);
}
if (state_ != kDrmRequestSent) {
LOGE("Not expecting a response: state = %s", StateToString(state_));
return CdmResponseType(PROVISIONING_UNEXPECTED_RESPONSE_ERROR);
}
bool error = false; bool error = false;
if (!signed_response.has_signature()) { if (!signed_response.has_signature()) {
LOGE("Signed response does not have signature"); LOGE("Signed response does not have signature");
@@ -753,6 +817,7 @@ CdmResponseType CertificateProvisioning::HandleProvisioningResponse(
if (cert_type_ == kCertificateX509) { if (cert_type_ == kCertificateX509) {
*cert = device_cert_data; *cert = device_cert_data;
*wrapped_key = private_key.key(); *wrapped_key = private_key.key();
state_ = kDrmResponseReceived;
return CdmResponseType(NO_ERROR); return CdmResponseType(NO_ERROR);
} }
@@ -799,6 +864,7 @@ CdmResponseType CertificateProvisioning::HandleProvisioningResponse(
return CdmResponseType(CERT_PROVISIONING_RESPONSE_ERROR_8); return CdmResponseType(CERT_PROVISIONING_RESPONSE_ERROR_8);
} }
state_ = kDrmResponseReceived;
return CdmResponseType(NO_ERROR); return CdmResponseType(NO_ERROR);
} }

View File

@@ -879,6 +879,8 @@ const char* CdmResponseEnumToString(CdmResponseEnum cdm_response_enum) {
return "SESSION_NOT_FOUND_GENERIC_CRYPTO"; return "SESSION_NOT_FOUND_GENERIC_CRYPTO";
case SESSION_NOT_FOUND_24: case SESSION_NOT_FOUND_24:
return "SESSION_NOT_FOUND_24"; return "SESSION_NOT_FOUND_24";
case PROVISIONING_UNEXPECTED_RESPONSE_ERROR:
return "PROVISIONING_UNEXPECTED_RESPONSE_ERROR";
} }
return UnknownValueRep(cdm_response_enum); return UnknownValueRep(cdm_response_enum);
} }

View File

@@ -413,6 +413,9 @@ TEST_P(CertificateProvisioningTest, ProvisioningRequestFailsEmptySignature) {
TEST_P(CertificateProvisioningTest, TEST_P(CertificateProvisioningTest,
ProvisioningResponseFailsWithEmptyResponse) { ProvisioningResponseFailsWithEmptyResponse) {
certificate_provisioning_->Init(""); certificate_provisioning_->Init("");
// Must set state if not generating request.
certificate_provisioning_->SetStateForTesting(
CertificateProvisioning::kDrmRequestSent);
MockFileSystem file_system; MockFileSystem file_system;
std::string certificate; std::string certificate;
@@ -425,6 +428,9 @@ TEST_P(CertificateProvisioningTest,
TEST_P(CertificateProvisioningTest, TEST_P(CertificateProvisioningTest,
ProvisioningResponseFailsIfDeviceIsRevoked) { ProvisioningResponseFailsIfDeviceIsRevoked) {
certificate_provisioning_->Init(""); certificate_provisioning_->Init("");
// Must set state if not generating request.
certificate_provisioning_->SetStateForTesting(
CertificateProvisioning::kDrmRequestSent);
MockFileSystem file_system; MockFileSystem file_system;
std::string response_certificate; std::string response_certificate;
@@ -445,6 +451,10 @@ TEST_P(CertificateProvisioningTest,
TEST_P(CertificateProvisioningTest, ProvisioningResponseSuccess) { TEST_P(CertificateProvisioningTest, ProvisioningResponseSuccess) {
certificate_provisioning_->Init(""); certificate_provisioning_->Init("");
// Must set state if not generating request.
certificate_provisioning_->SetStateForTesting(
CertificateProvisioning::kDrmRequestSent);
std::string expected_certificate; std::string expected_certificate;
std::string response; std::string response;
ASSERT_TRUE(MakeSignedDrmCertificate(kFakePublicKey, kSerialNumber, kSystemId, ASSERT_TRUE(MakeSignedDrmCertificate(kFakePublicKey, kSerialNumber, kSystemId,

View File

@@ -299,6 +299,9 @@ static inline WvStatus mapCdmResponseType(wvcdm::CdmResponseType res) {
case wvcdm::INVALID_QUERY_KEY: case wvcdm::INVALID_QUERY_KEY:
case wvcdm::KEY_NOT_FOUND_1: case wvcdm::KEY_NOT_FOUND_1:
case wvcdm::SAMPLE_AND_SUBSAMPLE_SIZE_MISMATCH: case wvcdm::SAMPLE_AND_SUBSAMPLE_SIZE_MISMATCH:
// Client provided a provisioning response without
// generating a provisioning request.
case wvcdm::PROVISIONING_UNEXPECTED_RESPONSE_ERROR:
err = Status::BAD_VALUE; err = Status::BAD_VALUE;
break; break;
case wvcdm::KEY_NOT_FOUND_3: case wvcdm::KEY_NOT_FOUND_3: