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().
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<CryptoSession> crypto_session_;
CdmCertificateType cert_type_;
std::unique_ptr<ServiceCertificate> service_certificate_;

View File

@@ -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

View File

@@ -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 "<unknown>";
}
// 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<int>(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);
}

View File

@@ -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);
}

View File

@@ -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,