Support for group license

Content keys in ECM v3 can now additionally be encrypted by group
entitlement keys.
This commit is contained in:
Lu Chen
2021-03-04 14:35:08 -08:00
parent 79e39b482d
commit 62777d7d3b
66 changed files with 1275 additions and 954 deletions

View File

@@ -34,6 +34,8 @@ cc_binary(
deps = [
":wv_cas_ca_descriptor",
":wv_cas_ecm",
":wv_cas_ecmg_client_handler",
":wv_cas_emm",
":wv_cas_key_fetcher",
":wv_cas_types",
],

View File

@@ -9,6 +9,7 @@
#include "media_cas_packager_sdk/public/wv_cas_ca_descriptor.h"
#include <bitset>
#include <cstdint>
#include <vector>
#include "glog/logging.h"

View File

@@ -11,6 +11,7 @@
#include <stddef.h>
#include <cstdint>
#include <string>
#include <vector>
@@ -60,7 +61,8 @@ class WvCasCaDescriptor {
// ECM stream). The descriptor will be 6 bytes plus any bytes added as
// (user-defined) private data.
virtual Status GenerateCaDescriptor(
uint16_t ca_pid, const std::string& provider, const std::string& content_id,
uint16_t ca_pid, const std::string& provider,
const std::string& content_id,
const std::vector<std::string>& entitlement_key_ids,
std::string* serialized_ca_desc) const;

View File

@@ -8,6 +8,8 @@
#include "media_cas_packager_sdk/public/wv_cas_ca_descriptor.h"
#include <cstdint>
#include "testing/gmock.h"
#include "testing/gunit.h"
#include "protos/public/media_cas.pb.h"

View File

@@ -8,6 +8,8 @@
#include "media_cas_packager_sdk/public/wv_cas_curl_key_fetcher.h"
#include <cstdint>
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "curl/curl.h"

View File

@@ -8,6 +8,7 @@
#include "media_cas_packager_sdk/public/wv_cas_ecm.h"
#include <cstdint>
#include <memory>
#include "glog/logging.h"
@@ -69,6 +70,7 @@ void WvCasEcm::SetServiceBlocking(
Status WvCasEcm::GenerateEcm(const WvCasContentKeyInfo& even_key,
const WvCasContentKeyInfo& odd_key,
const std::string& track_type,
const std::vector<std::string>& group_ids,
std::string* serialized_ecm) const {
EntitledKeyInfo entitled_even_key = ConvertToEntitledKeyInfo(even_key);
EntitledKeyInfo entitled_odd_key = ConvertToEntitledKeyInfo(odd_key);
@@ -84,11 +86,12 @@ Status WvCasEcm::GenerateEcm(const WvCasContentKeyInfo& even_key,
}
}
return ecm_->GenerateEcm(&entitled_even_key, &entitled_odd_key, track_type,
serialized_ecm);
group_ids, serialized_ecm);
}
Status WvCasEcm::GenerateSingleKeyEcm(const WvCasContentKeyInfo& key,
const std::string& track_type,
const std::vector<std::string>& group_ids,
std::string* serialized_ecm) const {
EntitledKeyInfo entitled_key = ConvertToEntitledKeyInfo(key);
// Make content key to 16 bytes if crypto mode is Csa2.
@@ -97,7 +100,8 @@ Status WvCasEcm::GenerateSingleKeyEcm(const WvCasContentKeyInfo& key,
entitled_key.key_value = entitled_key.key_value + entitled_key.key_value;
}
}
return ecm_->GenerateSingleKeyEcm(&entitled_key, track_type, serialized_ecm);
return ecm_->GenerateSingleKeyEcm(&entitled_key, track_type, group_ids,
serialized_ecm);
}
Status WvCasEcm::GenerateTsPacket(const std::string& ecm, uint16_t pid,

View File

@@ -9,6 +9,7 @@
#ifndef MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_ECM_H_
#define MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_ECM_H_
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
@@ -98,6 +99,8 @@ class WvCasEcm {
// |even_key| information for even key to be encoded into ECM.
// |odd_key| information for odd key to be encoded into ECM.
// |track_type| the track that the keys are being used to encrypt.
// |group_ids| If specified, the content keys will be additionally encrypted
// by group entitlement keys with specified |group_ids|.
// |serialized_ecm| caller-supplied std::string pointer to receive the ECM.
// The |even_key| and |odd_key| contents (specifically IV sizes) must be
// consistent with the initialized settings.
@@ -106,6 +109,7 @@ class WvCasEcm {
virtual Status GenerateEcm(const WvCasContentKeyInfo& even_key,
const WvCasContentKeyInfo& odd_key,
const std::string& track_type,
const std::vector<std::string>& group_ids,
std::string* serialized_ecm) const;
// Constructs a Widevine ECM using the provided key info. This call is
@@ -113,11 +117,14 @@ class WvCasEcm {
// Args:
// |key| information for key to be encoded into ECM.
// |track_type| the track that the key is being used to encrypt.
// |group_ids| If specified, the content keys will be additionally encrypted
// by group entitlement keys with specified |group_ids|.
// |serialized_ecm| caller-supplied std::string pointer to receive the ECM.
// The |key| contents (specifically IV sizes) must be consistent
// with the initialized settings.
virtual Status GenerateSingleKeyEcm(const WvCasContentKeyInfo& key,
const std::string& track_type,
const std::vector<std::string>& group_ids,
std::string* serialized_ecm) const;
// Generate a TS packet with the given |ecm| as payload.

View File

@@ -143,7 +143,7 @@ TEST_P(WvCasEcmTest, GenerateSingleKeyEcmKeyRotationEnabledError) {
EXPECT_EQ(error::INVALID_ARGUMENT,
wv_cas_ecm
.GenerateSingleKeyEcm(content_keys[0], kDefaultTrackTypeSd,
&actual_ecm)
/*group_ids=*/{}, &actual_ecm)
.error_code());
}
@@ -160,7 +160,7 @@ TEST_P(WvCasEcmTest, GenerateEcmKeyRotationDisabledError) {
EXPECT_EQ(error::INVALID_ARGUMENT,
wv_cas_ecm
.GenerateEcm(content_keys[0], content_keys[1],
kDefaultTrackTypeSd, &actual_ecm)
kDefaultTrackTypeSd, /*group_ids=*/{}, &actual_ecm)
.error_code());
}
@@ -177,10 +177,11 @@ TEST_P(WvCasEcmTest, GenerateEcmSuccess) {
std::string actual_ecm;
if (key_rotation_) {
EXPECT_OK(wv_cas_ecm.GenerateEcm(content_keys[0], content_keys[1],
kDefaultTrackTypeSd, &actual_ecm));
kDefaultTrackTypeSd, /*group_ids=*/{},
&actual_ecm));
} else {
EXPECT_OK(wv_cas_ecm.GenerateSingleKeyEcm(
content_keys[0], kDefaultTrackTypeSd, &actual_ecm));
content_keys[0], kDefaultTrackTypeSd, /*group_ids=*/{}, &actual_ecm));
}
EXPECT_EQ(absl::BytesToHexString(actual_ecm).substr(0, 7), "4ad4020");
if (content_iv_size_ == 8) {

View File

@@ -11,6 +11,7 @@
#include "glog/logging.h"
#include "absl/memory/memory.h"
#include "absl/strings/str_cat.h"
#include "common/status.h"
#include "media_cas_packager_sdk/internal/ecmg_client_handler.h"
namespace widevine {
@@ -55,12 +56,11 @@ Status WvCasEcmgClientHandler::HandleRequest(size_t request_buffer_size,
const char* const request_buffer,
size_t response_buffer_size,
char* response_buffer,
size_t* response_length) {
if (request_buffer == nullptr || response_buffer == nullptr ||
response_length == nullptr) {
size_t& response_length,
size_t& processed_request_length) {
if (request_buffer == nullptr || response_buffer == nullptr) {
std::string error_message =
"request_buffer, response_buffer and response_length must "
"not be null pointer.";
"request_buffer and response_buffer must not be null pointer.";
LOG(ERROR) << error_message;
return {error::INVALID_ARGUMENT, error_message};
}
@@ -69,7 +69,7 @@ Status WvCasEcmgClientHandler::HandleRequest(size_t request_buffer_size,
absl::StrCat("request_buffer_size is too small. Minimum required is ",
kSimulcryptMessageHeaderSizeBytes, " bytes.");
LOG(ERROR) << error_message;
*response_length = 0;
response_length = 0;
return {error::INVALID_ARGUMENT, error_message};
}
if (response_buffer_size < kMinimumResponseBufferSizeBytes) {
@@ -77,11 +77,15 @@ Status WvCasEcmgClientHandler::HandleRequest(size_t request_buffer_size,
absl::StrCat("response_buffer_size is too small. Minimum required is ",
kMinimumResponseBufferSizeBytes, " bytes.");
LOG(ERROR) << error_message;
*response_length = 0;
response_length = 0;
return {error::INVALID_ARGUMENT, error_message};
}
inner_handler_->HandleRequest(request_buffer, response_buffer,
response_length);
processed_request_length = inner_handler_->HandleRequest(
request_buffer, response_buffer, &response_length);
if (processed_request_length == 0) {
return {error::INTERNAL, "request not processed."};
}
return OkStatus();
}

View File

@@ -66,10 +66,13 @@ class WvCasEcmgClientHandler {
// enough to hold the max possible response message (at least 2048 bytes).
// - |response_buffer| is the buffer that holds the response message.
// - |response_length| is the actual length of the response message.
// - |processed_request_length| is the actual length of |request_buffer| that
// has been processed.
Status HandleRequest(size_t request_buffer_size,
const char* const request_buffer,
size_t response_buffer_size, char* response_buffer,
size_t* response_length);
size_t& response_length,
size_t& processed_request_length);
protected:
// For unit test only.

View File

@@ -24,7 +24,7 @@ constexpr size_t kBufferSize = 2048;
class MockEcmgClientHandler : public EcmgClientHandler {
public:
MockEcmgClientHandler() : EcmgClientHandler(&config_) {}
MOCK_METHOD(void, HandleRequest,
MOCK_METHOD(size_t, HandleRequest,
(const char* const request, char* response,
size_t* response_length),
(override));
@@ -47,15 +47,17 @@ TEST(WvCasEcmgClientHandlerTest, HandleRequestSuccess) {
size_t response_buffer_size = kBufferSize;
char response_buffer[kBufferSize];
size_t response_length = 0;
size_t processed_length = 0;
auto mock_internal_handler = absl::make_unique<MockEcmgClientHandler>();
EXPECT_CALL(*mock_internal_handler,
HandleRequest(request_buffer, response_buffer, &response_length));
HandleRequest(request_buffer, response_buffer, &response_length))
.WillOnce(testing::DoAll(testing::Return(100)));
auto handler = absl::make_unique<TestWvCasEcmgClientHandler>(
std::move(mock_internal_handler));
EXPECT_OK(handler->HandleRequest(request_buffer_size, request_buffer,
response_buffer_size, response_buffer,
&response_length));
response_length, processed_length));
}
class WvCasEcmgClientHandlerInvalidInputTest : public ::testing::Test {
@@ -65,6 +67,7 @@ class WvCasEcmgClientHandlerInvalidInputTest : public ::testing::Test {
size_t response_buffer_size_ = kBufferSize;
char response_buffer_[kBufferSize];
size_t response_length_ = 0;
size_t processed_length_ = 0;
};
TEST_F(WvCasEcmgClientHandlerInvalidInputTest, RequestBufferTooSmallFail) {
@@ -76,7 +79,7 @@ TEST_F(WvCasEcmgClientHandlerInvalidInputTest, RequestBufferTooSmallFail) {
EXPECT_EQ(handler
->HandleRequest(/*request_buffer_size=*/1, request_buffer_,
response_buffer_size_, response_buffer_,
&response_length_)
response_length_, processed_length_)
.error_code(),
error::INVALID_ARGUMENT);
EXPECT_EQ(response_length_, 0);
@@ -91,7 +94,7 @@ TEST_F(WvCasEcmgClientHandlerInvalidInputTest, ResponseBufferTooSmallFail) {
EXPECT_EQ(handler
->HandleRequest(request_buffer_size_, request_buffer_,
/*response_buffer_size=*/1, response_buffer_,
&response_length_)
response_length_, processed_length_)
.error_code(),
error::INVALID_ARGUMENT);
EXPECT_EQ(response_length_, 0);
@@ -107,7 +110,7 @@ TEST_F(WvCasEcmgClientHandlerInvalidInputTest, RequestBufferNullFail) {
handler
->HandleRequest(request_buffer_size_,
/*request_buffer=*/nullptr, response_buffer_size_,
response_buffer_, &response_length_)
response_buffer_, response_length_, processed_length_)
.error_code(),
error::INVALID_ARGUMENT);
EXPECT_EQ(response_length_, 0);
@@ -122,24 +125,27 @@ TEST_F(WvCasEcmgClientHandlerInvalidInputTest, ResponseBufferNullFail) {
EXPECT_EQ(handler
->HandleRequest(request_buffer_size_, request_buffer_,
response_buffer_size_,
/*response_buffer=*/nullptr, &response_length_)
/*response_buffer=*/nullptr, response_length_,
processed_length_)
.error_code(),
error::INVALID_ARGUMENT);
EXPECT_EQ(response_length_, 0);
}
TEST_F(WvCasEcmgClientHandlerInvalidInputTest, ResponseLengthNullFail) {
TEST_F(WvCasEcmgClientHandlerInvalidInputTest, HandleRequestReturnedZeroFail) {
auto mock_internal_handler = absl::make_unique<MockEcmgClientHandler>();
EXPECT_CALL(*mock_internal_handler, HandleRequest).Times(0);
// Processed length returned is zero.
EXPECT_CALL(*mock_internal_handler, HandleRequest)
.WillOnce(testing::DoAll(testing::Return(0)));
auto handler = absl::make_unique<TestWvCasEcmgClientHandler>(
std::move(mock_internal_handler));
EXPECT_EQ(handler
->HandleRequest(request_buffer_size_, request_buffer_,
response_buffer_size_, response_buffer_,
/*response_length=*/nullptr)
response_length_, processed_length_)
.error_code(),
error::INVALID_ARGUMENT);
error::INTERNAL);
}
} // namespace

View File

@@ -8,6 +8,8 @@
#include "media_cas_packager_sdk/public/wv_cas_emm.h"
#include <cstdint>
#include "glog/logging.h"
#include "absl/memory/memory.h"
#include "absl/types/span.h"

View File

@@ -9,6 +9,7 @@
#ifndef MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_EMM_H_
#define MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_EMM_H_
#include <cstdint>
#include <memory>
#include <string>
#include <vector>

View File

@@ -8,6 +8,7 @@
#include "media_cas_packager_sdk/public/wv_cas_emm.h"
#include <cstdint>
#include <memory>
#include <vector>

View File

@@ -9,8 +9,10 @@
#ifndef MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_TYPES_H_
#define MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_TYPES_H_
#include <cstdint>
#include <functional>
#include <string>
#include <tuple>
#include <vector>
#include "common/status.h"
@@ -52,18 +54,34 @@ enum EcmIvSize { kIvSize8 = 0, kIvSize16 = 1 };
// Information needed for the injected entitlement keys. Used for Ecm
// initialization.
// Fields:
// |track_type| the track that the key is being used to encrypt.
// |is_even_key| if true, this entitlement is an even key. For the single key
// case (no key rotation), is_even_key is always true.
// |key_id| key ID for this entitlement key, must be 16 bytes.
// |key_value| entitlement key value, must be 32 bytes. Used to decrypt
// content keys.
struct EntitlementKeyInfo {
// The track that the key is being used to encrypt. It used as the identifier
// to select which enetitlement keys to use when generating ECM. Must be
// non-empty.
std::string track_type;
// Used only if this is a group entitlement key (leave empty in case of non
// group key). If specified, it used as the secondary (to |track_type|)
// identifier to select which group enetitlement keys to use when generating
// ECM.
std::string group_id;
// If this entitlement key is even key or not.
bool is_even_key;
std::string key_id; // must be 16 bytes.
std::string key_value; // must be 32 bytes.
// Key ID of the entitlement key, must be 16 bytes.
std::string key_id;
// Key value of the entitlement key, must be 32 bytes. Used to encrypt content
// keys in the generated ECMs.
std::string key_value;
bool operator==(const EntitlementKeyInfo& other) const {
return std::forward_as_tuple(track_type, group_id, is_even_key, key_id,
key_value) ==
std::forward_as_tuple(other.track_type, other.group_id,
other.is_even_key, other.key_id,
other.key_value);
}
bool operator!=(const EntitlementKeyInfo& other) const {
return !(*this == other);
}
};
enum class EcmVersion : int { kV2 = 0, kV3 = 1 };
@@ -172,9 +190,13 @@ struct EcmgCustomParameters {
// |stream_id| is the stream id received at ECMG from SCS when setting up
// the stream.
// |access_criteria| the received access criteria from SCS.
// Returns EcmgCustomParameters. Negative or empty fields will be ignored.
typedef std::function<EcmgCustomParameters(uint16_t channel_id, uint16_t stream_id,
const std::string& access_criteria)>
// |ecmg_params| is the output parameters by processing |access_criteria|.
// Negative or empty fields will be ignored.
// Returns a Status. If status is not OK, error INVALID_MESSAGE with error info
// from status will be sent to SCS.
typedef std::function<Status(uint16_t channel_id, uint16_t stream_id,
const std::string& access_criteria,
EcmgCustomParameters& ecmg_params)>
CustomAccessCriteriaProcessFunc;
struct EcmFingerprintingParams {

View File

@@ -9,12 +9,14 @@
// Widevine MediaCAS ECMG server.
#include <netinet/in.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cerrno>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <iostream>
@@ -58,7 +60,7 @@ ABSL_FLAG(
"cryptography key.");
#define LISTEN_QUEUE_SIZE (20)
#define BUFFER_SIZE (1024)
#define BUFFER_SIZE (2048)
using widevine::cas::EcmgClientHandler;
using widevine::cas::EcmgConfig;
@@ -104,7 +106,6 @@ void ServeClient(int socket_fd, EcmgClientHandler* ecmg) {
char response[BUFFER_SIZE];
while (true) {
bzero(request, BUFFER_SIZE);
bzero(response, BUFFER_SIZE);
size_t response_length = 0;
size_t request_length = recv(socket_fd, request, BUFFER_SIZE, 0);
if (request_length == 0) {
@@ -116,11 +117,21 @@ void ServeClient(int socket_fd, EcmgClientHandler* ecmg) {
return;
}
PrintMessage("Request", request, request_length);
ecmg->HandleRequest(request, response, &response_length);
PrintMessage("Response", response, response_length);
if (send(socket_fd, response, response_length, 0) < 0) {
LOG(INFO) << "Failed to send response to client";
return;
size_t processed_total_length = 0;
while (processed_total_length < request_length) {
bzero(response, BUFFER_SIZE);
size_t processed_length = ecmg->HandleRequest(
&request[0] + processed_total_length, response, &response_length);
if (processed_length == 0) {
LOG(ERROR) << "Failed to process the request";
return;
}
processed_total_length += processed_length;
PrintMessage("Response", response, response_length);
if (send(socket_fd, response, response_length, 0) < 0) {
LOG(ERROR) << "Failed to send response to client";
return;
}
}
}
}
@@ -171,6 +182,9 @@ int main(int argc, char** argv) {
break;
}
// Ignoring SIGCHLD signal to prevent Zombie processes.
signal(SIGCHLD, SIG_IGN);
// While loop to serve different client connections.
while (true) {
struct sockaddr_in client_address;
@@ -179,7 +193,7 @@ int main(int argc, char** argv) {
listen_socket_fd, reinterpret_cast<struct sockaddr*>(&client_address),
&client_address_size);
LOG(INFO) << "\nTCP connection " << client_socket_fd << " start\n";
if (client_socket_fd < 0) {
if (client_socket_fd < 0 && errno != EINTR) {
LOG(ERROR) << "Failed to accept connection request from client";
} else {
// TODO(user): Consider limit the number of forked child processes.
@@ -200,8 +214,5 @@ int main(int argc, char** argv) {
usleep(1000);
}
// Close listening socket.
close(listen_socket_fd);
return 0;
}

View File

@@ -13,6 +13,7 @@
#include <sys/socket.h>
#include <unistd.h>
#include <cstdint>
#include <cstring>
#include <string>