diff --git a/WORKSPACE b/WORKSPACE index 0541e7c..ba0d5d3 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -11,8 +11,8 @@ git_repository( git_repository( name = "abseil_repo", - commit = "aa844899c937bde5d2b24f276b59997e5b668bde", #2019-08-08 remote = "https://github.com/abseil/abseil-cpp.git", + tag = "20200923.2", ) # Name com_google_protobuf instead of protobuf_repo because Bazel's proto rules @@ -42,7 +42,7 @@ http_archive( git_repository( name = "boringssl_repo", - commit = "14164f6fef47b7ebd97cdb0cea1624eabd6fe6b8", # 2018-11-26 + commit = "3ef9a6b03503ae25f9267473073fea9c39d9cdac", # 2020-10-27 remote = "https://github.com/google/boringssl.git", ) diff --git a/common/BUILD b/common/BUILD index 9438b43..02b0d27 100644 --- a/common/BUILD +++ b/common/BUILD @@ -16,7 +16,7 @@ filegroup( name = "binary_release_files", srcs = [ "certificate_type.h", - "default_device_security_profile_list.h", + "hash_algorithm.h", "security_profile_list.h", "status.h", ], @@ -96,6 +96,11 @@ cc_library( deps = [ ":client_id_util", ":device_status_list", + ":error_space", + ":hash_algorithm", + ":hash_algorithm_util", + ":rsa_key", + ":status", "//base", "//external:protobuf", "@abseil_repo//absl/synchronization", @@ -103,6 +108,8 @@ cc_library( "//protos/public:device_certificate_status_cc_proto", "//protos/public:device_common_cc_proto", "//protos/public:device_security_profile_data_cc_proto", + "//protos/public:device_security_profile_list_cc_proto", + "//protos/public:errors_cc_proto", "//protos/public:provisioned_device_info_cc_proto", "//protos/public:security_profile_cc_proto", ], @@ -114,47 +121,21 @@ cc_test( srcs = ["security_profile_list_test.cc"], deps = [ ":client_id_util", + ":error_space", + ":hash_algorithm", + ":hash_algorithm_util", + ":rsa_key", + ":rsa_test_keys", ":security_profile_list", + ":status", "//base", "//external:protobuf", "//testing:gunit_main", "@abseil_repo//absl/memory", "//protos/public:device_common_cc_proto", "//protos/public:device_security_profile_data_cc_proto", - "//protos/public:security_profile_cc_proto", - ], -) - -cc_library( - name = "default_device_security_profile_list", - srcs = ["default_device_security_profile_list.cc"], - hdrs = ["default_device_security_profile_list.h"], - deps = [ - ":client_id_util", - ":device_status_list", - ":security_profile_list", - "//base", - "//external:protobuf", - "//protos/public:client_identification_cc_proto", - "//protos/public:device_certificate_status_cc_proto", - "//protos/public:device_common_cc_proto", - "//protos/public:provisioned_device_info_cc_proto", - "//protos/public:security_profile_cc_proto", - ], -) - -cc_test( - name = "default_device_security_profile_list_test", - timeout = "short", - srcs = ["default_device_security_profile_list_test.cc"], - deps = [ - ":client_id_util", - ":default_device_security_profile_list", - "//base", - "//external:protobuf", - "//testing:gunit_main", - "@abseil_repo//absl/memory", - "//protos/public:device_common_cc_proto", + "//protos/public:device_security_profile_list_cc_proto", + "//protos/public:errors_cc_proto", "//protos/public:security_profile_cc_proto", ], ) @@ -189,8 +170,6 @@ cc_library( "certificate_client_cert.cc", "certificate_client_cert.h", "client_cert.cc", - "dual_certificate_client_cert.cc", - "dual_certificate_client_cert.h", "keybox_client_cert.cc", ], hdrs = [ @@ -426,7 +405,6 @@ cc_library( ":sha_util", "//base", "@abseil_repo//absl/base:core_headers", - "@abseil_repo//absl/strings", "//external:openssl", ], ) @@ -494,7 +472,7 @@ cc_test( "//base", "//testing:gunit", "//testing:gunit_main", - "@abseil_repo//absl/strings", + "//external:openssl", ], ) @@ -871,6 +849,7 @@ cc_library( "@abseil_repo//absl/base:core_headers", "@abseil_repo//absl/strings", "@abseil_repo//absl/synchronization", + "//external:openssl", "//protos/public:client_identification_cc_proto", "//protos/public:errors_cc_proto", "//protos/public:remote_attestation_cc_proto", @@ -944,11 +923,13 @@ cc_library( srcs = ["x509_cert.cc"], hdrs = ["x509_cert.h"], deps = [ + ":ec_key", ":openssl_util", ":rsa_key", ":status", "//base", "@abseil_repo//absl/base:core_headers", + "@abseil_repo//absl/memory", "@abseil_repo//absl/strings", "@abseil_repo//absl/synchronization", "//external:openssl", @@ -973,6 +954,7 @@ cc_test( srcs = ["x509_cert_test.cc"], deps = [ ":rsa_key", + ":status", ":test_utils", ":x509_cert", "//testing:gunit_main", @@ -1174,6 +1156,16 @@ cc_library( ], ) +cc_library( + name = "raw_ec_key_util", + srcs = ["raw_ec_key_util.cc"], + hdrs = ["raw_ec_key_util.h"], + deps = [ + ":x509_cert", + "//base", + ], +) + cc_test( name = "hash_algorithm_util_test", srcs = ["hash_algorithm_util_test.cc"], @@ -1183,3 +1175,46 @@ cc_test( "//testing:gunit_main", ], ) + +cc_test( + name = "raw_ec_key_util_test", + srcs = ["raw_ec_key_util_test.cc"], + deps = [ + ":ec_key", + ":ec_test_keys", + ":raw_ec_key_util", + ":rsa_test_keys", + ":x509_test_certificates", + "//base", + "//testing:gunit_main", + "@abseil_repo//absl/strings", + ], +) + +cc_library( + name = "x509_test_certificates", + testonly = 1, + srcs = ["x509_test_certificates.cc"], + hdrs = ["x509_test_certificates.h"], + deps = [ + "//base", + "@abseil_repo//absl/strings", + ], +) + +cc_test( + name = "remote_attestation_verifier_test", + srcs = ["remote_attestation_verifier_test.cc"], + deps = [ + ":drm_service_certificate", + ":remote_attestation_verifier", + ":status", + ":x509_test_certificates", + "//testing:gunit_main", + "@abseil_repo//absl/memory", + "@abseil_repo//absl/strings", + "//protos/public:client_identification_cc_proto", + "//protos/public:errors_cc_proto", + "//protos/public:remote_attestation_cc_proto", + ], +) diff --git a/common/crypto_util.cc b/common/crypto_util.cc index 1ff6747..a95f56e 100644 --- a/common/crypto_util.cc +++ b/common/crypto_util.cc @@ -15,6 +15,7 @@ #include "absl/strings/string_view.h" #include "openssl/aes.h" #include "openssl/cmac.h" +#include "openssl/digest.h" #include "openssl/evp.h" #include "openssl/hmac.h" #include "openssl/sha.h" @@ -59,12 +60,28 @@ const uint32_t kCBCSSchemeID = // Creates a SHA-256 HMAC signature for the given message. std::string CreateSignatureHmacSha256(absl::string_view key, absl::string_view message) { + unsigned char digest[SHA256_DIGEST_LENGTH]; + return CreateSignatureHmac(EVP_sha256(), digest, key, message); +} + +// Creates a SHA-384 HMAC signature for the given message. +std::string CreateSignatureHmacSha384(absl::string_view key, + absl::string_view message) { + unsigned char digest[SHA384_DIGEST_LENGTH]; + return CreateSignatureHmac(EVP_sha384(), digest, key, message); +} + +std::string CreateSignatureHmac(const EVP_MD* hash_algorithm, + unsigned char* digest, absl::string_view key, + absl::string_view message) { + if (hash_algorithm == nullptr || digest == nullptr) { + return ""; + } HMAC_CTX ctx; HMAC_CTX_init(&ctx); - HMAC_Init(&ctx, key.data(), key.size(), EVP_sha256()); + HMAC_Init(&ctx, key.data(), key.size(), hash_algorithm); HMAC_Update(&ctx, reinterpret_cast(message.data()), message.size()); - unsigned char digest[SHA256_DIGEST_LENGTH]; unsigned int digest_len; HMAC_Final(&ctx, digest, &digest_len); HMAC_CTX_cleanup(&ctx); @@ -79,20 +96,18 @@ bool VerifySignatureHmacSha256(absl::string_view key, return CreateSignatureHmacSha256(key, message) == signature; } +// Compares the SHA-384 HMAC against the provided signature. +bool VerifySignatureHmacSha384(absl::string_view key, + absl::string_view signature, + absl::string_view message) { + return CreateSignatureHmacSha384(key, message) == signature; +} + // Creates a SHA-1 HMAC signature for the given message. std::string CreateSignatureHmacSha1(absl::string_view key, absl::string_view message) { - HMAC_CTX ctx; - HMAC_CTX_init(&ctx); - HMAC_Init(&ctx, key.data(), key.size(), EVP_sha1()); - HMAC_Update(&ctx, reinterpret_cast(message.data()), - message.size()); unsigned char digest[SHA_DIGEST_LENGTH]; - unsigned int digest_len; - HMAC_Final(&ctx, digest, &digest_len); - HMAC_CTX_cleanup(&ctx); - std::string s(reinterpret_cast(digest), digest_len); - return s; + return CreateSignatureHmac(EVP_sha1(), digest, key, message); } // Compares the SHA-1 HMAC against the provided signature. diff --git a/common/crypto_util.h b/common/crypto_util.h index 0b6e31d..a78a013 100644 --- a/common/crypto_util.h +++ b/common/crypto_util.h @@ -14,7 +14,9 @@ #include +#include "absl/strings/escaping.h" #include "absl/strings/string_view.h" +#include "openssl/digest.h" namespace widevine { namespace crypto_util { @@ -64,12 +66,27 @@ std::string DeriveSigningKey(absl::string_view key, absl::string_view context, std::string CreateSignatureHmacSha256(absl::string_view key, absl::string_view message); +// Helper function to create a SHA-384 HMAC signature for the given message. +std::string CreateSignatureHmacSha384(absl::string_view key, + absl::string_view message); +// Helper function to create a HMAC signature for the specified hash algorithm +// and message. +std::string CreateSignatureHmac(const EVP_MD* hash_algorithm, + unsigned char* digest, absl::string_view key, + absl::string_view message); + // Helper function which compares the SHA-256 HMAC against the provided // signature. bool VerifySignatureHmacSha256(absl::string_view key, absl::string_view signature, absl::string_view message); +// Helper function which compares the SHA-384 HMAC against the provided +// signature. +bool VerifySignatureHmacSha384(absl::string_view key, + absl::string_view signature, + absl::string_view message); + // Helper function to create a SHA-1 HMAC signature for the given message. std::string CreateSignatureHmacSha1(absl::string_view key, absl::string_view message); diff --git a/common/default_device_security_profile_list.h b/common/default_device_security_profile_list.h deleted file mode 100644 index 33d96d9..0000000 --- a/common/default_device_security_profile_list.h +++ /dev/null @@ -1,39 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright 2020 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. -//////////////////////////////////////////////////////////////////////////////// -// -// Description: -// Container of Widevine default security profiless. - -#ifndef COMMON_DEFAULT_DEVICE_SECURITY_PROFILE_LIST_H_ -#define COMMON_DEFAULT_DEVICE_SECURITY_PROFILE_LIST_H_ - -#include "common/security_profile_list.h" - -namespace widevine { - -class DefaultDeviceSecurityProfileList : public SecurityProfileList { - public: - DefaultDeviceSecurityProfileList(); - ~DefaultDeviceSecurityProfileList() override {} - - // Initialize the security profile list. The list is initially empty, this - // function will populate the list with default profiles. The size of the - // list is returned. - int Init() override; - - private: - // Initialize the list with Widevine default profiles. The size of the - // profile list after the additions is returned. - virtual int AddDefaultProfiles(); - virtual int GetDefaultProfileStrings( - std::vector* default_profile_strings) const; -}; - -} // namespace widevine - -#endif // COMMON_DEFAULT_DEVICE_SECURITY_PROFILE_LIST_H_ diff --git a/common/ec_key.cc b/common/ec_key.cc index b5b4684..4b22d9a 100644 --- a/common/ec_key.cc +++ b/common/ec_key.cc @@ -11,6 +11,8 @@ #include "common/ec_key.h" +#include + #include "glog/logging.h" #include "absl/memory/memory.h" #include "openssl/base.h" @@ -203,6 +205,34 @@ bool ECPrivateKey::SerializedKey(std::string* serialized_key) const { return ec_util::SerializeECPrivateKey(key_.get(), serialized_key); } +bool ECPrivateKey::GetRawPrivateKey(std::string* raw_private_key) const { + if (raw_private_key == nullptr) { + LOG(WARNING) << "|raw_private_key| cannot be nullptr."; + return false; + } + const EC_GROUP* group = EC_KEY_get0_group(key()); + if (group == nullptr) { + LOG(WARNING) << "Failed to load EC key group."; + return false; + } + int key_size_bits = EC_GROUP_get_degree(group); + if (key_size_bits == 0) { + LOG(WARNING) << "Key size is 0."; + return false; + } + const BIGNUM* key_bn = EC_KEY_get0_private_key(key()); + int key_size_bytes = (key_size_bits + 7) / 8; + std::vector tmp_raw_private_key_data(key_size_bytes); + int num_bytes = BN_bn2bin(key_bn, tmp_raw_private_key_data.data()); + if (num_bytes != key_size_bytes) { + LOG(WARNING) << "Failed to convert EC private key BIGNUM to binary data."; + return false; + } + raw_private_key->assign(tmp_raw_private_key_data.begin(), + tmp_raw_private_key_data.end()); + return true; +} + ECPublicKey::ECPublicKey(EC_KEY* ec_key) : key_(ec_key) { CHECK(key() != nullptr); CHECK(EC_KEY_get0_private_key(key()) == nullptr); @@ -310,4 +340,24 @@ bool ECPublicKey::GetPointEncodedKey(std::string* encoded_key) const { return ec_util::GetPublicKeyPoint(key_.get(), encoded_key); } +bool ECPublicKey::GetRawPublicKey(std::string* raw_public_key) const { + if (raw_public_key == nullptr) { + LOG(WARNING) << "|raw_public_key| cannot be nullptr."; + return false; + } + std::string tmp_raw_public_key; + if (!ec_util::GetPublicKeyPoint(key(), &tmp_raw_public_key) || + tmp_raw_public_key.empty()) { + LOG(WARNING) << "Failed to encoded EC_KEY"; + } + CHECK_EQ(tmp_raw_public_key[0], POINT_CONVERSION_UNCOMPRESSED); + // Public key is X9.62 uncompressed format. Expected structure is , + // where tag = 0x04 indicating uncompressed, and X and Y lengths are equal but + // dependent on ECC curve. Tag is removed because Intel Sigma 2.1.0 library + // requires only X and Y bytes. + raw_public_key->assign(tmp_raw_public_key.begin() + 1, + tmp_raw_public_key.end()); + return true; +} + } // namespace widevine diff --git a/common/ec_key.h b/common/ec_key.h index 57a74ea..f1527a2 100644 --- a/common/ec_key.h +++ b/common/ec_key.h @@ -83,6 +83,11 @@ class ECPrivateKey { virtual bool SerializedKey(std::string* serialized_key) const; + // Gets raw private key bytes. + // |raw_private_key| is where the raw bytes are stored. + // Returns true on success and false on error. + virtual bool GetRawPrivateKey(std::string* raw_private_key) const; + private: friend class ECPublicKey; @@ -135,6 +140,11 @@ class ECPublicKey { // octets for the X and Y coordinates. virtual bool GetPointEncodedKey(std::string* encoded_key) const; + // Gets raw public key bytes. + // |raw_public_key| is where the raw bytes are stored. + // Returns true on success and false on error. + virtual bool GetRawPublicKey(std::string* raw_public_key) const; + private: friend class ECPrivateKey; diff --git a/common/rsa_key.cc b/common/rsa_key.cc index d788986..a54e9b1 100644 --- a/common/rsa_key.cc +++ b/common/rsa_key.cc @@ -27,6 +27,7 @@ #include "glog/logging.h" #include "openssl/bn.h" +#include "openssl/digest.h" #include "openssl/err.h" #include "openssl/evp.h" #include "openssl/rsa.h" @@ -67,6 +68,32 @@ std::string GetMessageDigest(const std::string& message, return ""; } +const EVP_MD* GetHashMd(widevine::HashAlgorithm hash_algorithm) { + switch (hash_algorithm) { + case widevine::HashAlgorithm::kUnspecified: + case widevine::HashAlgorithm::kSha1: + return EVP_sha1(); + case widevine::HashAlgorithm::kSha256: + return EVP_sha256(); + } + LOG(FATAL) << "Unexpected hash algorithm: " + << static_cast(hash_algorithm); + return nullptr; +} + +bool IsMessageTooSmall(const std::string& message) { + DCHECK(!message.empty()); + // The most significant byte is encoded first in the message. See + // https://tools.ietf.org/html/rfc8017. To make sure the big number is greater + // than 2^64, we need to make sure there is at least a non-zero number in the + // first "LENGTH_IN_BITS - 64" bits of the message, + // i.e. "LENGTH_IN_BYTES - 8" bytes of the message. + const int kMinimumSizeInBytes = 8; + for (int i = 0; i < message.length() - kMinimumSizeInBytes; i++) { + if (message[i] != 0) return false; + } + return true; +} } // namespace namespace widevine { @@ -117,7 +144,6 @@ bool RsaPrivateKey::Decrypt(const std::string& encrypted_message, return true; } - bool RsaPrivateKey::GenerateSignature(const std::string& message, HashAlgorithm hash_algorithm, std::string* signature) const { @@ -134,12 +160,18 @@ bool RsaPrivateKey::GenerateSignature(const std::string& message, return false; } + const EVP_MD* hash = GetHashMd(hash_algorithm); + if (hash == nullptr) { + LOG(ERROR) << "No hash md"; + return false; + } + // Add PSS padding. size_t rsa_size = RSA_size(key_); std::string padded_digest(rsa_size, 0); if (!RSA_padding_add_PKCS1_PSS_mgf1( key_, reinterpret_cast(&padded_digest[0]), - reinterpret_cast(&message_digest[0]), EVP_sha1(), + reinterpret_cast(&message_digest[0]), hash, EVP_sha1(), kPssSaltLength)) { LOG(ERROR) << "RSA padding failure: " << OpenSSLErrorString(ERR_get_error()); @@ -222,17 +254,22 @@ bool RsaPublicKey::Encrypt(const std::string& clear_message, } size_t rsa_size = RSA_size(key_); encrypted_message->assign(rsa_size, 0); - if (RSA_public_encrypt( - clear_message.size(), - const_cast( - reinterpret_cast(clear_message.data())), - reinterpret_cast(&(*encrypted_message)[0]), key_, - RSA_PKCS1_OAEP_PADDING) != static_cast(rsa_size)) { - LOG(ERROR) << "RSA public encrypt failure: " - << OpenSSLErrorString(ERR_get_error()); - return false; + const int kRetryAttempt = 1; + for (int i = 0; i < 1 + kRetryAttempt; i++) { + if (RSA_public_encrypt( + clear_message.size(), + const_cast( + reinterpret_cast(clear_message.data())), + reinterpret_cast(&(*encrypted_message)[0]), key_, + RSA_PKCS1_OAEP_PADDING) != static_cast(rsa_size)) { + LOG(ERROR) << "RSA public encrypt failure: " + << OpenSSLErrorString(ERR_get_error()); + return false; + } + if (!IsMessageTooSmall(*encrypted_message)) return true; } - return true; + LOG(ERROR) << "RSA public encryption randomness error"; + return false; } bool RsaPublicKey::VerifySignature(const std::string& message, @@ -261,18 +298,23 @@ bool RsaPublicKey::VerifySignature(const std::string& message, return false; } - // Hash the message using SHA1 using corresponding hash algorithm. + // Hash the message using the corresponding hash algorithm. std::string message_digest = GetMessageDigest(message, hash_algorithm); if (message_digest.empty()) { LOG(ERROR) << "Empty message digest"; return false; } + const EVP_MD* hash = GetHashMd(hash_algorithm); + if (hash == nullptr) { + LOG(ERROR) << "No hash md"; + return false; + } + // Verify PSS padding. if (RSA_verify_PKCS1_PSS_mgf1( - key_, reinterpret_cast(&message_digest[0]), - EVP_sha1(), EVP_sha1(), - reinterpret_cast(&padded_digest[0]), + key_, reinterpret_cast(&message_digest[0]), hash, + EVP_sha1(), reinterpret_cast(&padded_digest[0]), kPssSaltLength) == 0) { LOG(ERROR) << "RSA Verify PSS padding failure: " << OpenSSLErrorString(ERR_get_error()); diff --git a/common/security_profile_list.h b/common/security_profile_list.h index b6cdb96..37f4dfd 100644 --- a/common/security_profile_list.h +++ b/common/security_profile_list.h @@ -15,25 +15,32 @@ #define COMMON_SECURITY_PROFILE_LIST_H_ #include "absl/synchronization/mutex.h" +#include "common/hash_algorithm.h" +#include "common/status.h" #include "protos/public/client_identification.pb.h" #include "protos/public/device_security_profile_data.pb.h" +#include "protos/public/device_security_profile_list.pb.h" #include "protos/public/provisioned_device_info.pb.h" #include "protos/public/security_profile.pb.h" namespace widevine { using ClientCapabilities = ClientIdentification::ClientCapabilities; +const char kDefaultProfileOwnerName[] = "Widevine"; + // The SecurityProfileList will hold all security profiles. During license // acquisition, information from the client and information from the server are // combined to deternmine the device's security profile level. +// TODO(user): Clean up the virtual/protected functions once subclass +// default_device_security_profile_list gets removed. class SecurityProfileList { public: explicit SecurityProfileList(const std::string& profile_namespace); virtual ~SecurityProfileList() {} - // Initialize the security profile list. The size of the profile list is - // returned. + // Initialize the security profile list with Widevine default profiles. The + // size of the profile list is returned. virtual int Init(); // Add the specified profile to the existing list of profiles. Returns true @@ -45,7 +52,7 @@ class SecurityProfileList { // The number of profiles is returned. virtual int GetQualifiedProfilesFromSpecifiedProfiles( const std::vector& profiles_to_check, - const ClientIdentification& client_id, + const std::string& owner, const ClientIdentification& client_id, const ProvisionedDeviceInfo& device_info, std::vector* qualified_profiles) const; @@ -53,14 +60,28 @@ class SecurityProfileList { // requirements for the this device. The number of profiles is returned. virtual int GetQualifiedProfiles( const ClientIdentification& client_id, - const ProvisionedDeviceInfo& device_info, + const ProvisionedDeviceInfo& device_info, const std::string& owner, std::vector* qualified_profiles) const; - // Return true if a profile exist matching the specified |name|. - // |security_profile| is owned by the caller and is populated if a profile - // exist. - bool GetProfileByName(const std::string& name, - SecurityProfile* security_profile) const; + // Return true if a profile exist matching the specified parameters {|name|, + // |owner|}. |security_profiles| is owned by the caller and is populated if + // one or more profile exist. For default DSP, the output profiles should + // contain single record. For custom DSP, it may contain multiple records + // since active dsp and inactive dsp could share the same dsp_name under the + // same owner. + bool GetProfileByNameAndOwner( + const std::string& name, const std::string& owner, + std::vector* security_profiles) const; + + // Populates |security_profiles| owned by the content owner. + int GetProfilesByOwner(const std::string& owner, + std::vector* security_profiles) const; + + // Populates |owner_list| for security profiles. |is_default_dsp| boolean + // indicates the owner_list for default dsp or custom dsp. + int GetProfilesOwnerList(const bool is_default_dsp, + std::vector* owner_list) const; + // Return the device security capabilities. |drm_info| is populated with // data from |client_id| and |device_info|. |drm_info| must not be null and // is owned by the caller. @@ -74,10 +95,33 @@ class SecurityProfileList { // Return a list of profile names. virtual void GetProfileNames(std::vector* profile_names) const; + // Deserialized SignedDeviceSecurityProfiles for custom DSPs. + static Status DeserializeSignedDeviceSecurityProfiles( + const std::string& serialized_signed_device_security_profiles, + std::string* serialized_device_security_profiles, + HashAlgorithm* hash_algorithm, std::string* signature); + + // Validate signature and update security profile list for custom dsps. + Status ValidateAndUpdateProfileList( + const std::string& root_certificate_public_key, + const std::string& serialized_device_security_profiles, + HashAlgorithm hash_algorithm, const std::string& signature, + int* added_profile_num); + protected: void ClearAllProfiles(); private: + // Add Widevine default profiles into profile_list. The number of added + // default profiles will be returned. + virtual int AddDefaultProfiles(); + // Add Widevine custom profiles into profile_list. The number of added custom + // profiles will be returned. + virtual int AddCustomProfiles( + const DeviceSecurityProfileList& device_security_profile_list); + virtual int GetDefaultProfileStrings( + std::vector* default_profile_strings) const; + bool DoesProfileQualify(const SecurityProfile& profile, const ClientIdentification& client_id, const ProvisionedDeviceInfo& device_info) const; @@ -87,9 +131,19 @@ class SecurityProfileList { bool IsProfileActive(const SecurityProfile& profile, int64_t current_time_seconds) const; + bool InsertProfileLocked(const SecurityProfile& profile_to_insert) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Return true if a profile already exists in the profile_list. + bool DoesProfileExistLocked(const SecurityProfile& profile) const + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + void ClearAllDefaultProfilesLocked() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + void ClearAllCustomProfilesLocked() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); mutable absl::Mutex mutex_; // Security profiles std::string profile_namespace_; + // TODO(user): Modify as Map. std::vector security_profiles_ ABSL_GUARDED_BY(mutex_); }; diff --git a/example/test_ecmg_messages.h b/example/test_ecmg_messages.h index 9adb9ba..3e98387 100644 --- a/example/test_ecmg_messages.h +++ b/example/test_ecmg_messages.h @@ -104,7 +104,7 @@ constexpr char kTestEcmgChannelStatus[] = { constexpr char kTestEcmgStreamSetupWithPrivateParameters[] = { '\x03', // protocol_version '\x01', '\x01', // message_type - Stream_setup - '\x00', '\xae', // message_length + '\x00', '\xa8', // message_length '\x00', '\x0e', // parameter_type - ECM_channel_id '\x00', '\x02', // parameter_length '\x00', '\x01', // parameter_value @@ -117,9 +117,6 @@ constexpr char kTestEcmgStreamSetupWithPrivateParameters[] = { '\x00', '\x10', // parameter_type - nominal_CP_duration '\x00', '\x02', // parameter_length '\x00', '\x64', // parameter_value - '\x80', '\x02', // parameter_type - STREAM_TRACK_TYPE - '\x00', '\x02', // parameter_length - 'S', 'D', // parameter_value '\x80', '\x03', // parameter_type - CONTENT_IV '\x00', '\x10', // parameter_length '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', // @@ -245,7 +242,7 @@ constexpr char kTestEcmgCwProvision[] = { constexpr char kTestEcmgCwProvisionWithAccessCriteria[] = { '\x03', // protocol_version '\x02', '\x01', // message_type - CW_provision - '\x00', '\xee', // message_length + '\x00', '\xe8', // message_length '\x00', '\x0e', // parameter_type - ECM_channel_id '\x00', '\x02', // parameter_length '\x00', '\x01', // parameter_value @@ -269,16 +266,13 @@ constexpr char kTestEcmgCwProvisionWithAccessCriteria[] = { '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', // '\x18', '\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', // '\x00', '\x0d', // parameter_type - access_criteria - '\x00', '\xa6', // parameter_length + '\x00', '\xa0', // parameter_length '\x80', '\x00', // access_criteria parameter_type - AGE_RESTRICTION '\x00', '\x01', // parameter_length '\x00', // parameter_value '\x80', '\x01', // access_criteria parameter_type - CRYPTO_MODE '\x00', '\x07', // parameter_length 'A', 'e', 's', 'S', 'c', 't', 'e', // parameter_value - '\x80', '\x02', // access_criteria parameter_type - STREAM_TRACK_TYPE - '\x00', '\x02', // parameter_length - 'S', 'D', // parameter_value '\x80', '\x03', // access_criteria parameter_type - CONTENT_IV '\x00', '\x10', // parameter_length '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', // diff --git a/example/wv_cas_ecm_example.cc b/example/wv_cas_ecm_example.cc index c59e00e..88b38d0 100644 --- a/example/wv_cas_ecm_example.cc +++ b/example/wv_cas_ecm_example.cc @@ -21,37 +21,60 @@ #include "media_cas_packager_sdk/public/wv_cas_ecm.h" #include "media_cas_packager_sdk/public/wv_cas_types.h" -const size_t kContentIvSize = 16; // 8 or 16 -const bool kKeyRotation = true; // whether key rotation is enabled -const char kCryptoMode[] = +namespace { + +constexpr int kEcmVersion = 3; // Choices are 2 and 3. + +constexpr size_t kContentIvSize = 16; // 8 or 16 +constexpr bool kKeyRotation = true; // whether key rotation is enabled +constexpr char kCryptoMode[] = "AesScte"; // "AesCbc", "AesCtr", "DvbCsa2", "DvbCsa3", "AesOfb", "AesScte" -const int kEcmPid = 149; // PID for the ECM packet -const int kAgeRestriction = 0; // Age restriction for the ECM -const char kOutputFile[] = - "/tmp/ecm.ts"; // ECM TS packet will be output to here -const uint8_t kTableId = 0x80; // 0x80 or 0x81 -const char kDefaultTrackTypeSd[] = "SD"; +constexpr int kEcmPid = 149; // PID for the ECM packet +constexpr int kAgeRestriction = 0; // Age restriction for the ECM +constexpr char kOutputFile[] = + "/tmp/ecm.ts"; // ECM TS packet will be output to here +constexpr uint8_t kTableId = 0x80; // 0x80 or 0x81 +constexpr char kDefaultTrackTypeSd[] = "SD"; -const char kEvenKey[] = "even_key........"; // 16 bytes -const char kEvenKeyId[] = "even_key_id....."; // 16 bytes -const char kEvenContentIv8Bytes[] = "even_iv."; // 8 bytes -const char kEvenContentIv16Bytes[] = "even_iv.even_iv."; // 16 bytes -const char kEvenEntitlementKeyId[] = "fake_key_id1...."; // 16 bytes -const char kEvenEntitlementKey[] = - "fakefakefakefakefakefakefake1..."; // 32 bytes -const char kEvenWrapIv[] = "even_warp_iv...."; // 16 bytes +constexpr char kEvenKey[] = "even_key........"; // 16 bytes +constexpr char kEvenKeyId[] = "even_key_id....."; // 16 bytes +constexpr char kEvenContentIv8Bytes[] = "even_iv."; // 8 bytes +constexpr char kEvenContentIv16Bytes[] = "even_iv.even_iv."; // 16 bytes +constexpr char kEvenEntitlementKeyId[] = "fake_key_id1...."; // 16 bytes +constexpr char kEvenEntitlementKey[] = + "fakefakefakefakefakefakefake1..."; // 32 bytes +constexpr char kEvenWrapIv[] = "even_warp_iv...."; // 16 bytes -const char kOddKey[] = "odd_key........."; // 16 bytes -const char kOddKeyId[] = "odd_key_id......"; // 16 bytes -const char kOddContentIv8Bytes[] = "odd_iv.."; // 8 bytes -const char kOddContentIv16Bytes[] = "odd_iv..odd_iv.."; // 16 bytes -const char kOddEntitlementKeyId[] = "fake_key_id2...."; // 16 bytes -const char kOddEntitlementKey[] = - "fakefakefakefakefakefakefake2..."; // 32 bytes -const char kOddWrapIv[] = "odd_warp_iv....."; // 16 bytes +constexpr char kOddKey[] = "odd_key........."; // 16 bytes +constexpr char kOddKeyId[] = "odd_key_id......"; // 16 bytes +constexpr char kOddContentIv8Bytes[] = "odd_iv.."; // 8 bytes +constexpr char kOddContentIv16Bytes[] = "odd_iv..odd_iv.."; // 16 bytes +constexpr char kOddEntitlementKeyId[] = "fake_key_id2...."; // 16 bytes +constexpr char kOddEntitlementKey[] = + "fakefakefakefakefakefakefake2..."; // 32 bytes +constexpr char kOddWrapIv[] = "odd_warp_iv....."; // 16 bytes + +// Fingerprinting and service blocking are only available with ECM v3+. +constexpr char kFingerprintingControl[] = "ctr"; +constexpr char kServiceBlockingDeviceGroup1[] = "group"; +constexpr char kServiceBlockingDeviceGroup2[] = "g2"; +constexpr unsigned char kTestECPrivateKey2Secp256r1[] = { + 0x30, 0x77, 0x02, 0x01, 0x01, 0x04, 0x20, 0x34, 0x9a, 0xf2, 0x95, + 0x94, 0xd4, 0xca, 0xb9, 0xa0, 0x81, 0xe4, 0x1c, 0xf5, 0xde, 0x8d, + 0x23, 0xf6, 0x79, 0xba, 0x3c, 0x6e, 0xc9, 0x0b, 0x56, 0x0f, 0x07, + 0x5e, 0x9f, 0xe9, 0x38, 0x18, 0xfc, 0xa0, 0x0a, 0x06, 0x08, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0xa1, 0x44, 0x03, 0x42, + 0x00, 0x04, 0x7b, 0x2a, 0x61, 0x59, 0xe5, 0x1b, 0xb6, 0x30, 0x7b, + 0x59, 0x98, 0x42, 0x59, 0x37, 0xfb, 0x46, 0xfe, 0x53, 0xfe, 0x32, + 0xa1, 0x5c, 0x93, 0x36, 0x11, 0xb0, 0x5a, 0xae, 0xa4, 0x48, 0xe3, + 0x20, 0x12, 0xce, 0x78, 0xa7, 0x7f, 0xfd, 0x73, 0x5e, 0x09, 0x77, + 0x53, 0x77, 0x8f, 0xd6, 0x1b, 0x26, 0xfa, 0xc4, 0x2c, 0xc4, 0x11, + 0xfa, 0x72, 0x6a, 0xbe, 0x94, 0x78, 0x4d, 0x74, 0x20, 0x27, 0x86}; const size_t kTsPacketSize = 188; +const size_t kMaxPossibleTsPacketsSizeBytes = 6 * kTsPacketSize; +using widevine::cas::EcmVersion; using widevine::cas::EntitlementKeyInfo; using widevine::cas::WvCasContentKeyInfo; using widevine::cas::WvCasEcmParameters; @@ -68,6 +91,9 @@ WvCasEcmParameters CreateWvCasEcmParameters(bool key_rotation, std::cerr << "Unsupported crypto mode " << kCryptoMode << std::endl; } params.age_restriction = kAgeRestriction; + params.ecm_version = kEcmVersion <= 2 ? EcmVersion::kV2 : EcmVersion::kV3; + params.ecc_private_signing_key.assign(std::begin(kTestECPrivateKey2Secp256r1), + std::end(kTestECPrivateKey2Secp256r1)); return params; } @@ -115,6 +141,8 @@ std::vector CreateContentKeyInfo(bool key_rotation, return content_keys; } +} // namespace + int main(int argc, char** argv) { WvCasEcmParameters params = CreateWvCasEcmParameters(kKeyRotation, kContentIvSize); @@ -122,6 +150,17 @@ int main(int argc, char** argv) { CreateInjectedEntitlements(kKeyRotation); widevine::cas::WvCasEcm wv_cas_ecm(params, entitlements); + if (params.ecm_version == EcmVersion::kV3) { + widevine::cas::EcmFingerprintingParams fingerprinting_params; + fingerprinting_params.control = kFingerprintingControl; + wv_cas_ecm.SetFingerprinting(&fingerprinting_params); + + widevine::cas::EcmServiceBlockingParams service_blocking_params; + service_blocking_params.device_groups = {kServiceBlockingDeviceGroup1, + kServiceBlockingDeviceGroup2}; + wv_cas_ecm.SetServiceBlocking(&service_blocking_params); + } + std::vector content_keys = CreateContentKeyInfo(kKeyRotation, kContentIvSize); std::string ecm; @@ -147,16 +186,17 @@ int main(int argc, char** argv) { std::cout << std::endl; } // Generate ECM TS Packet. - uint8_t packet[kTsPacketSize]; + uint8_t packet[kMaxPossibleTsPacketsSizeBytes]; + ssize_t packet_size = kMaxPossibleTsPacketsSizeBytes; uint8_t continuity_counter; // not used. - status = wv_cas_ecm.GenerateTsPacket(ecm, kEcmPid, kTableId, - &continuity_counter, packet); + status = wv_cas_ecm.GenerateTsPacket( + ecm, kEcmPid, kTableId, &continuity_counter, packet, &packet_size); if (!status.ok()) { std::cerr << "Failed to create ECM TS packet: " << status << std::endl; return -1; } else { std::cout << "TS packet bytes: "; - for (size_t i = 0; i < kTsPacketSize; i++) { + for (size_t i = 0; i < packet_size; i++) { printf("'\\x%02x', ", static_cast(packet[i])); } std::cout << std::endl; @@ -165,7 +205,7 @@ int main(int argc, char** argv) { std::ofstream file; file.open(kOutputFile, std::ios_base::binary); assert(file.is_open()); - file.write(reinterpret_cast(packet), kTsPacketSize); + file.write(reinterpret_cast(packet), packet_size); file.close(); return 0; diff --git a/example/wv_ecmg_example.cc b/example/wv_ecmg_example.cc index f0cfd6f..409f15d 100644 --- a/example/wv_ecmg_example.cc +++ b/example/wv_ecmg_example.cc @@ -34,6 +34,18 @@ using widevine::cas::WvCasEcmgClientHandler; constexpr int kServerPortNumber = 1234; constexpr int kListenQueueSize = 20; constexpr int kBufferSizeBytes = 2048; +constexpr unsigned char kTestECPrivateKey2Secp256r1[] = { + 0x30, 0x77, 0x02, 0x01, 0x01, 0x04, 0x20, 0x34, 0x9a, 0xf2, 0x95, + 0x94, 0xd4, 0xca, 0xb9, 0xa0, 0x81, 0xe4, 0x1c, 0xf5, 0xde, 0x8d, + 0x23, 0xf6, 0x79, 0xba, 0x3c, 0x6e, 0xc9, 0x0b, 0x56, 0x0f, 0x07, + 0x5e, 0x9f, 0xe9, 0x38, 0x18, 0xfc, 0xa0, 0x0a, 0x06, 0x08, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0xa1, 0x44, 0x03, 0x42, + 0x00, 0x04, 0x7b, 0x2a, 0x61, 0x59, 0xe5, 0x1b, 0xb6, 0x30, 0x7b, + 0x59, 0x98, 0x42, 0x59, 0x37, 0xfb, 0x46, 0xfe, 0x53, 0xfe, 0x32, + 0xa1, 0x5c, 0x93, 0x36, 0x11, 0xb0, 0x5a, 0xae, 0xa4, 0x48, 0xe3, + 0x20, 0x12, 0xce, 0x78, 0xa7, 0x7f, 0xfd, 0x73, 0x5e, 0x09, 0x77, + 0x53, 0x77, 0x8f, 0xd6, 0x1b, 0x26, 0xfa, 0xc4, 0x2c, 0xc4, 0x11, + 0xfa, 0x72, 0x6a, 0xbe, 0x94, 0x78, 0x4d, 0x74, 0x20, 0x27, 0x86}; void BuildEcmgConfig(EcmgConfig* config) { config->delay_start = 200; // in milliseconds. @@ -43,6 +55,9 @@ void BuildEcmgConfig(EcmgConfig* config) { config->access_criteria_transfer_mode = 0; config->number_of_content_keys = 2; config->crypto_mode = widevine::cas::CryptoMode::kAesCtr; + config->ecc_private_signing_key.assign( + std::begin(kTestECPrivateKey2Secp256r1), + std::end(kTestECPrivateKey2Secp256r1)); } void PrintMessage(const std::string& description, const char* const message, diff --git a/media_cas_packager_sdk/internal/BUILD b/media_cas_packager_sdk/internal/BUILD index 7c3bd3f..ca747d7 100644 --- a/media_cas_packager_sdk/internal/BUILD +++ b/media_cas_packager_sdk/internal/BUILD @@ -25,9 +25,14 @@ cc_library( srcs = ["ecm.cc"], hdrs = ["ecm.h"], deps = [ + ":ecm_serializer_cc", ":util", "//base", + "@abseil_repo//absl/memory", "@abseil_repo//absl/strings", + "@abseil_repo//absl/synchronization", + "@abseil_repo//absl/types:optional", + "@abseil_repo//absl/types:span", "//common:aes_cbc_util", "//common:status", "//common:string_util", @@ -41,8 +46,12 @@ cc_test( srcs = ["ecm_test.cc"], deps = [ ":ecm", + ":ecm_serializer_cc", ":mpeg2ts", "//testing:gunit_main", + "@abseil_repo//absl/memory", + "//common:status", + "//media_cas_packager_sdk/public:wv_cas_types", ], ) @@ -92,6 +101,7 @@ cc_library( "@abseil_repo//absl/memory", "@abseil_repo//absl/strings", "@abseil_repo//absl/types:optional", + "@abseil_repo//absl/types:span", "//common:crypto_util", "//common:random_util", "//common:status", @@ -104,6 +114,7 @@ cc_test( size = "small", srcs = ["ecmg_client_handler_test.cc"], deps = [ + ":ecm", ":ecmg_client_handler", ":simulcrypt_util", ":util", @@ -231,6 +242,7 @@ cc_library( ":ts_packet", "//base", "@abseil_repo//absl/strings", + "@abseil_repo//absl/types:span", "//common:status", ], ) @@ -243,9 +255,7 @@ cc_test( ], deps = [ ":util", - "//base", "//testing:gunit_main", - "//common:status", ], ) @@ -258,6 +268,7 @@ cc_library( "//base", "@abseil_repo//absl/strings", "@abseil_repo//absl/time", + "@abseil_repo//absl/types:span", "//common:ec_key", "//common:hash_algorithm", "//common:status", @@ -266,6 +277,64 @@ cc_library( ], ) +cc_library( + name = "ecm_serializer_v2", + srcs = ["ecm_serializer_v2.cc"], + hdrs = ["ecm_serializer_v2.h"], + deps = [ + ":ecm_serializer", + "//base", + "@abseil_repo//absl/strings", + "//common:status", + "//common:string_util", + "//media_cas_packager_sdk/public:wv_cas_types", + ], +) + +cc_library( + name = "ecm_serializer", + hdrs = ["ecm_serializer.h"], + deps = [ + "//base", + "@abseil_repo//absl/types:optional", + "//common:status", + "//media_cas_packager_sdk/public:wv_cas_types", + ], +) + +cc_library( + name = "ecm_serializer_v3", + srcs = ["ecm_serializer_v3.cc"], + hdrs = ["ecm_serializer_v3.h"], + deps = [ + ":ecm_serializer", + "//base", + "@abseil_repo//absl/status", + "@abseil_repo//absl/status:statusor", + "@abseil_repo//absl/strings", + "//common:ec_key", + "//common:hash_algorithm", + "//common:status", + "//media_cas_packager_sdk/public:wv_cas_types", + "//protos/public:media_cas_cc_proto", + ], +) + +cc_library( + name = "ecm_serializer_cc", + srcs = ["ecm_serializer.cc"], + hdrs = ["ecm_serializer.h"], + deps = [ + ":ecm_serializer_v2", + ":ecm_serializer_v3", + "//base", + "@abseil_repo//absl/memory", + "@abseil_repo//absl/types:optional", + "//common:status", + "//media_cas_packager_sdk/public:wv_cas_types", + ], +) + cc_test( name = "emm_test", srcs = ["emm_test.cc"], @@ -282,3 +351,39 @@ cc_test( "//common:status", ], ) + +cc_test( + name = "ecm_serializer_v2_test", + srcs = ["ecm_serializer_v2_test.cc"], + deps = [ + ":ecm_serializer_v2", + "//testing:gunit_main", + "@abseil_repo//absl/strings", + "//common:status", + ], +) + +cc_test( + name = "ecm_serializer_v3_test", + srcs = ["ecm_serializer_v3_test.cc"], + deps = [ + ":ecm_serializer_v3", + "//testing:gunit_main", + "@abseil_repo//absl/types:optional", + "//common:ec_key", + "//common:ec_test_keys", + "//common:hash_algorithm", + "//common:status", + "//media_cas_packager_sdk/public:wv_cas_types", + "//protos/public:media_cas_cc_proto", + ], +) + +cc_test( + name = "ecm_serializer_test", + srcs = ["ecm_serializer_test.cc"], + deps = [ + ":ecm_serializer_cc", + "//testing:gunit_main", + ], +) diff --git a/media_cas_packager_sdk/internal/ecm.cc b/media_cas_packager_sdk/internal/ecm.cc index e9a2e0d..3cacd6a 100644 --- a/media_cas_packager_sdk/internal/ecm.cc +++ b/media_cas_packager_sdk/internal/ecm.cc @@ -13,6 +13,7 @@ #include #include "glog/logging.h" +#include "absl/memory/memory.h" #include "absl/strings/str_cat.h" #include "common/aes_cbc_util.h" #include "common/string_util.h" @@ -24,81 +25,16 @@ namespace cas { namespace { -// ECM constants -static constexpr size_t kMaxEcmSizeBytes = 184; +constexpr size_t kKeyIdSizeBytes = 16; +constexpr size_t kKeyDataSizeBytes = 16; +constexpr size_t kWrappedKeyIvSizeBytes = 16; +constexpr size_t kWrappingKeySizeBytes = 32; // entitlement key -// Bitfield lengths for the mp2ts ecm payload. -// This ECM structure is specific to the Widevine CAS implementation. - -// Byte 0, 1 -static constexpr int kNumBitsCaSystemIdField = 16; - -// Byte 2 -static constexpr int kNumBitsEcmVersionField = 8; - -// Byte 3 -// Unused bits (mbz, must be zero) -static constexpr int kNumBitsUnusedFieldByte3 = 3; -// Values for Decrypt Mode are from enum CryptoMode -static constexpr int kNumBitsDecryptModeField = 4; -static constexpr int kNumBitsRotationEnabledField = 1; - -// Byte 4 -// Size of IVs. -// Values for IV size fields are from enum EcmIvSize -// Note: Wrapped key IV size is always 16. The field is encoded, but it must -// always be set to 1. -static constexpr int kNumBitsWrappedKeyIvSizeField = 1; -static constexpr int kNumBitsContentIvSizeField = 1; -static constexpr int kNumBitsAgeRestriction = 5; -// Unused bits (mbz, must be zero) -static constexpr int kNumBitsUnusedFieldByte4 = 1; - -// Total bits equals number of bytes * 8. -static constexpr int kBitSetTotalSize = 40; - -// Remaining bytes (starting from the 6th byte) hold entitled key info. -static constexpr size_t kKeyIdSizeBytes = 16; -static constexpr size_t kKeyDataSizeBytes = 16; -static constexpr size_t kWrappedKeyIvSizeBytes = 16; -static constexpr size_t kWrappingKeySizeBytes = 32; // entitlement key -static constexpr size_t kWrappingKeyIvSizeBytes = 16; - -// BitField constants for the ECM payload - -// CA System ID for Widevine. From table in -// https://en.wikipedia.org/wiki/Conditional_access -// This should be the only file found. -static constexpr int kWvCasCaSystemId = 0x4AD4; - -// Version - this should be incremented if there are non-backwards compatible -// changes to the ECM. -static constexpr int kEcmVersion = 2; - -// Settings for RotationEnabled field. -static constexpr int kRotationDisabled = 0; -static constexpr int kRotationEnabled = 1; - -// Convert from boolean for rotation to rotation field value. -int RotationFieldValue(bool rotation_required) { - return rotation_required ? kRotationEnabled : kRotationDisabled; -} - -// Setting for Unused field. -static constexpr int kUnusedZero = 0; - -// Convert from IV size to IV size field value. -int IvSizeFieldValue(size_t iv_size) { - return (8 == iv_size) ? kIvSize8 : kIvSize16; -} - -void SerializeKeyInfo(const EntitledKeyInfo& key_info, std::string* ecm_buf) { - absl::StrAppend(ecm_buf, key_info.entitlement_key_id); - absl::StrAppend(ecm_buf, key_info.key_id); - absl::StrAppend(ecm_buf, key_info.wrapped_key_value); - absl::StrAppend(ecm_buf, key_info.wrapped_key_iv); - absl::StrAppend(ecm_buf, key_info.content_iv); -} +// CA System ID for Widevine. +constexpr uint16_t kWidevineSystemId = 0x4AD4; +// New CA System ID range for Widevine, all inclusive. +constexpr uint16_t kWidevineNewSystemIdLowerBound = 0x56C0; +constexpr uint16_t kWidevineNewSystemIdUpperBound = 0x56C9; bool ConvertIvSizeParam(EcmIvSize param, size_t* size) { if (param == kIvSize8) { @@ -131,6 +67,14 @@ Status Ecm::Initialize( crypto_mode_ = ecm_init_parameters.crypto_mode; paired_keys_required_ = ecm_init_parameters.key_rotation_enabled; age_restriction_ = ecm_init_parameters.age_restriction; + ecc_private_signing_key_ = ecm_init_parameters.ecc_private_signing_key; + + cas_id_ = ecm_init_parameters.cas_id; + if (cas_id_ != kWidevineSystemId && + (cas_id_ < kWidevineNewSystemIdLowerBound || + cas_id_ > kWidevineNewSystemIdUpperBound)) { + return {error::INVALID_ARGUMENT, "Invalid CA system ID."}; + } ClearEntitlementKeys(); for (const auto& entitlement : injected_entitlements) { @@ -143,91 +87,123 @@ Status Ecm::Initialize( "Improper injected entitlement keys."); } + if (ecm_init_parameters.ecm_version == EcmVersion::kV2) { + ecm_serializer_ = EcmSerializer::Create(EcmSerializerVersion::kV2); + } else { + ecm_serializer_ = EcmSerializer::Create(EcmSerializerVersion::kV3); + } + // Everything is set up including entitlement keys. initialized_ = true; return OkStatus(); } +void Ecm::SetFingerprinting(const EcmFingerprintingParams* fingerprinting) { + absl::WriterMutexLock lock(&ecm_params_mutex_); + fingerprinting_ = fingerprinting == nullptr + ? absl::nullopt + : absl::make_optional(*fingerprinting); +} + +void Ecm::SetServiceBlocking(const EcmServiceBlockingParams* service_blocking) { + absl::WriterMutexLock lock(&ecm_params_mutex_); + service_blocking_ = service_blocking == nullptr + ? absl::nullopt + : absl::make_optional(*service_blocking); +} + Status Ecm::GenerateEcm(EntitledKeyInfo* even_key, EntitledKeyInfo* odd_key, const std::string& track_type, std::string* serialized_ecm) const { + absl::ReaderMutexLock lock(&ecm_params_mutex_); + if (!initialized_) { return {error::INTERNAL, "Not initialized."}; } if (!HaveEntitlementKeys()) { return {error::INTERNAL, "Need entitlement key."}; } - if (!paired_keys_required_) { + if (even_key == nullptr) { + return {error::INVALID_ARGUMENT, "even_key must not be null."}; + } + if (!paired_keys_required_ && odd_key != nullptr) { return {error::INVALID_ARGUMENT, - "Key rotation not enabled - use GenerateSingleKeyEcm()."}; + "odd_key should be null as key rotation is disabled."}; + } + if (paired_keys_required_ && odd_key == nullptr) { + return {error::INVALID_ARGUMENT, + "odd_key can not be null as key rotation is enabled."}; } - std::vector keys; - keys.push_back(even_key); - keys.push_back(odd_key); + if (serialized_ecm == nullptr) { + return {error::INVALID_ARGUMENT, "No return ecm std::string pointer."}; + } - return GenerateEcmCommon(keys, track_type, serialized_ecm); + std::vector keys = {even_key}; + if (odd_key != nullptr) { + keys.push_back(odd_key); + } + Status status = WrapEntitledKeys(track_type, keys); + if (!status.ok()) { + return status; + } + + EcmSerializerParams serializer_params; + serializer_params.crypto_mode = crypto_mode_; + serializer_params.age_restriction = age_restriction_; + serializer_params.cas_id = cas_id_; + serializer_params.even_key = {even_key->entitlement_key_id, even_key->key_id, + even_key->wrapped_key_value, + even_key->wrapped_key_iv, even_key->content_iv}; + if (odd_key != nullptr) { + serializer_params.odd_key = {odd_key->entitlement_key_id, odd_key->key_id, + odd_key->wrapped_key_value, + odd_key->wrapped_key_iv, odd_key->content_iv}; + } + + serializer_params.fingerprinting = fingerprinting_; + serializer_params.service_blocking = service_blocking_; + serializer_params.ecc_private_signing_key = ecc_private_signing_key_; + + status = ecm_serializer_->SerializeEcm(serializer_params, serialized_ecm); + if (!status.ok()) { + return status; + } + + return OkStatus(); } Status Ecm::GenerateSingleKeyEcm(EntitledKeyInfo* key, const std::string& track_type, std::string* serialized_ecm) const { - if (!initialized_) { - return {error::INTERNAL, "Not initialized."}; - } - if (!HaveEntitlementKeys()) { - return {error::INTERNAL, "Need entitlement key."}; - } - if (paired_keys_required_) { - return {error::INVALID_ARGUMENT, - "Key rotation enabled - use GenerateEcm()."}; - } - - std::vector keys; - keys.push_back(key); - - return GenerateEcmCommon(keys, track_type, serialized_ecm); -} - -Status Ecm::GenerateEcmCommon(const std::vector& keys, - const std::string& track_type, - std::string* serialized_ecm) const { - if (serialized_ecm == nullptr) { - return {error::INVALID_ARGUMENT, "No return ecm std::string pointer."}; - } - - Status status = ValidateKeys(keys); - if (!status.ok()) { - return status; - } - - status = WrapEntitledKeys(track_type, keys); - if (!status.ok()) { - return status; - } - - status = ValidateWrappedKeys(keys); - if (!status.ok()) { - return status; - } - - // TODO(user): validate inputs, compare against current values - // TODO(user): replace current values. - - // Generate TS packet payload for ECM, pass back to caller. - serialized_ecm->assign(SerializeEcm(keys)); - return OkStatus(); + return GenerateEcm(key, nullptr, track_type, serialized_ecm); } Status Ecm::GenerateTsPacket(const std::string& ecm, uint16_t pid, uint8_t table_id, - uint8_t* continuity_counter, uint8_t* packet) { - ssize_t bytes_modified = 0; - Status status = InsertEcmAsTsPacket(ecm, pid, table_id, continuity_counter, - packet, &bytes_modified); - if (!status.ok() || bytes_modified != kTsPacketSize) { - memset(packet, 0, kTsPacketSize); - return {error::INTERNAL, "Failed to generate TS packet"}; + uint8_t* continuity_counter, + absl::Span packet, + ssize_t* bytes_modified) { + if (ecm.empty()) { + return {error::INVALID_ARGUMENT, "ecm must not be empty"}; } + if (continuity_counter == nullptr || bytes_modified == nullptr) { + return {error::INVALID_ARGUMENT, + "continuity_counter and bytes_modified must not be null"}; + } + + size_t buf_size_needed = CalculateTsBufferSizeForEcmEmm(ecm.size()); + if (packet.size() < buf_size_needed) { + return {error::INVALID_ARGUMENT, + absl::StrCat("Packet buffer is too small. Minimum required: ", + buf_size_needed, " bytes.")}; + } + + Status status = InsertEcmAsTsPacket(ecm, pid, table_id, continuity_counter, + packet.subspan(0, buf_size_needed)); + if (!status.ok()) { + return status; + } + *bytes_modified += buf_size_needed; return OkStatus(); } @@ -274,7 +250,7 @@ Status Ecm::WrapKey(const std::string& wrapping_key, if (!status.ok()) { return status; } - status = ValidateIv(wrapping_iv, kWrappingKeyIvSizeBytes); + status = ValidateIv(wrapping_iv, kWrappedKeyIvSizeBytes); if (!status.ok()) { return status; } @@ -291,54 +267,6 @@ Status Ecm::WrapKey(const std::string& wrapping_key, return OkStatus(); } -Status Ecm::ValidateKeys(const std::vector& keys) const { - for (const auto& key : keys) { - Status status; - status = ValidateKeyId(key->key_id); - if (!status.ok()) { - return status; - } - status = ValidateKeyValue(key->key_value, kKeyDataSizeBytes); - if (!status.ok()) { - return status; - } - status = ValidateIv(key->content_iv, content_iv_size_); - if (!status.ok()) { - return status; - } - status = ValidateIv(key->wrapped_key_iv, kWrappedKeyIvSizeBytes); - if (!status.ok()) { - return status; - } - } - return OkStatus(); -} - -Status Ecm::ValidateWrappedKeys( - const std::vector& keys) const { - for (const auto& key : keys) { - Status status; - status = ValidateKeyId(key->key_id); - if (!status.ok()) { - return status; - } - status = ValidateKeyValue(key->wrapped_key_value, kKeyDataSizeBytes); - if (!status.ok()) { - LOG(ERROR) << "Wrapped key is bad."; - return status; - } - status = ValidateIv(key->wrapped_key_iv, kWrappedKeyIvSizeBytes); - if (!status.ok()) { - return status; - } - status = ValidateIv(key->content_iv, content_iv_size_); - if (!status.ok()) { - return status; - } - } - return OkStatus(); -} - Status Ecm::ValidateKeyId(const std::string& key_id) const { if (key_id.size() != kKeyIdSizeBytes) { return {error::INVALID_ARGUMENT, "Key ID must be 16 bytes."}; @@ -363,47 +291,6 @@ Status Ecm::ValidateIv(const std::string& iv, size_t size) const { return OkStatus(); } -std::string Ecm::SerializeEcm(const std::vector& keys) const { - // Five bytes (40 bits including padding) - std::bitset ca_system_id(kWvCasCaSystemId); - std::bitset ecm_version(kEcmVersion); - std::bitset unused(kUnusedZero); - std::bitset decrypt_mode( - static_cast(crypto_mode())); - std::bitset rotation_enabled( - RotationFieldValue(paired_keys_required())); - std::bitset wrapped_key_iv_size( - IvSizeFieldValue(kWrappedKeyIvSizeBytes)); - std::bitset content_iv_size( - IvSizeFieldValue(this->content_iv_size())); - std::bitset age_restriction( - static_cast(this->age_restriction())); - std::bitset padding(kUnusedZero); - - // Converts bitset to string. - std::string ecm_bitset = absl::StrCat( - ca_system_id.to_string(), ecm_version.to_string(), unused.to_string(), - decrypt_mode.to_string(), rotation_enabled.to_string(), - wrapped_key_iv_size.to_string(), content_iv_size.to_string(), - age_restriction.to_string(), padding.to_string()); - if (ecm_bitset.size() != kBitSetTotalSize) { - LOG(FATAL) << "ECM bitset incorrect size: " << ecm_bitset.size(); - } - std::string serialized_ecm; - Status status = - string_util::BitsetStringToBinaryString(ecm_bitset, &serialized_ecm); - if (!status.ok() || serialized_ecm.empty()) { - LOG(FATAL) << "Failed to convert ECM bitset to std::string"; - } - - // Appends entitled key info. - for (const auto& key : keys) { - SerializeKeyInfo(*key, &serialized_ecm); - } - - return serialized_ecm; -} - size_t Ecm::CountEntitlementKeys() const { size_t count = 0; for (const auto& track : entitlement_keys_) { @@ -414,9 +301,9 @@ size_t Ecm::CountEntitlementKeys() const { bool Ecm::CheckEntitlementKeys() const { for (const auto& track : entitlement_keys_) { - if (track.second.size() < (paired_keys_required() ? 2 : 1)) { + if (track.second.size() < (paired_keys_required_ ? 2 : 1)) { LOG(ERROR) << " Wrong number of entitlement keys for track " - << track.first << ": " << (paired_keys_required() ? 2 : 1) + << track.first << ": " << (paired_keys_required_ ? 2 : 1) << " expected, " << track.second.size() << " received."; return false; } diff --git a/media_cas_packager_sdk/internal/ecm.h b/media_cas_packager_sdk/internal/ecm.h index fea7db5..34a4d20 100644 --- a/media_cas_packager_sdk/internal/ecm.h +++ b/media_cas_packager_sdk/internal/ecm.h @@ -12,12 +12,17 @@ #include #include #include +#include #include #include #include #include +#include "absl/synchronization/mutex.h" +#include "absl/types/optional.h" +#include "absl/types/span.h" #include "common/status.h" +#include "media_cas_packager_sdk/internal/ecm_serializer.h" #include "media_cas_packager_sdk/public/wv_cas_types.h" namespace widevine { @@ -39,22 +44,22 @@ struct EntitledKeyInfo { }; // Information needed to start a new ECM stream. -// Fields: -// |content_iv_size| size of all content key IVs in the ECM stream. -// A constant of type EcmIvSize specifying 8 or 16. -// |key_rotation_enabled| the encryption uses multiple keys in rotation. -// |crypto_mode| the encryption mode used for the content stream. -// A constant of type CryptoMode. -// |track_types| a vector of track ID (std::string) that specify the set of track -// types of interest. |track_types| is deprecated and is ignored by -// Ecm::Initialize(). -// |age_restriction| minimum age required; the value represents actual age. struct EcmInitParameters { + // Size of all content key IVs in the ECM stream. EcmIvSize content_iv_size = kIvSize8; + // The encryption uses multiple keys in rotation or not. bool key_rotation_enabled = true; + // The encryption mode used for the content stream. CryptoMode crypto_mode = CryptoMode::kAesCtr; - std::vector track_types; // deprecated. + // minimum age required; the value represents actual age. uint8_t age_restriction = 0; + // CA system id that is in the ECM. Must be 0x4AD4 or 0x56C0~0x56C9 (all + // inclusive). + uint16_t cas_id = 0x4AD4; + EcmVersion ecm_version = EcmVersion::kV2; + // Private signing key used to sign ECM data. Must be an elliptic-curve + // cryptography key. + std::string ecc_private_signing_key; }; // Generator for producing Widevine CAS-compliant ECMs. Used to construct the @@ -86,8 +91,22 @@ class Ecm { const EcmInitParameters& ecm_init_parameters, const std::vector& injected_entitlements); + // Set fingerprinting info that will be embedded into the generated ECM. The + // configuration will be used in all following ECMs generated by calling + // GenerateEcm() or GenerateSingleKeyEcm(). + // |fingerprinting| may be set to nullptr to clear the fingerprinting info. + virtual void SetFingerprinting(const EcmFingerprintingParams* fingerprinting); + + // Set service blocking info that will be embedded into the generated ECM. The + // configuration will be used in all following ECMs generated by calling + // GenerateEcm() or GenerateSingleKeyEcm(). + // |service_blocking| may be set to nullptr to clear the service blocking + // info. + virtual void SetServiceBlocking( + const EcmServiceBlockingParams* service_blocking); + // Accept keys and IVs and construct an ECM that will fit into a Transport - // Stream packet payload (184 bytes). + // Stream packet payload. // Args: // |even_key| information for even key to be encoded into ECM. // |odd_key| information for odd key to be encoded into ECM. @@ -103,8 +122,8 @@ class Ecm { std::string* serialized_ecm) const; // Accept a key and IV and construct an ECM that will fit into a Transport - // Stream packet payload (184 bytes). This call is specifically for the case - // where key rotation is disabled. + // Stream packet payload. This call is specifically for the case where key + // rotation is disabled. // Args: // |key| information for key to be encoded into ECM. // |track_type| the track that the key is being used to encrypt. @@ -128,20 +147,23 @@ class Ecm { // table ID so the client knows this is an important ECM it should process. // - |continuity_counter| continuity_counter for the ECM packet, // it will be incremented, only last 4 bits are used - // - |packet| a buffer of size at least 188 bytes to be used to return - // the generated TS packet + // - |packet| is the output buffer. Must be big enough to hold an additional + // 188 bytes * number of TS packets needed to put |ecm|. + // (kMaxPossibleTsPacketsSizeBytes = 1128 bytes). + // - |bytes_modified| will be incremented by the used buffer size if insertion + // of ECM into |buffer| is successful. // // Returns: // - A status indicating whether there was any error during processing static Status GenerateTsPacket(const std::string& ecm, uint16_t pid, uint8_t table_id, uint8_t* continuity_counter, - uint8_t* packet); + const absl::Span packet, + ssize_t* bytes_modified); protected: // For unit tests. - // Take the input entitled |keys| and our current state, and generate - // the ECM representing the keys. - virtual std::string SerializeEcm( - const std::vector& keys) const; + virtual void SetEcmSerializer(std::unique_ptr ecm_serializer) { + ecm_serializer_ = std::move(ecm_serializer); + } // Apply the Entitlement key with the given track type and polarity // (even vs. odd key) to wrap the given Entitled key. @@ -189,11 +211,6 @@ class Ecm { } } - // Common helper for GenerateEcm() and GenerateSingleKeyEcm() - virtual Status GenerateEcmCommon(const std::vector& keys, - const std::string& track_type, - std::string* serialized_ecm) const; - // Wrap |key_value| using |wrapping_key| (entitlement key) and |wrapping_iv|. // Returns the resulting wrapped key in |wrapped_key|. // Return a status indicating whether there has been any error. @@ -202,36 +219,32 @@ class Ecm { const std::string& key_value, std::string* wrapped_key) const; - virtual Status ValidateKeys(const std::vector& keys) const; - virtual Status ValidateWrappedKeys( - const std::vector& keys) const; - Status ValidateKeyId(const std::string& key_id) const; Status ValidateKeyValue(const std::string& key_value, size_t key_value_size) const; Status ValidateIv(const std::string& iv, size_t size) const; - virtual CryptoMode crypto_mode() const { return crypto_mode_; } - virtual uint8_t age_restriction() const { return age_restriction_; } - virtual bool paired_keys_required() const { return paired_keys_required_; } - virtual size_t content_iv_size() const { return content_iv_size_; } - // Set to true when the object has been properly initialized. bool initialized_ = false; - // Content ID for this ECM stream. - std::string content_id_; - // Provider ID for this ECM stream. - std::string content_provider_; // Content IV size may be 8 or 16. Size is set once during Initialize(). size_t content_iv_size_ = 8; // Remember if a pair of keys is required (for key rotation). bool paired_keys_required_ = false; CryptoMode crypto_mode_ = CryptoMode::kAesCtr; uint8_t age_restriction_ = 0; + uint16_t cas_id_ = 0; + std::string ecc_private_signing_key_; // Entitlement keys needed for ECM generation. // The keys are added when the CasEncryptionResponse is processed. // Maps from track_type to one/two EntitlementKeyIdValue with even key first. std::map> entitlement_keys_; + std::unique_ptr ecm_serializer_; + + mutable absl::Mutex ecm_params_mutex_; + absl::optional fingerprinting_ + ABSL_GUARDED_BY(ecm_params_mutex_); + absl::optional service_blocking_ + ABSL_GUARDED_BY(ecm_params_mutex_); }; } // namespace cas diff --git a/media_cas_packager_sdk/internal/ecm_serializer.cc b/media_cas_packager_sdk/internal/ecm_serializer.cc new file mode 100644 index 0000000..3e8f63f --- /dev/null +++ b/media_cas_packager_sdk/internal/ecm_serializer.cc @@ -0,0 +1,30 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 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. +//////////////////////////////////////////////////////////////////////////////// +#include "media_cas_packager_sdk/internal/ecm_serializer.h" + +#include "absl/memory/memory.h" +#include "media_cas_packager_sdk/internal/ecm_serializer_v2.h" +#include "media_cas_packager_sdk/internal/ecm_serializer_v3.h" + +namespace widevine { +namespace cas { + +std::unique_ptr EcmSerializer::Create( + EcmSerializerVersion version) { + switch (version) { + case EcmSerializerVersion::kV2: + return absl::make_unique(); + case EcmSerializerVersion::kV3: + return absl::make_unique(); + default: + return nullptr; + } +} + +} // namespace cas +} // namespace widevine diff --git a/media_cas_packager_sdk/internal/ecm_serializer.h b/media_cas_packager_sdk/internal/ecm_serializer.h new file mode 100644 index 0000000..0309080 --- /dev/null +++ b/media_cas_packager_sdk/internal/ecm_serializer.h @@ -0,0 +1,70 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 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. +//////////////////////////////////////////////////////////////////////////////// +// This file defines an interface class EcmSerializer that can be used to +// generate serialized ECMs. + +#ifndef MEDIA_CAS_PACKAGER_SDK_INTERNAL_ECM_SERIALIZER_H_ +#define MEDIA_CAS_PACKAGER_SDK_INTERNAL_ECM_SERIALIZER_H_ + +#include +#include + +#include +#include "absl/types/optional.h" +#include "common/status.h" +#include "media_cas_packager_sdk/public/wv_cas_types.h" + +namespace widevine { +namespace cas { + +// Content key infomartion used to generate serialized ecm. +struct EcmSerializerKeyInfo { + std::string entitlement_key_id; + // content_key_id has been deprecated since v3. + std::string content_key_id; + std::string wrapped_key_value; + std::string wrapped_key_iv; + std::string content_iv; +}; + +// Parameters passed into ECM serializer to generate ECM. +struct EcmSerializerParams { + uint16_t cas_id; + CryptoMode crypto_mode; + uint8_t age_restriction; + EcmSerializerKeyInfo even_key; + EcmSerializerKeyInfo odd_key; + absl::optional fingerprinting; + absl::optional service_blocking; + // Private signing key used to sign ECM data. Must be an elliptic-curve + // cryptography key. + std::string ecc_private_signing_key; +}; + +enum class EcmSerializerVersion { kV2 = 0, kV3 }; + +// Interface for different versions of ECM serializer. +class EcmSerializer { + public: + EcmSerializer() = default; + virtual ~EcmSerializer() = default; + static std::unique_ptr Create(EcmSerializerVersion version); + + // Generates a |serialize_ecm| based on input |params|. + // Returns a status inidicating the operation is success or not. + virtual Status SerializeEcm(const EcmSerializerParams& params, + std::string* serialized_ecm) const = 0; + + // Returns version of the serializer. + virtual EcmSerializerVersion Version() const = 0; +}; + +} // namespace cas +} // namespace widevine + +#endif // MEDIA_CAS_PACKAGER_SDK_INTERNAL_ECM_SERIALIZER_H_ diff --git a/media_cas_packager_sdk/internal/ecm_serializer_test.cc b/media_cas_packager_sdk/internal/ecm_serializer_test.cc new file mode 100644 index 0000000..10e8fce --- /dev/null +++ b/media_cas_packager_sdk/internal/ecm_serializer_test.cc @@ -0,0 +1,39 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 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. +//////////////////////////////////////////////////////////////////////////////// +#include "media_cas_packager_sdk/internal/ecm_serializer.h" + +#include + +#include "testing/gmock.h" +#include "testing/gunit.h" + +namespace widevine { +namespace cas { +namespace { + +class EcmSerializerCreateTest + : public testing::Test, + public testing::WithParamInterface {}; + +TEST_P(EcmSerializerCreateTest, VersionExpected) { + EcmSerializerVersion expected_version = GetParam(); + + std::unique_ptr serializer = + EcmSerializer::Create(expected_version); + + ASSERT_TRUE(serializer != nullptr); + EXPECT_EQ(serializer->Version(), expected_version); +} + +INSTANTIATE_TEST_SUITE_P(EcmSerializerCreateTest, EcmSerializerCreateTest, + testing::Values(EcmSerializerVersion::kV2, + EcmSerializerVersion::kV3)); + +} // namespace +} // namespace cas +} // namespace widevine diff --git a/media_cas_packager_sdk/internal/ecm_serializer_v2.cc b/media_cas_packager_sdk/internal/ecm_serializer_v2.cc new file mode 100644 index 0000000..d86e0e5 --- /dev/null +++ b/media_cas_packager_sdk/internal/ecm_serializer_v2.cc @@ -0,0 +1,192 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 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. +//////////////////////////////////////////////////////////////////////////////// +#include "media_cas_packager_sdk/internal/ecm_serializer_v2.h" + +#include + +#include "glog/logging.h" +#include "absl/strings/str_cat.h" +#include "common/status.h" +#include "common/string_util.h" +#include "media_cas_packager_sdk/public/wv_cas_types.h" + +namespace widevine { +namespace cas { + +namespace { + +constexpr int kEcmVersion = 2; +// Bitfield lengths for the mp2ts ecm payload. +// This ECM structure is specific to the Widevine CAS implementation. +// Byte 0, 1 +constexpr int kNumBitsCaSystemIdField = 16; + +// Byte 2 +constexpr int kNumBitsEcmVersionField = 8; + +// Byte 3 +// Unused bits (mbz, must be zero) +constexpr int kNumBitsUnusedFieldByte3 = 3; +// Values for Decrypt Mode are from enum CryptoMode +constexpr int kNumBitsDecryptModeField = 4; +constexpr int kNumBitsRotationEnabledField = 1; + +// Byte 4 +// Size of IVs. +// Values for IV size fields are from enum EcmIvSize +// Note: Wrapped key IV size is always 16. The field is encoded, but it must +// always be set to 1. +constexpr int kNumBitsWrappedKeyIvSizeField = 1; +constexpr int kNumBitsContentIvSizeField = 1; +constexpr int kNumBitsAgeRestriction = 5; +// Unused bits (mbz, must be zero) +constexpr int kNumBitsUnusedFieldByte4 = 1; + +// Total bits equals number of bytes * 8. +constexpr int kBitSetTotalSize = 40; + +// Remaining bytes (starting from the 6th byte) hold entitled key info. +constexpr size_t kKeyIdSizeBytes = 16; +constexpr size_t kKeyDataSizeBytes = 16; +constexpr size_t kWrappedKeyIvSizeBytes = 16; +constexpr size_t kContentIvSize8Bytes = 8; +constexpr size_t kContentIvSize16Bytes = 16; + +// Settings for RotationEnabled field. +constexpr int kRotationDisabled = 0; +constexpr int kRotationEnabled = 1; + +// Setting for Unused field. +constexpr int kUnusedZero = 0; + +// Convert from IV size to IV size field value. +int IvSizeFieldValue(size_t iv_size) { + return (iv_size == 8) ? kIvSize8 : kIvSize16; +} + +void SerializeKeyInfo(const EcmSerializerKeyInfo& key_info, + std::string* ecm_buf) { + absl::StrAppend(ecm_buf, key_info.entitlement_key_id); + absl::StrAppend(ecm_buf, key_info.content_key_id); + absl::StrAppend(ecm_buf, key_info.wrapped_key_value); + absl::StrAppend(ecm_buf, key_info.wrapped_key_iv); + absl::StrAppend(ecm_buf, key_info.content_iv); +} + +Status ValidateKeyInfo(const EcmSerializerKeyInfo& key_info) { + if (key_info.entitlement_key_id.size() != kKeyIdSizeBytes) { + return {error::INVALID_ARGUMENT, + absl::StrCat("Invalid entitlement_key_id size. Expect ", + kKeyIdSizeBytes, " but got ", + key_info.entitlement_key_id.size())}; + } + if (key_info.content_key_id.size() != kKeyIdSizeBytes) { + return { + error::INVALID_ARGUMENT, + absl::StrCat("Invalid content_key_id size. Expect ", kKeyIdSizeBytes, + " but got ", key_info.content_key_id.size())}; + } + if (key_info.wrapped_key_value.size() != kKeyDataSizeBytes) { + return {error::INVALID_ARGUMENT, + absl::StrCat("Invalid wrapped_key_value size. Expect ", + kKeyDataSizeBytes, " but got ", + key_info.wrapped_key_value.size())}; + } + if (key_info.wrapped_key_iv.size() != kWrappedKeyIvSizeBytes) { + return {error::INVALID_ARGUMENT, + absl::StrCat("Invalid wrapped_key_iv size. Expect ", + kWrappedKeyIvSizeBytes, " but got ", + key_info.wrapped_key_iv.size())}; + } + if ((key_info.content_iv.size() != kContentIvSize8Bytes) && + (key_info.content_iv.size() != kContentIvSize16Bytes)) { + return {error::INVALID_ARGUMENT, + absl::StrCat("Invalid content_iv size. Expect ", + kContentIvSize8Bytes, " or ", kContentIvSize16Bytes, + " but got ", key_info.content_iv.size())}; + } + return OkStatus(); +} + +bool HasNoneEmptyField(const EcmSerializerKeyInfo& key_info) { + return !(key_info.entitlement_key_id.empty() && + key_info.content_key_id.empty() && key_info.wrapped_key_iv.empty() && + key_info.wrapped_key_value.empty() && key_info.content_iv.empty()); +} + +} // namespace + +Status EcmSerializerV2::SerializeEcm(const EcmSerializerParams& params, + std::string* serialized_ecm) const { + if (serialized_ecm == nullptr) { + return {error::INVALID_ARGUMENT, "No return ecm std::string pointer."}; + } + + if (params.crypto_mode == CryptoMode::kInvalid) { + return {error::INVALID_ARGUMENT, "Invalid crypo mode."}; + } + Status status = ValidateKeyInfo(params.even_key); + if (!status.ok()) { + LOG(ERROR) << "Invalid even key info"; + return status; + } + bool has_odd_key = HasNoneEmptyField(params.odd_key); + if (has_odd_key) { + status = ValidateKeyInfo(params.odd_key); + if (!status.ok()) { + LOG(ERROR) << "Invalid odd key info"; + return status; + } + if (params.even_key.content_iv.size() != params.odd_key.content_iv.size()) { + return {error::INVALID_ARGUMENT, "Content IV size must match."}; + } + } + + // Five bytes (40 bits including padding) + std::bitset ca_system_id(params.cas_id); + std::bitset ecm_version(kEcmVersion); + std::bitset unused(kUnusedZero); + std::bitset decrypt_mode( + static_cast(params.crypto_mode)); + std::bitset rotation_enabled( + has_odd_key ? kRotationEnabled : kRotationDisabled); + std::bitset wrapped_key_iv_size( + IvSizeFieldValue(kWrappedKeyIvSizeBytes)); + std::bitset content_iv_size( + IvSizeFieldValue(params.even_key.content_iv.size())); + std::bitset age_restriction( + static_cast(params.age_restriction)); + std::bitset padding(kUnusedZero); + + // Converts bitset to string. + std::string ecm_bitset = absl::StrCat( + ca_system_id.to_string(), ecm_version.to_string(), unused.to_string(), + decrypt_mode.to_string(), rotation_enabled.to_string(), + wrapped_key_iv_size.to_string(), content_iv_size.to_string(), + age_restriction.to_string(), padding.to_string()); + if (ecm_bitset.size() != kBitSetTotalSize) { + LOG(ERROR) << "ECM bitset incorrect size: " << ecm_bitset.size(); + return {error::INTERNAL, "Failed to convert ECM header bitset."}; + } + + status = string_util::BitsetStringToBinaryString(ecm_bitset, serialized_ecm); + if (!status.ok() || serialized_ecm->empty()) { + LOG(ERROR) << "Failed to convert ECM bitset to std::string"; + return {error::INTERNAL, "Failed to convert ECM bitset to string."}; + } + + SerializeKeyInfo(params.even_key, serialized_ecm); + if (has_odd_key) { + SerializeKeyInfo(params.odd_key, serialized_ecm); + } + + return OkStatus(); +} + +} // namespace cas +} // namespace widevine diff --git a/media_cas_packager_sdk/internal/ecm_serializer_v2.h b/media_cas_packager_sdk/internal/ecm_serializer_v2.h new file mode 100644 index 0000000..5c4cdf7 --- /dev/null +++ b/media_cas_packager_sdk/internal/ecm_serializer_v2.h @@ -0,0 +1,39 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 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. +//////////////////////////////////////////////////////////////////////////////// +#ifndef MEDIA_CAS_PACKAGER_SDK_INTERNAL_ECM_SERIALIZER_V2_H_ +#define MEDIA_CAS_PACKAGER_SDK_INTERNAL_ECM_SERIALIZER_V2_H_ + +#include + +#include +#include "common/status.h" +#include "media_cas_packager_sdk/internal/ecm_serializer.h" + +namespace widevine { +namespace cas { + +// Generates serialized ECM version 2. +class EcmSerializerV2 : public EcmSerializer { + public: + EcmSerializerV2() = default; + EcmSerializerV2(const EcmSerializerV2&) = delete; + EcmSerializerV2& operator=(const EcmSerializerV2&) = delete; + ~EcmSerializerV2() override = default; + + Status SerializeEcm(const EcmSerializerParams& params, + std::string* serialized_ecm) const override; + + EcmSerializerVersion Version() const override { + return EcmSerializerVersion::kV2; + } +}; + +} // namespace cas +} // namespace widevine + +#endif // MEDIA_CAS_PACKAGER_SDK_INTERNAL_ECM_SERIALIZER_V2_H_ diff --git a/media_cas_packager_sdk/internal/ecm_serializer_v2_test.cc b/media_cas_packager_sdk/internal/ecm_serializer_v2_test.cc new file mode 100644 index 0000000..296b60c --- /dev/null +++ b/media_cas_packager_sdk/internal/ecm_serializer_v2_test.cc @@ -0,0 +1,254 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 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. +//////////////////////////////////////////////////////////////////////////////// +#include "media_cas_packager_sdk/internal/ecm_serializer_v2.h" + +#include +#include + +#include "testing/gmock.h" +#include "testing/gunit.h" +#include "absl/strings/str_cat.h" +#include "common/status.h" + +namespace widevine { +namespace cas { +namespace { + +constexpr uint16_t kWvCasCaSystemId = 0x4AD4; +constexpr char kWrappedKeyIv[] = "wrapped_key_iv.."; +constexpr char kWrappedKeyIv2[] = "wrapped_key_iv.2"; +constexpr char kEntitlementId[] = "entitlement_id.."; +constexpr char kEntitlementId2[] = "entitlement_id.2"; +constexpr char kContentKeyId[] = "content_key_id.."; +constexpr char kContentKeyId2[] = "content_key_id.2"; +constexpr char kContentIv8Bytes[] = "c_iv...."; +constexpr char kContentIv8Bytes2[] = "c_iv...2"; +constexpr char kContentIv16Bytes[] = "c_iv....c_iv...."; +constexpr char kContentIv16Bytes2[] = "c_iv....c_iv...2"; +constexpr char kWrappedContentKey[] = "wrapped_key....."; +constexpr char kWrappedContentKey2[] = "wrapped_key....2"; +constexpr int kAgeRestriction = 0; +constexpr size_t kEcmHeaderSize = 5; + +class EcmSerializerV2Test : public testing::Test { + protected: + void SetUp() override { + serializer_params_.cas_id = kWvCasCaSystemId; + serializer_params_.crypto_mode = CryptoMode::kAesCtr; + serializer_params_.age_restriction = kAgeRestriction; + serializer_params_.even_key = {kEntitlementId, kContentKeyId, + kWrappedContentKey, kWrappedKeyIv, + kContentIv16Bytes}; + serializer_params_.odd_key = {kEntitlementId2, kContentKeyId2, + kWrappedContentKey2, kWrappedKeyIv2, + kContentIv16Bytes2}; + } + + void ValidateEcmHeaderFields(const std::string& ecm_string, + bool rotation_enabled, int age_restriction, + CryptoMode crypto_mode, EcmIvSize content_iv) { + ASSERT_GE(ecm_string.size(), kEcmHeaderSize); + EXPECT_THAT('\x4A', ecm_string[0]); + EXPECT_THAT('\xD4', ecm_string[1]); + EXPECT_THAT('\x02', ecm_string[2]); // version + EXPECT_THAT(static_cast(crypto_mode), ((ecm_string[3] >> 1) & 0xf)); + EXPECT_THAT(rotation_enabled ? 1 : 0, ecm_string[3] & 1); + EXPECT_THAT(1, ((ecm_string[4] >> 7) & 1)); // wrapped key IV size, MB 1 + EXPECT_THAT(static_cast(content_iv), ((ecm_string[4] >> 6) & 1)); + EXPECT_THAT(age_restriction, ((ecm_string[4] >> 1) & 0x1f)); + EXPECT_THAT(0, ecm_string[4] & 1); // zero padding + } + + EcmSerializerParams serializer_params_; +}; + +TEST_F(EcmSerializerV2Test, SerializeEcmDoubleKeySuccess) { + EcmSerializerV2 ecm_serializer; + const std::string expected_key_fields = absl::StrCat( + kEntitlementId, kContentKeyId, kWrappedContentKey, kWrappedKeyIv, + kContentIv16Bytes, kEntitlementId2, kContentKeyId2, kWrappedContentKey2, + kWrappedKeyIv2, kContentIv16Bytes2); + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + ValidateEcmHeaderFields(serialized_ecm, /*rotation_enabled=*/true, + kAgeRestriction, CryptoMode::kAesCtr, kIvSize16); + EXPECT_EQ(serialized_ecm.substr(kEcmHeaderSize), expected_key_fields); +} + +TEST_F(EcmSerializerV2Test, SerializeEcmSingleKeySuccess) { + EcmSerializerV2 ecm_serializer; + serializer_params_.odd_key = {}; + const std::string expected_key_fields = + absl::StrCat(kEntitlementId, kContentKeyId, kWrappedContentKey, + kWrappedKeyIv, kContentIv16Bytes); + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + ValidateEcmHeaderFields(serialized_ecm, /*rotation_enabled=*/false, + kAgeRestriction, CryptoMode::kAesCtr, kIvSize16); + EXPECT_EQ(serialized_ecm.substr(kEcmHeaderSize), expected_key_fields); +} + +TEST_F(EcmSerializerV2Test, SerializeEcmDoubleKey8ByteIvs) { + EcmSerializerV2 ecm_serializer; + serializer_params_.even_key.content_iv = kContentIv8Bytes; + serializer_params_.odd_key.content_iv = kContentIv8Bytes2; + const std::string expected_key_fields = absl::StrCat( + kEntitlementId, kContentKeyId, kWrappedContentKey, kWrappedKeyIv, + kContentIv8Bytes, kEntitlementId2, kContentKeyId2, kWrappedContentKey2, + kWrappedKeyIv2, kContentIv8Bytes2); + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + ValidateEcmHeaderFields(serialized_ecm, /*rotation_enabled=*/true, + kAgeRestriction, CryptoMode::kAesCtr, kIvSize8); + EXPECT_EQ(serialized_ecm.substr(kEcmHeaderSize), expected_key_fields); +} + +TEST_F(EcmSerializerV2Test, SerializeEcmSingleKey8ByteIvs) { + EcmSerializerV2 ecm_serializer; + serializer_params_.even_key.content_iv = kContentIv8Bytes; + serializer_params_.odd_key = {}; + const std::string expected_key_fields = + absl::StrCat(kEntitlementId, kContentKeyId, kWrappedContentKey, + kWrappedKeyIv, kContentIv8Bytes); + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + ValidateEcmHeaderFields(serialized_ecm, /*rotation_enabled=*/false, + kAgeRestriction, CryptoMode::kAesCtr, kIvSize8); + EXPECT_EQ(serialized_ecm.substr(kEcmHeaderSize), expected_key_fields); +} + +TEST_F(EcmSerializerV2Test, SerializeEcmAgeRestriction) { + EcmSerializerV2 ecm_serializer; + serializer_params_.age_restriction = 18; + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + ValidateEcmHeaderFields(serialized_ecm, /*rotation_enabled=*/true, + serializer_params_.age_restriction, + CryptoMode::kAesCtr, kIvSize16); +} + +TEST_F(EcmSerializerV2Test, SerializeEcmCasId) { + EcmSerializerV2 ecm_serializer; + serializer_params_.cas_id = 0x56C0; + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + ASSERT_GE(serialized_ecm.size(), 2); + EXPECT_EQ((static_cast(serialized_ecm[0]) << 8) | + static_cast(serialized_ecm[1]), + serializer_params_.cas_id); +} + +TEST_F(EcmSerializerV2Test, SerializeEcmCryptoMode) { + EcmSerializerV2 ecm_serializer; + serializer_params_.crypto_mode = CryptoMode::kAesCbc; + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + ValidateEcmHeaderFields(serialized_ecm, /*rotation_enabled=*/true, + kAgeRestriction, CryptoMode::kAesCbc, kIvSize16); +} + +TEST_F(EcmSerializerV2Test, InvalidCryptoModeFail) { + EcmSerializerV2 ecm_serializer; + serializer_params_.crypto_mode = CryptoMode::kInvalid; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV2Test, MismatchContentIvFail) { + EcmSerializerV2 ecm_serializer; + serializer_params_.even_key.content_iv = kContentIv16Bytes; + serializer_params_.odd_key.content_iv = kContentIv8Bytes; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV2Test, InvalidEntitlementKeyIdFail) { + EcmSerializerV2 ecm_serializer; + serializer_params_.even_key.entitlement_key_id = "short_id"; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV2Test, InvalidContentKeyIdFail) { + EcmSerializerV2 ecm_serializer; + serializer_params_.even_key.content_key_id = "short_id"; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV2Test, InvalidWrappedKeyValueFail) { + EcmSerializerV2 ecm_serializer; + serializer_params_.even_key.wrapped_key_value = "short_v"; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV2Test, InvalidWrappedKeyIvFail) { + EcmSerializerV2 ecm_serializer; + serializer_params_.even_key.wrapped_key_iv = "short_iv"; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV2Test, InvalidContentIvFail) { + EcmSerializerV2 ecm_serializer; + serializer_params_.even_key.content_iv = "iv"; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV2Test, InvalidOddContentIvFail) { + EcmSerializerV2 ecm_serializer; + serializer_params_.odd_key.content_iv = "iv"; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV2Test, InvalidOddKeyFail) { + EcmSerializerV2 ecm_serializer; + serializer_params_.odd_key = {}; + serializer_params_.odd_key.content_iv = kContentIv16Bytes; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +} // namespace +} // namespace cas +} // namespace widevine diff --git a/media_cas_packager_sdk/internal/ecm_serializer_v3.cc b/media_cas_packager_sdk/internal/ecm_serializer_v3.cc new file mode 100644 index 0000000..16587e1 --- /dev/null +++ b/media_cas_packager_sdk/internal/ecm_serializer_v3.cc @@ -0,0 +1,208 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 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. +//////////////////////////////////////////////////////////////////////////////// +#include "media_cas_packager_sdk/internal/ecm_serializer_v3.h" + +#include + +#include "glog/logging.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "common/ec_key.h" +#include "common/hash_algorithm.h" +#include "common/status.h" +#include "media_cas_packager_sdk/public/wv_cas_types.h" +#include "protos/public/media_cas.pb.h" + +namespace widevine { +namespace cas { + +namespace { + +constexpr uint8_t kEcmVersion = 3; +constexpr int kEcmHeaderSize = 3; +constexpr size_t kKeyIdSizeBytes = 16; +constexpr size_t kKeyDataSizeBytes = 16; +constexpr size_t kWrappedKeyIvSizeBytes = 16; +constexpr size_t kContentIvSize8Bytes = 8; +constexpr size_t kContentIvSize16Bytes = 16; + +Status ValidateKeyInfo(const EcmSerializerKeyInfo& key_info, bool is_even_key) { + // For even key, fields entitlement_key_id, wrapped_key_value and + // wrapped_key_iv are required. For odd key, only wrapped_key_value field is + // required, + if ((is_even_key || !key_info.entitlement_key_id.empty()) && + (key_info.entitlement_key_id.size() != kKeyIdSizeBytes)) { + return {error::INVALID_ARGUMENT, + absl::StrCat("Invalid entitlement_key_id size. Expect ", + kKeyIdSizeBytes, " but got ", + key_info.entitlement_key_id.size())}; + } + if (key_info.wrapped_key_value.size() != kKeyDataSizeBytes) { + return {error::INVALID_ARGUMENT, + absl::StrCat("Invalid wrapped_key_value size. Expect ", + kKeyDataSizeBytes, " but got ", + key_info.wrapped_key_value.size())}; + } + if ((is_even_key || !key_info.wrapped_key_iv.empty()) && + (key_info.wrapped_key_iv.size() != kWrappedKeyIvSizeBytes)) { + return {error::INVALID_ARGUMENT, + absl::StrCat("Invalid wrapped_key_iv size. Expect ", + kWrappedKeyIvSizeBytes, " but got ", + key_info.wrapped_key_iv.size())}; + } + if ((!key_info.content_iv.empty()) && + (key_info.content_iv.size() != kContentIvSize8Bytes) && + (key_info.content_iv.size() != kContentIvSize16Bytes)) { + return {error::INVALID_ARGUMENT, + absl::StrCat("Invalid content_iv size. Expect ", + kContentIvSize8Bytes, " or ", kContentIvSize16Bytes, + " but got ", key_info.content_iv.size())}; + } + return OkStatus(); +} + +EcmMetaData::CipherMode ConvertCryptoModeToProtoCipherMode( + CryptoMode crypto_mode) { + switch (crypto_mode) { + case CryptoMode::kAesCbc: + return EcmMetaData::AES_CBC; + case CryptoMode::kAesCtr: + return EcmMetaData::AES_CTR; + case CryptoMode::kDvbCsa2: + return EcmMetaData::DVB_CSA2; + case CryptoMode::kDvbCsa3: + return EcmMetaData::DVB_CSA3; + case CryptoMode::kAesOfb: + return EcmMetaData::AES_OFB; + case CryptoMode::kAesScte: + return EcmMetaData::AES_SCTE52; + case CryptoMode::kInvalid: + default: + LOG(ERROR) << "Unknown crypto mode."; + return EcmMetaData::UNSPECIFIED; + } +} + +} // namespace + +Status EcmSerializerV3::SerializeEcm(const EcmSerializerParams& params, + std::string* serialized_ecm) const { + if (params.crypto_mode == CryptoMode::kInvalid) { + return {error::INVALID_ARGUMENT, "Invalid crypo mode."}; + } + Status status = ValidateKeyInfo(params.even_key, /*is_even_key=*/true); + if (!status.ok()) { + LOG(ERROR) << "Invalid even key info"; + return status; + } + bool has_odd_key = !params.odd_key.wrapped_key_value.empty(); + if (has_odd_key) { + status = ValidateKeyInfo(params.odd_key, /*is_even_key=*/false); + if (!status.ok()) { + LOG(ERROR) << "Invalid odd key info"; + return status; + } + if (!params.odd_key.content_iv.empty() && + (params.odd_key.content_iv.size() != + params.even_key.content_iv.size())) { + return {error::INVALID_ARGUMENT, "Content IV size must match."}; + } + } + + EcmPayload ecm_payload; + EcmMetaData* meta_data = ecm_payload.mutable_meta_data(); + meta_data->set_cipher_mode( + ConvertCryptoModeToProtoCipherMode(params.crypto_mode)); + if (params.age_restriction != 0) { + meta_data->set_age_restriction(params.age_restriction); + } + + EcmKeyData* even_key_data = ecm_payload.mutable_even_key_data(); + even_key_data->set_entitlement_key_id(params.even_key.entitlement_key_id); + even_key_data->set_wrapped_key_data(params.even_key.wrapped_key_value); + even_key_data->set_wrapped_key_iv(params.even_key.wrapped_key_iv); + // If content_iv is not present, a default value (all 0) will be assumed at + // the plugin side. This is usually used in CSA2 encryption mode. + if (!params.even_key.content_iv.empty()) { + even_key_data->set_content_iv(params.even_key.content_iv); + } + + if (has_odd_key) { + EcmKeyData* odd_key_data = ecm_payload.mutable_odd_key_data(); + odd_key_data->set_wrapped_key_data(params.odd_key.wrapped_key_value); + // Only put these fields if they are different from the even key. + if (!params.odd_key.entitlement_key_id.empty() && + (params.odd_key.entitlement_key_id != + params.even_key.entitlement_key_id)) { + odd_key_data->set_entitlement_key_id(params.odd_key.entitlement_key_id); + } + if (!params.odd_key.wrapped_key_iv.empty() && + (params.odd_key.wrapped_key_iv != params.even_key.wrapped_key_iv)) { + odd_key_data->set_wrapped_key_iv(params.odd_key.wrapped_key_iv); + } + if (!params.odd_key.content_iv.empty() && + (params.odd_key.content_iv != params.even_key.content_iv)) { + odd_key_data->set_content_iv(params.odd_key.content_iv); + } + } + + if (params.fingerprinting.has_value()) { + ecm_payload.mutable_fingerprinting()->set_control( + params.fingerprinting->control); + } + if (params.service_blocking.has_value()) { + for (const auto& device_group : params.service_blocking->device_groups) { + ecm_payload.mutable_service_blocking()->add_device_groups(device_group); + } + } + + const std::string serialized_ecm_payload = ecm_payload.SerializeAsString(); + SignedEcmPayload signed_ecm_payload; + signed_ecm_payload.set_serialized_payload(serialized_ecm_payload); + + // Sign the serialized ecm payload if the private key is available. + if (!params.ecc_private_signing_key.empty()) { + absl::StatusOr result = CalculateSignature( + params.ecc_private_signing_key, serialized_ecm_payload); + if (!result.ok()) { + LOG(ERROR) << "Failed to sign ECM: " << result.status(); + return Status(error::INTERNAL, "Failed to sign ECM."); + } + signed_ecm_payload.set_signature(result.value()); + } + + // Put ECM header. + serialized_ecm->reserve(kEcmHeaderSize); + serialized_ecm->push_back(static_cast(params.cas_id >> 8)); + serialized_ecm->push_back(static_cast(params.cas_id & 0xFF)); + serialized_ecm->push_back(static_cast(kEcmVersion)); + // Put ECM payload. + signed_ecm_payload.AppendToString(serialized_ecm); + + return OkStatus(); +} + +absl::StatusOr EcmSerializerV3::CalculateSignature( + absl::string_view private_signing_key, absl::string_view message) const { + std::unique_ptr ec_private_key = + ECPrivateKey::Create(std::string(private_signing_key)); + if (ec_private_key == nullptr) { + return absl::InternalError("Failed to construct a ECPrivateKey."); + } + std::string signature; + if (!ec_private_key->GenerateSignature(std::string(message), + HashAlgorithm::kSha256, &signature) || + signature.empty()) { + return absl::InternalError("Failed to generate signature."); + } + return signature; +} + +} // namespace cas +} // namespace widevine diff --git a/media_cas_packager_sdk/internal/ecm_serializer_v3.h b/media_cas_packager_sdk/internal/ecm_serializer_v3.h new file mode 100644 index 0000000..73e042b --- /dev/null +++ b/media_cas_packager_sdk/internal/ecm_serializer_v3.h @@ -0,0 +1,46 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 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. +//////////////////////////////////////////////////////////////////////////////// +#ifndef MEDIA_CAS_PACKAGER_SDK_INTERNAL_ECM_SERIALIZER_V3_H_ +#define MEDIA_CAS_PACKAGER_SDK_INTERNAL_ECM_SERIALIZER_V3_H_ + +#include + +#include +#include "absl/status/statusor.h" +#include "common/status.h" +#include "media_cas_packager_sdk/internal/ecm_serializer.h" + +namespace widevine { +namespace cas { + +// Generates serialized ECM version 3. +class EcmSerializerV3 : public EcmSerializer { + public: + EcmSerializerV3() = default; + EcmSerializerV3(const EcmSerializerV3&) = delete; + EcmSerializerV3& operator=(const EcmSerializerV3&) = delete; + ~EcmSerializerV3() override = default; + + Status SerializeEcm(const EcmSerializerParams& params, + std::string* serialized_ecm) const override; + + EcmSerializerVersion Version() const override { + return EcmSerializerVersion::kV3; + } + + private: + // Calculates signature of |message| using ECC |private_signing_key|. + // Returns the generated signature on success, and an error status otherwise. + absl::StatusOr CalculateSignature( + absl::string_view private_signing_key, absl::string_view message) const; +}; + +} // namespace cas +} // namespace widevine + +#endif // MEDIA_CAS_PACKAGER_SDK_INTERNAL_ECM_SERIALIZER_V3_H_ diff --git a/media_cas_packager_sdk/internal/ecm_serializer_v3_test.cc b/media_cas_packager_sdk/internal/ecm_serializer_v3_test.cc new file mode 100644 index 0000000..f36e528 --- /dev/null +++ b/media_cas_packager_sdk/internal/ecm_serializer_v3_test.cc @@ -0,0 +1,412 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2020 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. +//////////////////////////////////////////////////////////////////////////////// +#include "media_cas_packager_sdk/internal/ecm_serializer_v3.h" + +#include +#include + +#include "testing/gmock.h" +#include "testing/gunit.h" +#include "absl/types/optional.h" +#include "common/ec_key.h" +#include "common/ec_test_keys.h" +#include "common/hash_algorithm.h" +#include "common/status.h" +#include "media_cas_packager_sdk/public/wv_cas_types.h" +#include "protos/public/media_cas.pb.h" + +namespace widevine { +namespace cas { +namespace { + +using ::testing::IsEmpty; +using ::testing::Not; + +constexpr int kEcmVersion = 3; +constexpr uint16_t kWvCasCaSystemId = 0x4AD4; +constexpr char kWrappedKeyIv[] = "wrapped_key_iv.."; +constexpr char kWrappedKeyIv2[] = "wrapped_key_iv.2"; +constexpr char kEntitlementId[] = "entitlement_id.."; +constexpr char kEntitlementId2[] = "entitlement_id.2"; +constexpr char kContentKeyId[] = "content_key_id.."; +constexpr char kContentKeyId2[] = "content_key_id.2"; +constexpr char kContentIv8Bytes[] = "c_iv...."; +constexpr char kContentIv8Bytes2[] = "c_iv...2"; +constexpr char kContentIv16Bytes[] = "c_iv....c_iv...."; +constexpr char kContentIv16Bytes2[] = "c_iv....c_iv...2"; +constexpr char kWrappedContentKey[] = "wrapped_key....."; +constexpr char kWrappedContentKey2[] = "wrapped_key....2"; + +constexpr int kAgeRestriction = 0; +constexpr int kEcmProtoStartOffset = 3; + +class EcmSerializerV3Test : public testing::Test { + protected: + void SetUp() override { + serializer_params_.cas_id = kWvCasCaSystemId; + serializer_params_.crypto_mode = CryptoMode::kAesCtr; + serializer_params_.age_restriction = kAgeRestriction; + serializer_params_.even_key = {kEntitlementId, kContentKeyId, + kWrappedContentKey, kWrappedKeyIv, + kContentIv16Bytes}; + serializer_params_.odd_key = {kEntitlementId2, kContentKeyId2, + kWrappedContentKey2, kWrappedKeyIv2, + kContentIv16Bytes2}; + } + + void VerifyHeader(const std::string& serialized_ecm, uint16_t expected_cas_id, + uint8_t expected_version) { + ASSERT_GE(serialized_ecm.size(), kEcmProtoStartOffset); + EXPECT_EQ((static_cast(serialized_ecm[0]) << 8) | + static_cast(serialized_ecm[1]), + expected_cas_id); + EXPECT_EQ(serialized_ecm[2], expected_version); + } + + void VerifyMetaData(const EcmMetaData meta_data, + EcmMetaData::CipherMode expected_cipher_mode, + int expected_age_restriction) { + EXPECT_EQ(meta_data.cipher_mode(), expected_cipher_mode); + if (expected_age_restriction == 0) { + EXPECT_FALSE(meta_data.has_age_restriction()); + } + EXPECT_EQ(meta_data.age_restriction(), expected_age_restriction); + } + + void VerifyEcmKeyData(const EcmKeyData& ecm_key_data, + const EcmSerializerKeyInfo& key_info) { + EXPECT_EQ(ecm_key_data.content_iv(), key_info.content_iv); + EXPECT_EQ(ecm_key_data.entitlement_key_id(), key_info.entitlement_key_id); + EXPECT_EQ(ecm_key_data.wrapped_key_data(), key_info.wrapped_key_value); + EXPECT_EQ(ecm_key_data.wrapped_key_iv(), key_info.wrapped_key_iv); + } + + EcmSerializerParams serializer_params_; +}; + +TEST_F(EcmSerializerV3Test, SerializeEcmDoubleKeySuccess) { + EcmSerializerV3 ecm_serializer; + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + VerifyHeader(serialized_ecm, kWvCasCaSystemId, kEcmVersion); + SignedEcmPayload signed_payload; + ASSERT_TRUE(signed_payload.ParseFromString( + serialized_ecm.substr(kEcmProtoStartOffset))); + EXPECT_THAT(signed_payload.signature(), IsEmpty()); + EcmPayload payload; + ASSERT_TRUE(payload.ParseFromString(signed_payload.serialized_payload())); + VerifyMetaData(payload.meta_data(), EcmMetaData::AES_CTR, kAgeRestriction); + VerifyEcmKeyData(payload.even_key_data(), serializer_params_.even_key); + VerifyEcmKeyData(payload.odd_key_data(), serializer_params_.odd_key); + EXPECT_FALSE(payload.has_fingerprinting()); + EXPECT_FALSE(payload.has_service_blocking()); +} + +TEST_F(EcmSerializerV3Test, SerializeEcmSingleKeySuccess) { + EcmSerializerV3 ecm_serializer; + serializer_params_.odd_key = {}; + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + VerifyHeader(serialized_ecm, kWvCasCaSystemId, kEcmVersion); + SignedEcmPayload signed_payload; + ASSERT_TRUE(signed_payload.ParseFromString( + serialized_ecm.substr(kEcmProtoStartOffset))); + EcmPayload payload; + ASSERT_TRUE(payload.ParseFromString(signed_payload.serialized_payload())); + VerifyMetaData(payload.meta_data(), EcmMetaData::AES_CTR, kAgeRestriction); + VerifyEcmKeyData(payload.even_key_data(), serializer_params_.even_key); + EXPECT_FALSE(payload.has_odd_key_data()); +} + +TEST_F(EcmSerializerV3Test, SerializeEcmOmittedFieldsSuccess) { + EcmSerializerV3 ecm_serializer; + // odd_key has the same content as even_key. + serializer_params_.odd_key = serializer_params_.even_key; + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + VerifyHeader(serialized_ecm, kWvCasCaSystemId, kEcmVersion); + SignedEcmPayload signed_payload; + ASSERT_TRUE(signed_payload.ParseFromString( + serialized_ecm.substr(kEcmProtoStartOffset))); + EcmPayload payload; + ASSERT_TRUE(payload.ParseFromString(signed_payload.serialized_payload())); + VerifyMetaData(payload.meta_data(), EcmMetaData::AES_CTR, kAgeRestriction); + VerifyEcmKeyData(payload.even_key_data(), serializer_params_.even_key); + // Fields content_iv, entitlement id/iv will be omitted in the odd key. + EXPECT_FALSE(payload.odd_key_data().has_content_iv()); + EXPECT_FALSE(payload.odd_key_data().has_entitlement_key_id()); + EXPECT_FALSE(payload.odd_key_data().has_wrapped_key_iv()); + EXPECT_EQ(payload.odd_key_data().wrapped_key_data(), + serializer_params_.odd_key.wrapped_key_value); +} + +TEST_F(EcmSerializerV3Test, SerializeEcmOmitContentIvSuccess) { + EcmSerializerV3 ecm_serializer; + serializer_params_.even_key.content_iv.clear(); + serializer_params_.odd_key.content_iv.clear(); + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + SignedEcmPayload signed_payload; + ASSERT_TRUE(signed_payload.ParseFromString( + serialized_ecm.substr(kEcmProtoStartOffset))); + EcmPayload payload; + ASSERT_TRUE(payload.ParseFromString(signed_payload.serialized_payload())); + VerifyEcmKeyData(payload.even_key_data(), serializer_params_.even_key); + VerifyEcmKeyData(payload.odd_key_data(), serializer_params_.odd_key); +} + +TEST_F(EcmSerializerV3Test, SerializeEcmOmitOddKeyFieldsSuccess) { + EcmSerializerV3 ecm_serializer; + serializer_params_.odd_key.entitlement_key_id.clear(); + serializer_params_.odd_key.wrapped_key_iv.clear(); + serializer_params_.odd_key.content_iv.clear(); + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + SignedEcmPayload signed_payload; + ASSERT_TRUE(signed_payload.ParseFromString( + serialized_ecm.substr(kEcmProtoStartOffset))); + EcmPayload payload; + ASSERT_TRUE(payload.ParseFromString(signed_payload.serialized_payload())); + VerifyEcmKeyData(payload.even_key_data(), serializer_params_.even_key); + EXPECT_FALSE(payload.odd_key_data().has_content_iv()); + EXPECT_FALSE(payload.odd_key_data().has_entitlement_key_id()); + EXPECT_FALSE(payload.odd_key_data().has_wrapped_key_iv()); + EXPECT_EQ(payload.odd_key_data().wrapped_key_data(), + serializer_params_.odd_key.wrapped_key_value); +} + +class SerializeEcmAgeRestrictionTest + : public EcmSerializerV3Test, + public testing::WithParamInterface {}; + +TEST_P(SerializeEcmAgeRestrictionTest, ExpectedAgeRestriction) { + uint8_t expected_age_restriction = GetParam(); + EcmSerializerV3 ecm_serializer; + serializer_params_.age_restriction = expected_age_restriction; + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + SignedEcmPayload signed_payload; + ASSERT_TRUE(signed_payload.ParseFromString( + serialized_ecm.substr(kEcmProtoStartOffset))); + EcmPayload payload; + ASSERT_TRUE(payload.ParseFromString(signed_payload.serialized_payload())); + VerifyMetaData(payload.meta_data(), EcmMetaData::AES_CTR, + expected_age_restriction); +} + +INSTANTIATE_TEST_SUITE_P(SerializeEcmAgeRestrictionTest, + SerializeEcmAgeRestrictionTest, + testing::Values(0, 3, 18)); + +class SerializeEcmCipherModeTest + : public EcmSerializerV3Test, + public testing::WithParamInterface< + testing::tuple> {}; + +TEST_P(SerializeEcmCipherModeTest, ExpectedCipherMode) { + EcmSerializerV3 ecm_serializer; + serializer_params_.crypto_mode = std::get<0>(GetParam()); + EcmMetaData::CipherMode expected_cipher_mode = std::get<1>(GetParam()); + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + SignedEcmPayload signed_payload; + ASSERT_TRUE(signed_payload.ParseFromString( + serialized_ecm.substr(kEcmProtoStartOffset))); + EcmPayload payload; + ASSERT_TRUE(payload.ParseFromString(signed_payload.serialized_payload())); + VerifyMetaData(payload.meta_data(), expected_cipher_mode, kAgeRestriction); +} + +INSTANTIATE_TEST_SUITE_P( + SerializeEcmCipherModeTest, SerializeEcmCipherModeTest, + testing::Values( + std::make_tuple(CryptoMode::kAesCbc, EcmMetaData::AES_CBC), + std::make_tuple(CryptoMode::kAesCtr, EcmMetaData::AES_CTR), + std::make_tuple(CryptoMode::kDvbCsa2, EcmMetaData::DVB_CSA2), + std::make_tuple(CryptoMode::kDvbCsa3, EcmMetaData::DVB_CSA3), + std::make_tuple(CryptoMode::kAesOfb, EcmMetaData::AES_OFB), + std::make_tuple(CryptoMode::kAesScte, EcmMetaData::AES_SCTE52))); + +TEST_F(EcmSerializerV3Test, SerializeEcmDoubleKey8ByteIvs) { + EcmSerializerV3 ecm_serializer; + serializer_params_.even_key.content_iv = kContentIv8Bytes; + serializer_params_.odd_key.content_iv = kContentIv8Bytes2; + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); +} + +TEST_F(EcmSerializerV3Test, SerializeEcmSingleKey8ByteIvs) { + EcmSerializerV3 ecm_serializer; + serializer_params_.even_key.content_iv = kContentIv8Bytes; + serializer_params_.odd_key = {}; + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); +} + +TEST_F(EcmSerializerV3Test, SerializeEcmCasId) { + EcmSerializerV3 ecm_serializer; + const uint16_t expected_cas_id = 0x56C0; + serializer_params_.cas_id = expected_cas_id; + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + VerifyHeader(serialized_ecm, expected_cas_id, kEcmVersion); +} + +TEST_F(EcmSerializerV3Test, InvalidCryptoModeFail) { + EcmSerializerV3 ecm_serializer; + serializer_params_.crypto_mode = CryptoMode::kInvalid; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV3Test, MismatchContentIvFail) { + EcmSerializerV3 ecm_serializer; + serializer_params_.even_key.content_iv = kContentIv16Bytes; + serializer_params_.odd_key.content_iv = kContentIv8Bytes; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV3Test, InvalidEntitlementKeyIdFail) { + EcmSerializerV3 ecm_serializer; + serializer_params_.even_key.entitlement_key_id = "short_id"; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV3Test, InvalidWrappedKeyValueFail) { + EcmSerializerV3 ecm_serializer; + serializer_params_.even_key.wrapped_key_value = "short_v"; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV3Test, InvalidWrappedKeyIvFail) { + EcmSerializerV3 ecm_serializer; + serializer_params_.even_key.wrapped_key_iv = "short_iv"; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV3Test, InvalidContentIvFail) { + EcmSerializerV3 ecm_serializer; + serializer_params_.even_key.content_iv = "iv"; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV3Test, InvalidOddContentIvFail) { + EcmSerializerV3 ecm_serializer; + serializer_params_.odd_key.content_iv = "iv"; + std::string serialized_ecm; + + ASSERT_FALSE( + ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm).ok()); +} + +TEST_F(EcmSerializerV3Test, FingerprintingSuccess) { + const std::string expected_control = "control"; + Fingerprinting expected_proto; + expected_proto.set_control(expected_control); + EcmFingerprintingParams fingerprinting; + fingerprinting.control = expected_control; + EcmSerializerV3 ecm_serializer; + serializer_params_.fingerprinting = absl::make_optional(fingerprinting); + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + SignedEcmPayload signed_payload; + ASSERT_TRUE(signed_payload.ParseFromString( + serialized_ecm.substr(kEcmProtoStartOffset))); + EcmPayload payload; + ASSERT_TRUE(payload.ParseFromString(signed_payload.serialized_payload())); + // Use testing::EqualsProto once available in external googletest repo. + EXPECT_EQ(payload.fingerprinting().SerializeAsString(), + expected_proto.SerializeAsString()); +} + +TEST_F(EcmSerializerV3Test, ServiceBlockingSuccess) { + const std::vector expected_device_groups = {"group1", "group2"}; + ServiceBlocking expected_proto; + for (const auto& expected_device_group : expected_device_groups) { + expected_proto.add_device_groups(expected_device_group); + } + EcmServiceBlockingParams service_blocking; + service_blocking.device_groups = expected_device_groups; + EcmSerializerV3 ecm_serializer; + serializer_params_.service_blocking = absl::make_optional(service_blocking); + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + SignedEcmPayload signed_payload; + ASSERT_TRUE(signed_payload.ParseFromString( + serialized_ecm.substr(kEcmProtoStartOffset))); + EcmPayload payload; + ASSERT_TRUE(payload.ParseFromString(signed_payload.serialized_payload())); + // Use testing::EqualsProto once available in external googletest repo. + EXPECT_EQ(payload.service_blocking().SerializeAsString(), + expected_proto.SerializeAsString()); +} + +TEST_F(EcmSerializerV3Test, GenerateSignatureSuccess) { + EcmSerializerV3 ecm_serializer; + ECTestKeys test_keys; + serializer_params_.ecc_private_signing_key = + test_keys.private_test_key_1_secp256r1(); + std::string serialized_ecm; + + ASSERT_OK(ecm_serializer.SerializeEcm(serializer_params_, &serialized_ecm)); + + SignedEcmPayload signed_payload; + ASSERT_TRUE(signed_payload.ParseFromString( + serialized_ecm.substr(kEcmProtoStartOffset))); + EXPECT_THAT(signed_payload.signature(), Not(IsEmpty())); + // Check the signature can be verified. + std::unique_ptr public_key( + ECPublicKey::Create(test_keys.public_test_key_1_secp256r1())); + ASSERT_TRUE(public_key != nullptr); + EXPECT_TRUE(public_key->VerifySignature(signed_payload.serialized_payload(), + HashAlgorithm::kSha256, + signed_payload.signature())); +} + +} // namespace +} // namespace cas +} // namespace widevine diff --git a/media_cas_packager_sdk/internal/ecm_test.cc b/media_cas_packager_sdk/internal/ecm_test.cc index 2d93259..ffb81ee 100644 --- a/media_cas_packager_sdk/internal/ecm_test.cc +++ b/media_cas_packager_sdk/internal/ecm_test.cc @@ -10,32 +10,31 @@ #include "testing/gmock.h" #include "testing/gunit.h" +#include "absl/memory/memory.h" +#include "common/status.h" +#include "media_cas_packager_sdk/internal/ecm_serializer.h" #include "media_cas_packager_sdk/internal/mpeg2ts.h" - -using ::testing::Return; +#include "media_cas_packager_sdk/public/wv_cas_types.h" namespace widevine { namespace cas { - -// TODO(user): Encryption options yield a small number of possible sizes -// for ECMS - Explain the distinct cases and add/use literals for each one. - namespace { +using ::testing::DoAll; +using ::testing::NotNull; +using ::testing::Return; +using ::testing::SetArgPointee; + +constexpr uint16_t kWvCasCaSystemId = 0x4AD4; +constexpr uint16_t kWidevineNewSystemIdLowerBound = 0x56C0; +constexpr uint16_t kWidevineNewSystemIdUpperBound = 0x56C9; constexpr char kTrackTypeSD[] = "SD"; constexpr char kTrackTypeHD[] = "HD"; -constexpr char kTrackTypeAUDIO[] = "AUDIO"; -constexpr char kTrackTypeBAD[] = "BAD"; constexpr char kWrappedKeyIv[] = "wrapped_key_iv.."; +constexpr char kEntitlementKeyId[] = "entitlement_Mock"; +constexpr char kWrappedKeyValue[] = "MockMockMockMock"; -constexpr size_t kEcmHeaderSize = 5; -constexpr size_t kEcmKeyIdSize = 16; -constexpr size_t kEcmKeyDataSize = 16; -constexpr size_t kEcmIvSize16 = 16; -constexpr size_t kEcmIvSize8 = 8; - -constexpr size_t kEcmKeyInfoSize = - kEcmKeyIdSize + kEcmKeyIdSize + kEcmKeyDataSize; +constexpr size_t kEcmVersionIndex = 2; // ECM payload data taken from a CETS encrypted file at Google Fiber // default_key_id = 0000000000000002, random_iv = 0x30EB, 0xDAF4, 0xC66D, 0x57DC @@ -76,10 +75,6 @@ constexpr char kExpectedEcmPacket[] = { '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF'}; -size_t IvExpectedSize(EcmIvSize iv_size) { - return (iv_size == kIvSize8) ? kEcmIvSize8 : kEcmIvSize16; -} - void InitEntitledKey(EntitledKeyInfo* key, const std::string& id, const std::string& value, const std::string& iv) { key->key_id = id; @@ -88,44 +83,6 @@ void InitEntitledKey(EntitledKeyInfo* key, const std::string& id, key->wrapped_key_iv = kWrappedKeyIv; } -} // namespace - -class MockEcm : public Ecm { - public: - MockEcm() = default; - ~MockEcm() override = default; - - MOCK_METHOD(uint8_t, age_restriction, (), (const, override)); - MOCK_METHOD(CryptoMode, crypto_mode, (), (const, override)); - MOCK_METHOD(bool, paired_keys_required, (), (const, override)); - MOCK_METHOD(size_t, content_iv_size, (), (const, override)); - - std::string CallSerializeEcm(const std::vector& keys) { - return SerializeEcm(keys); - } - - virtual Status MockWrapEntitledKeys( - const std::string& track_type, - const std::vector& keys) { - for (auto entitled_key : keys) { - entitled_key->entitlement_key_id = "entitlement_Mock"; - entitled_key->wrapped_key_value = "MockMockMockMock"; - entitled_key->wrapped_key_iv = "WRAPPED_KIV....x"; - } - return OkStatus(); - } - - void MockSetup(bool two_keys, const std::string& track_type, - uint8_t age_restriction, CryptoMode crypto_mode, - size_t civ_size) { - EXPECT_CALL(*this, age_restriction()) - .WillRepeatedly(Return(age_restriction)); - EXPECT_CALL(*this, crypto_mode()).WillRepeatedly(Return(crypto_mode)); - EXPECT_CALL(*this, paired_keys_required()).WillRepeatedly(Return(two_keys)); - EXPECT_CALL(*this, content_iv_size()).WillRepeatedly(Return(civ_size)); - } -}; - class EcmTest : public testing::Test { protected: void SetUp() override { @@ -135,10 +92,6 @@ class EcmTest : public testing::Test { "ct_iv..2"); InitEntitledKey(&valid3_iv_16_16_, "keyid_16b......3", "keyvalue_16b...3", "content_iv.....3"); - InitEntitledKey(&valid4_iv_16_16_, "keyid_16b....xx4", "keyvalue_16b...4", - "content_iv.....4"); - InitEntitledKey(&invalid1_key_id_iv_16_8_, "keyid_12b..1", - "keyvalue_16b...1", "ct_iv..5"); params_one_key_.key_rotation_enabled = false; params_two_keys_.key_rotation_enabled = true; @@ -156,54 +109,11 @@ class EcmTest : public testing::Test { EcmInitParameters params_one_key_; EcmInitParameters params_two_keys_; EcmInitParameters params_simple_; - EcmInitParameters params_default_; EntitlementKeyInfo injected_entitlement_one_; EntitlementKeyInfo injected_entitlement_two_; EntitledKeyInfo valid1_iv_16_8_; EntitledKeyInfo valid2_iv_16_8_; EntitledKeyInfo valid3_iv_16_16_; - EntitledKeyInfo valid4_iv_16_16_; - EntitledKeyInfo invalid1_key_id_iv_16_8_; -}; - -class EcmSerializeEcmTest : public EcmTest { - public: - void ValidateEcmHeaderFields(const std::string& ecm_string, - bool rotation_enabled, int age_restriction, - CryptoMode crypto_mode, EcmIvSize content_iv) { - EXPECT_THAT('\x4A', ecm_string[0]); - EXPECT_THAT('\xD4', ecm_string[1]); - EXPECT_THAT('\x02', ecm_string[2]); // version - EXPECT_THAT(static_cast(crypto_mode), ((ecm_string[3] >> 1) & 0xf)); - EXPECT_THAT(rotation_enabled ? 1 : 0, ecm_string[3] & 1); - EXPECT_THAT(1, ((ecm_string[4] >> 7) & 1)); // wrapped key IV size, MB 1 - EXPECT_THAT(static_cast(content_iv), ((ecm_string[4] >> 6) & 1)); - EXPECT_THAT(age_restriction, ((ecm_string[4] >> 1) & 0x1f)); - EXPECT_THAT(0, ecm_string[4] & 1); // zero padding - } - - void ValidateEcmFieldsOneKey(const std::string& buf_string, - int age_restriction, CryptoMode crypto_mode, - EcmIvSize content_iv) { - size_t expected_size = kEcmHeaderSize + kEcmKeyInfoSize + kEcmIvSize16 + - IvExpectedSize(content_iv); - size_t ecm_len = buf_string.size(); - EXPECT_THAT(expected_size, ecm_len); - ValidateEcmHeaderFields(buf_string, false, age_restriction, crypto_mode, - content_iv); - } - - void ValidateEcmFieldsTwoKeys(const std::string& buf_string, - int age_restriction, CryptoMode crypto_mode, - EcmIvSize content_iv) { - size_t expected_size = - kEcmHeaderSize + - (2 * (kEcmKeyInfoSize + kEcmIvSize16 + IvExpectedSize(content_iv))); - size_t ecm_len = buf_string.size(); - EXPECT_THAT(expected_size, ecm_len); - ValidateEcmHeaderFields(buf_string, true, age_restriction, crypto_mode, - content_iv); - } }; TEST_F(EcmTest, GenerateEcmNotInitialized) { @@ -268,15 +178,55 @@ TEST_F(EcmTest, InitIvSize16x16OK) { ASSERT_OK(ecm_gen.Initialize(params_one_key_, {injected_entitlement_one_})); } -TEST_F(EcmTest, GenerateOneKeyOK) { +class EcmInitWithValidCasIdTest : public EcmTest, + public testing::WithParamInterface {}; +TEST_P(EcmInitWithValidCasIdTest, InitWithValidCasIdSucceed) { + params_simple_.cas_id = GetParam(); Ecm ecm_gen; - InitParams(¶ms_one_key_, kIvSize8); - ASSERT_OK(ecm_gen.Initialize(params_one_key_, {injected_entitlement_one_})); - - EntitledKeyInfo key1 = valid1_iv_16_8_; - std::string ecm; - ASSERT_OK(ecm_gen.GenerateSingleKeyEcm(&key1, kTrackTypeSD, &ecm)); + EXPECT_OK(ecm_gen.Initialize( + params_simple_, {injected_entitlement_one_, injected_entitlement_two_})); } +INSTANTIATE_TEST_SUITE_P(EcmInitWithValidCasIdTest, EcmInitWithValidCasIdTest, + testing::Values(kWvCasCaSystemId, + kWidevineNewSystemIdLowerBound, 0x56C5, + kWidevineNewSystemIdUpperBound)); + +class EcmInitWithInvalidCasIdTest : public EcmTest, + public testing::WithParamInterface { +}; +TEST_P(EcmInitWithInvalidCasIdTest, InitWithInvalidCasIdFail) { + params_simple_.cas_id = GetParam(); + Ecm ecm_gen; + EXPECT_EQ(ecm_gen.Initialize(params_simple_, {injected_entitlement_one_, + injected_entitlement_two_}), + Status(error::INVALID_ARGUMENT, "Invalid CA system ID.")); +} +INSTANTIATE_TEST_SUITE_P(EcmInitWithInvalidCasIdTest, + EcmInitWithInvalidCasIdTest, + testing::Values(0, 0xABCD, + kWidevineNewSystemIdLowerBound - 1, + kWidevineNewSystemIdUpperBound + 1)); + +class EcmSerializerVersionTest + : public EcmTest, + public testing::WithParamInterface {}; +TEST_P(EcmSerializerVersionTest, EcmSerializerOk) { + Ecm ecm_gen; + EcmInitParameters params; + params.key_rotation_enabled = false; + params.ecm_version = GetParam(); + + ASSERT_OK(ecm_gen.Initialize(params, {injected_entitlement_one_})); + + std::string ecm; + ASSERT_OK( + ecm_gen.GenerateSingleKeyEcm(&valid3_iv_16_16_, kTrackTypeSD, &ecm)); + ASSERT_GT(ecm.size(), kEcmVersionIndex); + EXPECT_EQ(ecm[kEcmVersionIndex], + params.ecm_version == EcmVersion::kV2 ? 2 : 3); +} +INSTANTIATE_TEST_SUITE_P(EcmSerializerVersionTest, EcmSerializerVersionTest, + testing::Values(EcmVersion::kV2, EcmVersion::kV3)); TEST_F(EcmTest, GenerateWithBadTrackType) { Ecm ecm_gen; @@ -291,183 +241,132 @@ TEST_F(EcmTest, GenerateWithBadTrackType) { ecm_gen.GenerateEcm(&key1, &key2, kTrackTypeHD, &ecm).error_code()); } -TEST_F(EcmTest, GenerateTwoKeysOK) { - Ecm ecm_gen; - InitParams(¶ms_two_keys_, kIvSize8); +class MockEcmSerializer : public EcmSerializer { + public: + MockEcmSerializer() = default; + ~MockEcmSerializer() override = default; + + MOCK_METHOD(Status, SerializeEcm, + (const EcmSerializerParams& params, std::string* serialized_ecm), + (const, override)); + MOCK_METHOD(EcmSerializerVersion, Version, (), (const, override)); +}; + +class FakeEcm : public Ecm { + public: + FakeEcm() = default; + ~FakeEcm() override = default; + + void SetEcmSerializer( + std::unique_ptr ecm_serializer) override { + return Ecm::SetEcmSerializer(std::move(ecm_serializer)); + } + + Status WrapEntitledKeys( + const std::string& track_type, + const std::vector& keys) const override { + for (auto entitled_key : keys) { + entitled_key->entitlement_key_id = kEntitlementKeyId; + entitled_key->wrapped_key_value = kWrappedKeyValue; + } + return OkStatus(); + } +}; + +bool IsEcmSerializerKeyInfoEq(EcmSerializerKeyInfo a, EcmSerializerKeyInfo b) { + return (a.entitlement_key_id == b.entitlement_key_id) && + (a.content_key_id == b.content_key_id) && + (a.wrapped_key_value == b.wrapped_key_value) && + (a.wrapped_key_iv == b.wrapped_key_iv) && + (a.content_iv == b.content_iv); +} + +MATCHER_P(EcmSerializerParamsEq, expected, "") { + return (arg.cas_id == expected.cas_id) && + (arg.crypto_mode == expected.crypto_mode) && + (arg.age_restriction == expected.age_restriction) && + IsEcmSerializerKeyInfoEq(arg.even_key, expected.even_key) && + IsEcmSerializerKeyInfoEq(arg.odd_key, expected.odd_key); +} + +TEST_F(EcmTest, GenerateEcmTwoKeysOK) { + FakeEcm ecm_gen; ASSERT_OK(ecm_gen.Initialize(params_two_keys_, {injected_entitlement_one_, injected_entitlement_two_})); - + auto ecm_serializer = absl::make_unique(); + const MockEcmSerializer* ecm_serializer_pointer = ecm_serializer.get(); + ecm_gen.SetEcmSerializer(std::move(ecm_serializer)); EntitledKeyInfo key1 = valid1_iv_16_8_; EntitledKeyInfo key2 = valid2_iv_16_8_; - std::string ecm; - ASSERT_OK(ecm_gen.GenerateEcm(&key1, &key2, kTrackTypeSD, &ecm)); + EcmSerializerParams expected_params; + expected_params.cas_id = kWvCasCaSystemId; + expected_params.age_restriction = 0; + expected_params.crypto_mode = CryptoMode::kAesCtr; + expected_params.even_key = {kEntitlementKeyId, key1.key_id, kWrappedKeyValue, + key1.wrapped_key_iv, key1.content_iv}; + expected_params.odd_key = {kEntitlementKeyId, key2.key_id, kWrappedKeyValue, + key2.wrapped_key_iv, key2.content_iv}; + std::string expected_serialized_ecm = "expected_serialized_ecm"; + EXPECT_CALL(*ecm_serializer_pointer, + SerializeEcm(EcmSerializerParamsEq(expected_params), NotNull())) + .WillOnce( + DoAll(SetArgPointee<1>(expected_serialized_ecm), Return(OkStatus()))); + std::string actual_generated_ecm; + + ASSERT_OK( + ecm_gen.GenerateEcm(&key1, &key2, kTrackTypeSD, &actual_generated_ecm)); + + EXPECT_EQ(actual_generated_ecm, expected_serialized_ecm); } -TEST_F(EcmTest, GenerateOneKeyBadKeyIdFail) { - Ecm ecm_gen; - EntitledKeyInfo key1 = invalid1_key_id_iv_16_8_; - InitParams(¶ms_one_key_, kIvSize8); - ASSERT_OK(ecm_gen.Initialize(params_one_key_, {injected_entitlement_one_})); - - std::string ecm; - EXPECT_EQ( - error::INVALID_ARGUMENT, - ecm_gen.GenerateSingleKeyEcm(&key1, kTrackTypeSD, &ecm).error_code()); -} - -TEST_F(EcmTest, GenerateOneKeyWrong) { - Ecm ecm_gen; - EntitledKeyInfo key1 = valid1_iv_16_8_; - InitParams(¶ms_one_key_, kIvSize8); - ASSERT_OK(ecm_gen.Initialize(params_one_key_, {injected_entitlement_one_})); - - std::string ecm; - ASSERT_OK(ecm_gen.GenerateSingleKeyEcm(&key1, kTrackTypeSD, &ecm)); - EXPECT_THAT(77, ecm.size()); - EXPECT_EQ(error::INVALID_ARGUMENT, - ecm_gen.GenerateEcm(&key1, &key1, kTrackTypeSD, &ecm).error_code()); -} - -TEST_F(EcmTest, GenerateTwoKeysIvSizeFail) { - Ecm ecm_gen; +TEST_F(EcmTest, GenerateEcmParamsPassedDown) { + FakeEcm ecm_gen; + EcmInitParameters params; + params.age_restriction = 3; + params.cas_id = 0x56C0; + params.crypto_mode = CryptoMode::kAesCbc; + params.ecc_private_signing_key = "key"; + params.key_rotation_enabled = true; + params.content_iv_size = kIvSize8; + ASSERT_OK(ecm_gen.Initialize( + params, {injected_entitlement_one_, injected_entitlement_two_})); + auto ecm_serializer = absl::make_unique(); + const MockEcmSerializer* ecm_serializer_pointer = ecm_serializer.get(); + ecm_gen.SetEcmSerializer(std::move(ecm_serializer)); EntitledKeyInfo key1 = valid1_iv_16_8_; EntitledKeyInfo key2 = valid2_iv_16_8_; - // IV size mismatch. - InitParams(¶ms_two_keys_, kIvSize16); - std::string request; + EcmSerializerParams expected_params; + expected_params.cas_id = params.cas_id; + expected_params.age_restriction = params.age_restriction; + expected_params.crypto_mode = params.crypto_mode; + expected_params.ecc_private_signing_key = params.ecc_private_signing_key; + expected_params.even_key = {kEntitlementKeyId, key1.key_id, kWrappedKeyValue, + key1.wrapped_key_iv, key1.content_iv}; + expected_params.odd_key = {kEntitlementKeyId, key2.key_id, kWrappedKeyValue, + key2.wrapped_key_iv, key2.content_iv}; + EXPECT_CALL(*ecm_serializer_pointer, + SerializeEcm(EcmSerializerParamsEq(expected_params), NotNull())) + .WillOnce(Return(OkStatus())); + std::string generated_ecm; + + ASSERT_OK(ecm_gen.GenerateEcm(&key1, &key2, kTrackTypeSD, &generated_ecm)); +} + +TEST_F(EcmTest, GenerateEcmSerializeErrorOK) { + FakeEcm ecm_gen; ASSERT_OK(ecm_gen.Initialize(params_two_keys_, {injected_entitlement_one_, injected_entitlement_two_})); + auto ecm_serializer = absl::make_unique(); + const MockEcmSerializer* ecm_serializer_pointer = ecm_serializer.get(); + ecm_gen.SetEcmSerializer(std::move(ecm_serializer)); + Status expected_status = {error::INVALID_ARGUMENT, "err"}; + EXPECT_CALL(*ecm_serializer_pointer, SerializeEcm) + .WillOnce(Return(expected_status)); std::string ecm; - EXPECT_EQ(error::INVALID_ARGUMENT, - ecm_gen.GenerateEcm(&key1, &key2, kTrackTypeSD, &ecm).error_code()); -} -TEST_F(EcmTest, GenerateTwoKeysIvSize16x8OK) { - Ecm ecm_gen; - EntitledKeyInfo key1 = valid1_iv_16_8_; - EntitledKeyInfo key2 = valid2_iv_16_8_; - InitParams(¶ms_two_keys_, kIvSize8); - ASSERT_OK(ecm_gen.Initialize(params_two_keys_, {injected_entitlement_one_, - injected_entitlement_two_})); - - std::string ecm; - ASSERT_OK(ecm_gen.GenerateEcm(&key1, &key2, kTrackTypeSD, &ecm)); - EXPECT_THAT(149, ecm.size()); - ASSERT_OK(ecm_gen.GenerateEcm(&key1, &key2, kTrackTypeSD, &ecm)); - EXPECT_THAT(149, ecm.size()); -} - -TEST_F(EcmTest, GenerateTwoKeysIvSize16x16OK) { - Ecm ecm_gen; - EntitledKeyInfo key1 = valid3_iv_16_16_; - EntitledKeyInfo key2 = valid4_iv_16_16_; - InitParams(¶ms_two_keys_, kIvSize16); - ASSERT_OK(ecm_gen.Initialize(params_two_keys_, {injected_entitlement_one_, - injected_entitlement_two_})); - - std::string ecm; - ASSERT_OK(ecm_gen.GenerateEcm(&key1, &key2, kTrackTypeSD, &ecm)); - EXPECT_THAT(165, ecm.size()); - ASSERT_OK(ecm_gen.GenerateEcm(&key1, &key2, kTrackTypeSD, &ecm)); - EXPECT_THAT(165, ecm.size()); -} - -// TODO(user): Add more unit tests for error paths around SerializeEcm. -TEST_F(EcmSerializeEcmTest, SerializeEcmDoubleKey16ByteIvs) { - MockEcm ecm_gen; - EntitledKeyInfo key1 = valid3_iv_16_16_; - EntitledKeyInfo key2 = valid4_iv_16_16_; - - ecm_gen.MockSetup(true, kTrackTypeSD, 0, CryptoMode::kAesCtr, 16); - - std::vector keys; - keys.push_back(&key1); - keys.push_back(&key2); - ASSERT_OK(ecm_gen.MockWrapEntitledKeys(kTrackTypeSD, keys)); - - std::string buf_string = ecm_gen.CallSerializeEcm(keys); - ValidateEcmFieldsTwoKeys(buf_string, 0, CryptoMode::kAesCtr, kIvSize16); - - buf_string = ecm_gen.CallSerializeEcm(keys); - ValidateEcmFieldsTwoKeys(buf_string, 0, CryptoMode::kAesCtr, kIvSize16); -} - -TEST_F(EcmSerializeEcmTest, SerializeEcmSingleKey16ByteIvs) { - MockEcm ecm_gen; - EntitledKeyInfo key1 = valid3_iv_16_16_; - - ecm_gen.MockSetup(false, kTrackTypeSD, 0, CryptoMode::kAesCtr, 16); - - std::vector keys; - keys.push_back(&key1); - ASSERT_OK(ecm_gen.MockWrapEntitledKeys(kTrackTypeSD, keys)); - - std::string buf_string = ecm_gen.CallSerializeEcm(keys); - ValidateEcmFieldsOneKey(buf_string, 0, CryptoMode::kAesCtr, kIvSize16); - - buf_string = ecm_gen.CallSerializeEcm(keys); - ValidateEcmFieldsOneKey(buf_string, 0, CryptoMode::kAesCtr, kIvSize16); -} - -TEST_F(EcmSerializeEcmTest, SerializeEcmDoubleKey16x8ByteIvs) { - MockEcm ecm_gen; - EntitledKeyInfo key1 = valid1_iv_16_8_; - EntitledKeyInfo key2 = valid2_iv_16_8_; - - ecm_gen.MockSetup(true, kTrackTypeSD, 0, CryptoMode::kAesCtr, 8); - - std::vector keys; - keys.push_back(&key1); - keys.push_back(&key2); - ASSERT_OK(ecm_gen.MockWrapEntitledKeys(kTrackTypeSD, keys)); - - std::string buf_string = ecm_gen.CallSerializeEcm(keys); - ValidateEcmFieldsTwoKeys(buf_string, 0, CryptoMode::kAesCtr, kIvSize8); - - buf_string = ecm_gen.CallSerializeEcm(keys); - ValidateEcmFieldsTwoKeys(buf_string, 0, CryptoMode::kAesCtr, kIvSize8); -} - -TEST_F(EcmSerializeEcmTest, SerializeEcmSingleKey16x8ByteIvs) { - MockEcm ecm_gen; - EntitledKeyInfo key1 = valid1_iv_16_8_; - - ecm_gen.MockSetup(false, kTrackTypeSD, 0, CryptoMode::kAesCtr, 8); - - std::vector keys; - keys.push_back(&key1); - ASSERT_OK(ecm_gen.MockWrapEntitledKeys(kTrackTypeSD, keys)); - - std::string buf_string = ecm_gen.CallSerializeEcm(keys); - ValidateEcmFieldsOneKey(buf_string, 0, CryptoMode::kAesCtr, kIvSize8); - - buf_string = ecm_gen.CallSerializeEcm(keys); - ValidateEcmFieldsOneKey(buf_string, 0, CryptoMode::kAesCtr, kIvSize8); -} - -TEST_F(EcmSerializeEcmTest, SerializeEcmAgeRestriction) { - MockEcm ecm_gen; - EntitledKeyInfo key1 = valid3_iv_16_16_; - EntitledKeyInfo key2 = valid4_iv_16_16_; - uint8_t age_restriction = 18; - - ecm_gen.MockSetup(true, kTrackTypeSD, age_restriction, CryptoMode::kAesCtr, - 16); - - std::vector keys; - keys.push_back(&key1); - keys.push_back(&key2); - ASSERT_OK(ecm_gen.MockWrapEntitledKeys(kTrackTypeSD, keys)); - - EXPECT_CALL(ecm_gen, age_restriction()) - .WillRepeatedly(Return(age_restriction)); - std::string buf_string = ecm_gen.CallSerializeEcm(keys); - ValidateEcmFieldsTwoKeys(buf_string, age_restriction, CryptoMode::kAesCtr, - kIvSize16); - - buf_string = ecm_gen.CallSerializeEcm(keys); - ValidateEcmFieldsTwoKeys(buf_string, age_restriction, CryptoMode::kAesCtr, - kIvSize16); + EXPECT_EQ(ecm_gen.GenerateEcm(&valid1_iv_16_8_, &valid2_iv_16_8_, + kTrackTypeSD, &ecm), + expected_status); } class EcmTsPacketTest : public ::testing::Test {}; @@ -477,9 +376,14 @@ TEST_F(EcmTsPacketTest, GenerateTsPacket_TableId80) { ProgramId pid = 0x1FFD; ContinuityCounter cc = 0; uint8_t packet[188]; - EXPECT_OK(Ecm::GenerateTsPacket(ecm, pid, kTsPacketTableId80, &cc, packet)); + ssize_t bytes_modified = 0; + + EXPECT_OK(Ecm::GenerateTsPacket(ecm, pid, kTsPacketTableId80, &cc, packet, + &bytes_modified)); + EXPECT_EQ(0, memcmp(kExpectedEcmPacket, packet, sizeof(packet))); EXPECT_EQ(1, cc); + EXPECT_EQ(bytes_modified, 188); } TEST_F(EcmTsPacketTest, GenerateTsPacket_TableId81) { @@ -487,13 +391,19 @@ TEST_F(EcmTsPacketTest, GenerateTsPacket_TableId81) { ProgramId pid = 0x1FFD; ContinuityCounter cc = 0; uint8_t packet[188]; - EXPECT_OK(Ecm::GenerateTsPacket(ecm, pid, kTsPacketTableId81, &cc, packet)); + ssize_t bytes_modified = 0; + + EXPECT_OK(Ecm::GenerateTsPacket(ecm, pid, kTsPacketTableId81, &cc, packet, + &bytes_modified)); + char expected_ecm[188]; memcpy(expected_ecm, kExpectedEcmPacket, 188); expected_ecm[5] = '\x81'; EXPECT_EQ(0, memcmp(expected_ecm, packet, sizeof(packet))); EXPECT_EQ(1, cc); + EXPECT_EQ(bytes_modified, 188); } +} // namespace } // namespace cas } // namespace widevine diff --git a/media_cas_packager_sdk/internal/ecmg_client_handler.cc b/media_cas_packager_sdk/internal/ecmg_client_handler.cc index 14c8ae9..f49a674 100644 --- a/media_cas_packager_sdk/internal/ecmg_client_handler.cc +++ b/media_cas_packager_sdk/internal/ecmg_client_handler.cc @@ -14,6 +14,8 @@ #include "glog/logging.h" #include "absl/memory/memory.h" #include "absl/strings/str_cat.h" +#include "absl/types/optional.h" +#include "absl/types/span.h" #include "common/crypto_util.h" #include "common/random_util.h" #include "media_cas_packager_sdk/internal/ecmg_constants.h" @@ -21,6 +23,7 @@ #include "media_cas_packager_sdk/internal/simulcrypt_constants.h" #include "media_cas_packager_sdk/internal/simulcrypt_util.h" #include "media_cas_packager_sdk/internal/util.h" +#include "media_cas_packager_sdk/public/wv_cas_types.h" // CA System ID for Widevine. static constexpr uint16_t kWidevineSystemId = 0x4AD4; @@ -33,7 +36,7 @@ static constexpr uint16_t kWidevineNewSystemIdUpperBound = 0x56C9; // TODO(user): Understand the difference between the two formats. static constexpr uint8_t kSectionTSpktFlag = 0x01; // 'max_stream' parameter defines the max number of simultaneous opened streams -// suppported by an ECMG on a channel. +// supported by an ECMG on a channel. // A value of 0 means that this maximum is not known. static constexpr uint16_t kMaxStream = 0; // Size of Wrapped_Key_IV (IV for decrypting the wrapped key) in bytes. @@ -44,6 +47,8 @@ static constexpr size_t kMaxAllowedAgeRestriction = 99; static constexpr size_t kEntitlementKeyIdSizeBytes = 16; // Size of entitlement key value in bytes. static constexpr size_t kEntitlementKeyValueSizeBytes = 32; +// The default track type value fed to inner ECM generator. +static constexpr char kDefaultTrackType[] = "track_type"; namespace widevine { namespace cas { @@ -59,7 +64,8 @@ Status ProcessPrivateParameters(const char* const request, uint16_t param_type, *offset += param_length; break; case STREAM_TRACK_TYPE: - params->stream_track_type = std::string(request + *offset, param_length); + LOG(WARNING) << "Ignoring STREAM_TRACK_TYPE parameter of " << param_length + << " bytes long"; *offset += param_length; break; case CONTENT_IV: @@ -98,6 +104,16 @@ Status ProcessPrivateParameters(const char* const request, uint16_t param_type, *offset += kEntitlementKeyValueSizeBytes; break; } + case FINGERPRINTING_CONTROL: + params->fingerprinting_control = + std::string(request + *offset, param_length); + *offset += param_length; + break; + case SERVICE_BLOCKING_GROUP: + params->service_blocking_groups.push_back( + std::string(request + *offset, param_length)); + *offset += param_length; + break; default: return Status( error::UNIMPLEMENTED, @@ -132,7 +148,7 @@ Status HandleAccessCriteria(const char* const request, size_t request_length, // Local helper function that processes all the params in an ECMG request. Status HandleParameters(const char* const request, size_t request_length, - EcmgParameters* params) { + EcmgParameters* params, bool process_access_criteria) { DCHECK(request); DCHECK(params); Status status; @@ -230,9 +246,13 @@ Status HandleParameters(const char* const request, size_t request_length, offset += param_length; break; case ACCESS_CRITERIA: - status = HandleAccessCriteria(request + offset, param_length, params); - if (!status.ok()) { - return status; + if (process_access_criteria) { + status = HandleAccessCriteria(request + offset, param_length, params); + if (!status.ok()) { + return status; + } + } else { + params->access_criteria = std::string(request + offset, param_length); } offset += param_length; break; @@ -399,9 +419,8 @@ void BuildStreamCloseResponse(uint16_t channel_id, uint16_t stream_id, } void BuildEcmResponse(uint16_t channel_id, uint16_t stream_id, uint16_t cp_number, - uint8_t* ecm_datagram, char* message, + absl::Span ecm_datagram, char* message, size_t* message_length) { - DCHECK(ecm_datagram); DCHECK(message); DCHECK(message_length); simulcrypt_util::BuildMessageHeader( @@ -412,16 +431,67 @@ void BuildEcmResponse(uint16_t channel_id, uint16_t stream_id, uint16_t cp_numbe message_length); simulcrypt_util::AddUint16Param(CP_NUMBER, cp_number, message, message_length); - simulcrypt_util::AddParam(ECM_DATAGRAM, ecm_datagram, kTsPacketSize, message, - message_length); + simulcrypt_util::AddParam(ECM_DATAGRAM, ecm_datagram.data(), + ecm_datagram.size(), message, message_length); uint16_t total_param_length = *message_length - 5; Host16ToBigEndian(message + 3, &total_param_length); } +Status CheckReceivedContentIv(const std::vector& content_ivs, + int expected_size) { + if (content_ivs.size() > expected_size) { + return { + error::INVALID_ARGUMENT, + absl::StrCat("Too many content ivs, expecting ", expected_size, ".")}; + } + if (content_ivs.size() < expected_size) { + return { + error::INVALID_ARGUMENT, + absl::StrCat("Not enough content ivs, expecting ", expected_size, ".")}; + } + if (content_ivs[0].size() != 8 && content_ivs[0].size() != 16) { + return {error::INVALID_ARGUMENT, "Wrong content iv size: must be 8 or 16."}; + } + for (const auto& content_iv : content_ivs) { + if (content_iv.size() != content_ivs[0].size()) { + return {error::INVALID_ARGUMENT, "Size of content ivs must match."}; + } + } + return OkStatus(); +} + +std::vector ConvertEntitlementKeyInfo( + const std::vector& fetched_entitlements) { + std::vector converted; + // Only process if the fetched size is expected (1 or 2 entries). + if (fetched_entitlements.empty()) { + LOG(WARNING) << "EntitlementKeyFetcherFunc returned empty result."; + } else if (fetched_entitlements.size() > 2) { + LOG(ERROR) + << "EntitlementKeyFetcherFunc should return at most 2 entries, but " + << fetched_entitlements.size() << " entries are returned."; + } else { + converted.reserve(fetched_entitlements.size()); + for (auto const& fetched_entitlement : fetched_entitlements) { + EntitlementIdKeyComb entitlement_comb; + entitlement_comb.key_id = fetched_entitlement.key_id; + entitlement_comb.key_value = fetched_entitlement.key_value; + // In the case of two entitlement keys, the first in |entitlements| must + // be even key followed by the odd key. + converted.insert( + fetched_entitlement.is_even_key ? converted.begin() : converted.end(), + entitlement_comb); + } + } + return converted; +} + } // namespace EcmgClientHandler::EcmgClientHandler(EcmgConfig* ecmg_config) - : channel_id_set_(false), custom_key_fetcher_(nullptr) { + : channel_id_set_(false), + custom_key_fetcher_(nullptr), + custom_ac_processor_(nullptr) { DCHECK(ecmg_config); ecmg_config_ = ecmg_config; } @@ -448,7 +518,10 @@ void EcmgClientHandler::HandleRequest(const char* const request, char* response, BigEndianToHost16(&request_length, request + offset); offset += MESSAGE_LENGTH_SIZE; EcmgParameters params; - Status status = HandleParameters(request + offset, request_length, ¶ms); + const bool should_sdk_process_access_criteria = + custom_ac_processor_ == nullptr; + Status status = HandleParameters(request + offset, request_length, ¶ms, + should_sdk_process_access_criteria); if (!status.ok()) { LOG(ERROR) << status.ToString(); BuildChannelError(params.ecm_channel_id, StatusToDvbErrorCode(status), @@ -475,6 +548,9 @@ void EcmgClientHandler::HandleRequest(const char* const request, char* response, HandleStreamCloseRequest(params, response, response_length); break; case ECMG_CW_PROVISION: + if (!should_sdk_process_access_criteria) { + UpdateParamsWithCustomAccessCriteriaProcessor(params); + } HandleCwProvision(params, response, response_length); break; case ECMG_CHANNEL_ERROR: @@ -503,10 +579,10 @@ void EcmgClientHandler::HandleChannelSetup(const EcmgParameters& params, } // The super_cas_id is a 32-bit identifier formed by the concatenation of the // CA_system_id (16 bit) and the CA_subsystem_id (16 bit). - uint16_t cas_id = params.super_cas_id >> 16; - if (cas_id != kWidevineSystemId && - (cas_id < kWidevineNewSystemIdLowerBound || - cas_id > kWidevineNewSystemIdUpperBound)) { + cas_id_ = params.super_cas_id >> 16; + if (cas_id_ != kWidevineSystemId && + (cas_id_ < kWidevineNewSystemIdLowerBound || + cas_id_ > kWidevineNewSystemIdUpperBound)) { BuildChannelError(params.ecm_channel_id, UNKNOWN_SUPER_CAS_ID_VALUE, "", response, response_length); return; @@ -596,8 +672,12 @@ void EcmgClientHandler::HandleStreamSetup(const EcmgParameters& params, return; } - streams_info_[params.ecm_stream_id] = absl::make_unique(); - streams_info_[params.ecm_stream_id]->ecm_id = params.ecm_id; + auto stream_info = absl::make_unique(); + stream_info->ecm_id = params.ecm_id; + stream_info->crypto_mode = ecmg_config_->crypto_mode; + stream_info->age_restriction = age_restriction_; + stream_info->content_ivs.assign(content_ivs_.begin(), content_ivs_.end()); + streams_info_[params.ecm_stream_id] = std::move(stream_info); Status status = UpdateStreamPrivateParameters(params); if (!status.ok()) { @@ -615,27 +695,10 @@ void EcmgClientHandler::HandleStreamSetup(const EcmgParameters& params, const std::vector& fetched_entitlements = custom_key_fetcher_(params.ecm_channel_id, params.ecm_stream_id, params.ecm_id); - // Only process if the fetched size is expected (1 or 2 entries). - if (fetched_entitlements.empty()) { - LOG(WARNING) << "EntitlementKeyFetcherFunc returned empty result."; - } else if (fetched_entitlements.size() > 2) { - LOG(ERROR) - << "EntitlementKeyFetcherFunc should return at most 2 entries, but " - << fetched_entitlements.size() << " entries are returned."; - } else { - entitlements.reserve(fetched_entitlements.size()); - for (auto const& fetched_entitlement : fetched_entitlements) { - EntitlementIdKeyComb entitlement_comb; - entitlement_comb.key_id = fetched_entitlement.key_id; - entitlement_comb.key_value = fetched_entitlement.key_value; - // In the case of two entitlement keys, the first in |entitlements| must - // be even key followed by the odd key. - entitlements.insert(fetched_entitlement.is_even_key - ? entitlements.begin() - : entitlements.end(), - entitlement_comb); - } - } + const std::vector& converted_entitlements = + ConvertEntitlementKeyInfo(fetched_entitlements); + entitlements.assign(converted_entitlements.begin(), + converted_entitlements.end()); } BuildStreamStatus(params.ecm_channel_id, params.ecm_stream_id, params.ecm_id, @@ -715,6 +778,35 @@ void EcmgClientHandler::HandleStreamError(const EcmgParameters& params, *response_length = 0; } +void EcmgClientHandler::UpdateParamsWithCustomAccessCriteriaProcessor( + EcmgParameters& params) const { + DCHECK(custom_ac_processor_ != nullptr); + const EcmgCustomParameters& custom_params = custom_ac_processor_( + params.ecm_channel_id, params.ecm_stream_id, params.access_criteria); + // Load results to |params|. + if (custom_params.age_restriction >= 0) { + params.age_restriction = custom_params.age_restriction; + } + if (!custom_params.crypto_mode.empty()) { + params.crypto_mode = custom_params.crypto_mode; + } + if (!custom_params.content_ivs.empty()) { + params.content_ivs = custom_params.content_ivs; + } + if (!custom_params.entitlement_keys.empty()) { + const std::vector& converted_entitlements = + ConvertEntitlementKeyInfo(custom_params.entitlement_keys); + params.entitlement_comb.assign(converted_entitlements.begin(), + converted_entitlements.end()); + } + if (!custom_params.fingerprinting_control.empty()) { + params.fingerprinting_control = custom_params.fingerprinting_control; + } + if (!custom_params.service_blocking_groups.empty()) { + params.service_blocking_groups = custom_params.service_blocking_groups; + } +} + void EcmgClientHandler::HandleCwProvision(const EcmgParameters& params, char* response, size_t* response_length) { @@ -766,8 +858,9 @@ void EcmgClientHandler::HandleCwProvision(const EcmgParameters& params, } // Build Ecm datagram. - uint8_t ecm_datagram[kTsPacketSize]; - status = BuildEcmDatagram(params, ecm_datagram); + uint8_t ecm_datagram[kMaxPossibleTsPacketsSizeBytes]; + ssize_t bytes_modified = 0; + status = BuildEcmDatagram(params, ecm_datagram, &bytes_modified); if (!status.ok()) { LOG(ERROR) << status.ToString(); BuildStreamError(params.ecm_channel_id, params.ecm_stream_id, @@ -777,43 +870,18 @@ void EcmgClientHandler::HandleCwProvision(const EcmgParameters& params, } // Success. Send response message containing Ecm datagram. - BuildEcmResponse(params.ecm_channel_id, params.ecm_stream_id, - params.cp_number, ecm_datagram, response, response_length); -} - -Status EcmgClientHandler::UpdateCommonPrivateParameters( - const EcmgParameters& params) { - if (params.age_restriction != 0xff) { - if (params.age_restriction > kMaxAllowedAgeRestriction) { - return {error::INVALID_ARGUMENT, "Age restriction too large."}; - } - age_restriction_ = params.age_restriction; - } - if (!params.content_ivs.empty()) { - if (params.content_ivs.size() < ecmg_config_->number_of_content_keys) { - return {error::INVALID_ARGUMENT, - absl::StrCat("Not enough content ivs, expecting ", - ecmg_config_->number_of_content_keys, ".")}; - } - if (params.content_ivs[0].size() != 8 && - params.content_ivs[0].size() != 16) { - return {error::INVALID_ARGUMENT, - "Wrong content iv size: must be 8 or 16."}; - } - for (size_t i = 1; i < ecmg_config_->number_of_content_keys; i++) { - if (params.content_ivs[0].size() != params.content_ivs[i].size()) { - return {error::INVALID_ARGUMENT, "Size of content ivs must match."}; - } - } - } - return OkStatus(); + BuildEcmResponse( + params.ecm_channel_id, params.ecm_stream_id, params.cp_number, + absl::MakeSpan(ecm_datagram, bytes_modified), response, response_length); } Status EcmgClientHandler::UpdateChannelPrivateParameters( const EcmgParameters& params) { - Status status = UpdateCommonPrivateParameters(params); - if (!status.ok()) { - return status; + if (params.age_restriction >= 0) { + if (params.age_restriction > kMaxAllowedAgeRestriction) { + return {error::INVALID_ARGUMENT, "Age restriction too large."}; + } + age_restriction_ = params.age_restriction; } if (!params.crypto_mode.empty()) { if (!StringToCryptoMode(params.crypto_mode, &ecmg_config_->crypto_mode)) { @@ -822,6 +890,11 @@ Status EcmgClientHandler::UpdateChannelPrivateParameters( } } if (!params.content_ivs.empty()) { + Status status = CheckReceivedContentIv( + params.content_ivs, ecmg_config_->number_of_content_keys); + if (!status.ok()) { + return status; + } content_ivs_.assign(params.content_ivs.begin(), params.content_ivs.end()); } return OkStatus(); @@ -832,27 +905,57 @@ Status EcmgClientHandler::UpdateStreamPrivateParameters( DCHECK(streams_info_.contains(params.ecm_stream_id)); EcmgStreamInfo* stream_info = streams_info_[params.ecm_stream_id].get(); - Status status = UpdateCommonPrivateParameters(params); - if (!status.ok()) { - return status; + // Changing parameters used in Ecm::Initialize() needs to destroy the current + // ECM generator held by the stream. + bool invalidate_ecm_gen = false; + + if (params.age_restriction >= 0) { + if (params.age_restriction > kMaxAllowedAgeRestriction) { + return {error::INVALID_ARGUMENT, "Age restriction too large."}; + } + if (stream_info->age_restriction != params.age_restriction) { + stream_info->age_restriction = params.age_restriction; + invalidate_ecm_gen = true; + } } + if (!params.crypto_mode.empty()) { - if (!StringToCryptoMode(params.crypto_mode, &stream_info->crypto_mode)) { + CryptoMode new_crypto_mode; + if (!StringToCryptoMode(params.crypto_mode, &new_crypto_mode)) { return {error::INVALID_ARGUMENT, absl::StrCat("Unknown crypto mode: ", params.crypto_mode, ".")}; } + if (stream_info->crypto_mode != new_crypto_mode) { + stream_info->crypto_mode = new_crypto_mode; + invalidate_ecm_gen = true; + } } - if (!params.stream_track_type.empty()) { - stream_info->track_type = params.stream_track_type; - } + if (!params.content_ivs.empty()) { + Status status = CheckReceivedContentIv( + params.content_ivs, ecmg_config_->number_of_content_keys); + if (!status.ok()) { + return status; + } + if (stream_info->content_ivs.empty() || + params.content_ivs[0].size() != stream_info->content_ivs[0].size()) { + invalidate_ecm_gen = true; + } stream_info->content_ivs.assign(params.content_ivs.begin(), params.content_ivs.end()); } - if (!params.entitlement_comb.empty()) { + + if (!params.entitlement_comb.empty() && + stream_info->entitlement_comb != params.entitlement_comb) { stream_info->entitlement_comb.assign(params.entitlement_comb.begin(), params.entitlement_comb.end()); + invalidate_ecm_gen = true; } + + if (invalidate_ecm_gen) { + stream_info->ecm.reset(); + } + return OkStatus(); } @@ -861,7 +964,7 @@ Status EcmgClientHandler::CheckAndInitializeEcm(const EcmgParameters& params) { EcmgStreamInfo* stream_info = streams_info_.at(params.ecm_stream_id).get(); DCHECK(stream_info->ecm == nullptr); - if (stream_info->content_ivs.empty() && content_ivs_.empty()) { + if (stream_info->content_ivs.empty()) { return {error::NOT_FOUND, "Content iv not specified."}; } if (stream_info->entitlement_comb.empty()) { @@ -877,35 +980,33 @@ Status EcmgClientHandler::CheckAndInitializeEcm(const EcmgParameters& params) { bool key_rotation = ecmg_config_->number_of_content_keys > 1; EcmInitParameters ecm_init_params; ecm_init_params.key_rotation_enabled = key_rotation; - ecm_init_params.age_restriction = age_restriction_; - ecm_init_params.crypto_mode = stream_info->crypto_mode == CryptoMode::kInvalid - ? ecmg_config_->crypto_mode - : stream_info->crypto_mode; - ecm_init_params.content_iv_size = kIvSize8; - if ((!stream_info->content_ivs.empty() && - stream_info->content_ivs[0].size() == 16) || - (!content_ivs_.empty() && content_ivs_[0].size() == 16)) { - ecm_init_params.content_iv_size = kIvSize16; - } + ecm_init_params.age_restriction = stream_info->age_restriction; + ecm_init_params.crypto_mode = stream_info->crypto_mode; + ecm_init_params.content_iv_size = + stream_info->content_ivs[0].size() == 16 ? kIvSize16 : kIvSize8; + ecm_init_params.cas_id = cas_id_; + ecm_init_params.ecm_version = ecmg_config_->ecm_version; + ecm_init_params.ecc_private_signing_key = + ecmg_config_->ecc_private_signing_key; std::vector entitlements; entitlements.reserve(stream_info->entitlement_comb.size()); for (size_t i = 0; i < stream_info->entitlement_comb.size(); i++) { entitlements.emplace_back(); EntitlementKeyInfo* entitlement = &entitlements.back(); - entitlement->track_type = stream_info->track_type; + entitlement->track_type = kDefaultTrackType; entitlement->is_even_key = key_rotation ? i % 2 == 0 : true; entitlement->key_id = stream_info->entitlement_comb[i].key_id; entitlement->key_value = stream_info->entitlement_comb[i].key_value; } - stream_info->ecm = absl::make_unique(); + stream_info->ecm = CreateEcmInstance(); return stream_info->ecm->Initialize(ecm_init_params, entitlements); } Status EcmgClientHandler::BuildEcmDatagram(const EcmgParameters& params, - uint8_t* ecm_datagram) const { - DCHECK(ecm_datagram); + absl::Span ecm_datagram, + ssize_t* bytes_modified) const { DCHECK(streams_info_.contains(params.ecm_stream_id)); EcmgStreamInfo* stream_info = streams_info_.at(params.ecm_stream_id).get(); DCHECK(stream_info->ecm); @@ -918,12 +1019,8 @@ Status EcmgClientHandler::BuildEcmDatagram(const EcmgParameters& params, } // Generate serialized ECM. - CryptoMode crypto_mode = stream_info->crypto_mode == CryptoMode::kInvalid - ? ecmg_config_->crypto_mode - : stream_info->crypto_mode; - - // If two keys are present, the even key should be put first, followed by the - // odd key. + // If two keys are present, the even key should be put first, followed by + // the odd key. std::vector keys; keys.reserve(key_count); for (const auto& cp_cw : params.cp_cw_combinations) { @@ -931,7 +1028,7 @@ Status EcmgClientHandler::BuildEcmDatagram(const EcmgParameters& params, : keys.emplace(keys.end()); key_info->key_value = cp_cw.cw; // Make content key to 16 bytes if crypto mode is Csa2. - if (crypto_mode == CryptoMode::kDvbCsa2 && + if (stream_info->crypto_mode == CryptoMode::kDvbCsa2 && key_info->key_value.size() == 8) { key_info->key_value = absl::StrCat(key_info->key_value, key_info->key_value); @@ -945,25 +1042,57 @@ Status EcmgClientHandler::BuildEcmDatagram(const EcmgParameters& params, key_info->wrapped_key_iv = generated_key_iv.value(); } - if (content_ivs_.size() < key_count && - stream_info->content_ivs.size() < key_count) { + if (stream_info->content_ivs.size() < key_count) { return {error::INVALID_ARGUMENT, "Not enough content iv."}; } // The first iv received is for even key and second is for odd key. for (size_t i = 0; i < key_count; ++i) { - keys[i].content_iv = stream_info->content_ivs.size() >= key_count - ? stream_info->content_ivs[i] - : content_ivs_[i]; + keys[i].content_iv = stream_info->content_ivs[i]; } + EcmFingerprintingParams fingerprinting_params; + if (fingerprinting_func_ != nullptr) { + fingerprinting_params = + fingerprinting_func_(params.ecm_channel_id, params.ecm_stream_id); + } + // Fingerprinting info received from SCS has higher priority. + if (!params.fingerprinting_control.empty()) { + if (!fingerprinting_params.control.empty()) { + LOG(WARNING) << "Received fingerprinting info from both SCS and callback " + "function. Using the one received from SCS."; + } + fingerprinting_params.control = params.fingerprinting_control; + } + stream_info->ecm->SetFingerprinting( + fingerprinting_params.control.empty() ? nullptr : &fingerprinting_params); + + EcmServiceBlockingParams service_blocking_params; + if (service_blocking_func_ != nullptr) { + service_blocking_params = + service_blocking_func_(params.ecm_channel_id, params.ecm_stream_id); + } + // Service blocking info received from SCS has higher priority. + if (!params.service_blocking_groups.empty()) { + if (!service_blocking_params.device_groups.empty()) { + LOG(WARNING) << "Received service blocking info from both SCS and " + "callback function. Using the one received from SCS."; + } + service_blocking_params.device_groups.assign( + params.service_blocking_groups.begin(), + params.service_blocking_groups.end()); + } + stream_info->ecm->SetServiceBlocking( + service_blocking_params.device_groups.empty() ? nullptr + : &service_blocking_params); + Status status; std::string serialized_ecm; if (key_count > 1) { - status = stream_info->ecm->GenerateEcm( - &keys[0], &keys[1], stream_info->track_type, &serialized_ecm); + status = stream_info->ecm->GenerateEcm(&keys[0], &keys[1], + kDefaultTrackType, &serialized_ecm); } else { - status = stream_info->ecm->GenerateSingleKeyEcm( - &keys[0], stream_info->track_type, &serialized_ecm); + status = stream_info->ecm->GenerateSingleKeyEcm(&keys[0], kDefaultTrackType, + &serialized_ecm); } if (!status.ok()) { return status; @@ -978,7 +1107,7 @@ Status EcmgClientHandler::BuildEcmDatagram(const EcmgParameters& params, status = Ecm::GenerateTsPacket( serialized_ecm, pid, params.cp_number % 2 == 0 ? kTsPacketTableId80 : kTsPacketTableId81, - &continuity_counter, ecm_datagram); + &continuity_counter, ecm_datagram, bytes_modified); if (!status.ok()) { LOG(ERROR) << status; return {error::INTERNAL, "GenerateTsPacket failed."}; diff --git a/media_cas_packager_sdk/internal/ecmg_client_handler.h b/media_cas_packager_sdk/internal/ecmg_client_handler.h index 616fbab..cd16f57 100644 --- a/media_cas_packager_sdk/internal/ecmg_client_handler.h +++ b/media_cas_packager_sdk/internal/ecmg_client_handler.h @@ -17,6 +17,7 @@ #include #include "absl/container/flat_hash_map.h" #include "absl/types/optional.h" +#include "absl/types/span.h" #include "common/status.h" #include "media_cas_packager_sdk/internal/ecm.h" #include "media_cas_packager_sdk/public/wv_cas_types.h" @@ -34,6 +35,13 @@ struct EcmgCpCwCombination { struct EntitlementIdKeyComb { std::string key_id; // must be 16 bytes std::string key_value; // must be 32 bytes + + bool operator==(const EntitlementIdKeyComb& other) const { + return (other.key_id == key_id) && (other.key_value == key_value); + } + bool operator!=(const EntitlementIdKeyComb& other) const { + return !operator==(other); + } }; // A struct that is used to hold all possible params for a ECMG request. @@ -50,22 +58,24 @@ struct EcmgParameters { std::vector error_information; // User defined paremeters below. - uint8_t age_restriction = 0xff; // Assume 0xff (255) is an invalid value. + int age_restriction = -1; // Negative value to indidate the field is not set. std::string crypto_mode; - std::string stream_track_type; std::vector content_ivs; // 8 or 16 bytes, one for each key. std::vector entitlement_comb; + std::string access_criteria; + std::string fingerprinting_control; + std::vector service_blocking_groups; }; struct EcmgStreamInfo { uint16_t ecm_id; std::string track_type; - // Will use |ecmg_config_|.crypto_mode if invalid. - CryptoMode crypto_mode = CryptoMode::kInvalid; - // 8 or 16 bytes, one for each key. Will use |content_ivs_| if empty. + CryptoMode crypto_mode; + // 8 or 16 bytes, one for each key. std::vector content_ivs; std::vector entitlement_comb; std::unique_ptr ecm; + uint8_t age_restriction; }; // A class that handles one (and only one) ECMG client. @@ -92,6 +102,30 @@ class EcmgClientHandler { custom_key_fetcher_ = fetcher; } + // Sets the custom access criteria processing function used by ECMG. If + // set, the function will be called when an access criteria is received from + // SCS. + virtual void SetCustomAccessCriteriaProcessFunc( + CustomAccessCriteriaProcessFunc ac_processor) { + custom_ac_processor_ = ac_processor; + } + + // Sets the fingerprinting setting function, which will be called upon each + // CwProvisioning request. It controls the fingerprinting information carried + // in ECMs. + virtual void SetFingerprintingSettingFunc( + FingerprintingSettingFunc fingerprinting_func) { + fingerprinting_func_ = fingerprinting_func; + } + + // Sets the service blocking setting function, which will be called upon each + // CwProvisioning request. It controls the service blocking information + // carried in ECMs. + virtual void SetServiceBlockingSettingFunc( + ServiceBlockingSettingFunc service_blocking_func) { + service_blocking_func_ = service_blocking_func; + } + private: void HandleChannelSetup(const EcmgParameters& params, char* response, size_t* response_length); @@ -111,11 +145,16 @@ class EcmgClientHandler { size_t* response_length) const; void HandleCwProvision(const EcmgParameters& params, char* response, size_t* response_length); + void UpdateParamsWithCustomAccessCriteriaProcessor( + EcmgParameters& params) const; - // Update private parameters using |params|. + // Update channel level private parameters using |params|. Status UpdateChannelPrivateParameters(const EcmgParameters& params); + // Update stream level private parameters using |params|. If changes in + // |params| makes the stream hold Ecm instance (EcmgStreamInfo.ecm) no longer + // usable (changing params used in Ecm::Initialize()), it will invalidate + // (destroy) it. Status UpdateStreamPrivateParameters(const EcmgParameters& params); - Status UpdateCommonPrivateParameters(const EcmgParameters& params); // Check if all required parameters have been set. If so, initialize |ecm_| by // fetching entitlement keys. @@ -123,16 +162,22 @@ class EcmgClientHandler { // Gather all information needed to build a TS packet |ecm_datagram| // conatianing an ECM. Status BuildEcmDatagram(const EcmgParameters& params, - uint8_t* ecm_datagram) const; + absl::Span ecm_datagram, + ssize_t* bytes_modified) const; // Generates a random wrapped key iv string. Returns true on success, false // otherwise. The main purpose for this function is easier testing. virtual absl::optional GenerateRandomWrappedKeyIv() const; + // For easier testing. + virtual std::unique_ptr CreateEcmInstance() const { + return absl::make_unique(); + } EcmgConfig* ecmg_config_; // Per spec, "There is always one (and only one) channel per TCP connection". bool channel_id_set_ = false; uint16_t channel_id_; + uint16_t cas_id_; // Channel specific information. uint8_t age_restriction_ = 0; @@ -142,6 +187,12 @@ class EcmgClientHandler { absl::flat_hash_map> streams_info_; // Used to fetch entitlement keys if none are received from SCS. EntitlementKeyFetcherFunc custom_key_fetcher_; + // Used to process custom aceess criteria received in CwProvision message. + CustomAccessCriteriaProcessFunc custom_ac_processor_; + // Used to get fingerprinting information that will be embedded in ECMs. + FingerprintingSettingFunc fingerprinting_func_; + // Used to get service blocking information that will be embedded in ECMs. + ServiceBlockingSettingFunc service_blocking_func_; }; } // namespace cas diff --git a/media_cas_packager_sdk/internal/ecmg_client_handler_test.cc b/media_cas_packager_sdk/internal/ecmg_client_handler_test.cc index 900b531..0650901 100644 --- a/media_cas_packager_sdk/internal/ecmg_client_handler_test.cc +++ b/media_cas_packager_sdk/internal/ecmg_client_handler_test.cc @@ -21,6 +21,7 @@ #include "absl/strings/str_format.h" #include "absl/types/span.h" #include "example/test_ecmg_messages.h" +#include "media_cas_packager_sdk/internal/ecm.h" #include "media_cas_packager_sdk/internal/ecmg_constants.h" #include "media_cas_packager_sdk/internal/mpeg2ts.h" #include "media_cas_packager_sdk/internal/simulcrypt_util.h" @@ -35,7 +36,9 @@ using simulcrypt_util::AddUint16Param; using simulcrypt_util::AddUint32Param; using simulcrypt_util::AddUint8Param; using simulcrypt_util::BuildMessageHeader; +using ::testing::ByMove; using ::testing::ElementsAreArray; +using ::testing::Return; constexpr size_t kBufferSize = 1024; constexpr uint16_t kWidevineSystemId = 0x4AD4; @@ -61,8 +64,11 @@ constexpr size_t kAgeRestriction = 3; constexpr char kCryptoMode[] = "AesScte"; constexpr char kCryptoModeCsa2[] = "DvbCsa2"; constexpr char kTrackTypesSD[] = "SD"; -constexpr char kTrackTypesHD[] = "HD"; constexpr absl::string_view kWrappedKeyIv = "0123456701234567"; +constexpr int kExpectedEcmResponseLength = sizeof(kTestEcmgEcmResponse); +constexpr char kFingerprintingControl[] = "control"; +constexpr char kServiceBlockingGroup1[] = "group1"; +constexpr char kServiceBlockingGroup2[] = "group2"; std::vector FakeEntitlementKeyFetcherFunc( uint16_t /*channel_id*/, uint16_t /*stream_id*/, uint16_t /*ecm_id*/) { @@ -72,6 +78,21 @@ std::vector FakeEntitlementKeyFetcherFunc( kEntitlementKeyValueEven}}; } +EcmgCustomParameters FakeCustomAcProcessorFunc( + uint16_t /*channel_id*/, uint16_t /*stream_id*/, + const std::string& access_criteria) { + EcmgCustomParameters params; + params.age_restriction = access_criteria.empty() ? 0 : 3; + params.content_ivs = {kContentKeyIvEven, kContentKeyIvOdd}; + params.crypto_mode = kCryptoMode; + params.entitlement_keys = {{kTrackTypesSD, /*is_even_key*/ false, + kEntitlementKeyIdOdd, kEntitlementKeyValueOdd}, + {kTrackTypesSD, /*is_even_key*/ true, + kEntitlementKeyIdEven, kEntitlementKeyValueEven}}; + + return params; +} + class MockEcmgClientHandler : public EcmgClientHandler { public: explicit MockEcmgClientHandler(EcmgConfig* ecmg_config) @@ -79,11 +100,15 @@ class MockEcmgClientHandler : public EcmgClientHandler { absl::optional GenerateRandomWrappedKeyIv() const override { return std::string(kWrappedKeyIv); } + + MOCK_METHOD(std::unique_ptr, CreateEcmInstance, (), (const, override)); }; class EcmgClientHandlerTest : public ::testing::Test { protected: - EcmgClientHandlerTest() { + EcmgClientHandlerTest() {} + + void SetUp() override { config_.delay_start = 200; config_.delay_stop = 200; config_.ecm_rep_period = 100; @@ -91,6 +116,9 @@ class EcmgClientHandlerTest : public ::testing::Test { config_.access_criteria_transfer_mode = 1; config_.number_of_content_keys = 2; handler_ = absl::make_unique(&config_); + EXPECT_CALL(*handler_.get(), CreateEcmInstance).WillRepeatedly([] { + return absl::make_unique(); + }); } protected: @@ -107,19 +135,32 @@ class EcmgClientHandlerTest : public ::testing::Test { fflush(stdout); } - void SetupValidChannel() { + void SetupValidChannelWithPrivateParameters() { handler_->HandleRequest(kTestEcmgChannelSetupWithPrivateParameters, response_, &response_len_); - EXPECT_EQ(sizeof(kTestEcmgChannelStatus), response_len_); - EXPECT_EQ(0, memcmp(kTestEcmgChannelStatus, response_, response_len_)); + EXPECT_THAT(absl::MakeSpan(response_, response_len_), + ElementsAreArray(kTestEcmgChannelStatus)); } - void SetupValidChannelStream() { - SetupValidChannel(); + void SetupValidChannelStreamWithPrivateParameters() { + SetupValidChannelWithPrivateParameters(); handler_->HandleRequest(kTestEcmgStreamSetupWithPrivateParameters, response_, &response_len_); - EXPECT_EQ(sizeof(kTestEcmgStreamStatus), response_len_); - EXPECT_EQ(0, memcmp(kTestEcmgStreamStatus, response_, response_len_)); + EXPECT_THAT(absl::MakeSpan(response_, response_len_), + ElementsAreArray(kTestEcmgStreamStatus)); + } + + void SetupValidStandardChannel() { + handler_->HandleRequest(kTestEcmgChannelSetup, response_, &response_len_); + EXPECT_THAT(absl::MakeSpan(response_, response_len_), + ElementsAreArray(kTestEcmgChannelStatus)); + } + + void SetupValidStandardChannelStream() { + SetupValidStandardChannel(); + handler_->HandleRequest(kTestEcmgStreamSetup, response_, &response_len_); + EXPECT_THAT(absl::MakeSpan(response_, response_len_), + ElementsAreArray(kTestEcmgStreamStatus)); } void BuildChannelSetupRequest(uint16_t channel_id, uint32_t super_cas_id, @@ -146,7 +187,6 @@ class EcmgClientHandlerTest : public ::testing::Test { void BuildStreamSetupRequest(uint16_t channel_id, uint16_t stream_id, uint16_t ecm_id, uint16_t nominal_CP_duration, - const std::string& stream_track_type, const std::vector& entitlements, const std::vector& content_ivs, char* message, size_t* message_length) { @@ -160,11 +200,6 @@ class EcmgClientHandlerTest : public ::testing::Test { AddUint16Param(NOMINAL_CP_DURATION, nominal_CP_duration, message, message_length); - if (!stream_track_type.empty()) { - AddParam(STREAM_TRACK_TYPE, - reinterpret_cast(stream_track_type.c_str()), - stream_track_type.size(), message, message_length); - } if (!entitlements.empty()) { for (const auto& entitlement : entitlements) { AddParam(ENTITLEMENT_ID_KEY_COMBINATION, @@ -185,7 +220,9 @@ class EcmgClientHandlerTest : public ::testing::Test { void BuildCwProvisionRequest( uint16_t channel_id, uint16_t stream_id, uint16_t cp_number, - const std::vector& cp_cw_combination, char* message, + const std::vector& cp_cw_combination, + const std::string& fingerprinting, + const std::vector& service_blockings, char* message, size_t* message_length) { EXPECT_TRUE(message != nullptr); EXPECT_TRUE(message_length != nullptr); @@ -201,6 +238,17 @@ class EcmgClientHandlerTest : public ::testing::Test { AddParam(CP_CW_COMBINATION, combined, 2 + cp_cw.cw.length(), message, message_length); } + if (!fingerprinting.empty()) { + AddParam(FINGERPRINTING_CONTROL, + reinterpret_cast(fingerprinting.c_str()), + fingerprinting.size(), message, message_length); + } + for (auto const& sb : service_blockings) { + AddParam(SERVICE_BLOCKING_GROUP, + reinterpret_cast(sb.c_str()), sb.size(), message, + message_length); + } + uint16_t total_param_length = *message_length - 5; Host16ToBigEndian(message + 3, &total_param_length); } @@ -230,7 +278,7 @@ class EcmgClientHandlerTest : public ::testing::Test { } EcmgConfig config_; - std::unique_ptr handler_; + std::unique_ptr handler_; char response_[kBufferSize]; size_t response_len_ = 0; char request_[kBufferSize]; @@ -323,7 +371,7 @@ TEST_F(EcmgClientHandlerTest, SuccessSequenceInjectedEntitlements) { EXPECT_EQ(0, memcmp(kTestEcmgChannelStatus, response_, response_len_)); BuildStreamSetupRequest( - kChannelId, kStreamId, kEcmId, kNominalCpDuration, kTrackTypesSD, + kChannelId, kStreamId, kEcmId, kNominalCpDuration, {absl::StrCat(kEntitlementKeyIdEven, kEntitlementKeyValueEven), absl::StrCat(kEntitlementKeyIdOdd, kEntitlementKeyValueOdd)}, {kContentKeyEven, kContentKeyEven}, request_, &request_len_); @@ -332,7 +380,7 @@ TEST_F(EcmgClientHandlerTest, SuccessSequenceInjectedEntitlements) { EXPECT_EQ(0, memcmp(kTestEcmgStreamStatus, response_, response_len_)); BuildStreamSetupRequest( - kChannelId, kStreamId + 1, kEcmId, kNominalCpDuration, kTrackTypesHD, + kChannelId, kStreamId + 1, kEcmId, kNominalCpDuration, {absl::StrCat(kEntitlementKeyIdEven, kEntitlementKeyValueEven), absl::StrCat(kEntitlementKeyIdOdd, kEntitlementKeyValueOdd)}, {kContentKeyEven, kContentKeyEven}, request_, &request_len_); @@ -342,13 +390,15 @@ TEST_F(EcmgClientHandlerTest, SuccessSequenceInjectedEntitlements) { const std::vector cp_cw_combination = { {kCpNumber, kContentKeyEven}, {kCpNumber + 1, kContentKeyOdd}}; BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", /*service_blockings=*/{}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); EXPECT_EQ(sizeof(kTestEcmgEcmResponse), response_len_); EXPECT_EQ(0, memcmp(kTestEcmgEcmResponse, response_, 27)); BuildCwProvisionRequest(kChannelId, kStreamId + 1, kCpNumber, - cp_cw_combination, request_, &request_len_); + cp_cw_combination, /*fingerprinting=*/"", + /*service_blockings=*/{}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); EXPECT_EQ(sizeof(kTestEcmgEcmResponse), response_len_); } @@ -363,16 +413,17 @@ TEST_F(EcmgClientHandlerTest, SuccessSequenceFetchedEntitlements) { ElementsAreArray(kTestEcmgChannelStatus)); // No entitlement keys are injected. - BuildStreamSetupRequest( - kChannelId, kStreamId, kEcmId, kNominalCpDuration, kTrackTypesSD, - /*entitlements=*/{}, {kContentKeyIvEven, kContentKeyIvOdd}, request_, - &request_len_); + BuildStreamSetupRequest(kChannelId, kStreamId, kEcmId, kNominalCpDuration, + /*entitlements=*/{}, + {kContentKeyIvEven, kContentKeyIvOdd}, request_, + &request_len_); handler_->HandleRequest(request_, response_, &response_len_); EXPECT_EQ(response_len_, sizeof(kTestEcmgStreamStatus)); const std::vector cp_cw_combination = { {kCpNumber, kContentKeyEven}, {kCpNumber + 1, kContentKeyOdd}}; BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", /*service_blockings=*/{}, request_, &request_len_); EXPECT_THAT(absl::MakeSpan(response_, response_len_), ElementsAreArray(kTestEcmgStreamStatus)); @@ -380,6 +431,46 @@ TEST_F(EcmgClientHandlerTest, SuccessSequenceFetchedEntitlements) { EXPECT_EQ(response_len_, sizeof(kTestEcmgEcmResponse)); } +TEST_F(EcmgClientHandlerTest, + SuccessSequenceWithCustomAccessCriteriaProcessor) { + handler_->SetCustomAccessCriteriaProcessFunc(FakeCustomAcProcessorFunc); + SetupValidStandardChannelStream(); + + handler_->HandleRequest(kTestEcmgCwProvision, response_, &response_len_); + + EXPECT_EQ(response_len_, kExpectedEcmResponseLength); +} + +TEST_F(EcmgClientHandlerTest, SameParametersReuseEcm) { + handler_->SetCustomAccessCriteriaProcessFunc(FakeCustomAcProcessorFunc); + SetupValidStandardChannelStream(); + EXPECT_CALL(*handler_.get(), CreateEcmInstance) + .Times(1) + .WillOnce(Return(ByMove(absl::make_unique()))); + + handler_->HandleRequest(kTestEcmgCwProvision, response_, &response_len_); + EXPECT_EQ(response_len_, kExpectedEcmResponseLength); + // Calling again will not invoke new Ecm. + handler_->HandleRequest(kTestEcmgCwProvision, response_, &response_len_); + EXPECT_EQ(response_len_, kExpectedEcmResponseLength); +} + +TEST_F(EcmgClientHandlerTest, ChangedParametersReinitializeEcm) { + handler_->SetCustomAccessCriteriaProcessFunc(FakeCustomAcProcessorFunc); + SetupValidStandardChannelStream(); + EXPECT_CALL(*handler_.get(), CreateEcmInstance).Times(2).WillRepeatedly([] { + return absl::make_unique(); + }); + + handler_->HandleRequest(kTestEcmgCwProvision, response_, &response_len_); + EXPECT_EQ(response_len_, kExpectedEcmResponseLength); + // The fake FakeCustomAcProcessorFunc will set age restriction as the length + // of access criteria, causing parameter change. + handler_->HandleRequest(kTestEcmgCwProvisionWithAccessCriteria, response_, + &response_len_); + EXPECT_EQ(response_len_, kExpectedEcmResponseLength); +} + TEST_F(EcmgClientHandlerTest, SuccessSequenceCsa2) { BuildChannelSetupRequest(kChannelId, kSuperCasId, kAgeRestriction, kCryptoModeCsa2, request_, &request_len_); @@ -388,7 +479,7 @@ TEST_F(EcmgClientHandlerTest, SuccessSequenceCsa2) { EXPECT_EQ(0, memcmp(kTestEcmgChannelStatus, response_, response_len_)); BuildStreamSetupRequest( - kChannelId, kStreamId, kEcmId, kNominalCpDuration, kTrackTypesSD, + kChannelId, kStreamId, kEcmId, kNominalCpDuration, {absl::StrCat(kEntitlementKeyIdEven, kEntitlementKeyValueEven), absl::StrCat(kEntitlementKeyIdOdd, kEntitlementKeyValueOdd)}, {kContentKeyEven, kContentKeyEven}, request_, &request_len_); @@ -400,6 +491,7 @@ TEST_F(EcmgClientHandlerTest, SuccessSequenceCsa2) { {kCpNumber, kContentKeyEven8Bytes}, {kCpNumber + 1, kContentKeyOdd8Bytes}}; BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", /*service_blockings=*/{}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); EXPECT_EQ(sizeof(kTestEcmgEcmResponse), response_len_); @@ -407,13 +499,13 @@ TEST_F(EcmgClientHandlerTest, SuccessSequenceCsa2) { } TEST_F(EcmgClientHandlerTest, SuccessChannelError) { - SetupValidChannel(); + SetupValidChannelWithPrivateParameters(); handler_->HandleRequest(kTestEcmgChannelError, response_, &response_len_); EXPECT_EQ(0, response_len_); } TEST_F(EcmgClientHandlerTest, SuccessStreamError) { - SetupValidChannelStream(); + SetupValidChannelStreamWithPrivateParameters(); handler_->HandleRequest(kTestEcmgStreamError, response_, &response_len_); EXPECT_EQ(0, response_len_); } @@ -445,7 +537,7 @@ TEST_F(EcmgClientHandlerTest, WrongSequenceInvalidChannel) { } TEST_F(EcmgClientHandlerTest, WrongSequenceMultipleChannel) { - SetupValidChannel(); + SetupValidChannelWithPrivateParameters(); // Setup channels muiltiple times (error). handler_->HandleRequest(kTestEcmgChannelSetup, response_, &response_len_); @@ -453,7 +545,7 @@ TEST_F(EcmgClientHandlerTest, WrongSequenceMultipleChannel) { } TEST_F(EcmgClientHandlerTest, WrongSequenceInvalidStream) { - SetupValidChannel(); + SetupValidChannelWithPrivateParameters(); // Cw provision without a valid stream (error). handler_->HandleRequest(kTestEcmgCwProvision, response_, &response_len_); @@ -469,7 +561,7 @@ TEST_F(EcmgClientHandlerTest, WrongSequenceInvalidStream) { } TEST_F(EcmgClientHandlerTest, WrongSequenceMultipleSameIdStream) { - SetupValidChannelStream(); + SetupValidChannelStreamWithPrivateParameters(); // Setup streams with the same stream_id muiltiple times (error). handler_->HandleRequest(kTestEcmgStreamSetup, response_, &response_len_); CheckStreamError(ECM_STREAM_ID_VALUE_ALREADY_IN_USE, response_, @@ -477,7 +569,7 @@ TEST_F(EcmgClientHandlerTest, WrongSequenceMultipleSameIdStream) { } TEST_F(EcmgClientHandlerTest, WrongParameterInsufficientKey) { - SetupValidChannelStream(); + SetupValidChannelStreamWithPrivateParameters(); // Cw provision with only one key while expecting two (error). handler_->HandleRequest(kTestEcmgCwProvisionSingleKey, response_, &response_len_); @@ -487,6 +579,7 @@ TEST_F(EcmgClientHandlerTest, WrongParameterInsufficientKey) { // Cw provision with no key while expecting two (error). std::vector cp_cw_combination = {}; BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", /*service_blockings=*/{}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); CheckStreamError(NOT_ENOUGH_CONTROL_WORDS_TO_COMPUTE_ECM, response_, @@ -494,12 +587,13 @@ TEST_F(EcmgClientHandlerTest, WrongParameterInsufficientKey) { } TEST_F(EcmgClientHandlerTest, WrongContentKeySize) { - SetupValidChannelStream(); + SetupValidChannelStreamWithPrivateParameters(); // Content keys should be 16 byes. const std::vector cp_cw_combination = { {kCpNumber, kContentKeyEven8Bytes}, {kCpNumber + 1, kContentKeyOdd8Bytes}}; BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", /*service_blockings=*/{}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); CheckStreamError(INVALID_VALUE_FOR_DVB_PARAMETER, response_, response_len_); @@ -514,10 +608,10 @@ TEST_F(EcmgClientHandlerTest, WrongParameterSuperCasId) { } TEST_F(EcmgClientHandlerTest, WrongParameterChannelId) { - SetupValidChannel(); + SetupValidChannelWithPrivateParameters(); // Setup a stream with an unexpected channel id (expecting kChannelId). BuildStreamSetupRequest( - 0, kStreamId, kEcmId, kNominalCpDuration, kTrackTypesSD, + 0, kStreamId, kEcmId, kNominalCpDuration, {absl::StrCat(kEntitlementKeyIdEven, kEntitlementKeyValueEven), absl::StrCat(kEntitlementKeyIdOdd, kEntitlementKeyValueOdd)}, {kContentKeyEven, kContentKeyEven}, request_, &request_len_); @@ -547,10 +641,10 @@ TEST_F(EcmgClientHandlerTest, NoInjectedEntitlements) { EXPECT_EQ(sizeof(kTestEcmgChannelStatus), response_len_); EXPECT_EQ(0, memcmp(kTestEcmgChannelStatus, response_, response_len_)); - BuildStreamSetupRequest( - kChannelId, kStreamId, kEcmId, kNominalCpDuration, kTrackTypesSD, - /*entitlements*/ {}, {kContentKeyEven, kContentKeyEven}, request_, - &request_len_); + BuildStreamSetupRequest(kChannelId, kStreamId, kEcmId, kNominalCpDuration, + /*entitlements*/ {}, + {kContentKeyEven, kContentKeyEven}, request_, + &request_len_); handler_->HandleRequest(request_, response_, &response_len_); EXPECT_EQ(sizeof(kTestEcmgStreamStatus), response_len_); EXPECT_EQ(0, memcmp(kTestEcmgStreamStatus, response_, response_len_)); @@ -558,6 +652,7 @@ TEST_F(EcmgClientHandlerTest, NoInjectedEntitlements) { const std::vector cp_cw_combination = { {kCpNumber, kContentKeyEven}, {kCpNumber + 1, kContentKeyOdd}}; BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", /*service_blockings=*/{}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); CheckStreamError(MISSING_MANDATORY_DVB_PARAMETER, response_, response_len_); @@ -571,7 +666,7 @@ TEST_F(EcmgClientHandlerTest, NotEnoughInjectedEntitlements) { EXPECT_EQ(0, memcmp(kTestEcmgChannelStatus, response_, response_len_)); BuildStreamSetupRequest( - kChannelId, kStreamId, kEcmId, kNominalCpDuration, kTrackTypesSD, + kChannelId, kStreamId, kEcmId, kNominalCpDuration, {absl::StrCat(kEntitlementKeyIdEven, kEntitlementKeyValueEven)}, {kContentKeyEven, kContentKeyEven}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); @@ -581,6 +676,7 @@ TEST_F(EcmgClientHandlerTest, NotEnoughInjectedEntitlements) { const std::vector cp_cw_combination = { {kCpNumber, kContentKeyEven}, {kCpNumber + 1, kContentKeyOdd}}; BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", /*service_blockings=*/{}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); CheckStreamError(MISSING_MANDATORY_DVB_PARAMETER, response_, response_len_); @@ -594,7 +690,7 @@ TEST_F(EcmgClientHandlerTest, TooManyInjectedEntitlements) { EXPECT_EQ(0, memcmp(kTestEcmgChannelStatus, response_, response_len_)); BuildStreamSetupRequest( - kChannelId, kStreamId, kEcmId, kNominalCpDuration, kTrackTypesSD, + kChannelId, kStreamId, kEcmId, kNominalCpDuration, {absl::StrCat(kEntitlementKeyIdEven, kEntitlementKeyValueEven), absl::StrCat(kEntitlementKeyIdOdd, kEntitlementKeyValueOdd), absl::StrCat(kEntitlementKeyIdEven, kEntitlementKeyValueEven)}, @@ -606,6 +702,7 @@ TEST_F(EcmgClientHandlerTest, TooManyInjectedEntitlements) { const std::vector cp_cw_combination = { {kCpNumber, kContentKeyEven}, {kCpNumber + 1, kContentKeyOdd}}; BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", /*service_blockings=*/{}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); CheckStreamError(MISSING_MANDATORY_DVB_PARAMETER, response_, response_len_); @@ -619,11 +716,12 @@ TEST_F(EcmgClientHandlerTest, WrongMessageLength) { } TEST_F(EcmgClientHandlerTest, BuildEcmDatagramSequenceOfEvenOdd) { - SetupValidChannelStream(); + SetupValidChannelStreamWithPrivateParameters(); std::vector cp_cw_combination = { {kCpNumber, kContentKeyEven}, {kCpNumber + 1, kContentKeyOdd}}; BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", /*service_blockings=*/{}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); EXPECT_EQ(response_len_, sizeof(kTestEcmgEcmResponse)); @@ -633,6 +731,7 @@ TEST_F(EcmgClientHandlerTest, BuildEcmDatagramSequenceOfEvenOdd) { cp_cw_combination = {{kCpNumber + 1, kContentKeyOdd}, {kCpNumber, kContentKeyEven}}; BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", /*service_blockings=*/{}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); // Sequence of cp_cw_combination does not matter as even/odd is based on cp @@ -643,12 +742,34 @@ TEST_F(EcmgClientHandlerTest, BuildEcmDatagramSequenceOfEvenOdd) { cp_cw_combination = {{kCpNumber, kContentKeyOdd}, {kCpNumber + 1, kContentKeyEven}}; BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", /*service_blockings=*/{}, request_, &request_len_); handler_->HandleRequest(request_, response_, &response_len_); // Swapping key value changes generated ecm. EXPECT_NE(std::string(response_, response_len_), first_response); } +TEST_F(EcmgClientHandlerTest, NewCasIdEmbeddedInEcmSuccess) { + uint16_t expected_cas_id = kWidevineNewSystemIdLowerBound + 1; + // Set up test channel. + BuildChannelSetupRequest(kChannelId, /*super_cas_id=*/expected_cas_id << 16, + /*age_restriction=*/0, + /*crypto_mode=*/"", request_, &request_len_); + handler_->HandleRequest(request_, response_, &response_len_); + ASSERT_EQ(response_len_, sizeof(kTestEcmgChannelStatus)); + // Set up test stream. + handler_->HandleRequest(kTestEcmgStreamSetup, response_, &response_len_); + ASSERT_EQ(response_len_, sizeof(kTestEcmgStreamStatus)); + + handler_->HandleRequest(kTestEcmgCwProvisionWithAccessCriteria, response_, + &response_len_); + + ASSERT_EQ(response_len_, sizeof(kTestEcmgEcmResponse)); + uint16_t actual_cas_id = (static_cast(response_[35]) << 8) | + static_cast(response_[36]); + EXPECT_EQ(actual_cas_id, expected_cas_id); +} + class CasIdTest : public EcmgClientHandlerTest, public ::testing::WithParamInterface<::testing::tuple> {}; @@ -681,6 +802,99 @@ INSTANTIATE_TEST_SUITE_P( kWidevineNewSystemIdUpperBound + 1), ::testing::Values(false))); +class MockEcm : public Ecm { + public: + MockEcm() = default; + ~MockEcm() override = default; + MOCK_METHOD(void, SetFingerprinting, (const EcmFingerprintingParams*), + (override)); + MOCK_METHOD(void, SetServiceBlocking, (const EcmServiceBlockingParams*), + (override)); +}; + +EcmFingerprintingParams FakeFingerprintingSettingFunc(uint16_t channel_id, + uint16_t stream_id) { + return {kFingerprintingControl}; +} + +MATCHER(CorrectFingerprinting, "") { + return arg->control == kFingerprintingControl; +} + +TEST_F(EcmgClientHandlerTest, SetFingerprintingSuccess) { + handler_->SetFingerprintingSettingFunc(FakeFingerprintingSettingFunc); + auto ecm = absl::make_unique(); + MockEcm* ecm_ptr = ecm.get(); + EXPECT_CALL(*handler_.get(), CreateEcmInstance) + .Times(1) + .WillOnce(Return(ByMove(std::move(ecm)))); + EXPECT_CALL(*ecm_ptr, SetFingerprinting(CorrectFingerprinting())).Times(1); + SetupValidStandardChannelStream(); + + handler_->HandleRequest(kTestEcmgCwProvisionWithAccessCriteria, response_, + &response_len_); +} + +TEST_F(EcmgClientHandlerTest, SetFingerprintingViaAceessCriteriaSuccess) { + auto ecm = absl::make_unique(); + MockEcm* ecm_ptr = ecm.get(); + EXPECT_CALL(*handler_.get(), CreateEcmInstance) + .Times(1) + .WillOnce(Return(ByMove(std::move(ecm)))); + EXPECT_CALL(*ecm_ptr, SetFingerprinting(CorrectFingerprinting())).Times(1); + SetupValidChannelStreamWithPrivateParameters(); + const std::vector cp_cw_combination = { + {kCpNumber, kContentKeyEven}, {kCpNumber + 1, kContentKeyOdd}}; + BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + kFingerprintingControl, + /*service_blockings=*/{}, request_, &request_len_); + + handler_->HandleRequest(request_, response_, &response_len_); +} + +EcmServiceBlockingParams FakeServiceBlockingSettingFunc(uint16_t channel_id, + uint16_t stream_id) { + return {{kServiceBlockingGroup1, kServiceBlockingGroup2}}; +} + +MATCHER(CorrectServiceBlocking, "") { + const std::vector expected = {kServiceBlockingGroup1, + kServiceBlockingGroup2}; + return arg->device_groups == expected; +} + +TEST_F(EcmgClientHandlerTest, SetServiceBlockingSuccess) { + handler_->SetServiceBlockingSettingFunc(FakeServiceBlockingSettingFunc); + auto ecm = absl::make_unique(); + MockEcm* ecm_ptr = ecm.get(); + EXPECT_CALL(*handler_.get(), CreateEcmInstance) + .Times(1) + .WillOnce(Return(ByMove(std::move(ecm)))); + EXPECT_CALL(*ecm_ptr, SetServiceBlocking(CorrectServiceBlocking())).Times(1); + SetupValidStandardChannelStream(); + + handler_->HandleRequest(kTestEcmgCwProvisionWithAccessCriteria, response_, + &response_len_); +} + +TEST_F(EcmgClientHandlerTest, SetServiceBlockingViaAceessCriteriaSuccess) { + auto ecm = absl::make_unique(); + MockEcm* ecm_ptr = ecm.get(); + EXPECT_CALL(*handler_.get(), CreateEcmInstance) + .Times(1) + .WillOnce(Return(ByMove(std::move(ecm)))); + EXPECT_CALL(*ecm_ptr, SetServiceBlocking(CorrectServiceBlocking())).Times(1); + SetupValidChannelStreamWithPrivateParameters(); + const std::vector cp_cw_combination = { + {kCpNumber, kContentKeyEven}, {kCpNumber + 1, kContentKeyOdd}}; + BuildCwProvisionRequest(kChannelId, kStreamId, kCpNumber, cp_cw_combination, + /*fingerprinting=*/"", + {kServiceBlockingGroup1, kServiceBlockingGroup2}, + request_, &request_len_); + + handler_->HandleRequest(request_, response_, &response_len_); +} + } // namespace } // namespace cas } // namespace widevine diff --git a/media_cas_packager_sdk/internal/ecmg_constants.h b/media_cas_packager_sdk/internal/ecmg_constants.h index 91aca33..1dd9396 100644 --- a/media_cas_packager_sdk/internal/ecmg_constants.h +++ b/media_cas_packager_sdk/internal/ecmg_constants.h @@ -66,6 +66,8 @@ #define STREAM_TRACK_TYPE (0x8002) #define CONTENT_IV (0x8003) #define ENTITLEMENT_ID_KEY_COMBINATION (0x8004) +#define FINGERPRINTING_CONTROL (0x8005) +#define SERVICE_BLOCKING_GROUP (0x8006) // ECMG protocol error values. #define INVALID_MESSAGE (0x0001) diff --git a/media_cas_packager_sdk/internal/emm.cc b/media_cas_packager_sdk/internal/emm.cc index a10de47..1bf66b5 100644 --- a/media_cas_packager_sdk/internal/emm.cc +++ b/media_cas_packager_sdk/internal/emm.cc @@ -122,11 +122,11 @@ Status Emm::GenerateEmm(std::string* serialized_emm) const { } Status Emm::GenerateEmmTsPackets(uint16_t pid, uint8_t* continuity_counter, - uint8_t* packet, ssize_t* packet_size) const { - if (continuity_counter == nullptr || packet == nullptr || - packet_size == nullptr) { + const absl::Span packet, + ssize_t* bytes_modified) const { + if (continuity_counter == nullptr || bytes_modified == nullptr) { return {error::INVALID_ARGUMENT, - "continuity_counter, packet and packet_size must not be null"}; + "continuity_counter and bytes_modified must not be null"}; } std::string serialized_emm; @@ -134,25 +134,22 @@ Status Emm::GenerateEmmTsPackets(uint16_t pid, uint8_t* continuity_counter, if (!status.ok()) { return status; } - if (*packet_size < static_cast(CalculateTsBufferSizeForEcmEmm( - serialized_emm.size()))) { - return {error::INVALID_ARGUMENT, "packet buffer is too small"}; + + size_t buf_size_needed = + CalculateTsBufferSizeForEcmEmm(serialized_emm.size()); + if (packet.size() < buf_size_needed) { + return {error::INVALID_ARGUMENT, + absl::StrCat("Packet buffer is too small. Minimum required: ", + buf_size_needed, " bytes.")}; } - ssize_t bytes_modified = 0; - status = InsertEmmAsTsPacket(serialized_emm, pid, continuity_counter, packet, - &bytes_modified); - if (!status.ok() || bytes_modified <= 0 || - bytes_modified % kTsPacketSize != 0) { - LOG(ERROR) << "Failed to generate TS packet: " - << (status.ok() - ? absl::StrCat("Unexpected bytes_modified value ", - bytes_modified) - : status.error_message()); - return {error::INTERNAL, "Failed to generate TS packet"}; - } + status = InsertEmmAsTsPacket(serialized_emm, pid, continuity_counter, + packet.subspan(0, buf_size_needed)); - *packet_size = bytes_modified; + if (!status.ok()) { + return status; + } + *bytes_modified += buf_size_needed; return OkStatus(); } diff --git a/media_cas_packager_sdk/internal/emm.h b/media_cas_packager_sdk/internal/emm.h index 7863e48..51e89c0 100644 --- a/media_cas_packager_sdk/internal/emm.h +++ b/media_cas_packager_sdk/internal/emm.h @@ -13,6 +13,7 @@ #include #include +#include "absl/types/span.h" #include "common/status.h" #include "protos/public/media_cas.pb.h" @@ -67,11 +68,11 @@ class Emm { // - |packet| a buffer to be used to return the generated TS packet. Must be // able to hold the maximum possible size of genereated Ts packets // (kMaxPossibleTsPacketsSizeBytes = 1128 bytes). - // - |packet_size| is the size of the allocated |packet|. It will be updated - // as the number of bytes actually used. + // - |bytes_modified| will be incremented by the used buffer size if insertion + // of ECM into |buffer| is successful. virtual Status GenerateEmmTsPackets(uint16_t pid, uint8_t* continuity_counter, - uint8_t* packet, - ssize_t* packet_size) const; + const absl::Span packet, + ssize_t* bytes_modified) const; private: struct EmmSerializingParameters { diff --git a/media_cas_packager_sdk/internal/emm_test.cc b/media_cas_packager_sdk/internal/emm_test.cc index b861f64..9bd7291 100644 --- a/media_cas_packager_sdk/internal/emm_test.cc +++ b/media_cas_packager_sdk/internal/emm_test.cc @@ -376,11 +376,11 @@ TEST_F(EmmTest, GenerateEmmNoPayloadSuccess) { TEST_F(EmmTest, GenerateEmmTsPacketsNoPayloadSuccess) { uint8_t counter = 0; uint8_t packet[kTsPacketSize]; - ssize_t packet_size = kTsPacketSize; + ssize_t bytes_modified = 0; EXPECT_OK( - emm_->GenerateEmmTsPackets(kTestPid, &counter, packet, &packet_size)); + emm_->GenerateEmmTsPackets(kTestPid, &counter, packet, &bytes_modified)); EXPECT_THAT(packet, ElementsAreArray(kExpectedEmptyEmmPacket)); - EXPECT_EQ(packet_size, kTsPacketSize); + EXPECT_EQ(bytes_modified, kTsPacketSize); } // Verifies GenerateSignature is successful with empty payload. @@ -450,39 +450,24 @@ TEST(GenerateEmmTsPacketsTest, MockPayloadSuccess) { uint8_t counter = 0; uint8_t packet[kTsPacketSize]; - ssize_t packet_size = kTsPacketSize; - EXPECT_OK( - emm_gen.GenerateEmmTsPackets(kTestPid, &counter, packet, &packet_size)); + ssize_t bytes_modified = 0; + EXPECT_OK(emm_gen.GenerateEmmTsPackets(kTestPid, &counter, packet, + &bytes_modified)); EXPECT_THAT(packet, ElementsAreArray(kExpectedMockEmmPacket)); - EXPECT_EQ(packet_size, kTsPacketSize); + EXPECT_EQ(bytes_modified, kTsPacketSize); } // Verifies GenerateEmmTsPackets fails if the given continuity counter is null. TEST(GenerateEmmTsPacketsTest, NullContinuityCounterPointerFail) { Emm emm_gen; uint8_t packet[kTsPacketSize]; - ssize_t packet_size = kTsPacketSize; + ssize_t bytes_modified = 0; EXPECT_EQ( - emm_gen.GenerateEmmTsPackets(kTestPid, nullptr, packet, &packet_size) + emm_gen.GenerateEmmTsPackets(kTestPid, nullptr, packet, &bytes_modified) .error_code(), error::INVALID_ARGUMENT); } -// Verifies GenerateEmmTsPackets fails if the given packet buffer is null. -TEST(GenerateEmmTsPacketsTest, NullPacketBufferFail) { - Emm emm_gen; - uint8_t counter = 0; - uint8_t packet[kTsPacketSize]; - ssize_t packet_size = kTsPacketSize; - EXPECT_EQ( - emm_gen.GenerateEmmTsPackets(kTestPid, &counter, nullptr, &packet_size) - .error_code(), - error::INVALID_ARGUMENT); - EXPECT_EQ(emm_gen.GenerateEmmTsPackets(kTestPid, &counter, packet, nullptr) - .error_code(), - error::INVALID_ARGUMENT); -} - // Verifies GenerateEmmTsPackets fails if the given buffer is too small. TEST(GenerateEmmTsPacketsTest, PacketBufferTooSmallFail) { MockEmmGenerate emm_gen; @@ -491,10 +476,10 @@ TEST(GenerateEmmTsPacketsTest, PacketBufferTooSmallFail) { .WillOnce(DoAll(SetArgPointee<0>(serialized_emm), Return(OkStatus()))); uint8_t counter = 0; - uint8_t packet[kTsPacketSize]; - ssize_t packet_size = 1; + uint8_t packet[1]; + ssize_t bytes_modified = 0; Status status = - emm_gen.GenerateEmmTsPackets(kTestPid, &counter, packet, &packet_size); + emm_gen.GenerateEmmTsPackets(kTestPid, &counter, packet, &bytes_modified); EXPECT_EQ(status.error_code(), error::INVALID_ARGUMENT); EXPECT_THAT(status.error_message(), HasSubstr("buffer is too small")); } diff --git a/media_cas_packager_sdk/internal/emmg.cc b/media_cas_packager_sdk/internal/emmg.cc index c86b3d7..b3a9463 100644 --- a/media_cas_packager_sdk/internal/emmg.cc +++ b/media_cas_packager_sdk/internal/emmg.cc @@ -283,16 +283,16 @@ Status Emmg::GenerateEmmData() { } uint8_t datagram[kMaxPossibleTsPacketsSizeBytes]; - ssize_t datagram_size = kMaxPossibleTsPacketsSizeBytes; + ssize_t bytes_modified = 0; status = emm_impl_->GenerateEmmTsPackets( - /*pid=*/0, &continuity_counter_, datagram, &datagram_size); + /*pid=*/0, &continuity_counter_, datagram, &bytes_modified); if (!status.ok()) { return status; } - if (datagram_size <= 0 || datagram_size % kTsPacketSize != 0) { + if (bytes_modified <= 0 || bytes_modified % kTsPacketSize != 0) { return {error::INTERNAL, "Failed to generate EMM TS packet"}; } - simulcrypt_util::AddParam(EMMG_DATAGRAM, datagram, datagram_size, request_, + simulcrypt_util::AddParam(EMMG_DATAGRAM, datagram, bytes_modified, request_, &request_length_); return OkStatus(); } diff --git a/media_cas_packager_sdk/internal/util.cc b/media_cas_packager_sdk/internal/util.cc index 25064a0..46278eb 100644 --- a/media_cas_packager_sdk/internal/util.cc +++ b/media_cas_packager_sdk/internal/util.cc @@ -48,24 +48,28 @@ ContinuityCounter Increment(ContinuityCounter continuity_counter) { Status InsertDataAsTsPacket(const std::string& insert_data, ProgramId pid, uint8_t table_id, ContinuityCounter* cc, - uint8_t* buffer, ssize_t* bytes_modified) { + absl::Span buffer) { if (insert_data.empty()) { return {error::INVALID_ARGUMENT, "insert_data must not be empty"}; } - if (cc == nullptr || buffer == nullptr || bytes_modified == nullptr) { - return {error::INVALID_ARGUMENT, - "cc, buffer and bytes_modified must not be null"}; - } - if (*bytes_modified % kTsPacketSize != 0) { - return {error::INVALID_ARGUMENT, - "bytes_modified must be a multiple of TS packet size (188 bytes)"}; + if (cc == nullptr) { + return {error::INVALID_ARGUMENT, "Continuity counter must not be null"}; } if (insert_data.size() > kMaxSectionSize) { return {error::INVALID_ARGUMENT, absl::StrCat("Data is too large. Max: ", kMaxSectionSize)}; } + size_t expected_buffer_size = + CalculateTsBufferSizeForEcmEmm(insert_data.size()); + if (buffer.size() != expected_buffer_size) { + return { + error::INVALID_ARGUMENT, + absl::StrCat("Invalid buffer size. Expected: ", expected_buffer_size, + " bytes , but got ", buffer.size(), " bytes.")}; + } int reading_index = 0; + int buffer_write_offset = 0; while (reading_index < insert_data.size()) { bool is_first_packet = reading_index == 0; // Create a TS payload of 184 bytes (Max TS size - ts header length). @@ -110,9 +114,9 @@ Status InsertDataAsTsPacket(const std::string& insert_data, ProgramId pid, if (serialized_ts_packet.size() != kTsPacketSize) { return {error::INTERNAL, "Unexpected TS packet size generated."}; } - memcpy(buffer + *bytes_modified, serialized_ts_packet.data(), + memcpy(buffer.data() + buffer_write_offset, serialized_ts_packet.data(), serialized_ts_packet.size()); - *bytes_modified += serialized_ts_packet.size(); + buffer_write_offset += serialized_ts_packet.size(); } return OkStatus(); @@ -157,19 +161,17 @@ void Host32ToBigEndian(void* destination, const uint32_t* source) { } Status InsertEcmAsTsPacket(const std::string& ecm, ProgramId pid, - uint8_t table_id, ContinuityCounter* cc, uint8_t* buffer, - ssize_t* bytes_modified) { - return InsertDataAsTsPacket(ecm, pid, table_id, cc, buffer, bytes_modified); + uint8_t table_id, ContinuityCounter* cc, + absl::Span buffer) { + return InsertDataAsTsPacket(ecm, pid, table_id, cc, buffer); } Status InsertEmmAsTsPacket(const std::string& emm, ProgramId pid, - ContinuityCounter* cc, uint8_t* buffer, - ssize_t* bytes_modified) { - return InsertDataAsTsPacket(emm, pid, kEmmTableId, cc, buffer, - bytes_modified); + ContinuityCounter* cc, absl::Span buffer) { + return InsertDataAsTsPacket(emm, pid, kEmmTableId, cc, buffer); } -uint16_t CalculateTsBufferSizeForEcmEmm(uint16_t ecm_emm_size) { +size_t CalculateTsBufferSizeForEcmEmm(size_t ecm_emm_size) { return kTsPacketSize * std::ceil(static_cast(kSectionHeaderSize + ecm_emm_size) / kMaxTsPayloadSize); diff --git a/media_cas_packager_sdk/internal/util.h b/media_cas_packager_sdk/internal/util.h index d471920..3aaeb29 100644 --- a/media_cas_packager_sdk/internal/util.h +++ b/media_cas_packager_sdk/internal/util.h @@ -14,6 +14,7 @@ #include #include +#include "absl/types/span.h" #include "common/status.h" #include "media_cas_packager_sdk/internal/mpeg2ts.h" @@ -48,31 +49,22 @@ void Host32ToBigEndian(void* destination, const uint32_t* source); // table ID so the client knows this is an important ECM it should process. // - |cc| is the continuity counter to use in the header, which will also be // incremented by this function. -// - |buffer| is the output buffer. Must be big enough to hold an additional -// 188 bytes * number of TS packets needed to put |ecm|. -// - |bytes_modified| the number of bytes which have already been modified in -// the |buffer| and is used as an offset. -// |bytes_modified| will be incremented by a multiple of 188 if insertion of -// ECM into |buffer| is successful. +// - |buffer| is the output buffer. The size of |buffer| must be equal to +// CalculateTsBufferSizeForEcmEmm(ecm.size()). Status InsertEcmAsTsPacket(const std::string& ecm, ProgramId pid, - uint8_t table_id, ContinuityCounter* cc, uint8_t* buffer, - ssize_t* bytes_modified); + uint8_t table_id, ContinuityCounter* cc, + absl::Span buffer); // Packages an EMM as a TS packet and inserts it into a buffer. // Args: // - |emm| is the serialized EMM. -// - |pid| is the PID used for ECMs in the TS header. +// - |pid| is the PID used for EMMs in the TS header. // - |cc| is the continuity counter to use in the header, which will also be // incremented by this function. -// - |buffer| is the output buffer. Must be big enough to hold an additional -// 188 bytes * number of TS packets needed to put |emm|. -// - |bytes_modified| the number of bytes which have already been modified in -// the |buffer| and is used as an offset. -// |bytes_modified| will be incremented by a multiple of 188 if insertion of -// ECM into |buffer| is successful. +// - |buffer| is the output buffer. The size of |buffer| must be equal to +// CalculateTsBufferSizeForEcmEmm(emm.size()). Status InsertEmmAsTsPacket(const std::string& emm, ProgramId pid, - ContinuityCounter* cc, uint8_t* buffer, - ssize_t* bytes_modified); + ContinuityCounter* cc, absl::Span buffer); // Calculates the buffer size needed to hold TS packets generated by inserting // ECM/EMM calls InsertEcmAsTsPacket() and InsertEmmAsTsPacket(). The returned @@ -80,7 +72,7 @@ Status InsertEmmAsTsPacket(const std::string& emm, ProgramId pid, // Args: // - |ecm_emm_size| is the size of the serialized ECM or EMM that will be // inserted to TS packets. -uint16_t CalculateTsBufferSizeForEcmEmm(uint16_t ecm_emm_size); +size_t CalculateTsBufferSizeForEcmEmm(size_t ecm_emm_size); } // namespace cas } // namespace widevine diff --git a/media_cas_packager_sdk/internal/util_test.cc b/media_cas_packager_sdk/internal/util_test.cc index 5f0db0f..ee381ba 100644 --- a/media_cas_packager_sdk/internal/util_test.cc +++ b/media_cas_packager_sdk/internal/util_test.cc @@ -12,6 +12,7 @@ #include "testing/gmock.h" #include "testing/gunit.h" +#include "media_cas_packager_sdk/internal/mpeg2ts.h" namespace widevine { namespace cas { @@ -152,98 +153,102 @@ TEST(InsertEcmAsTsPacketTest, SingleTsPacket) { // declare variables used by InsertEcmAsTsPacket() ContinuityCounter ecm_cc = 0; uint8_t buffer[kTsPacketSize]; // output ts packet size - ssize_t output_bytes_modified = 0; // convert kEcmPayload[] into a std::string to be accepted by the method std::string ecm_data(kEcmPayload); EXPECT_OK(InsertEcmAsTsPacket(ecm_data, kTestPid, kTsPacketTableId80, &ecm_cc, - buffer, &output_bytes_modified)); + buffer)); EXPECT_EQ(memcmp(kExpectedEcmPacket, buffer, sizeof(buffer)), 0); EXPECT_EQ(ecm_cc, 1); - EXPECT_EQ(output_bytes_modified, kTsPacketSize); } TEST(InsertEcmAsTsPacketTest, MultipleTsPackets) { ContinuityCounter ecm_cc = 0; uint8_t buffer[kTsPacketSize * 2]; - ssize_t output_bytes_modified = 0; std::string long_payload(kLongPayload); EXPECT_OK(InsertEcmAsTsPacket(long_payload, kTestPid, kTsPacketTableId80, - &ecm_cc, buffer, &output_bytes_modified)); + &ecm_cc, buffer)); EXPECT_EQ(memcmp(kExpectedLongTs, buffer, sizeof(buffer)), 0); EXPECT_EQ(ecm_cc, 2); - EXPECT_EQ(output_bytes_modified, kTsPacketSize * 2); } TEST(InsertEmmAsTsPacketTest, SingleTsPacket) { ContinuityCounter counter = 0; uint8_t buffer[kTsPacketSize]; - ssize_t output_bytes_modified = 0; std::string payload(kEcmPayload); - EXPECT_OK(InsertEmmAsTsPacket(payload, kTestPid, &counter, buffer, - &output_bytes_modified)); + EXPECT_OK(InsertEmmAsTsPacket(payload, kTestPid, &counter, buffer)); char expectedEmmPacket[sizeof(kExpectedEcmPacket)]; memcpy(expectedEmmPacket, kExpectedEcmPacket, sizeof(kExpectedEcmPacket)); expectedEmmPacket[5] = 0x82; // Expected table id for emm EXPECT_EQ(memcmp(expectedEmmPacket, buffer, sizeof(buffer)), 0); EXPECT_EQ(counter, 1); - EXPECT_EQ(output_bytes_modified, kTsPacketSize); } TEST(InsertEmmAsTsPacketTest, MultipleTsPackets) { ContinuityCounter counter = 0; uint8_t buffer[kTsPacketSize * 2]; - ssize_t output_bytes_modified = 0; std::string long_payload(kLongPayload); - EXPECT_OK(InsertEmmAsTsPacket(long_payload, kTestPid, &counter, buffer, - &output_bytes_modified)); + EXPECT_OK(InsertEmmAsTsPacket(long_payload, kTestPid, &counter, buffer)); char expectedEmmPacket[sizeof(kExpectedLongTs)]; memcpy(expectedEmmPacket, kExpectedLongTs, sizeof(kExpectedLongTs)); expectedEmmPacket[5] = 0x82; // Expected table id for emm EXPECT_EQ(memcmp(expectedEmmPacket, buffer, sizeof(buffer)), 0); EXPECT_EQ(counter, 2); - EXPECT_EQ(output_bytes_modified, kTsPacketSize * 2); } -TEST(InsertEcmAsTsPacketTest, InvalidArgument) { - ContinuityCounter counter = 0; +TEST(InsertEcmAsTsPacketTest, NullContinuityCounterFail) { uint8_t buffer[kTsPacketSize]; - ssize_t output_bytes_modified = 0; std::string payload(kEcmPayload); - // Continuity counter is null. EXPECT_EQ(InsertEcmAsTsPacket(payload, kTestPid, kTsPacketTableId80, nullptr, - buffer, &output_bytes_modified) - .error_code(), - error::INVALID_ARGUMENT); - // Buffer is null. - EXPECT_EQ(InsertEcmAsTsPacket(payload, kTestPid, kTsPacketTableId80, &counter, - nullptr, &output_bytes_modified) - .error_code(), - error::INVALID_ARGUMENT); - // Output bytes modified is null. - EXPECT_EQ(InsertEcmAsTsPacket(payload, kTestPid, kTsPacketTableId80, &counter, - buffer, nullptr) + buffer) .error_code(), error::INVALID_ARGUMENT); +} - // Payload is empty. - EXPECT_EQ(InsertEcmAsTsPacket("", kTestPid, kTsPacketTableId80, &counter, - buffer, &output_bytes_modified) +TEST(InsertEcmAsTsPacketTest, PayloadEmptyFail) { + ContinuityCounter counter = 0; + uint8_t buffer[kTsPacketSize]; + + EXPECT_EQ(InsertEcmAsTsPacket(/*ecm=*/"", kTestPid, kTsPacketTableId80, + &counter, buffer) .error_code(), error::INVALID_ARGUMENT); +} - // Payload is too large. +TEST(InsertEcmAsTsPacketTest, PayloadTooLargeFail) { + ContinuityCounter counter = 0; + uint8_t buffer[kTsPacketSize]; std::string too_large_payload(kMaxSectionSize + 1, '\x11'); EXPECT_EQ(InsertEcmAsTsPacket(too_large_payload, kTestPid, kTsPacketTableId80, - &counter, buffer, &output_bytes_modified) + &counter, buffer) + .error_code(), + error::INVALID_ARGUMENT); +} + +TEST(InsertEcmAsTsPacketTest, SmallBufferFail) { + ContinuityCounter counter = 0; + uint8_t buffer[kTsPacketSize]; + std::string double_size_payload(kTsPacketSize * 2, '\x11'); + EXPECT_EQ(InsertEcmAsTsPacket(double_size_payload, kTestPid, + kTsPacketTableId80, &counter, buffer) + .error_code(), + error::INVALID_ARGUMENT); +} + +TEST(InsertEcmAsTsPacketTest, LargeBufferFail) { + ContinuityCounter counter = 0; + uint8_t buffer[kTsPacketSize * 2]; + std::string payload(kEcmPayload); + EXPECT_EQ(InsertEcmAsTsPacket(payload, kTestPid, kTsPacketTableId80, &counter, + buffer) .error_code(), error::INVALID_ARGUMENT); } diff --git a/media_cas_packager_sdk/public/BUILD b/media_cas_packager_sdk/public/BUILD index 0e18eee..8058769 100644 --- a/media_cas_packager_sdk/public/BUILD +++ b/media_cas_packager_sdk/public/BUILD @@ -90,6 +90,7 @@ cc_library( ":wv_cas_types", "//base", "@abseil_repo//absl/memory", + "@abseil_repo//absl/types:span", "//common:status", "//media_cas_packager_sdk/internal:ecm", ], @@ -184,6 +185,7 @@ cc_library( deps = [ "//base", "@abseil_repo//absl/memory", + "@abseil_repo//absl/types:span", "//common:status", "//media_cas_packager_sdk/internal:emm", ], diff --git a/media_cas_packager_sdk/public/wv_cas_ecm.cc b/media_cas_packager_sdk/public/wv_cas_ecm.cc index 09676d2..19c4c12 100644 --- a/media_cas_packager_sdk/public/wv_cas_ecm.cc +++ b/media_cas_packager_sdk/public/wv_cas_ecm.cc @@ -12,6 +12,8 @@ #include "glog/logging.h" #include "absl/memory/memory.h" +#include "absl/types/span.h" +#include "common/status.h" #include "media_cas_packager_sdk/internal/ecm.h" namespace widevine { @@ -35,6 +37,10 @@ EcmInitParameters ConvertToEcmInitParameters( init_params.key_rotation_enabled = ecm_parameters.key_rotation_enabled; init_params.crypto_mode = ecm_parameters.crypto_mode; init_params.age_restriction = ecm_parameters.age_restriction; + init_params.cas_id = ecm_parameters.cas_id; + init_params.ecm_version = ecm_parameters.ecm_version; + init_params.ecc_private_signing_key = ecm_parameters.ecc_private_signing_key; + return init_params; } } // namespace @@ -42,25 +48,28 @@ EcmInitParameters ConvertToEcmInitParameters( WvCasEcm::WvCasEcm( const WvCasEcmParameters& ecm_parameters, const std::vector& injected_entitlements) { - CHECK(!injected_entitlements.empty()); ecm_param_ = ecm_parameters; - injected_entitlements_.assign(injected_entitlements.begin(), - injected_entitlements.end()); + ecm_ = absl::make_unique(); + Status status = ecm_->Initialize(ConvertToEcmInitParameters(ecm_parameters), + injected_entitlements); + CHECK(status.ok()) << "Failed to get initialize ECM class." << status; +} +WvCasEcm::~WvCasEcm() = default; + +void WvCasEcm::SetFingerprinting( + const EcmFingerprintingParams* fingerprinting) { + ecm_->SetFingerprinting(fingerprinting); +} + +void WvCasEcm::SetServiceBlocking( + const EcmServiceBlockingParams* service_blocking) { + ecm_->SetServiceBlocking(service_blocking); } Status WvCasEcm::GenerateEcm(const WvCasContentKeyInfo& even_key, const WvCasContentKeyInfo& odd_key, const std::string& track_type, std::string* serialized_ecm) const { - // Create an instance of Ecm in order to set the entitlement keys. - auto cas_ecm = absl::make_unique(); - EcmInitParameters init_params = ConvertToEcmInitParameters(ecm_param_); - Status status = cas_ecm->Initialize(init_params, injected_entitlements_); - if (!status.ok()) { - LOG(ERROR) << "Failed to get initialize ECM class." << status; - return status; - } - EntitledKeyInfo entitled_even_key = ConvertToEntitledKeyInfo(even_key); EntitledKeyInfo entitled_odd_key = ConvertToEntitledKeyInfo(odd_key); // Make content key to 16 bytes if crypto mode is Csa2. @@ -74,22 +83,13 @@ Status WvCasEcm::GenerateEcm(const WvCasContentKeyInfo& even_key, entitled_odd_key.key_value + entitled_odd_key.key_value; } } - return cas_ecm->GenerateEcm(&entitled_even_key, &entitled_odd_key, track_type, - serialized_ecm); + return ecm_->GenerateEcm(&entitled_even_key, &entitled_odd_key, track_type, + serialized_ecm); } Status WvCasEcm::GenerateSingleKeyEcm(const WvCasContentKeyInfo& key, const std::string& track_type, std::string* serialized_ecm) const { - // Create an instance of Ecm in order to set the entitlement keys. - auto cas_ecm = absl::make_unique(); - EcmInitParameters init_params = ConvertToEcmInitParameters(ecm_param_); - Status status = cas_ecm->Initialize(init_params, injected_entitlements_); - if (!status.ok()) { - LOG(ERROR) << "Failed to get initialize ECM class." << status; - return status; - } - EntitledKeyInfo entitled_key = ConvertToEntitledKeyInfo(key); // Make content key to 16 bytes if crypto mode is Csa2. if (ecm_param_.crypto_mode == CryptoMode::kDvbCsa2) { @@ -97,14 +97,27 @@ Status WvCasEcm::GenerateSingleKeyEcm(const WvCasContentKeyInfo& key, entitled_key.key_value = entitled_key.key_value + entitled_key.key_value; } } - return cas_ecm->GenerateSingleKeyEcm(&entitled_key, track_type, - serialized_ecm); + return ecm_->GenerateSingleKeyEcm(&entitled_key, track_type, serialized_ecm); } Status WvCasEcm::GenerateTsPacket(const std::string& ecm, uint16_t pid, uint8_t table_id, uint8_t* continuity_counter, - uint8_t* packet) { - return Ecm::GenerateTsPacket(ecm, pid, table_id, continuity_counter, packet); + uint8_t* packet, ssize_t* packet_size) { + if (continuity_counter == nullptr || packet == nullptr || + packet_size == nullptr) { + return {error::INVALID_ARGUMENT, + "continuity_counter, packet and packet_size must not be null"}; + } + ssize_t bytes_modified = 0; + Status status = Ecm::GenerateTsPacket(ecm, pid, table_id, continuity_counter, + absl::MakeSpan(packet, *packet_size), + &bytes_modified); + if (!status.ok()) { + return status; + } + + *packet_size = bytes_modified; + return OkStatus(); } } // namespace cas diff --git a/media_cas_packager_sdk/public/wv_cas_ecm.h b/media_cas_packager_sdk/public/wv_cas_ecm.h index 5aa2cb7..e191f08 100644 --- a/media_cas_packager_sdk/public/wv_cas_ecm.h +++ b/media_cas_packager_sdk/public/wv_cas_ecm.h @@ -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 #include #include @@ -19,6 +20,8 @@ namespace widevine { namespace cas { +class Ecm; + // Information needed to generate content key portion of ECM. // Fields: // |key_id| key ID for the content key, must be 16 bytes. @@ -44,11 +47,19 @@ struct WvCasContentKeyInfo { // |crypto_mode| the encryption mode used for the content stream. // A constant of type CryptoMode. // |age_restriction| minimum age required; the value represents actual age. +// |cas_id| CA system id that is in the ECM. Must be 0x4AD4 or 0x56C0~0x56C9 +// (all inclusive). +// |ecm_version| version of generated ECM. +// |ecc_private_signing_key| Private signing key used to sign ECM data. Must +// be an elliptic-curve cryptography key. struct WvCasEcmParameters { EcmIvSize content_iv_size = kIvSize8; bool key_rotation_enabled = true; CryptoMode crypto_mode = CryptoMode::kAesCtr; uint8_t age_restriction = 0; + uint16_t cas_id = 0x4AD4; + EcmVersion ecm_version = EcmVersion::kV2; + std::string ecc_private_signing_key; }; // Class for generating Widevine CAS ECMs. @@ -66,10 +77,23 @@ class WvCasEcm { const std::vector& injected_entitlements); WvCasEcm(const WvCasEcm&) = delete; WvCasEcm& operator=(const WvCasEcm&) = delete; - virtual ~WvCasEcm() = default; + virtual ~WvCasEcm(); - // Accept keys and IVs and construct an ECM that will fit into a Transport - // Stream packet payload (184 bytes). + // Set fingerprinting info that will be embedded into the generated ECM. The + // configuration will be used in all following ECMs generated by calling + // GenerateEcm() or GenerateSingleKeyEcm(). + // |fingerprinting| may be set to nullptr to clear the fingerprinting info. + virtual void SetFingerprinting(const EcmFingerprintingParams* fingerprinting); + + // Set service blocking info that will be embedded into the generated ECM. The + // configuration will be used in all following ECMs generated by calling + // GenerateEcm() or GenerateSingleKeyEcm(). + // |service_blocking| may be set to nullptr to clear the service blocking + // info. + virtual void SetServiceBlocking( + const EcmServiceBlockingParams* service_blocking); + + // Constructs a Widevine ECM using the provided key info. // Args: // |even_key| information for even key to be encoded into ECM. // |odd_key| information for odd key to be encoded into ECM. @@ -78,15 +102,14 @@ class WvCasEcm { // The |even_key| and |odd_key| contents (specifically IV sizes) must be // consistent with the initialized settings. // The even_key and odd_key will be wrapped using the appropriate - // entitlement key. Wrapping modifies the original structure. + // entitlement key. virtual Status GenerateEcm(const WvCasContentKeyInfo& even_key, const WvCasContentKeyInfo& odd_key, const std::string& track_type, std::string* serialized_ecm) const; - // Accept a key and IV and construct an ECM that will fit into a Transport - // Stream packet payload (184 bytes). This call is specifically for the case - // where key rotation is disabled. + // Constructs a Widevine ECM using the provided key info. This call is + // specifically for the case where key rotation is disabled. // Args: // |key| information for key to be encoded into ECM. // |track_type| the track that the key is being used to encrypt. @@ -112,16 +135,18 @@ class WvCasEcm { // it will be incremented, only last 4 bits are used // - |packet| a buffer of size at least 188 bytes to be used to return // the generated TS packet + // - |packet_size| is the size of the allocated |packet|. It will be updated + // as the number of bytes actually used. // // Returns: // - A status indicating whether there was any error during processing static Status GenerateTsPacket(const std::string& ecm, uint16_t pid, uint8_t table_id, uint8_t* continuity_counter, - uint8_t* packet); + uint8_t* packet, ssize_t* packet_size); private: + std::unique_ptr ecm_; WvCasEcmParameters ecm_param_; - std::vector injected_entitlements_; }; } // namespace cas diff --git a/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.cc b/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.cc index 8eb05a7..f500e53 100644 --- a/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.cc +++ b/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.cc @@ -36,6 +36,21 @@ void WvCasEcmgClientHandler::SetCustomEntitlementKeyFetcherFunc( return inner_handler_->SetCustomEntitlementKeyFetcherFunc(fetcher); } +void WvCasEcmgClientHandler::SetCustomAccessCriteriaProcessFunc( + CustomAccessCriteriaProcessFunc ac_processor) { + return inner_handler_->SetCustomAccessCriteriaProcessFunc(ac_processor); +} + +void WvCasEcmgClientHandler::SetFingerprintingSettingFunc( + FingerprintingSettingFunc fingerprinting_func) { + return inner_handler_->SetFingerprintingSettingFunc(fingerprinting_func); +} + +void WvCasEcmgClientHandler::SetServiceBlockingSettingFunc( + ServiceBlockingSettingFunc service_blocking_func) { + return inner_handler_->SetServiceBlockingSettingFunc(service_blocking_func); +} + Status WvCasEcmgClientHandler::HandleRequest(size_t request_buffer_size, const char* const request_buffer, size_t response_buffer_size, diff --git a/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.h b/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.h index c326db8..083f356 100644 --- a/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.h +++ b/media_cas_packager_sdk/public/wv_cas_ecmg_client_handler.h @@ -36,6 +36,27 @@ class WvCasEcmgClientHandler { // Calling this function is optional. void SetCustomEntitlementKeyFetcherFunc(EntitlementKeyFetcherFunc fetcher); + // Sets the custom access criteria processing function used by ECMG to get + // information including entitlement keys, content iv, crypto mode, etc. + // Calling this function is optional: If the function is set, access criteria + // received in the CwProvision message from SCS will be processed by this + // callback function (SDK will no longer process the access criteria). If not + // set, access criteria will be processed by the SDK. + void SetCustomAccessCriteriaProcessFunc( + CustomAccessCriteriaProcessFunc ac_processor); + + // Sets the fingerprinting setting function, which will be called upon each + // CwProvisioning request. It controls the fingerprinting information carried + // in ECMs. + void SetFingerprintingSettingFunc( + FingerprintingSettingFunc fingerprinting_func); + + // Sets the service blocking setting function, which will be called upon each + // CwProvisioning request. It controls the service blocking information + // carried in ECMs. + void SetServiceBlockingSettingFunc( + ServiceBlockingSettingFunc service_blocking_func); + // Handles a |request| from the SCS client. If any response is generated, it // will return the response via |response_buffer| and |response_length|. // Args: diff --git a/media_cas_packager_sdk/public/wv_cas_emm.cc b/media_cas_packager_sdk/public/wv_cas_emm.cc index 39973fd..0d858f3 100644 --- a/media_cas_packager_sdk/public/wv_cas_emm.cc +++ b/media_cas_packager_sdk/public/wv_cas_emm.cc @@ -10,6 +10,7 @@ #include "glog/logging.h" #include "absl/memory/memory.h" +#include "absl/types/span.h" #include "media_cas_packager_sdk/internal/emm.h" namespace widevine { @@ -93,8 +94,16 @@ Status WvCasEmm::GenerateEmmTsPackets(uint16_t pid, uint8_t* continuity_counter, return {error::INVALID_ARGUMENT, "continuity_counter, packet and packet_size must not be null"}; } - return emm_->GenerateEmmTsPackets(pid, continuity_counter, packet, - packet_size); + ssize_t bytes_modified = 0; + Status status = emm_->GenerateEmmTsPackets( + pid, continuity_counter, absl::MakeSpan(packet, *packet_size), + &bytes_modified); + if (!status.ok()) { + return status; + } + + *packet_size = bytes_modified; + return OkStatus(); } } // namespace cas diff --git a/media_cas_packager_sdk/public/wv_cas_emm_test.cc b/media_cas_packager_sdk/public/wv_cas_emm_test.cc index 022481b..ee7da7b 100644 --- a/media_cas_packager_sdk/public/wv_cas_emm_test.cc +++ b/media_cas_packager_sdk/public/wv_cas_emm_test.cc @@ -16,6 +16,7 @@ #include "media_cas_packager_sdk/internal/emm.h" #include "media_cas_packager_sdk/internal/mpeg2ts.h" +using ::testing::_; using ::testing::DoAll; using ::testing::ElementsAreArray; using ::testing::IsEmpty; @@ -25,6 +26,7 @@ using ::testing::Pointwise; using ::testing::Return; using ::testing::SetArgPointee; using ::testing::SetArrayArgument; +using ::testing::WithArg; namespace widevine { namespace cas { @@ -62,8 +64,8 @@ class MockEmm : public Emm { MOCK_METHOD(Status, GenerateEmm, (std::string * serialized_emm), (const, override)); MOCK_METHOD(Status, GenerateEmmTsPackets, - (uint16_t pid, uint8_t* continuity_counter, uint8_t* packet, - ssize_t* packet_size), + (uint16_t pid, uint8_t* continuity_counter, + const absl::Span packet, ssize_t* bytes_modified), (const, override)); }; @@ -204,12 +206,13 @@ TEST_F(WvCasEmmTest, GenerateEmmTsPacketsSuccess) { uint8_t expected_continuity_counter = 2; uint8_t expected_packet[] = {1, 2, 3, 4, 5}; EXPECT_CALL(*mock_internal_emm_, - GenerateEmmTsPackets(kTestPid, NotNull(), NotNull(), NotNull())) - .WillOnce( - DoAll(SetArgPointee<1>(expected_continuity_counter), - SetArrayArgument<2>(expected_packet, - expected_packet + sizeof(expected_packet)), - SetArgPointee<3>(sizeof(expected_packet)), Return(OkStatus()))); + GenerateEmmTsPackets(kTestPid, NotNull(), _, NotNull())) + .WillOnce(DoAll( + SetArgPointee<1>(expected_continuity_counter), + WithArg<2>([&expected_packet](absl::Span packet) { + memcpy(packet.data(), expected_packet, sizeof(expected_packet)); + }), + SetArgPointee<3>(sizeof(expected_packet)), Return(OkStatus()))); uint8_t continuity_counter = 1; uint8_t packet[sizeof(expected_packet)]; @@ -222,7 +225,7 @@ TEST_F(WvCasEmmTest, GenerateEmmTsPacketsSuccess) { TEST_F(WvCasEmmTest, GenerateEmmTsPacketsFail) { EXPECT_CALL(*mock_internal_emm_, - GenerateEmmTsPackets(kTestPid, NotNull(), NotNull(), NotNull())) + GenerateEmmTsPackets(kTestPid, NotNull(), _, NotNull())) .WillOnce(Return(Status(error::INVALID_ARGUMENT))); uint8_t continuity_counter = 1; diff --git a/media_cas_packager_sdk/public/wv_cas_types.h b/media_cas_packager_sdk/public/wv_cas_types.h index 52e378a..c907623 100644 --- a/media_cas_packager_sdk/public/wv_cas_types.h +++ b/media_cas_packager_sdk/public/wv_cas_types.h @@ -9,6 +9,7 @@ #ifndef MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_TYPES_H_ #define MEDIA_CAS_PACKAGER_SDK_PUBLIC_WV_CAS_TYPES_H_ +#include #include #include @@ -65,6 +66,8 @@ struct EntitlementKeyInfo { std::string key_value; // must be 32 bytes. }; +enum class EcmVersion : int { kV2 = 0, kV3 = 1 }; + // A struct that captures the Simulcrypt ECMG configurations. Most fields are // Simulcrypt standard fields (See ETSI TS 103 197 V1.5.1 (2008-10) // Section 5.3). @@ -109,6 +112,11 @@ struct EcmgConfig { // will be included in the ECM. If new crypto_mode is received from SCS, ECM // will use the new one. CryptoMode crypto_mode; + // Version of generated ECM. + EcmVersion ecm_version = EcmVersion::kV2; + // Private signing key used to sign ECM data. Must be an elliptic-curve + // cryptography key. + std::string ecc_private_signing_key; }; // A custom entitlement key fetching function used by ECMG to fetch entitlement @@ -127,6 +135,82 @@ struct EcmgConfig { using EntitlementKeyFetcherFunc = std::vector (*)( uint16_t channel_id, uint16_t stream_id, uint16_t ecm_id); +struct EcmgCustomParameters { + // Minimum age required for the content. The value represents actual age. + // A negative value means the field is not set. + int age_restriction = -1; + // The encryption mode used for the content stream. + std::string crypto_mode; + // Initial vector used in the content encryption, must be 8 (DvbCsa2) or 16 + // bytes (all other crypto modes). The size of the vector must not exceed 2. + // The first entry is for the even key slot, the second entry is for the odd + // key slot. It is suggested that the same IV used for both even and odd key + // slot for better decrypting performance. + std::vector content_ivs; + // Entitlement keys used to encrypt the content keys (control words). + // The size of the vector must not exceed 2. When the vector size is 2, one of + // them must be specified as even key and the other must be odd key (sequence + // does not matter). + std::vector entitlement_keys; + // Fingerprinting control that will be embedded in ECM. The control is opaque + // to Widevine. + std::string fingerprinting_control; + // Devices that should have service blocking enforced. The blocking starts + // when the ECM is received, and stops util the device is no longer in + // |device_groups|. + std::vector service_blocking_groups; +}; + +// A custom access control processing function used by ECMG to get information +// including entitlement keys, content iv, crypto mode, etc. If set, the +// function will be called when an access criteria is received in the +// CwProvision message from SCS. The access criteria will no longer be processed +// by the SDK. +// Args: +// |channel_id| is the channel id received at ECMG from SCS when setting up +// the channel. +// |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 + CustomAccessCriteriaProcessFunc; + +struct EcmFingerprintingParams { + // The |control| is opaque to Widevine. + std::string control; +}; + +// Function that will be called upon each CwProvisioning request. It controls +// the fingerprinting information carried in ECMs. +// Args: +// |channel_id| is the channel id received at ECMG from SCS when setting up +// the channel. +// |stream_id| is the stream id received at ECMG from SCS when setting up +// the stream. +typedef std::function + FingerprintingSettingFunc; + +struct EcmServiceBlockingParams { + // Devices that should have service blocking enforced. The blocking starts + // when the ECM is received, and stops util the device is no longer in + // |device_groups|. + std::vector device_groups; +}; + +// Function that will be called upon each CwProvisioning request. It controls +// the service blocking information carried in ECMs. +// Args: +// |channel_id| is the channel id received at ECMG from SCS when setting up +// the channel. +// |stream_id| is the stream id received at ECMG from SCS when setting up +// the stream. +typedef std::function + ServiceBlockingSettingFunc; + struct WvCasEncryptionRequest { std::string content_id; std::string provider; diff --git a/media_cas_packager_sdk/public/wv_ecmg.cc b/media_cas_packager_sdk/public/wv_ecmg.cc index 8b84469..20fa199 100644 --- a/media_cas_packager_sdk/public/wv_ecmg.cc +++ b/media_cas_packager_sdk/public/wv_ecmg.cc @@ -49,6 +49,13 @@ ABSL_FLAG(std::string, crypto_mode, "AesCtr", "Default encryption mode if not provided in API calls. Choices are " "\"AesCtr\", \"AesCbc\", \"DvbCsa2\", \"DvbCsa3\", \"AesOfb\" and " "\"AesScte\"."); +ABSL_FLAG( + int32_t, ecm_version, 2, + "Version of the generated ECM. Crrently supported versions are 2 and 3."); +ABSL_FLAG( + std::string, ecc_private_signing_key, "", + "Private signing key used to sign ECM data. Must be an elliptic-curve " + "cryptography key."); #define LISTEN_QUEUE_SIZE (20) #define BUFFER_SIZE (1024) @@ -71,6 +78,13 @@ void BuildEcmgConfig(EcmgConfig* config) { CHECK(StringToCryptoMode(absl::GetFlag(FLAGS_crypto_mode), &config->crypto_mode)) << "Unknown crypto mode."; + CHECK(absl::GetFlag(FLAGS_ecm_version) <= 3) + << "Invalid ecm version. Must be 2 or 3."; + config->ecm_version = absl::GetFlag(FLAGS_ecm_version) <= 2 + ? widevine::cas::EcmVersion::kV2 + : widevine::cas::EcmVersion::kV3; + config->ecc_private_signing_key = + absl::GetFlag(FLAGS_ecc_private_signing_key); } void PrintMessage(const std::string& description, const char* const message, diff --git a/protos/public/media_cas.proto b/protos/public/media_cas.proto index e9aea06..405ecac 100644 --- a/protos/public/media_cas.proto +++ b/protos/public/media_cas.proto @@ -50,3 +50,62 @@ message EmmPayload { repeated Fingerprinting fingerprinting = 1; repeated ServiceBlocking service_blocking = 2; } + +message EcmMetaData { + enum CipherMode { + UNSPECIFIED = 0; + AES_CBC = 1; + AES_CTR = 2; + DVB_CSA2 = 3; + DVB_CSA3 = 4; + AES_OFB = 5; + AES_SCTE52 = 6; + } + // Required. The cipher mode used to encrypt/decrypt the content. + optional CipherMode cipher_mode = 1; + // Optional. The minimum age required to watch the content. The value + // represents actual age, with 0 means no restriction. + optional uint32 age_restriction = 2 [default = 0]; +} + +message EcmKeyData { + // The wrapped content key data (aka control word). + // Required. + optional bytes wrapped_key_data = 1; + // The ID of the entitlement key used to wrap the content key. The secure key + // data associated with this ID is held by the license server. The client gets + // the key from the license server through a license request. + // Required for the even key data, optional for the odd key data if it is the + // same as the even key data. + optional bytes entitlement_key_id = 2; + // IV for decrypting the wrapped_key_data. + // Required for the even key data, optional for the odd key data if it is the + // same as the even key data. + optional bytes wrapped_key_iv = 3; + // IV for decrypting the content stream. + // Optional. If not specified in the even key data, 8 bytes 0x00 will be used; + // If not specified in the odd key data, the same content iv in the even key + // data will be used. + optional bytes content_iv = 4; +} + +message EcmPayload { + // Required. Meta info carried by the ECM. + optional EcmMetaData meta_data = 1; + // Required. The key data for the even slot. + optional EcmKeyData even_key_data = 2; + // Optional. The key data for the odd slot if key rotation is enabled. + optional EcmKeyData odd_key_data = 3; + // Optional. Widevine fingerprinting information. + optional Fingerprinting fingerprinting = 4; + // Optional. Widevine service blocking information. + optional ServiceBlocking service_blocking = 5; +} + +// The payload field for an ECM with signature. +message SignedEcmPayload { + // Serialized EcmPayload. + optional bytes serialized_payload = 1; + // ECC (Elliptic Curve Cryptography) signature of |serialized_payload|. + optional bytes signature = 2; +}