Changed Prov4.0 handler to accept only recent requests.
[ Cherry-pick of v19 http://go/wvgerrit/219291 ] [ Merge of http://go/wvgerrit/219432 ] If the same app/origin generates multiple provisioning 4.0 requests it is possible that a mismatch between the OEM/DRM certificate and the wrapped OEM/DRM private key occurs. The CDM would use the OEM/DRM certificate of the first response one received, and the wrapped private key of the last request generated. To avoid this issue, the public key from the most recent request is cached and checked against the responses received. If the keys match, that response is accepted; if the keys don't match than the response is assumed "stale" and the response is dropped. In an attempt to maintain existing behavior of the CDM, "stale" responses will return NO_ERROR to the app. Note: This was tested using both RSA and ECC cert key types. VIC-specific: Needed to add implementation of StringContains() and StringEndsWith(). Bug: 391469176 Test: run_prov40_tests Change-Id: Id45d40d9af355c46a61c3cc2c19c252cf17c7489
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 <algorithm>
|
||||
|
||||
#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<uint8_t>(message.front());
|
||||
if (type_header != kSequenceHeader) return false;
|
||||
|
||||
// Verify length.
|
||||
const uint8_t length_header = static_cast<uint8_t>(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<size_t>(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<size_t>(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<uint8_t>(message[kPayloadLengthOffset + i]);
|
||||
payload_length = (payload_length << 8) + static_cast<size_t>(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user