Minimal implementation of Widevine MediaCAS ECMG.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=226515998
This commit is contained in:
Fang Yu
2018-12-21 11:17:37 -08:00
parent 7487ce5aa8
commit bc68878bdf
88 changed files with 2456 additions and 2774 deletions

View File

@@ -20,7 +20,9 @@ PUBLIC_COPTS = ["-fvisibility=default"]
filegroup(
name = "binary_release_files",
srcs = glob(["*.h"]),
srcs = glob(["*.h"]) + [
":wv_ecmg",
],
)
cc_binary(
@@ -49,15 +51,6 @@ cc_library(
],
)
cc_binary(
name = "simulcrypt_server",
srcs = ["simulcrypt_server.cc"],
deps = [
"//base",
"@abseil_repo//absl/base:core_headers",
],
)
cc_library(
name = "wv_cas_ca_descriptor",
srcs = ["wv_cas_ca_descriptor.cc"],
@@ -97,8 +90,8 @@ cc_library(
"@abseil_repo//absl/base:core_headers", # buildcleaner: keep
"@abseil_repo//absl/memory", # buildcleaner: keep
"@abseil_repo//absl/strings", # buildcleaner: keep
"//common:status",
"//common:crypto_util",
"//common:status",
"//example:constants",
"//media_cas_packager_sdk/internal:ecm",
"//media_cas_packager_sdk/internal:ecm_generator",
@@ -134,7 +127,6 @@ cc_library(
"@abseil_repo//absl/base:core_headers",
"@abseil_repo//absl/strings",
"@curl_repo//:curl",
"//common:status",
"//common:signature_util",
"//media_cas_packager_sdk/internal:key_fetcher",
"//protos/public:media_cas_encryption_proto",
@@ -153,7 +145,6 @@ cc_test(
"//external:protobuf",
"//testing:gunit_main",
"@abseil_repo//absl/strings",
"//common:status",
"//protos/public:media_cas_encryption_proto",
],
)
@@ -175,3 +166,14 @@ cc_test(
"//testing:gunit_main",
],
)
cc_binary(
name = "wv_ecmg",
srcs = ["wv_ecmg.cc"],
deps = [
"//base",
"@abseil_repo//absl/base:core_headers",
"@abseil_repo//absl/strings",
"//media_cas_packager_sdk/internal:ecmg_client_handler",
],
)

View File

@@ -1,82 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
// Copyright 2018 Google LLC.
//
// This software is licensed under the terms defined in the Widevine Master
// License Agreement. For a copy of this agreement, please contact
// widevine-licensing@google.com.
////////////////////////////////////////////////////////////////////////////////
// Example server that listens on a port for Simulcrypt API messages.
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <iostream>
#include "gflags/gflags.h"
#include "glog/logging.h"
DEFINE_int32(port, 0, "Server port number");
constexpr uint32_t kBufferSize = 256;
constexpr uint32_t kLicenseBacklog = 5;
constexpr uint32_t kWriteChunkSize = 18;
int main(int argc, char **argv) {
gflags::ParseCommandLineFlags(&argc, &argv, true);
CHECK(FLAGS_port != 0) << "need --port";
struct sockaddr_in server_address;
bzero(reinterpret_cast<char *>(&server_address), sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(FLAGS_port);
int listen_socket_fd = socket(AF_INET, SOCK_STREAM, /* protocol= */ 0);
CHECK(listen_socket_fd >= 0) << "failed to open socket";
CHECK(bind(listen_socket_fd, (struct sockaddr *)&server_address,
sizeof(server_address)) >= 0)
<< "error on binding";
std::cout << "Server listening ..." << std::endl << std::flush;
int return_val = listen(listen_socket_fd, kLicenseBacklog);
switch (return_val) {
case EADDRINUSE:
LOG(FATAL) << "Another socket is already listening on the same port.";
break;
case EBADF:
LOG(FATAL) << "The argument sockfd is not a valid descriptor.";
break;
case ENOTSOCK:
LOG(FATAL) << "The argument sockfd is not a socket.";
break;
case EOPNOTSUPP:
LOG(FATAL) << "The socket is not of a type that supports the listen() "
"operation.";
default:
break;
}
struct sockaddr_in client_address;
socklen_t clilet_address_size = sizeof(client_address);
int client_socket_fd = accept(
listen_socket_fd, reinterpret_cast<struct sockaddr *>(&client_address),
&clilet_address_size);
CHECK(client_socket_fd >= 0) << "error on accept";
char buffer[kBufferSize];
bzero(buffer, kBufferSize);
if (read(client_socket_fd, buffer, kBufferSize - 1) < 0) {
LOG(FATAL) << "ERROR reading from socket";
}
printf("Here is the message: %s", buffer);
if (write(client_socket_fd, "I got your message", kWriteChunkSize) < 0) {
LOG(FATAL) << "ERROR writing to socket";
}
close(client_socket_fd);
close(listen_socket_fd);
return 0;
}

View File

@@ -95,7 +95,7 @@ WvCasStatus WvCasCaDescriptor::GenerateCaDescriptor(
return INTERNAL;
}
std::string descriptor;
util::Status status =
Status status =
string_util::BitsetStringToBinaryString(descriptor_bitset, &descriptor);
*serialized_ca_desc = descriptor;

View File

@@ -13,10 +13,10 @@
#include <vector>
#include "glog/logging.h"
#include "common/status.h"
#include "absl/memory/memory.h"
#include "absl/strings/str_cat.h"
#include "common/crypto_util.h"
#include "common/status.h"
#include "example/constants.h"
#include "media_cas_packager_sdk/internal/ecm.h"
#include "media_cas_packager_sdk/internal/ecm_generator.h"
@@ -154,7 +154,7 @@ WvCasStatus WvCasEcm::GenerateEcm(
// TODO(user): When we want to retrieve entitlement key from License Server
// we need to figure out a way to provide real 'content_id' and 'provder'
// to this function here.
util::Status status;
Status status;
if (!(status = cas_ecm->Initialize(kDefaultContentId, kDefaultProvider,
ecm_init_params, &entitlement_request))
.ok()) {
@@ -265,7 +265,7 @@ WvCasStatus WvCasEcm::GenerateSingleKeyEcm(
// TODO(user): When we want to retrieve entitlement key from License Server
// we need to figure out a way to provide real 'content_id' and 'provder'
// to this function here.
util::Status status;
Status status;
if (!(status = cas_ecm->Initialize(kDefaultContentId, kDefaultProvider,
ecm_init_params, &entitlement_request))
.ok()) {
@@ -319,8 +319,8 @@ WvCasStatus WvCasEcm::GenerateTsPacket(const std::string& ecm, uint16_t pid,
uint8_t* continuity_counter,
uint8_t* packet) {
ssize_t bytes_modified = 0;
util::Status status = InsertEcmAsTsPacket(
ecm, pid, table_id, continuity_counter, packet, &bytes_modified);
Status status = InsertEcmAsTsPacket(ecm, pid, table_id, continuity_counter,
packet, &bytes_modified);
if (!status.ok() || bytes_modified != kTsPacketSize) {
memset(packet, 0, kTsPacketSize);
LOG(ERROR) << "Failed to generate TS packet: " << status;

View File

@@ -20,7 +20,6 @@
#include "absl/strings/string_view.h"
#include "curl/curl.h"
#include "curl/easy.h"
#include "common/status.h"
#include "common/signature_util.h"
#include "protos/public/media_cas_encryption.pb.h"
@@ -41,12 +40,12 @@ DEFINE_string(signing_iv, "",
namespace widevine {
namespace cas {
util::Status WvCasKeyFetcher::RequestEntitlementKey(
const std::string& request_string, std::string* signed_response_string) {
Status WvCasKeyFetcher::RequestEntitlementKey(const std::string& request_string,
std::string* signed_response_string) {
if (FLAGS_signing_provider.empty() || FLAGS_signing_key.empty() ||
FLAGS_signing_iv.empty()) {
return util::Status(
util::error::INVALID_ARGUMENT,
return Status(
error::INVALID_ARGUMENT,
"Flag 'signing_provider', 'signing_key' or 'signing_iv' is empty");
}
@@ -63,8 +62,8 @@ util::Status WvCasKeyFetcher::RequestEntitlementKey(
// NOTE: MessageToJsonString will automatically converts 'bytes' type fields
// to base64. For example content ID '21140844' becomes 'MjExNDA4NDQ='.
if (!MessageToJsonString(request, &request_json, print_options).ok()) {
return util::Status(util::error::INTERNAL,
"Failed to convert request message to json.");
return Status(error::INTERNAL,
"Failed to convert request message to json.");
}
LOG(INFO) << "Json CasEncryptionRequest: " << request_json;
@@ -76,7 +75,7 @@ util::Status WvCasKeyFetcher::RequestEntitlementKey(
request_json, absl::HexStringToBytes(FLAGS_signing_key),
absl::HexStringToBytes(FLAGS_signing_iv), &signature)
.ok()) {
return util::Status(util::error::INTERNAL, "Failed to sign the request.");
return Status(error::INTERNAL, "Failed to sign the request.");
}
signed_request.set_signature(signature);
signed_request.set_signer(FLAGS_signing_provider);
@@ -85,23 +84,22 @@ util::Status WvCasKeyFetcher::RequestEntitlementKey(
// 'signature' fields in SignedCasEncryptionRequest to base64, because they
// are of type 'bytes'.
if (!MessageToJsonString(signed_request, &signed_request_json).ok()) {
return util::Status(util::error::INTERNAL,
"Failed to convert signed request message to json.");
return Status(error::INTERNAL,
"Failed to convert signed request message to json.");
}
LOG(INFO) << "Json SignedCasEncryptionRequest: " << signed_request_json;
// Makes HTTP request against License Server.
std::string http_response_json;
util::Status status =
MakeHttpRequest(signed_request_json, &http_response_json);
Status status = MakeHttpRequest(signed_request_json, &http_response_json);
if (!status.ok()) {
return status;
}
LOG(INFO) << "Json HTTP response: " << http_response_json;
HttpResponse http_response;
if (!JsonStringToMessage(http_response_json, &http_response).ok()) {
return util::Status(util::error::INTERNAL,
"Failed to convert http response json to message.");
return Status(error::INTERNAL,
"Failed to convert http response json to message.");
}
// Processes signed response.
@@ -110,13 +108,13 @@ util::Status WvCasKeyFetcher::RequestEntitlementKey(
LOG(INFO) << "Json CasEncryptionResponse: " << http_response.response();
CasEncryptionResponse response;
if (!JsonStringToMessage(http_response.response(), &response).ok()) {
return util::Status(util::error::INTERNAL,
"Failed to convert response json to message.");
return Status(error::INTERNAL,
"Failed to convert response json to message.");
}
SignedCasEncryptionResponse signed_response;
signed_response.set_response(response.SerializeAsString());
signed_response.SerializeToString(signed_response_string);
return util::OkStatus();
return OkStatus();
}
size_t AppendToString(void* ptr, size_t size, size_t count, std::string* output) {
@@ -125,12 +123,11 @@ size_t AppendToString(void* ptr, size_t size, size_t count, std::string* output)
return data.size();
}
util::Status WvCasKeyFetcher::MakeHttpRequest(
const std::string& signed_request_json, std::string* http_response_json) const {
Status WvCasKeyFetcher::MakeHttpRequest(const std::string& signed_request_json,
std::string* http_response_json) const {
CHECK(http_response_json);
if (FLAGS_license_server.empty()) {
return util::Status(util::error::INVALID_ARGUMENT,
"Flag 'license_server' is empty");
return Status(error::INVALID_ARGUMENT, "Flag 'license_server' is empty");
}
CURL* curl;
CURLcode curl_code;
@@ -145,15 +142,14 @@ util::Status WvCasKeyFetcher::MakeHttpRequest(
(int64_t)strlen(signed_request_json.c_str()));
curl_code = curl_easy_perform(curl);
if (curl_code != CURLE_OK) {
return util::Status(util::error::INTERNAL,
"curl_easy_perform() failed: " +
std::string(curl_easy_strerror(curl_code)));
return Status(error::INTERNAL, "curl_easy_perform() failed: " +
std::string(curl_easy_strerror(curl_code)));
}
curl_easy_cleanup(curl);
} else {
return util::Status(util::error::INTERNAL, "curl_easy_init() failed");
return Status(error::INTERNAL, "curl_easy_init() failed");
}
return util::OkStatus();
return OkStatus();
}
} // namespace cas

View File

@@ -12,7 +12,6 @@
#include <string>
#include "gflags/gflags.h"
#include "common/status.h"
#include "media_cas_packager_sdk/internal/key_fetcher.h"
DECLARE_string(license_server);
@@ -41,15 +40,15 @@ class WvCasKeyFetcher : public KeyFetcher {
// |signed_response_string| a serialized SignedCasEncryptionResponse
// message. It should be passed into
// widevine::cas::Ecm::ProcessCasEncryptionResponse().
virtual util::Status RequestEntitlementKey(const std::string& request_string,
std::string* signed_response_string);
Status RequestEntitlementKey(const std::string& request_string,
std::string* signed_response_string) override;
protected:
// Makes a HTTP request to License Server for entitlement key(s).
// Returns the HTTP response in Json format in |http_response_json|.
// Protected visibility to support unit testing.
virtual util::Status MakeHttpRequest(const std::string& signed_request_json,
std::string* http_response_json) const;
virtual Status MakeHttpRequest(const std::string& signed_request_json,
std::string* http_response_json) const;
};
} // namespace cas

View File

@@ -14,7 +14,6 @@
#include "testing/gmock.h"
#include "testing/gunit.h"
#include "absl/strings/escaping.h"
#include "common/status.h"
#include "protos/public/media_cas_encryption.pb.h"
using testing::_;
@@ -55,9 +54,8 @@ class MockWvCasKeyFetcher : public WvCasKeyFetcher {
public:
MockWvCasKeyFetcher() : WvCasKeyFetcher() {}
~MockWvCasKeyFetcher() override {}
MOCK_CONST_METHOD2(MakeHttpRequest,
util::Status(const std::string& signed_request_json,
std::string* http_response_json));
MOCK_CONST_METHOD2(MakeHttpRequest, Status(const std::string& signed_request_json,
std::string* http_response_json));
};
class WvCasKeyFetcherTest : public ::testing::Test {
@@ -93,7 +91,7 @@ TEST_F(WvCasKeyFetcherTest, TestRequestEntitlementKey) {
EXPECT_CALL(mock_key_fetcher_,
MakeHttpRequest(kSignedCasEncryptionRequest, _))
.WillOnce(DoAll(SetArgumentPointee<1>(std::string(kHttpResponse)),
Return(util::OkStatus())));
Return(OkStatus())));
std::string actual_signed_response;
EXPECT_OK(mock_key_fetcher_.RequestEntitlementKey(

View File

@@ -0,0 +1,190 @@
////////////////////////////////////////////////////////////////////////////////
// Copyright 2018 Google LLC.
//
// This software is licensed under the terms defined in the Widevine Master
// License Agreement. For a copy of this agreement, please contact
// widevine-licensing@google.com.
////////////////////////////////////////////////////////////////////////////////
// Example server that listens on a port for Simulcrypt API messages.
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <iostream>
#include "gflags/gflags.h"
#include "glog/logging.h"
#include "absl/strings/str_cat.h"
#include "media_cas_packager_sdk/internal/ecmg_client_handler.h"
static constexpr int32_t kDefaultDelayStart = 200;
static constexpr int32_t kDefaultDelayStop = 200;
static constexpr int32_t kDefaultEcmRepPeriod = 100;
static constexpr int32_t kDefaultMaxCompTime = 100;
static constexpr int32_t kAccessCriteriaTransferMode = 1;
DEFINE_int32(port, 0, "Server port number");
// ECMG related flags.
// TODO(user): Consider adding flags 'ac_delay_start', 'ac_delay_stop',
// 'transition_delay_start', 'transition_delay_stop'.
DEFINE_int32(delay_start, kDefaultDelayStart,
absl::StrCat("This flag sets the DVB SimulCrypt delay_start "
"parameter, in milliseconds. Default: ",
kDefaultDelayStart, " ms")
.c_str());
DEFINE_int32(delay_stop, kDefaultDelayStop,
absl::StrCat("This flag sets the DVB SimulCrypt delay_stop "
"parameter, in milliseconds. Default: ",
kDefaultDelayStop, " ms")
.c_str());
DEFINE_int32(ecm_rep_period, kDefaultEcmRepPeriod,
absl::StrCat("It sets the DVB SimulCrypt parameter "
"ECM_rep_period, in milliseconds. Default: ",
kDefaultEcmRepPeriod, " ms")
.c_str());
DEFINE_int32(max_comp_time, kDefaultMaxCompTime,
absl::StrCat("It sets the DVB SimulCrypt parameter max_comp_time, "
"in milliseconds. Default: ",
kDefaultMaxCompTime, " ms")
.c_str());
DEFINE_int32(access_criteria_transfer_mode, kAccessCriteriaTransferMode,
absl::StrCat("It sets the DVB SimulCrypt parameter "
"access_criteria_transfer_mode. Default: ",
kAccessCriteriaTransferMode)
.c_str());
#define LISTEN_QUEUE_SIZE (20)
#define BUFFER_SIZE (1024)
using widevine::cas::EcmgClientHandler;
using widevine::cas::EcmgConfig;
void BuildEcmgConfig(EcmgConfig* config) {
DCHECK(config);
config->delay_start = FLAGS_delay_start;
config->delay_stop = FLAGS_delay_stop;
config->ecm_rep_period = FLAGS_ecm_rep_period;
config->max_comp_time = FLAGS_max_comp_time;
config->access_criteria_transfer_mode = FLAGS_access_criteria_transfer_mode;
}
void PrintMessage(const std::string& description, const char* const message,
size_t length) {
LOG(INFO) << description;
for (size_t i = 0; i < length; i++) {
printf("'\\x%02x', ", static_cast<uint16_t>(*(message + i)));
fflush(stdout);
}
printf("\n");
fflush(stdout);
}
void ServeClient(int socket_fd, EcmgClientHandler* ecmg) {
DCHECK(ecmg);
char request[BUFFER_SIZE];
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) {
LOG(ERROR) << "No more request from client";
return;
}
if (request_length < 0) {
LOG(ERROR) << "Failed to receive request from client";
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;
}
}
}
int main(int argc, char** argv) {
gflags::ParseCommandLineFlags(&argc, &argv, true);
CHECK(FLAGS_port != 0) << "need --port";
EcmgConfig ecmg_config;
BuildEcmgConfig(&ecmg_config);
// Server address.
struct sockaddr_in server_address;
bzero(reinterpret_cast<char*>(&server_address), sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(FLAGS_port);
// Create a listening socket.
int listen_socket_fd = socket(AF_INET, SOCK_STREAM, /* protocol= */ 0);
CHECK(listen_socket_fd >= 0) << "Failed to open listening socket";
// Set SO_REUSEADDR on a socket to true (1).
int optval = 1;
setsockopt(listen_socket_fd, SOL_SOCKET, SO_REUSEADDR, &optval,
sizeof(optval));
// Bind address.
CHECK(bind(listen_socket_fd, (struct sockaddr*)&server_address,
sizeof(server_address)) >= 0)
<< "Failed to bind on server socket";
// Listen for connection from clients.
std::cout << "Server listening ..." << std::endl << std::flush;
int return_val = listen(listen_socket_fd, LISTEN_QUEUE_SIZE);
switch (return_val) {
case EADDRINUSE:
LOG(FATAL) << "Another socket is already listening on the same port.";
break;
case EBADF:
LOG(FATAL) << "The argument sockfd is not a valid descriptor.";
break;
case ENOTSOCK:
LOG(FATAL) << "The argument sockfd is not a socket.";
break;
case EOPNOTSUPP:
LOG(FATAL) << "The socket is not of a type that supports the listen() "
"operation.";
default:
break;
}
// A single client handler, allow only 1 TCP connection / 1 channel at a time.
EcmgClientHandler client_handler(&ecmg_config);
// While loop to serve different client connections.
while (true) {
struct sockaddr_in client_address;
socklen_t client_address_size = sizeof(client_address);
int client_socket_fd = accept(
listen_socket_fd, reinterpret_cast<struct sockaddr*>(&client_address),
&client_address_size);
LOG(INFO) << "\nTCP connection start\n";
if (client_socket_fd < 0) {
LOG(ERROR) << "Failed to accept connection request from client";
} else {
// TODO(user): Support multi-threading of serving concurrent clients.
// TODO(user): Per jfore@ suggestion, look into using
// http://man7.org/linux/man-pages/man7/epoll.7.html
ServeClient(client_socket_fd, &client_handler);
}
LOG(INFO) << "\nTCP connection closed\n";
close(client_socket_fd);
usleep(1000);
}
// Close listening socket.
close(listen_socket_fd);
return 0;
}