From bee799748752fac84d9c3ecd549aa54f72c88d02 Mon Sep 17 00:00:00 2001 From: Aaron Vaage Date: Thu, 23 Jul 2020 14:42:42 -0700 Subject: [PATCH] Manual Code Push This update brings the partner repo in sync with the internal repo's commit 040460be8b9556a699a6cd3813c88ce710f68146. --- .gitignore | 0 oemcrypto/odk/Android.bp | 88 ++ oemcrypto/odk/README | 10 + oemcrypto/odk/include/OEMCryptoCENCCommon.h | 171 +++ .../odk/include/core_message_deserialize.h | 59 + .../odk/include/core_message_serialize.h | 68 + .../include/core_message_serialize_proto.h | 62 + oemcrypto/odk/include/core_message_types.h | 99 ++ oemcrypto/odk/include/odk.h | 637 +++++++++ oemcrypto/odk/include/odk_structs.h | 221 +++ oemcrypto/odk/include/odk_target.h | 13 + .../odk/src/core_message_deserialize.cpp | 133 ++ oemcrypto/odk/src/core_message_serialize.cpp | 125 ++ .../odk/src/core_message_serialize_proto.cpp | 183 +++ oemcrypto/odk/src/kdo.gypi | 18 + oemcrypto/odk/src/odk.c | 410 ++++++ oemcrypto/odk/src/odk.gyp | 24 + oemcrypto/odk/src/odk.gypi | 17 + oemcrypto/odk/src/odk_assert.h | 24 + oemcrypto/odk/src/odk_endian.h | 29 + oemcrypto/odk/src/odk_overflow.c | 36 + oemcrypto/odk/src/odk_overflow.h | 23 + oemcrypto/odk/src/odk_serialize.c | 213 +++ oemcrypto/odk/src/odk_serialize.h | 49 + oemcrypto/odk/src/odk_structs_priv.h | 104 ++ oemcrypto/odk/src/odk_timer.c | 502 +++++++ oemcrypto/odk/src/odk_util.c | 34 + oemcrypto/odk/src/odk_util.h | 28 + oemcrypto/odk/src/serialization_base.c | 259 ++++ oemcrypto/odk/src/serialization_base.h | 96 ++ oemcrypto/odk/test/fuzzing/Android.bp | 168 +++ oemcrypto/odk/test/fuzzing/README.md | 19 + .../602c63d2f3d13ca3206cdf204cde24e7d8f4266c | Bin 0 -> 20 bytes .../8cebdcc0161125a10e19c45f055051712873de25 | Bin 0 -> 20 bytes .../4e578d6c9628e832c099623b44f56d95aa37f94b | Bin 0 -> 272 bytes .../5b693511ef850e42c5ffded171794dbeb9460cc0 | Bin 0 -> 282200 bytes .../b6b865b095697164ad032c2f695ed828f5754749 | 1 + .../dba39b6cf6524e996397ddc1e08b928b5c92bb5d | Bin 0 -> 352 bytes .../dd1bc1827a331b7aed2a6fb6740da032123aa0a8 | Bin 0 -> 432 bytes .../53c26407b39c997143146a0dce8ff0ac11f565e1 | Bin 0 -> 88 bytes .../fab3c99d604bab7b7bf5c54c5bd995fc98d4d96f | Bin 0 -> 88 bytes .../91e10d030fbdd3374e57a2720f09488f2b03ce69 | Bin 0 -> 204 bytes .../12a72efb395e731ec4470b5f5b6768d6806e9131 | Bin 0 -> 76 bytes .../21de033b9baf2a0e82ae3b4185b22aa0acf69bbc | Bin 0 -> 48 bytes .../97bf96be666434bfa93dbfb36b81baeefed14170 | Bin 0 -> 76 bytes .../a7b0e7dca597331d7f051204096c9d01ba6d468e | Bin 0 -> 76 bytes .../38df40a320f60e955006aaa294b74d45a316e50f | Bin 0 -> 148 bytes .../9962997b5ea87005276319cbfff67884846485cf | Bin 0 -> 148 bytes .../c84663115c890873dd585987c1223193d29aef16 | Bin 0 -> 148 bytes .../test/fuzzing/corpus_generator/Android.bp | 27 + .../test/fuzzing/corpus_generator/README.md | 79 ++ .../corpus_generator/odk_corpus_generator.c | 160 +++ .../odk_corpus_generator_helper.c | 22 + .../odk_corpus_generator_helper.h | 19 + .../odk_fuzz_corpus_generator.gyp | 33 + oemcrypto/odk/test/fuzzing/odk_fuzz.gyp | 40 + .../odk/test/fuzzing/odk_fuzz_helper.cpp | 159 +++ oemcrypto/odk/test/fuzzing/odk_fuzz_helper.h | 206 +++ oemcrypto/odk/test/fuzzing/odk_fuzz_structs.h | 28 + .../test/fuzzing/odk_license_request_fuzz.cpp | 22 + .../fuzzing/odk_license_response_fuzz.cpp | 20 + ...odk_license_response_fuzz_with_mutator.cpp | 35 + .../fuzzing/odk_provisioning_request_fuzz.cpp | 22 + .../odk_provisioning_response_fuzz.cpp | 21 + ...rovisioning_response_fuzz_with_mutator.cpp | 38 + .../test/fuzzing/odk_renewal_request_fuzz.cpp | 22 + .../fuzzing/odk_renewal_response_fuzz.cpp | 20 + ...odk_renewal_response_fuzz_with_mutator.cpp | 36 + oemcrypto/odk/test/odk_core_message_test.cpp | 37 + oemcrypto/odk/test/odk_test.cpp | 729 ++++++++++ oemcrypto/odk/test/odk_test.gypi | 13 + oemcrypto/odk/test/odk_test_helper.cpp | 488 +++++++ oemcrypto/odk/test/odk_test_helper.h | 99 ++ oemcrypto/odk/test/odk_timer_test.cpp | 1236 +++++++++++++++++ 74 files changed, 7534 insertions(+) create mode 100644 .gitignore create mode 100644 oemcrypto/odk/Android.bp create mode 100644 oemcrypto/odk/README create mode 100644 oemcrypto/odk/include/OEMCryptoCENCCommon.h create mode 100644 oemcrypto/odk/include/core_message_deserialize.h create mode 100644 oemcrypto/odk/include/core_message_serialize.h create mode 100644 oemcrypto/odk/include/core_message_serialize_proto.h create mode 100644 oemcrypto/odk/include/core_message_types.h create mode 100644 oemcrypto/odk/include/odk.h create mode 100644 oemcrypto/odk/include/odk_structs.h create mode 100644 oemcrypto/odk/include/odk_target.h create mode 100644 oemcrypto/odk/src/core_message_deserialize.cpp create mode 100644 oemcrypto/odk/src/core_message_serialize.cpp create mode 100644 oemcrypto/odk/src/core_message_serialize_proto.cpp create mode 100644 oemcrypto/odk/src/kdo.gypi create mode 100644 oemcrypto/odk/src/odk.c create mode 100644 oemcrypto/odk/src/odk.gyp create mode 100644 oemcrypto/odk/src/odk.gypi create mode 100644 oemcrypto/odk/src/odk_assert.h create mode 100644 oemcrypto/odk/src/odk_endian.h create mode 100644 oemcrypto/odk/src/odk_overflow.c create mode 100644 oemcrypto/odk/src/odk_overflow.h create mode 100644 oemcrypto/odk/src/odk_serialize.c create mode 100644 oemcrypto/odk/src/odk_serialize.h create mode 100644 oemcrypto/odk/src/odk_structs_priv.h create mode 100644 oemcrypto/odk/src/odk_timer.c create mode 100644 oemcrypto/odk/src/odk_util.c create mode 100644 oemcrypto/odk/src/odk_util.h create mode 100644 oemcrypto/odk/src/serialization_base.c create mode 100644 oemcrypto/odk/src/serialization_base.h create mode 100644 oemcrypto/odk/test/fuzzing/Android.bp create mode 100644 oemcrypto/odk/test/fuzzing/README.md create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_request_corpus/602c63d2f3d13ca3206cdf204cde24e7d8f4266c create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_request_corpus/8cebdcc0161125a10e19c45f055051712873de25 create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/4e578d6c9628e832c099623b44f56d95aa37f94b create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/5b693511ef850e42c5ffded171794dbeb9460cc0 create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/b6b865b095697164ad032c2f695ed828f5754749 create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/dba39b6cf6524e996397ddc1e08b928b5c92bb5d create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/dd1bc1827a331b7aed2a6fb6740da032123aa0a8 create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/provisioning_request_corpus/53c26407b39c997143146a0dce8ff0ac11f565e1 create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/provisioning_request_corpus/fab3c99d604bab7b7bf5c54c5bd995fc98d4d96f create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/provisioning_response_corpus/91e10d030fbdd3374e57a2720f09488f2b03ce69 create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_request_corpus/12a72efb395e731ec4470b5f5b6768d6806e9131 create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_request_corpus/21de033b9baf2a0e82ae3b4185b22aa0acf69bbc create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_request_corpus/97bf96be666434bfa93dbfb36b81baeefed14170 create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_request_corpus/a7b0e7dca597331d7f051204096c9d01ba6d468e create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_response_corpus/38df40a320f60e955006aaa294b74d45a316e50f create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_response_corpus/9962997b5ea87005276319cbfff67884846485cf create mode 100644 oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_response_corpus/c84663115c890873dd585987c1223193d29aef16 create mode 100644 oemcrypto/odk/test/fuzzing/corpus_generator/Android.bp create mode 100644 oemcrypto/odk/test/fuzzing/corpus_generator/README.md create mode 100644 oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator.c create mode 100644 oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator_helper.c create mode 100644 oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator_helper.h create mode 100644 oemcrypto/odk/test/fuzzing/corpus_generator/odk_fuzz_corpus_generator.gyp create mode 100644 oemcrypto/odk/test/fuzzing/odk_fuzz.gyp create mode 100644 oemcrypto/odk/test/fuzzing/odk_fuzz_helper.cpp create mode 100644 oemcrypto/odk/test/fuzzing/odk_fuzz_helper.h create mode 100644 oemcrypto/odk/test/fuzzing/odk_fuzz_structs.h create mode 100644 oemcrypto/odk/test/fuzzing/odk_license_request_fuzz.cpp create mode 100644 oemcrypto/odk/test/fuzzing/odk_license_response_fuzz.cpp create mode 100644 oemcrypto/odk/test/fuzzing/odk_license_response_fuzz_with_mutator.cpp create mode 100644 oemcrypto/odk/test/fuzzing/odk_provisioning_request_fuzz.cpp create mode 100644 oemcrypto/odk/test/fuzzing/odk_provisioning_response_fuzz.cpp create mode 100644 oemcrypto/odk/test/fuzzing/odk_provisioning_response_fuzz_with_mutator.cpp create mode 100644 oemcrypto/odk/test/fuzzing/odk_renewal_request_fuzz.cpp create mode 100644 oemcrypto/odk/test/fuzzing/odk_renewal_response_fuzz.cpp create mode 100644 oemcrypto/odk/test/fuzzing/odk_renewal_response_fuzz_with_mutator.cpp create mode 100644 oemcrypto/odk/test/odk_core_message_test.cpp create mode 100644 oemcrypto/odk/test/odk_test.cpp create mode 100644 oemcrypto/odk/test/odk_test.gypi create mode 100644 oemcrypto/odk/test/odk_test_helper.cpp create mode 100644 oemcrypto/odk/test/odk_test_helper.h create mode 100644 oemcrypto/odk/test/odk_timer_test.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/oemcrypto/odk/Android.bp b/oemcrypto/odk/Android.bp new file mode 100644 index 0000000..41aa3e3 --- /dev/null +++ b/oemcrypto/odk/Android.bp @@ -0,0 +1,88 @@ +// Copyright 2019 Google LLC. All rights reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +// ---------------------------------------------------------------- +// Builds libwv_odk.a, The ODK Library (libwv_odk) is used by +// the CDM and by oemcrypto implementations. +cc_library_static { + name: "libwv_odk", + include_dirs: [ + "vendor/widevine/libwvdrmengine/oemcrypto/include", + "vendor/widevine/libwvdrmengine/oemcrypto/odk/include", + "vendor/widevine/libwvdrmengine/oemcrypto/odk/src", + ], + + srcs: [ + "src/odk.c", + "src/odk_overflow.c", + "src/odk_serialize.c", + "src/odk_timer.c", + "src/odk_util.c", + "src/serialization_base.c", + ], + proprietary: true, + + owner: "widevine", +} + +// ---------------------------------------------------------------- +// Builds libwv_kdo.a, The ODK Library companion (libwv_kdo) is used by +// the CDM and by oemcrypto tests, but not by oemcrypto implementations. +cc_library_static { + name: "libwv_kdo", + include_dirs: [ + "vendor/widevine/libwvdrmengine/oemcrypto/include", + "vendor/widevine/libwvdrmengine/oemcrypto/odk/include", + "vendor/widevine/libwvdrmengine/oemcrypto/odk/src", + ], + + srcs: [ + "src/core_message_deserialize.cpp", + "src/core_message_serialize.cpp", + "src/core_message_serialize_proto.cpp", + ], + + static_libs: [ + "libcdm_protos", + "libwv_odk", + ], + + proprietary: true, + + owner: "widevine", +} + +// ---------------------------------------------------------------- +// Builds odk_test executable, which tests the ODK library. +cc_test { + name: "odk_test", + include_dirs: [ + "vendor/widevine/libwvdrmengine/oemcrypto/include", + "vendor/widevine/libwvdrmengine/oemcrypto/odk/include", + "vendor/widevine/libwvdrmengine/oemcrypto/odk/src", + ], + + // WARNING: Module tags are not supported in Soong. + // For native test binaries, use the "cc_test" module type. Some differences: + // - If you don't use gtest, set "gtest: false" + // - Binaries will be installed into /data/nativetest[64]// + // - Both 32 & 64 bit versions will be built (as appropriate) + + owner: "widevine", + proprietary: true, + + static_libs: [ + "libcdm_protos", + "libcdm", + "libwv_odk", + "libwv_kdo", + ], + + srcs: [ + "test/odk_test.cpp", + "test/odk_test_helper.cpp", + "test/odk_timer_test.cpp", + ], + +} diff --git a/oemcrypto/odk/README b/oemcrypto/odk/README new file mode 100644 index 0000000..ba8c8c7 --- /dev/null +++ b/oemcrypto/odk/README @@ -0,0 +1,10 @@ +This ODK Library is used to generate and parse core OEMCrypto messages for +OEMCrypto v16 and above. + +This library is used by both OEMCrypto on a device, and by Widevine license and +provisioning servers. + +The source of truth for these files is in the server code base on piper. Do not +edit these files in the Android directory tree or in the Widevine Git +repository. If you need to edit these files and are not sure how to procede, +please ask for help from an engineer on the Widevine server or device teams. diff --git a/oemcrypto/odk/include/OEMCryptoCENCCommon.h b/oemcrypto/odk/include/OEMCryptoCENCCommon.h new file mode 100644 index 0000000..03d7030 --- /dev/null +++ b/oemcrypto/odk/include/OEMCryptoCENCCommon.h @@ -0,0 +1,171 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +/********************************************************************* + * OEMCryptoCENCCommon.h + * + * Common structures and error codes between WV servers and OEMCrypto. + * + *********************************************************************/ + +#ifndef WIDEVINE_ODK_INCLUDE_OEMCRYPTOCENCCOMMON_H_ +#define WIDEVINE_ODK_INCLUDE_OEMCRYPTOCENCCOMMON_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* clang-format off */ +typedef enum OEMCryptoResult { + OEMCrypto_SUCCESS = 0, + OEMCrypto_ERROR_INIT_FAILED = 1, + OEMCrypto_ERROR_TERMINATE_FAILED = 2, + OEMCrypto_ERROR_OPEN_FAILURE = 3, + OEMCrypto_ERROR_CLOSE_FAILURE = 4, + OEMCrypto_ERROR_ENTER_SECURE_PLAYBACK_FAILED = 5, /* deprecated */ + OEMCrypto_ERROR_EXIT_SECURE_PLAYBACK_FAILED = 6, /* deprecated */ + OEMCrypto_ERROR_SHORT_BUFFER = 7, + OEMCrypto_ERROR_NO_DEVICE_KEY = 8, /* no keybox device key. */ + OEMCrypto_ERROR_NO_ASSET_KEY = 9, + OEMCrypto_ERROR_KEYBOX_INVALID = 10, + OEMCrypto_ERROR_NO_KEYDATA = 11, + OEMCrypto_ERROR_NO_CW = 12, + OEMCrypto_ERROR_DECRYPT_FAILED = 13, + OEMCrypto_ERROR_WRITE_KEYBOX = 14, + OEMCrypto_ERROR_WRAP_KEYBOX = 15, + OEMCrypto_ERROR_BAD_MAGIC = 16, + OEMCrypto_ERROR_BAD_CRC = 17, + OEMCrypto_ERROR_NO_DEVICEID = 18, + OEMCrypto_ERROR_RNG_FAILED = 19, + OEMCrypto_ERROR_RNG_NOT_SUPPORTED = 20, + OEMCrypto_ERROR_SETUP = 21, + OEMCrypto_ERROR_OPEN_SESSION_FAILED = 22, + OEMCrypto_ERROR_CLOSE_SESSION_FAILED = 23, + OEMCrypto_ERROR_INVALID_SESSION = 24, + OEMCrypto_ERROR_NOT_IMPLEMENTED = 25, + OEMCrypto_ERROR_NO_CONTENT_KEY = 26, + OEMCrypto_ERROR_CONTROL_INVALID = 27, + OEMCrypto_ERROR_UNKNOWN_FAILURE = 28, + OEMCrypto_ERROR_INVALID_CONTEXT = 29, + OEMCrypto_ERROR_SIGNATURE_FAILURE = 30, + OEMCrypto_ERROR_TOO_MANY_SESSIONS = 31, + OEMCrypto_ERROR_INVALID_NONCE = 32, + OEMCrypto_ERROR_TOO_MANY_KEYS = 33, + OEMCrypto_ERROR_DEVICE_NOT_RSA_PROVISIONED = 34, + OEMCrypto_ERROR_INVALID_RSA_KEY = 35, + OEMCrypto_ERROR_KEY_EXPIRED = 36, + OEMCrypto_ERROR_INSUFFICIENT_RESOURCES = 37, + OEMCrypto_ERROR_INSUFFICIENT_HDCP = 38, + OEMCrypto_ERROR_BUFFER_TOO_LARGE = 39, + OEMCrypto_WARNING_GENERATION_SKEW = 40, /* Warning, not error. */ + OEMCrypto_ERROR_GENERATION_SKEW = 41, + OEMCrypto_LOCAL_DISPLAY_ONLY = 42, /* Info, not an error. */ + OEMCrypto_ERROR_ANALOG_OUTPUT = 43, + OEMCrypto_ERROR_WRONG_PST = 44, + OEMCrypto_ERROR_WRONG_KEYS = 45, + OEMCrypto_ERROR_MISSING_MASTER = 46, + OEMCrypto_ERROR_LICENSE_INACTIVE = 47, + OEMCrypto_ERROR_ENTRY_NEEDS_UPDATE = 48, + OEMCrypto_ERROR_ENTRY_IN_USE = 49, + OEMCrypto_ERROR_USAGE_TABLE_UNRECOVERABLE = 50, /* Obsolete. Don't use. */ + /* Use OEMCrypto_ERROR_NO_CONTENT_KEY instead of KEY_NOT_LOADED. */ + OEMCrypto_KEY_NOT_LOADED = 51, /* Obsolete. */ + OEMCrypto_KEY_NOT_ENTITLED = 52, + OEMCrypto_ERROR_BAD_HASH = 53, + OEMCrypto_ERROR_OUTPUT_TOO_LARGE = 54, + OEMCrypto_ERROR_SESSION_LOST_STATE = 55, + OEMCrypto_ERROR_SYSTEM_INVALIDATED = 56, + OEMCrypto_ERROR_LICENSE_RELOAD = 57, + OEMCrypto_ERROR_MULTIPLE_USAGE_ENTRIES = 58, + OEMCrypto_WARNING_MIXED_OUTPUT_PROTECTION = 59, + /* ODK return values */ + ODK_ERROR_BASE = 1000, + ODK_ERROR_CORE_MESSAGE = ODK_ERROR_BASE, + ODK_SET_TIMER = ODK_ERROR_BASE + 1, + ODK_DISABLE_TIMER = ODK_ERROR_BASE + 2, + ODK_TIMER_EXPIRED = ODK_ERROR_BASE + 3, + ODK_UNSUPPORTED_API = ODK_ERROR_BASE + 4, + ODK_STALE_RENEWAL = ODK_ERROR_BASE + 5, +} OEMCryptoResult; +/* clang-format on */ + +/* + * OEMCrypto_Usage_Entry_Status. + * Valid values for status in the usage table. + */ +typedef enum OEMCrypto_Usage_Entry_Status { + kUnused = 0, + kActive = 1, + kInactive = 2, /* Deprecated. Use kInactiveUsed or kInactiveUnused. */ + kInactiveUsed = 3, + kInactiveUnused = 4, +} OEMCrypto_Usage_Entry_Status; + +/* + * OEMCrypto_LicenseType is used in the license message to indicate if the key + * objects are for content keys, or for entitlement keys. + */ +typedef enum OEMCrypto_LicenseType { + OEMCrypto_ContentLicense = 0, + OEMCrypto_EntitlementLicense = 1, + OEMCrypto_LicenstType_MaxValue = OEMCrypto_EntitlementLicense, +} OEMCrypto_LicenseType; + +/* Private key type used in the provisioning response. */ +typedef enum OEMCrypto_PrivateKeyType { + OEMCrypto_RSA_Private_Key = 0, + OEMCrypto_ECC_Private_Key = 1, +} OEMCrypto_PrivateKeyType; + +/* + * OEMCrypto_Substring + * + * Used to indicate a substring of a signed message in OEMCrypto_LoadKeys and + * other functions which must verify that a parameter is contained within a + * signed message. + */ +typedef struct { + size_t offset; + size_t length; +} OEMCrypto_Substring; + +/* + * OEMCrypto_KeyObject + * Points to the relevant fields for a content key. The fields are extracted + * from the License Response message offered to OEMCrypto_LoadKeys(). Each + * field points to one of the components of the key. Key data, key control, + * and both IV fields are 128 bits (16 bytes): + * key_id - the unique id of this key. + * key_id_length - the size of key_id. OEMCrypto may assume this is at + * most 16. However, OEMCrypto shall correctly handle key id lengths + * from 1 to 16 bytes. + * key_data_iv - the IV for performing AES-128-CBC decryption of the + * key_data field. + * key_data - the key data. It is encrypted (AES-128-CBC) with the + * session's derived encrypt key and the key_data_iv. + * key_control_iv - the IV for performing AES-128-CBC decryption of the + * key_control field. + * key_control - the key control block. It is encrypted (AES-128-CBC) with + * the content key from the key_data field. + * + * The memory for the OEMCrypto_KeyObject fields is allocated and freed + * by the caller of OEMCrypto_LoadKeys(). + */ +typedef struct { + OEMCrypto_Substring key_id; + OEMCrypto_Substring key_data_iv; + OEMCrypto_Substring key_data; + OEMCrypto_Substring key_control_iv; + OEMCrypto_Substring key_control; +} OEMCrypto_KeyObject; + +#ifdef __cplusplus +} +#endif + +#endif /* WIDEVINE_ODK_INCLUDE_OEMCRYPTOCENCCOMMON_H_ */ diff --git a/oemcrypto/odk/include/core_message_deserialize.h b/oemcrypto/odk/include/core_message_deserialize.h new file mode 100644 index 0000000..c2967dd --- /dev/null +++ b/oemcrypto/odk/include/core_message_deserialize.h @@ -0,0 +1,59 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +/********************************************************************* + * core_message_deserialize.h + * + * OEMCrypto v16 Core Message Serialization library counterpart (a.k.a. KDO) + * + * This file declares functions to deserialize request messages prepared by + * Widevine clients (OEMCrypto/ODK). + * + * Please refer to core_message_types.h for details. + * + *********************************************************************/ + +#ifndef WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_DESERIALIZE_H_ +#define WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_DESERIALIZE_H_ + +#include "core_message_types.h" + +namespace oemcrypto_core_message { +namespace deserialize { + +/** + * Counterpart (deserializer) of ODK_PrepareCoreLicenseRequest (serializer) + * + * Parameters: + * [in] oemcrypto_core_message + * [out] core_license_request + */ +bool CoreLicenseRequestFromMessage(const std::string& oemcrypto_core_message, + ODK_LicenseRequest* core_license_request); + +/** + * Counterpart (deserializer) of ODK_PrepareCoreRenewalRequest (serializer) + * + * Parameters: + * [in] oemcrypto_core_message + * [out] core_renewal_request + */ +bool CoreRenewalRequestFromMessage(const std::string& oemcrypto_core_message, + ODK_RenewalRequest* core_renewal_request); + +/** + * Counterpart (deserializer) of ODK_PrepareCoreProvisioningRequest (serializer) + * + * Parameters: + * [in] oemcrypto_core_message + * [out] core_provisioning_request + */ +bool CoreProvisioningRequestFromMessage( + const std::string& oemcrypto_core_message, + ODK_ProvisioningRequest* core_provisioning_request); + +} /* namespace deserialize */ +} /* namespace oemcrypto_core_message */ + +#endif /* WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_DESERIALIZE_H_ */ diff --git a/oemcrypto/odk/include/core_message_serialize.h b/oemcrypto/odk/include/core_message_serialize.h new file mode 100644 index 0000000..ffa43f5 --- /dev/null +++ b/oemcrypto/odk/include/core_message_serialize.h @@ -0,0 +1,68 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +/********************************************************************* + * core_message_serialize.h + * + * OEMCrypto v16 Core Message Serialization library counterpart (a.k.a. KDO) + * + * This file declares functions to serialize response messages that will be + * parsed by Widevine clients (OEMCrypto/ODK). + * + * Please refer to core_message_types.h for details. + * + *********************************************************************/ + +#ifndef WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_SERIALIZE_H_ +#define WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_SERIALIZE_H_ + +#include "core_message_types.h" +#include "odk_structs.h" + +namespace oemcrypto_core_message { +namespace serialize { + +/** + * Counterpart (serializer) of ODK_ParseLicense (deserializer) + * struct-input variant + * + * Parameters: + * [in] parsed_lic + * [in] core_request + * [in] core_request_sha256 + * [out] oemcrypto_core_message + */ +bool CreateCoreLicenseResponse(const ODK_ParsedLicense& parsed_lic, + const ODK_LicenseRequest& core_request, + const std::string& core_request_sha256, + std::string* oemcrypto_core_message); + +/** + * Counterpart (serializer) of ODK_ParseRenewal (deserializer) + * + * Parameters: + * [in] core_request + * [in] renewal_duration_seconds + * [out] oemcrypto_core_message + */ +bool CreateCoreRenewalResponse(const ODK_RenewalRequest& core_request, + uint64_t renewal_duration_seconds, + std::string* oemcrypto_core_message); + +/** + * Counterpart (serializer) of ODK_ParseProvisioning (deserializer) + * struct-input variant + * + * Parameters: + * [in] parsed_prov + * [in] core_request + * [out] oemcrypto_core_message + */ +bool CreateCoreProvisioningResponse(const ODK_ParsedProvisioning& parsed_prov, + const ODK_ProvisioningRequest& core_request, + std::string* oemcrypto_core_message); +} /* namespace serialize */ +} /* namespace oemcrypto_core_message */ + +#endif /* WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_SERIALIZE_H_ */ diff --git a/oemcrypto/odk/include/core_message_serialize_proto.h b/oemcrypto/odk/include/core_message_serialize_proto.h new file mode 100644 index 0000000..0f494de --- /dev/null +++ b/oemcrypto/odk/include/core_message_serialize_proto.h @@ -0,0 +1,62 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +/********************************************************************* + * core_message_serialize_proto.h + * + * These functions are an extension of those found in + * core_message_serialize.h. The difference is that these use the + * license and provisioning messages in protobuf format to create the core + * message. + *********************************************************************/ + +#ifndef WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_SERIALIZE_PROTO_H_ +#define WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_SERIALIZE_PROTO_H_ + +#include +#include + +#include "core_message_types.h" +#include "license_protocol.pb.h" + +namespace oemcrypto_core_message { +namespace serialize { + +/* @ public create response (serializer) functions accepting proto input */ + +/** + * Counterpart (serializer) of ODK_ParseLicense (deserializer) + * + * Parameters: + * [in] serialized_license + serialized video_widevine::License + * [in] core_request oemcrypto core message from request. + * [in] core_request_sha256 - hash of serialized core request. + * [in] nonce_required - if the device should require a nonce match. + * [out] oemcrypto_core_message - the serialized oemcrypto core response. + */ +bool CreateCoreLicenseResponseFromProto(const std::string& serialized_license, + const ODK_LicenseRequest& core_request, + const std::string& core_request_sha256, + const bool nonce_required, + std::string* oemcrypto_core_message); + +/** + * Counterpart (serializer) of ODK_ParseProvisioning (deserializer) + * + * Parameters: + * [in] serialized_provisioning_response + * serialized video_widevine::ProvisioningResponse + * [in] core_request + * [out] oemcrypto_core_message + */ +bool CreateCoreProvisioningResponseFromProto( + const std::string& serialized_provisioning_response, + const ODK_ProvisioningRequest& core_request, + std::string* oemcrypto_core_message); + +} /* namespace serialize */ +} /* namespace oemcrypto_core_message */ + +#endif /* WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_SERIALIZE_PROTO_H_ */ diff --git a/oemcrypto/odk/include/core_message_types.h b/oemcrypto/odk/include/core_message_types.h new file mode 100644 index 0000000..488e5d2 --- /dev/null +++ b/oemcrypto/odk/include/core_message_types.h @@ -0,0 +1,99 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +/* clang-format off */ +/********************************************************************* + * core_message_types.h + * + * OEMCrypto v16 Core Message Serialization library counterpart (a.k.a. KDO) + * + * For Widevine Modular DRM, there are six message types between a server and + * a client device: license request and response, provisioning request and + * response, and renewal request and response. + * + * In OEMCrypto v15 and earlier, messages from the server were parsed by the + * CDM layer above OEMCrypto; the CDM in turn gave OEMCrypto a collection of + * pointers to protected data within the message. However, the pointers + * themselves were not signed by the server. + * + * Starting from OEMCrypto v16, all fields used by OEMCrypto in each of these + * messages have been identified in the document "Widevine Core Message + * Serialization". These fields are called the core of the message. Core + * message fields are (de)serialized using the ODK, a C library provided by + * Widevine. OEMCrypto will parse and verify the core of the message with + * help from the ODK. + * + * The KDO library is the counterpart of ODK used in the CDM & Widevine + * servers. For each message type generated by the ODK, KDO provides a + * corresponding parser. For each message type to be parsed by the ODK, + * KDO provides a corresponding writer. + * + * Table: ODK vs KDO (s: serialize; d: deserialize) + * +----------------------------------------+---------------------------------------+ + * | ODK | KDO | + * +---+------------------------------------+---+-----------------------------------+ + * | s | ODK_PrepareCoreLicenseRequest | d | CoreLicenseRequestFromMessage | + * | +------------------------------------+ +-----------------------------------+ + * | | ODK_PrepareCoreRenewalRequest | | CoreRenewalRequestFromMessage | + * | +------------------------------------+ +-----------------------------------+ + * | | ODK_PrepareCoreProvisioningRequest | | CoreProvisioningRequestFromMessage| + * +---+------------------------------------+---+-----------------------------------+ + * | d | ODK_ParseLicense | s | CreateCoreLicenseResponse | + * | +------------------------------------+ +-----------------------------------+ + * | | ODK_ParseRenewal | | CreateCoreRenewalResponse | + * | +------------------------------------+ +-----------------------------------+ + * | | ODK_ParseProvisioning | | CreateCoreProvisioningResponse | + * +---+------------------------------------+---+-----------------------------------+ + * + *********************************************************************/ +/* clang-format on */ + +#ifndef WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_TYPES_H_ +#define WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_TYPES_H_ + +#include +#include + +namespace oemcrypto_core_message { + +/* @ input/output structs */ + +/** + * Output structure for CoreLicenseRequestFromMessage + * Input structure for CreateCoreLicenseResponse + */ +struct ODK_LicenseRequest { + uint16_t api_minor_version; + uint16_t api_major_version; + uint32_t nonce; + uint32_t session_id; +}; + +/** + * Output structure for CoreRenewalRequestFromMessage + * Input structure for CreateCoreRenewalResponse + */ +struct ODK_RenewalRequest { + uint16_t api_minor_version; + uint16_t api_major_version; + uint32_t nonce; + uint32_t session_id; + uint64_t playback_time_seconds; +}; + +/** + * Output structure for CoreProvisioningRequestFromMessage + * Input structure for CreateCoreProvisioningResponse + */ +struct ODK_ProvisioningRequest { + uint16_t api_minor_version; + uint16_t api_major_version; + uint32_t nonce; + uint32_t session_id; + std::string device_id; +}; + +} /* namespace oemcrypto_core_message */ + +#endif /* WIDEVINE_ODK_INCLUDE_CORE_MESSAGE_TYPES_H_ */ diff --git a/oemcrypto/odk/include/odk.h b/oemcrypto/odk/include/odk.h new file mode 100644 index 0000000..6bbf36f --- /dev/null +++ b/oemcrypto/odk/include/odk.h @@ -0,0 +1,637 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +/********************************************************************* + * odk.h + * + * OEMCrypto v16 Core Message Serialization library + * + * For Widevine Modular DRM, there are six message types between a server and + * a client device: license request and response, provisioning request and + * response, and renewal request and response. + * + * In OEMCrypto v15 and earlier, messages from the server were parsed by the + * CDM layer above OEMCrypto; the CDM in turn gave OEMCrypto a collection of + * pointers to protected data within the message. However, the pointers + * themselves were not signed by the server. + * + * Starting from OEMCrypto v16, all fields used by OEMCrypto in each of these + * messages have been identified in the document "Widevine Core Message + * Serialization". These fields are called the core of the message. Core + * message fields are (de)serialized using the ODK, a C library provided by + * Widevine. OEMCrypto will parse and verify the core of the message with + * help from the ODK. + * + * The ODK functions that parse code will fill out structs that have similar + * formats to the function parameters of the OEMCrypto v15 functions being + * replaced. The ODK will be provided in source code and it is Widevine's + * intention that partners can build and link ODK with their implementation + * of OEMCrypto with no or few code changes. + * + * OEMCrypto implementers shall build the ODK library as part of the Trusted + * Application (TA) running in the TEE. All memory and buffers used by the + * ODK library shall be sanitized by the OEMCrypto implementer to prevent + * modification by any process running the REE. + * + * See the documents "Widevine Core Message Serialization" and "License + * Duration and Renewal" for a detailed description of the ODK API. You can + * find these documents in the widevine repository as + * docs/Widevine_Core_Message_Serialization.pdf and + * docs/License_Duration_and_Renewal.pdf + * + *********************************************************************/ + +#ifndef WIDEVINE_ODK_INCLUDE_ODK_H_ +#define WIDEVINE_ODK_INCLUDE_ODK_H_ + +#include + +#include "OEMCryptoCENCCommon.h" +#include "odk_structs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * ODK_InitializeSessionValues + * + * Description: + * This function initializes the session's data structures. It shall be + * called from OEMCrypto_OpenSession. + * + * Parameters: + * [out] timer_limits: the session's timer limits. + * [out] clock_values: the session's clock values. + * [out] nonce_values: the session's ODK nonce values. + * [in] api_major_version: the API version of OEMCrypto. + * [in] session_id: the session id of the newly created session. + * + * Returns: + * OEMCrypto_SUCCESS + * OEMCrypto_ERROR_INVALID_CONTEXT + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_InitializeSessionValues(ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + ODK_NonceValues* nonce_values, + uint32_t api_major_version, + uint32_t session_id); + +/* + * ODK_SetNonceValues + * + * Description: + * This function sets the nonce value in the session's nonce structure. It + * shall be called from OEMCrypto_GenerateNonce. + * + * Parameters: + * [in/out] nonce_values: the session's nonce data. + * [in] nonce: the new nonce that was just generated. + * + * Returns: + * true on success + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_SetNonceValues(ODK_NonceValues* nonce_values, + uint32_t nonce); + +/* + * ODK_InitializeClockValues + * + * Description: + * This function initializes the clock values in the session clock_values + * structure. It shall be called from OEMCrypto_PrepAndSignLicenseRequest. + * + * Parameters: + * [in/out] clock_values: the session's clock data. + * [in] system_time_seconds: the current time on OEMCrypto's monotonic clock. + * + * Returns: + * OEMCrypto_SUCCESS + * OEMCrypto_ERROR_INVALID_CONTEXT + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_InitializeClockValues(ODK_ClockValues* clock_values, + uint64_t system_time_seconds); + +/* + * ODK_ReloadClockValues + * + * Description: + * This function sets the values in the clock_values structure. It shall be + * called from OEMCrypto_LoadUsageEntry. When a usage entry from a v15 or + * earlier license is loaded, the value time_of_license_loaded shall be used + * in place of time_of_license_signed. + * + * Parameters: + * [in/out] clock_values: the session's clock data. + * [in] time_of_license_signed: the value time_license_received from the + * loaded usage entry. + * [in] time_of_first_decrypt: the value time_of_first_decrypt from the + * loaded usage entry. + * [in] time_of_last_decrypt: the value time_of_last_decrypt from the loaded + * usage entry. + * [in] status: the value status from the loaded usage entry. + * [in] system_time_seconds: the current time on OEMCrypto's monotonic clock. + * + * Returns: + * OEMCrypto_SUCCESS + * OEMCrypto_ERROR_INVALID_CONTEXT + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_ReloadClockValues(ODK_ClockValues* clock_values, + uint64_t time_of_license_signed, + uint64_t time_of_first_decrypt, + uint64_t time_of_last_decrypt, + enum OEMCrypto_Usage_Entry_Status status, + uint64_t system_time_seconds); + +/* + * ODK_AttemptFirstPlayback + * + * Description: + * This updates the clock values, and determines if playback may start based + * on the given system time. It uses the values in clock_values to determine + * if this is the first playback for the license or the first playback for + * just this session. + * + * This shall be called from the first call in a session to any of + * OEMCrypto_DecryptCENC or any of the OEMCrypto_Generic* functions. + * + * If OEMCrypto uses a hardware timer, and this function returns + * ODK_SET_TIMER, then the timer should be set to the value pointed to by + * timer_value. + * + * Parameters: + * [in] system_time_seconds: the current time on OEMCrypto's monotonic clock, + * in seconds. + * [in] timer_limits: timer limits specified in the license. + * [in/out] clock_values: the sessions clock values. + * [out] timer_value: set to the new timer value. Only used if the return + * value is ODK_SET_TIMER. This must be non-null if OEMCrypto uses a + * hardware timer. + * + * Returns: + * ODK_SET_TIMER: Success. The timer should be reset to the specified value + * and playback is allowed. + * ODK_DISABLE_TIMER: Success, but disable timer. Unlimited playback is + * allowed. + * ODK_TIMER_EXPIRED: Set timer as disabled. Playback is not allowed. + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_AttemptFirstPlayback(uint64_t system_time_seconds, + const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + uint64_t* timer_value); + +/* + * ODK_UpdateLastPlaybackTime + * + * Description: + * Vendors that do not implement their own timer should call + * ODK_UpdateLastPlaybackTime regularly during playback. This updates the + * clock values, and determines if playback may continue based on the given + * system time. This shall be called from any of OEMCrypto_DecryptCENC or any + * of the OEMCrypto_Generic* functions. + * + * All Vendors (i.e. those that do or do not implement their own timer) shall + * call ODK_UpdateLastPlaybackTime from the function + * OEMCrypto_UpdateUsageEntry before updating the usage entry so that the + * clock values are accurate. + * + * Parameters: + * [in] system_time_seconds: the current time on OEMCrypto's monotonic clock, + * in seconds. + * [in] timer_limits: timer limits specified in the license. + * [in/out] clock_values: the sessions clock values. + * + * Returns: + * OEMCrypto_SUCCESS: Success. Playback is allowed. + * ODK_TIMER_EXPIRED: Set timer as disabled. Playback is not allowed. + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_UpdateLastPlaybackTime(uint64_t system_time_seconds, + const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values); + +/* + * ODK_DeactivateUsageEntry + * + * Description: + * This function modifies the session's clock values to indicate that the + * license has been deactivated. It shall be called from + * OEMCrypto_DeactivateUsageEntry + * + * Parameters: + * [in/out] clock_values: the sessions clock values. + * + * Returns: + * OEMCrypto_SUCCESS + * OEMCrypto_ERROR_INVALID_CONTEXT + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_DeactivateUsageEntry(ODK_ClockValues* clock_values); + +/* + * ODK_PrepareCoreLicenseRequest + * + * Description: + * Modifies the message to include a core license request at the beginning of + * the message buffer. The values in nonce_values are used to populate the + * message. + * + * This shall be called by OEMCrypto from OEMCrypto_PrepAndSignLicenseRequest. + * + * NOTE: if the message pointer is null and/or input core_message_size is + * zero, this function returns OEMCrypto_ERROR_SHORT_BUFFER and sets output + * core_message_size to the size needed. + * + * Parameters: + * [in/out] message: Pointer to memory for the entire message. Modified by + * the ODK library. + * [in] message_length: length of the entire message buffer. + * [in/out] core_message_size: length of the core message at the beginning of + * the message. (in) size of buffer reserved for the core message, in + * bytes. (out) actual length of the core message, in bytes. + * [in] nonce_values: pointer to the session's nonce data. + * + * Returns: + * OEMCrypto_SUCCESS + * OEMCrypto_ERROR_SHORT_BUFFER: core_message_size is too small + * OEMCrypto_ERROR_INVALID_CONTEXT + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_PrepareCoreLicenseRequest( + uint8_t* message, size_t message_length, size_t* core_message_size, + const ODK_NonceValues* nonce_values); + +/* + * ODK_PrepareCoreRenewalRequest + * + * Description: + * Modifies the message to include a core renewal request at the beginning of + * the message buffer. The values in nonce_values, clock_values and + * system_time_seconds are used to populate the message. The nonce_values + * should match those from the license. + * + * This shall be called by OEMCrypto from OEMCrypto_PrepAndSignRenewalRequest. + * + * If status in clock_values indicates that a license has not been loaded, + * then this is a license release. The ODK library will change the value of + * nonce_values.api_major_version to 15. This will make + * OEMCrypto_PrepAndSignRenewalRequest sign just the message body, as it does + * for all legacy licenses. + * + * NOTE: if the message pointer is null and/or input core_message_size is + * zero, this function returns OEMCrypto_ERROR_SHORT_BUFFER and sets output + * core_message_size to the size needed. + * + * Parameters: + * [in/out] message: Pointer to memory for the entire message. Modified by + * the ODK library. + * [in] message_length: length of the entire message buffer. + * [in/out] core_message_size: length of the core message at the beginning of + * the message. (in) size of buffer reserved for the core message, in + * bytes. (out) actual length of the core message, in bytes. + * [in/out] nonce_values: pointer to the session's nonce data. + * [in/out] clock_values: the session's clock values. + * [in] system_time_seconds: the current time on OEMCrypto's clock, in + * seconds. + * + * Returns: + * OEMCrypto_SUCCESS + * OEMCrypto_ERROR_SHORT_BUFFER: core_message_size is too small + * OEMCrypto_ERROR_INVALID_CONTEXT + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_PrepareCoreRenewalRequest(uint8_t* message, + size_t message_length, + size_t* core_message_size, + ODK_NonceValues* nonce_values, + ODK_ClockValues* clock_values, + uint64_t system_time_seconds); + +/* + * ODK_PrepareCoreProvisioningRequest + * + * Description: + * Modifies the message to include a core provisioning request at the + * beginning of the message buffer. The values in nonce_values are used to + * populate the message. + * + * This shall be called by OEMCrypto from + * OEMCrypto_PrepAndSignProvisioningRequest. + * + * The buffer device_id shall be the same string returned by + * OEMCrypto_GetDeviceID. The device ID shall be unique to the device, and + * stable across reboots and factory resets for an L1 device. + * + * NOTE: if the message pointer is null and/or input core_message_size is + * zero, this function returns OEMCrypto_ERROR_SHORT_BUFFER and sets output + * core_message_size to the size needed. + * + * Parameters: + * [in/out] message: Pointer to memory for the entire message. Modified by + * the ODK library. + * [in] message_length: length of the entire message buffer. + * [in/out] core_message_size: length of the core message at the beginning of + * the message. (in) size of buffer reserved for the core message, in + * bytes. (out) actual length of the core message, in bytes. + * [in] nonce_values: pointer to the session's nonce data. + * [in] device_id: For devices with a keybox, this is the device ID from the + * keybox. For devices with an OEM Certificate, this is a device unique + * id string. + * [in] device_id_length: length of device_id. The device ID can be at most + * 64 bytes. + * + * Returns: + * OEMCrypto_SUCCESS + * OEMCrypto_ERROR_SHORT_BUFFER: core_message_size is too small + * OEMCrypto_ERROR_INVALID_CONTEXT + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_PrepareCoreProvisioningRequest( + uint8_t* message, size_t message_length, size_t* core_message_size, + const ODK_NonceValues* nonce_values, const uint8_t* device_id, + size_t device_id_length); + +/* + * ODK_InitializeV15Values + * + * Description: + * This function sets all limits in the timer_limits struct to the + * key_duration and initializes the other values. The field + * nonce_values.api_major_version will be set to 15. It shall be called from + * OEMCrypto_LoadKeys when loading a legacy license. + * + * Parameters: + * [out] timer_limits: The session's timer limits. + * [in/out] clock_values: The session's clock values. + * [in/out] nonce_values: The session's ODK nonce values. + * [in] key_duration: The duration from the first key's key control block. In + * practice, the key duration is the same for all keys and is the same + * as the license duration. + * [in] system_time_seconds: The current time on the system clock, as + * described in the document "License Duration and Renewal". + * + * Returns: + * OEMCrypto_SUCCESS + * OEMCrypto_ERROR_INVALID_CONTEXT + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_InitializeV15Values(ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + ODK_NonceValues* nonce_values, + uint32_t key_duration, + uint64_t system_time_seconds); + +/* + * ODK_RefreshV15Values + * + * Description: + * This function updates the clock_values as needed if a v15 renewal is + * accepted. The field nonce_values.api_major_version is verified to be 15. + * + * This is called from OEMCrypto_RefreshKeys for a valid license renewal. + * OEMCrypto shall pass in the current system time, and the key duration from + * the first object in the OEMCrypto_KeyRefreshObject. + * + * Parameters: + * [in] timer_limits: The session's timer limits. + * [in/out] clock_values: The session's clock values. + * [in] nonce_values: The session's ODK nonce values. + * [in] system_time_seconds: The current time on the system clock, as + * described in the document "License Duration and Renewal". + * [in] new_key_duration: The duration from the first + * OEMCrypto_KeyRefreshObject in key_array. + * [out] timer_value: set to the new timer value. Only used if the return + * value is ODK_SET_TIMER. This must be non-null if OEMCrypto uses a + * hardware timer. + * + * Returns: + * OEMCrypto_SUCCESS + * OEMCrypto_ERROR_UNKNOWN_FAILURE + * ODK_SET_TIMER: Success. The timer should be reset to the specified value + * and playback is allowed. + * ODK_DISABLE_TIMER: Success, but disable timer. Unlimited playback is + * allowed. + * ODK_TIMER_EXPIRED: Set timer as disabled. Playback is not allowed. + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_RefreshV15Values(const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + const ODK_NonceValues* nonce_values, + uint64_t system_time_seconds, + uint32_t new_key_duration, + uint64_t* timer_value); + +/* + * ODK_ParseLicense + * + * Description: + * The function ODK_ParseLicense will parse the message and verify fields in + * the message. + * + * If the message does not parse correctly, ODK_VerifyAndParseLicense will + * return ODK_ERROR_CORE_MESSAGE that OEMCrypto should return to the CDM + * layer above. + * + * If the API in the message is not 16, then ODK_UNSUPPORTED_API is returned. + * + * If initial_license_load is true, and nonce_required in the license is + * true, then the ODK library shall verify that nonce_values->nonce and + * nonce_values->session_id are the same as those in the message. If + * verification fails, then it shall return OEMCrypto_ERROR_INVALID_NONCE. + * + * If initial_license_load is false, and nonce_required is true, then + * ODK_ParseLicense will set the values in nonce_values from those in the + * message. + * + * The function ODK_ParseLicense will verify that each substring points to a + * location in the message body. The message body is the buffer starting at + * message + core_message_length with size message_length - + * core_message_length. + * + * If initial_license_load is true, then ODK_ParseLicense shall verify that + * the parameter request_hash matches request_hash in the parsed license. If + * verification fails, then it shall return ODK_ERROR_CORE_MESSAGE. This was + * computed by OEMCrypto when the license was requested. + * + * If usage_entry_present is true, then ODK_ParseLicense shall verify that + * the pst in the license has a nonzero length. + * + * Parameters: + * [in] message: pointer to the message buffer. + * [in] message_length: length of the entire message buffer. + * [in] core_message_size: length of the core message, at the beginning of + * the message buffer. + * [in] initial_license_load: true when called for OEMCrypto_LoadLicense and + * false when called for OEMCrypto_ReloadLicense. + * [in] usage_entry_present: true if the session has a new usage entry + * associated with it created via OEMCrypto_CreateNewUsageEntry. + * [in] request_hash: the hash of the license request core message. This was + * computed by OEMCrypto when the license request was signed. + * [in/out] timer_limits: The session's timer limits. These will be updated. + * [in/out] clock_values: The session's clock values. These will be updated. + * [in/out] nonce_values: The session's nonce values. These will be updated. + * [out] parsed_license: the destination for the data. + * + * Returns: + * OEMCrypto_SUCCESS + * ODK_ERROR_CORE_MESSAGE: if the message did not parse correctly, or there + * were other incorrect values. An error should be returned to the CDM + * layer. + * ODK_UNSUPPORTED_API + * OEMCrypto_ERROR_INVALID_NONCE + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_ParseLicense( + const uint8_t* message, size_t message_length, size_t core_message_length, + bool initial_license_load, bool usage_entry_present, + const uint8_t request_hash[ODK_SHA256_HASH_SIZE], + ODK_TimerLimits* timer_limits, ODK_ClockValues* clock_values, + ODK_NonceValues* nonce_values, ODK_ParsedLicense* parsed_license); + +/* + * ODK_ParseRenewal + * + * Description: + * The function ODK_ParseRenewal will parse the message and verify its + * contents. If the message does not parse correctly, an error of + * ODK_ERROR_CORE_MESSAGE is returned. + * + * ODK_ParseRenewal shall verify that all fields in nonce_values match those + * in the license. Otherwise it shall return OEMCrypto_ERROR_INVALID_NONCE. + * + * After parsing the message, this function updates the clock_values based on + * the timer_limits and the current system time. If playback may not + * continue, then ODK_TIMER_EXPIRED is returned. + * + * If playback may continue, a return value of ODK_SET_TIMER or + * ODK_TIMER_EXPIRED is returned. If the return value is ODK_SET_TIMER, then + * playback may continue until the timer expires. If the return value is + * ODK_DISABLE_TIMER, then playback time is not limited. + * + * If OEMCrypto uses a hardware timer, and this function returns + * ODK_SET_TIMER, then OEMCrypto shall set the timer to the value pointed to + * by timer_value. + * + * Parameters: + * [in] message: pointer to the message buffer. + * [in] message_length: length of the entire message buffer. + * [in] core_message_size: length of the core message, at the beginning of + * the message buffer. + * [in] nonce_values: pointer to the session's nonce data. + * [in] system_time_seconds: the current time on OEMCrypto's clock, in + * seconds. + * [in] timer_limits: timer limits specified in the license. + * [in/out] clock_values: the sessions clock values. + * [out] timer_value: set to the new timer value. Only used if the return + * value is ODK_SET_TIMER. This must be non-null if OEMCrypto uses a + * hardware timer. + * + * Returns: + * ODK_ERROR_CORE_MESSAGE: the message did not parse correctly, or there were + * other incorrect values. An error should be returned to the CDM layer. + * ODK_SET_TIMER: Success. The timer should be reset to the specified timer + * value. + * ODK_DISABLE_TIMER: Success, but disable timer. Unlimited playback is + * allowed. + * ODK_TIMER_EXPIRED: Set timer as disabled. Playback is not allowed. + * ODK_UNSUPPORTED_API + * ODK_STALE_RENEWAL: This renewal is not the most recently signed. It is + * rejected. + * OEMCrypto_ERROR_INVALID_NONCE + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_ParseRenewal(const uint8_t* message, size_t message_length, + size_t core_message_length, + const ODK_NonceValues* nonce_values, + uint64_t system_time_seconds, + const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + uint64_t* timer_value); + +/* + * ODK_ParseProvisioning + * + * Description: + * The function ODK_ParseProvisioning will parse the message and verify the + * nonce values match those in the license. + * + * If the message does not parse correctly, ODK_ParseProvisioning will return + * an error that OEMCrypto should return to the CDM layer above. + * + * If the API in the message is larger than 16, then ODK_UNSUPPORTED_API is + * returned. + * + * ODK_ParseProvisioning shall verify that nonce_values->nonce and + * nonce_values->session_id are the same as those in the message. Otherwise + * it shall return OEMCrypto_ERROR_INVALID_NONCE. + * + * The function ODK_ParseProvisioning will verify that each substring points + * to a location in the message body. The message body is the buffer starting + * at message + core_message_length with size message_length - + * core_message_length. + * + * Parameters: + * [in] message: pointer to the message buffer. + * [in] message_length: length of the entire message buffer. + * [in] core_message_size: length of the core message, at the beginning of + * the message buffer. + * [in] nonce_values: pointer to the session's nonce data. + * [in] device_id: a pointer to a buffer containing the device ID of the + * device. The ODK function will verify it matches that in the message. + * [in] device_id_length: the length of the device ID. + * [out] parsed_response: destination for the parse data. + * + * Returns: + * OEMCrypto_SUCCESS + * ODK_ERROR_CORE_MESSAGE: the message did not parse correctly, or there were + * other incorrect values. An error should be returned to the CDM layer. + * ODK_UNSUPPORTED_API + * OEMCrypto_ERROR_INVALID_NONCE + * + * Version: + * This method is new in version 16 of the API. + */ +OEMCryptoResult ODK_ParseProvisioning( + const uint8_t* message, size_t message_length, size_t core_message_length, + const ODK_NonceValues* nonce_values, const uint8_t* device_id, + size_t device_id_length, ODK_ParsedProvisioning* parsed_response); + +#ifdef __cplusplus +} +#endif + +#endif /* WIDEVINE_ODK_INCLUDE_ODK_H_ */ diff --git a/oemcrypto/odk/include/odk_structs.h b/oemcrypto/odk/include/odk_structs.h new file mode 100644 index 0000000..bae689c --- /dev/null +++ b/oemcrypto/odk/include/odk_structs.h @@ -0,0 +1,221 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#ifndef WIDEVINE_ODK_INCLUDE_ODK_STRUCTS_H_ +#define WIDEVINE_ODK_INCLUDE_ODK_STRUCTS_H_ + +#include + +#include "OEMCryptoCENCCommon.h" +#include "odk_target.h" + +/* The version of this library. */ +#define ODK_MAJOR_VERSION 16 +#define ODK_MINOR_VERSION 3 + +/* ODK Version string. Date changed automatically on each release. */ +#define ODK_RELEASE_DATE "ODK v16.3 2020-07-19" + +/* The lowest version number for an ODK message. */ +#define ODK_FIRST_VERSION 16 + +/* Some useful constants. */ +#define ODK_DEVICE_ID_LEN_MAX 64 +#define ODK_SHA256_HASH_SIZE 32 + +/* + * ODK_TimerLimits Structure + * + * Description: + * Timer limits are specified in a license and are used to determine when + * playback is allowed. See the document "License Duration and Renewal" for a + * discussion on the time restrictions that may be placed on a license. The + * fields in this structure are directly related to the fields in the core + * license message. The fields are set when OEMCrypto calls the function + * ODK_ParseLicense or ODK_InitializeV15Values. + * + * Fields: + * soft_enforce_rental_duration: A boolean controlling the soft or hard + * enforcement of rental duration. + * soft_enforce_playback_duration: A boolean controlling the soft or hard + * enforcement of playback duration. + * earliest_playback_start_seconds: The earliest time that the first playback + * is allowed. Measured in seconds since the license request was signed. For + * most use cases, this is zero. + * rental_duration_seconds: Window of time for the allowed first playback. + * Measured in seconds since the earliest playback start. If + * soft_enforce_rental_duration is true, this applies only to the first + * playback. If soft_enforce_rental_duration is false, then this restricts + * any playback. A value of zero means no limit. + * total_playback_duration_seconds: Window of time for allowed playback. + * Measured in seconds since the first playback start. If + * soft_enforce_playback_duration is true, this applies only to the start of + * playback for any session. If soft_enforce_playback_duration is false, then + * this restricts any playback. A value of zero means no limit. + * initial_renewal_duration_seconds: Window of time for allowed playback. + * Measured in seconds since the first playback start. This value is only + * used to start the renewal timer. After a renewal message is loaded, the + * timer will be reset. A value of zero means no limit. + * + * Version: + * This struct changed in API version 16.2. + */ +typedef struct { + bool soft_enforce_rental_duration; + bool soft_enforce_playback_duration; + uint64_t earliest_playback_start_seconds; + uint64_t rental_duration_seconds; + uint64_t total_playback_duration_seconds; + uint64_t initial_renewal_duration_seconds; +} ODK_TimerLimits; + +/* + * ODK_ClockValues Structure + * + * Description: + * Clock values are modified when decryption occurs or when a renewal is + * processed. They are used to track the current status of the license -- + * i.e. has playback started? When does the timer expire? See the section + * "Complete ODK API" of the document "Widevine Core Message Serialization" + * for a complete list of all fields in this structure. Most of these values + * shall be saved with the usage entry. + * + * All times are in seconds. Most of the fields in this structure are saved + * in the usage entry. This structure should be initialized when a usage + * entry is created or loaded, and should be used to save a usage entry. It + * is updated using the ODK functions listed below. The time values are based + * on OEMCrypto's system clock, as described in the document "License + * Duration and Renewal". + * + * Fields: + * time_of_license_signed: Time that the license request was signed, based on + * OEMCrypto's system clock. This value shall be stored and reloaded with + * usage entry as time_of_license_received. + * time_of_first_decrypt: Time of the first decrypt or call select key, based + * on OEMCrypto's system clock. This is 0 if the license has not been used to + * decrypt any data. This value shall be stored and reloaded with usage entry. + * time_of_last_decrypt: Time of the most recent decrypt call, based on + * OEMCrypto's system clock. This value shall be stored and reloaded with + * usage entry. + * time_of_renewal_request: Time of the most recent renewal request, based on + * OEMCrypto's system clock. This is used to verify that a renewal is not + * stale. + * time_when_timer_expires: Time that the current timer expires, based on + * OEMCrypto's system clock. If the timer is active, this is used by the ODK + * library to determine if it has expired. + * timer_status: Used internally by the ODK library to indicate the current + * timer status. + * status: The license or usage entry status. This value shall be stored and + * reloaded with usage entry. + * + * Version: + * This struct changed in API version 16.2. + */ +typedef struct { + uint64_t time_of_license_signed; + uint64_t time_of_first_decrypt; + uint64_t time_of_last_decrypt; + uint64_t time_of_renewal_request; + uint64_t time_when_timer_expires; + uint32_t timer_status; + enum OEMCrypto_Usage_Entry_Status status; +} ODK_ClockValues; + +/* + * ODK_NonceValues Structure + * + * Description: + * Nonce values are used to match a license or provisioning request to a + * license or provisioning response. They are also used to match a renewal + * request and response to a license. For this reason, the api_version might + * be lower than that supported by OEMCrypto. The api_version matches the + * version of the license. Similarly the nonce and session_id match the + * session that generated the license request. For an offline license, these + * might not match the session that is loading the license. We use the nonce + * to prevent a license from being replayed. By also including a session_id + * in the license request and license response, we prevent an attack using + * the birthday paradox to generate nonce collisions on a single device. + * + * Fields: + * api_major_version: the API version of the license. This is initialized to + * the API version of the ODK library, but may be lower. + * api_minor_version: the minor version of the ODK library. This is used by + * the server to verify that device is not using an obsolete version of the + * ODK library. + * nonce: a randomly generated number used to prevent replay attacks. + * session_id: the session id of the session which signed the license or + * provisioning request. It is used to prevent replay attacks from one + * session to another. + * + * Version: + * This struct changed in API version 16.2. + */ +typedef struct { + uint16_t api_minor_version; + uint16_t api_major_version; + uint32_t nonce; + uint32_t session_id; +} ODK_NonceValues; + +/* + * ODK_ParsedLicense Structure + * + * Description: + * The parsed license structure contains information from the license + * message. The function ODK_ParseLicense will fill in the fields of this + * message. All substrings are contained within the message body. + * + * Fields: + * enc_mac_keys_iv: IV for decrypting new mac_key. Size is 128 bits. + * enc_mac_keys: encrypted mac_keys for generating new mac_keys. Size is 512 + * bits. + * pst: the Provider Session Token. + * srm_restriction_data: optional data specifying the minimum SRM version. + * license_type: specifies if the license contains content keys or + * entitlement keys. + * nonce_required: indicates if the license requires a nonce. + * timer_limits: time limits of the for the license. + * key_array_length: number of keys present. + * key_array: set of keys to be installed. + * + * Version: + * This struct changed in API version 16.2. + */ +typedef struct { + OEMCrypto_Substring enc_mac_keys_iv; + OEMCrypto_Substring enc_mac_keys; + OEMCrypto_Substring pst; + OEMCrypto_Substring srm_restriction_data; + OEMCrypto_LicenseType license_type; + bool nonce_required; + ODK_TimerLimits timer_limits; + uint32_t key_array_length; + OEMCrypto_KeyObject key_array[ODK_MAX_NUM_KEYS]; +} ODK_ParsedLicense; + +/* + * ODK_ParsedProvisioning Structure + * + * Description: + * The parsed provisioning structure contains information from the license + * message. The function ODK_ParseProvisioning will fill in the fields of + * this message. All substrings are contained within the message body. + * + * Fields: + * key_type: indicates if this key is an RSA or ECC private key. + * enc_private_key: encrypted private key for the DRM certificate. + * enc_private_key_iv: IV for decrypting new private key. Size is 128 bits. + * encrypted_message_key: used for provisioning 3.0 to derive keys. + * + * Version: + * This struct changed in API version 16.2. + */ +typedef struct { + OEMCrypto_PrivateKeyType key_type; + OEMCrypto_Substring enc_private_key; + OEMCrypto_Substring enc_private_key_iv; + OEMCrypto_Substring encrypted_message_key; /* Used for Prov 3.0 */ +} ODK_ParsedProvisioning; + +#endif /* WIDEVINE_ODK_INCLUDE_ODK_STRUCTS_H_ */ diff --git a/oemcrypto/odk/include/odk_target.h b/oemcrypto/odk/include/odk_target.h new file mode 100644 index 0000000..9225210 --- /dev/null +++ b/oemcrypto/odk/include/odk_target.h @@ -0,0 +1,13 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file is distributed */ +/* under the Widevine Master License Agreement. */ + +/* Partners are expected to edit this file to support target specific code */ +/* and limits. */ + +#ifndef WIDEVINE_ODK_INCLUDE_ODK_TARGET_H_ +#define WIDEVINE_ODK_INCLUDE_ODK_TARGET_H_ + +/* Maximum number of keys can be modified to suit target's resource tier. */ +#define ODK_MAX_NUM_KEYS 32 + +#endif /* WIDEVINE_ODK_INCLUDE_ODK_TARGET_H_ */ diff --git a/oemcrypto/odk/src/core_message_deserialize.cpp b/oemcrypto/odk/src/core_message_deserialize.cpp new file mode 100644 index 0000000..1cef5ee --- /dev/null +++ b/oemcrypto/odk/src/core_message_deserialize.cpp @@ -0,0 +1,133 @@ +// Copyright 2019 Google LLC. All rights reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "core_message_deserialize.h" + +#include +#include +#include +#include +#include + +#include "odk_serialize.h" +#include "odk_structs.h" +#include "odk_structs_priv.h" +#include "serialization_base.h" + +namespace oemcrypto_core_message { +namespace deserialize { +namespace { + +/** + * Template for parsing requests + * + * Template arguments: + * S: kdo output struct + * T: struct serialized by odk + * U: auto-generated deserializing function for |T| + */ +template +bool ParseRequest(uint32_t message_type, + const std::string& oemcrypto_core_message, S* core_request, + T* prepared, const U unpacker) { + if (core_request == nullptr || prepared == nullptr) { + return false; + } + + const uint8_t* buf = + reinterpret_cast(oemcrypto_core_message.c_str()); + const size_t buf_length = oemcrypto_core_message.size(); + + Message* msg = nullptr; + AllocateMessage(&msg, message_block); + InitMessage(msg, const_cast(buf), buf_length); + SetSize(msg, buf_length); + + unpacker(msg, prepared); + if (!ValidMessage(msg)) { + return false; + } + + const auto& core_message = prepared->core_message; + core_request->api_major_version = core_message.nonce_values.api_major_version; + core_request->api_minor_version = core_message.nonce_values.api_minor_version; + core_request->nonce = core_message.nonce_values.nonce; + core_request->session_id = core_message.nonce_values.session_id; + // Verify that the minor version matches the released version for the given + // major version. + if (core_request->api_major_version < ODK_FIRST_VERSION) { + // Non existing versions are not supported. + return false; + } else if (core_request->api_major_version == 16) { + // For version 16, we demand a minor version of at least 2. + // We accept 16.2, 16.3, or higher. + if (core_request->api_major_version < 2) return false; + } else { + // Other versions do not (yet) have a restriction on minor number. + // In particular, future versions are accepted for forward compatibility. + } + // For v16, a release and a renewal use the same message structure. + // However, for future API versions, the release might be a separate + // message. Otherwise, we expect an exact match of message types. + if (core_message.message_type != message_type && + !(message_type == ODK_Renewal_Request_Type && + core_message.message_type == ODK_Release_Request_Type)) { + return false; + } + // Verify that the amount of buffer we read, which is GetOffset, is not more + // than the total message size. We allow the total message size to be larger + // for forward compatibility because future messages might have extra fields + // that we can ignore. + if (core_message.message_length < GetOffset(msg)) return false; + return true; +} + +} // namespace + +bool CoreLicenseRequestFromMessage(const std::string& oemcrypto_core_message, + ODK_LicenseRequest* core_license_request) { + const auto unpacker = Unpack_ODK_PreparedLicenseRequest; + ODK_PreparedLicenseRequest prepared_license = {}; + return ParseRequest(ODK_License_Request_Type, oemcrypto_core_message, + core_license_request, &prepared_license, unpacker); +} + +bool CoreRenewalRequestFromMessage(const std::string& oemcrypto_core_message, + ODK_RenewalRequest* core_renewal_request) { + const auto unpacker = Unpack_ODK_PreparedRenewalRequest; + ODK_PreparedRenewalRequest prepared_renewal = {}; + if (!ParseRequest(ODK_Renewal_Request_Type, oemcrypto_core_message, + core_renewal_request, &prepared_renewal, unpacker)) { + return false; + } + core_renewal_request->playback_time_seconds = prepared_renewal.playback_time; + return true; +} + +bool CoreProvisioningRequestFromMessage( + const std::string& oemcrypto_core_message, + ODK_ProvisioningRequest* core_provisioning_request) { + const auto unpacker = Unpack_ODK_PreparedProvisioningRequest; + ODK_PreparedProvisioningRequest prepared_provision = {}; + if (!ParseRequest(ODK_Provisioning_Request_Type, oemcrypto_core_message, + core_provisioning_request, &prepared_provision, unpacker)) { + return false; + } + const uint8_t* device_id = prepared_provision.device_id; + const uint32_t device_id_length = prepared_provision.device_id_length; + if (device_id_length > ODK_DEVICE_ID_LEN_MAX) { + return false; + } + uint8_t zero[ODK_DEVICE_ID_LEN_MAX] = {}; + if (memcmp(zero, device_id + device_id_length, + ODK_DEVICE_ID_LEN_MAX - device_id_length)) { + return false; + } + core_provisioning_request->device_id.assign( + reinterpret_cast(device_id), device_id_length); + return true; +} + +} // namespace deserialize +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/src/core_message_serialize.cpp b/oemcrypto/odk/src/core_message_serialize.cpp new file mode 100644 index 0000000..4546460 --- /dev/null +++ b/oemcrypto/odk/src/core_message_serialize.cpp @@ -0,0 +1,125 @@ +// Copyright 2019 Google LLC. All rights reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "core_message_serialize.h" + +#include +#include +#include +#include +#include + +#include "odk_serialize.h" +#include "odk_structs.h" +#include "odk_structs_priv.h" +#include "serialization_base.h" + +namespace oemcrypto_core_message { +namespace serialize { +namespace { + +/** + * Template for parsing requests + * + * Template arguments: + * T: struct to be deserialized by odk + * S: kdo input struct + * P: auto-generated serializing function for |T| + */ +template +bool CreateResponse(uint32_t message_type, const S& core_request, + std::string* oemcrypto_core_message, T& response, + const P& packer) { + if (!oemcrypto_core_message) { + return false; + } + + auto* header = &response.request.core_message; + header->message_type = message_type; + header->nonce_values.api_major_version = core_request.api_major_version; + header->nonce_values.api_minor_version = core_request.api_minor_version; + header->nonce_values.nonce = core_request.nonce; + header->nonce_values.session_id = core_request.session_id; + // The message API version for the response is the minimum of our version and + // the request's version. + if (core_request.api_major_version > ODK_MAJOR_VERSION) { + header->nonce_values.api_major_version = ODK_MAJOR_VERSION; + header->nonce_values.api_minor_version = ODK_MINOR_VERSION; + } + + static constexpr size_t BUF_CAPACITY = 2048; + std::vector buf(BUF_CAPACITY, 0); + Message* msg = nullptr; + AllocateMessage(&msg, message_block); + InitMessage(msg, buf.data(), buf.capacity()); + packer(msg, &response); + if (!ValidMessage(msg)) { + return false; + } + + uint32_t message_length = GetSize(msg); + InitMessage(msg, buf.data() + sizeof(header->message_type), + sizeof(header->message_length)); + Pack_uint32_t(msg, &message_length); + oemcrypto_core_message->assign(reinterpret_cast(buf.data()), + message_length); + return true; +} + +bool CopyDeviceId(const ODK_ProvisioningRequest& src, + ODK_ProvisioningResponse* dest) { + auto& request = dest->request; + const std::string& device_id = src.device_id; + if (request.device_id_length > sizeof(request.device_id)) { + return false; + } + request.device_id_length = device_id.size(); + memset(request.device_id, 0, sizeof(request.device_id)); + memcpy(request.device_id, device_id.data(), request.device_id_length); + return true; +} + +} // namespace + +bool CreateCoreLicenseResponse(const ODK_ParsedLicense& parsed_lic, + const ODK_LicenseRequest& core_request, + const std::string& core_request_sha256, + std::string* oemcrypto_core_message) { + ODK_LicenseResponse license_response{ + {}, const_cast(&parsed_lic), {0}}; + if (core_request_sha256.size() != sizeof(license_response.request_hash)) + return false; + memcpy(license_response.request_hash, core_request_sha256.data(), + sizeof(license_response.request_hash)); + return CreateResponse(ODK_License_Response_Type, core_request, + oemcrypto_core_message, license_response, + Pack_ODK_LicenseResponse); +} + +bool CreateCoreRenewalResponse(const ODK_RenewalRequest& core_request, + uint64_t renewal_duration_seconds, + std::string* oemcrypto_core_message) { + ODK_RenewalResponse renewal_response{{}, core_request.playback_time_seconds}; + renewal_response.request.playback_time = core_request.playback_time_seconds; + renewal_response.renewal_duration_seconds = renewal_duration_seconds; + return CreateResponse(ODK_Renewal_Response_Type, core_request, + oemcrypto_core_message, renewal_response, + Pack_ODK_RenewalResponse); +} + +bool CreateCoreProvisioningResponse(const ODK_ParsedProvisioning& parsed_prov, + const ODK_ProvisioningRequest& core_request, + std::string* oemcrypto_core_message) { + ODK_ProvisioningResponse prov_response{ + {}, const_cast(&parsed_prov)}; + if (!CopyDeviceId(core_request, &prov_response)) { + return false; + } + return CreateResponse(ODK_Provisioning_Response_Type, core_request, + oemcrypto_core_message, prov_response, + Pack_ODK_ProvisioningResponse); +} + +} // namespace serialize +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/src/core_message_serialize_proto.cpp b/oemcrypto/odk/src/core_message_serialize_proto.cpp new file mode 100644 index 0000000..4b8da06 --- /dev/null +++ b/oemcrypto/odk/src/core_message_serialize_proto.cpp @@ -0,0 +1,183 @@ +// Copyright 2019 Google LLC. All rights reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "core_message_serialize_proto.h" + +#include +#include +#include +#include +#include + +#include "core_message_serialize.h" +#include "license_protocol.pb.h" +#include "odk_serialize.h" +#include "odk_structs.h" +#include "odk_structs_priv.h" +#include "serialization_base.h" + +namespace oemcrypto_core_message { +namespace serialize { +namespace { + +/* @ private functions */ + +/** + * Extract OEMCrypto_Substring (offset, length) from serialized protobuf + * + * Parameters: + * message: serialized license protobuf + * field: substring value + */ +OEMCrypto_Substring GetOecSubstring(const std::string& message, + const std::string& field) { + OEMCrypto_Substring substring = {}; + size_t pos = message.find(field); + if (pos != std::string::npos) { + substring = OEMCrypto_Substring{pos, field.length()}; + } + return substring; +} + +OEMCrypto_KeyObject KeyContainerToOecKey( + const std::string& proto, const video_widevine::License::KeyContainer& k) { + OEMCrypto_KeyObject obj = {}; + obj.key_id = GetOecSubstring(proto, k.id()); + obj.key_data_iv = GetOecSubstring(proto, k.iv()); + // Strip off PKCS#5 padding - since we know the key is 16 or 32 bytes, + // the padding will always be 16 bytes. + const std::string& key_data = k.key(); + const size_t PKCS5_PADDING_SIZE = 16; + obj.key_data = GetOecSubstring( + proto, key_data.substr(0, std::max(PKCS5_PADDING_SIZE, key_data.size()) - + PKCS5_PADDING_SIZE)); + if (k.has_key_control()) { + const auto& key_control = k.key_control(); + obj.key_control_iv = GetOecSubstring(proto, key_control.iv()); + obj.key_control = GetOecSubstring(proto, key_control.key_control_block()); + } + return obj; +} + +} // namespace + +// @ public create response functions + +bool CreateCoreLicenseResponseFromProto(const std::string& serialized_license, + const ODK_LicenseRequest& core_request, + const std::string& core_request_sha256, + const bool nonce_required, + std::string* oemcrypto_core_message) { + video_widevine::License lic; + if (!lic.ParseFromString(serialized_license)) { + return false; + } + + ODK_ParsedLicense parsed_lic{}; + bool any_content = false; + bool any_entitlement = false; + + for (int i = 0; i < lic.key_size(); ++i) { + const auto& k = lic.key(i); + switch (k.type()) { + case video_widevine::License_KeyContainer::SIGNING: { + if (!k.has_key()) { + continue; + } + parsed_lic.enc_mac_keys_iv = + GetOecSubstring(serialized_license, k.iv()); + parsed_lic.enc_mac_keys = GetOecSubstring(serialized_license, k.key()); + break; + } + case video_widevine::License_KeyContainer::CONTENT: + case video_widevine::License_KeyContainer::OPERATOR_SESSION: + case video_widevine::License_KeyContainer::ENTITLEMENT: { + if (k.type() == video_widevine::License_KeyContainer::ENTITLEMENT) { + any_entitlement = true; + } else { + any_content = true; + } + if (parsed_lic.key_array_length >= ODK_MAX_NUM_KEYS) { + return false; + } + uint32_t& n = parsed_lic.key_array_length; + parsed_lic.key_array[n++] = KeyContainerToOecKey(serialized_license, k); + break; + } + default: { + continue; + } + } + } + if (any_content && any_entitlement) { + // TODO(b/147513335): this should be logged -- both type of keys. + return false; + } + if (!any_content && !any_entitlement) { + // TODO(b/147513335): this should be logged -- no keys? + return false; + } + parsed_lic.license_type = + any_content ? OEMCrypto_ContentLicense : OEMCrypto_EntitlementLicense; + const auto& lid = lic.id(); + if (lid.has_provider_session_token()) { + parsed_lic.pst = + GetOecSubstring(serialized_license, lid.provider_session_token()); + } + + if (lic.has_srm_requirement()) { + parsed_lic.srm_restriction_data = + GetOecSubstring(serialized_license, lic.srm_requirement()); + } + + parsed_lic.nonce_required = nonce_required; + const auto& policy = lic.policy(); + ODK_TimerLimits& timer_limits = parsed_lic.timer_limits; + timer_limits.soft_enforce_rental_duration = + policy.soft_enforce_rental_duration(); + timer_limits.soft_enforce_playback_duration = + policy.soft_enforce_playback_duration(); + timer_limits.earliest_playback_start_seconds = 0; + timer_limits.rental_duration_seconds = policy.rental_duration_seconds(); + timer_limits.total_playback_duration_seconds = + policy.playback_duration_seconds(); + timer_limits.initial_renewal_duration_seconds = + policy.renewal_delay_seconds() + + policy.renewal_recovery_duration_seconds(); + + return CreateCoreLicenseResponse(parsed_lic, core_request, + core_request_sha256, oemcrypto_core_message); +} + +bool CreateCoreProvisioningResponseFromProto( + const std::string& serialized_provisioning_resp, + const ODK_ProvisioningRequest& core_request, + std::string* oemcrypto_core_message) { + ODK_ParsedProvisioning parsed_prov{}; + video_widevine::ProvisioningResponse prov; + if (!prov.ParseFromString(serialized_provisioning_resp)) { + return false; + } + + parsed_prov.key_type = + OEMCrypto_RSA_Private_Key; // TODO(b/148404408): ECC or RSA + if (prov.has_device_rsa_key()) { + parsed_prov.enc_private_key = + GetOecSubstring(serialized_provisioning_resp, prov.device_rsa_key()); + } + if (prov.has_device_rsa_key_iv()) { + parsed_prov.enc_private_key_iv = + GetOecSubstring(serialized_provisioning_resp, prov.device_rsa_key_iv()); + } + if (prov.has_wrapping_key()) { + parsed_prov.encrypted_message_key = + GetOecSubstring(serialized_provisioning_resp, prov.wrapping_key()); + } + + return CreateCoreProvisioningResponse(parsed_prov, core_request, + oemcrypto_core_message); +} + +} // namespace serialize +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/src/kdo.gypi b/oemcrypto/odk/src/kdo.gypi new file mode 100644 index 0000000..555a8f0 --- /dev/null +++ b/oemcrypto/odk/src/kdo.gypi @@ -0,0 +1,18 @@ +# Copyright 2019 Google LLC. All rights reserved. This file and proprietary +# source code may only be used and distributed under the Widevine Master License +# Agreement. + +# These files are used by the server and by some ODK test code. These files are +# not built into the ODK library on the device. +{ + 'sources': [ + 'core_message_deserialize.cpp', + 'core_message_serialize.cpp', + 'core_message_serialize_proto.cpp', + ], + 'include_dirs': [ + 'src', + '../include', + ], +} + diff --git a/oemcrypto/odk/src/odk.c b/oemcrypto/odk/src/odk.c new file mode 100644 index 0000000..42e82d5 --- /dev/null +++ b/oemcrypto/odk/src/odk.c @@ -0,0 +1,410 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#include "odk.h" + +#include +#include +#include + +#include "odk_overflow.h" +#include "odk_serialize.h" +#include "odk_structs.h" +#include "odk_structs_priv.h" +#include "odk_util.h" +#include "serialization_base.h" + +/* @ private odk functions */ + +static OEMCryptoResult ODK_PrepareRequest( + uint8_t* message, size_t message_length, size_t* core_message_length, + uint32_t message_type, const ODK_NonceValues* nonce_values, + void* prepared_request_buffer, size_t prepared_request_buffer_length) { + if (nonce_values == NULL || core_message_length == NULL || + prepared_request_buffer == NULL || + *core_message_length > message_length) { + return ODK_ERROR_CORE_MESSAGE; + } + + Message* msg = NULL; + AllocateMessage(&msg, message_block); + InitMessage(msg, message, *core_message_length); + + /* The core message should be at the beginning of the buffer, and with a + * shorter length. */ + if (sizeof(ODK_CoreMessage) > prepared_request_buffer_length) { + return ODK_ERROR_CORE_MESSAGE; + } + ODK_CoreMessage* core_message = (ODK_CoreMessage*)prepared_request_buffer; + *core_message = (ODK_CoreMessage){ + message_type, + 0, + *nonce_values, + }; + + /* Set core message length, and pack prepared request into message if the + * message buffer has been correctly initialized by the caller. */ + switch (message_type) { + case ODK_License_Request_Type: { + core_message->message_length = ODK_LICENSE_REQUEST_SIZE; + if (sizeof(ODK_PreparedLicenseRequest) > prepared_request_buffer_length) { + return ODK_ERROR_CORE_MESSAGE; + } + Pack_ODK_PreparedLicenseRequest( + msg, (ODK_PreparedLicenseRequest*)prepared_request_buffer); + break; + } + case ODK_Renewal_Request_Type: { + core_message->message_length = ODK_RENEWAL_REQUEST_SIZE; + if (sizeof(ODK_PreparedRenewalRequest) > prepared_request_buffer_length) { + return ODK_ERROR_CORE_MESSAGE; + } + Pack_ODK_PreparedRenewalRequest( + msg, (ODK_PreparedRenewalRequest*)prepared_request_buffer); + break; + } + case ODK_Provisioning_Request_Type: { + core_message->message_length = ODK_PROVISIONING_REQUEST_SIZE; + if (sizeof(ODK_PreparedProvisioningRequest) > + prepared_request_buffer_length) { + return ODK_ERROR_CORE_MESSAGE; + } + Pack_ODK_PreparedProvisioningRequest( + msg, (ODK_PreparedProvisioningRequest*)prepared_request_buffer); + break; + } + default: { + return ODK_ERROR_CORE_MESSAGE; + } + } + + *core_message_length = core_message->message_length; + if (GetStatus(msg) != MESSAGE_STATUS_OK) { + /* This is to indicate the caller that the core_message_length has been + * appropriately set, but the message buffer is either empty or too small, + * which needs to be initialized and filled in the subsequent call. */ + return OEMCrypto_ERROR_SHORT_BUFFER; + } + if (GetSize(msg) != *core_message_length) { + /* This should not happen. Something is wrong. */ + return ODK_ERROR_CORE_MESSAGE; + } + return OEMCrypto_SUCCESS; +} + +static OEMCryptoResult ODK_ParseResponse( + const uint8_t* message, size_t message_length, size_t core_message_length, + uint32_t message_type, const ODK_NonceValues* nonce_values, + void* response_buffer, uint32_t response_buffer_length) { + if (message == NULL || response_buffer == NULL || + core_message_length > message_length) { + return ODK_ERROR_CORE_MESSAGE; + } + + Message* msg = NULL; + AllocateMessage(&msg, message_block); + /* We initialize the message buffer with a size of the entire message + * length. */ + InitMessage(msg, (uint8_t*)message, message_length); + /* The core message should be at the beginning of the buffer, and with a + * shorter length. The core message is the part we are parsing. */ + SetSize(msg, core_message_length); + + /* Parse message and unpack it into response buffer. */ + switch (message_type) { + case ODK_License_Response_Type: { + if (sizeof(ODK_LicenseResponse) > response_buffer_length) { + return ODK_ERROR_CORE_MESSAGE; + } + Unpack_ODK_LicenseResponse(msg, (ODK_LicenseResponse*)response_buffer); + break; + } + case ODK_Renewal_Response_Type: { + if (sizeof(ODK_RenewalResponse) > response_buffer_length) { + return ODK_ERROR_CORE_MESSAGE; + } + Unpack_ODK_RenewalResponse(msg, (ODK_RenewalResponse*)response_buffer); + break; + } + case ODK_Provisioning_Response_Type: { + if (sizeof(ODK_ProvisioningResponse) > response_buffer_length) { + return ODK_ERROR_CORE_MESSAGE; + } + Unpack_ODK_ProvisioningResponse( + msg, (ODK_ProvisioningResponse*)response_buffer); + break; + } + default: { + return ODK_ERROR_CORE_MESSAGE; + } + } + + ODK_CoreMessage* core_message = (ODK_CoreMessage*)response_buffer; + if (GetStatus(msg) != MESSAGE_STATUS_OK || + message_type != core_message->message_type || + GetOffset(msg) != core_message->message_length) { + return ODK_ERROR_CORE_MESSAGE; + } + + if (nonce_values) { + /* always verify nonce_values for Renewal and Provisioning responses */ + if (!ODK_NonceValuesEqual(nonce_values, &(core_message->nonce_values))) { + return OEMCrypto_ERROR_INVALID_NONCE; + } + } + + return OEMCrypto_SUCCESS; +} + +/* @ public odk functions */ + +/* @@ prepare request functions */ + +OEMCryptoResult ODK_PrepareCoreLicenseRequest( + uint8_t* message, size_t message_length, size_t* core_message_length, + const ODK_NonceValues* nonce_values) { + if (core_message_length == NULL || nonce_values == NULL) { + return ODK_ERROR_CORE_MESSAGE; + } + ODK_PreparedLicenseRequest license_request = { + {0}, + }; + return ODK_PrepareRequest( + message, message_length, core_message_length, ODK_License_Request_Type, + nonce_values, &license_request, sizeof(ODK_PreparedLicenseRequest)); +} + +OEMCryptoResult ODK_PrepareCoreRenewalRequest(uint8_t* message, + size_t message_length, + size_t* core_message_size, + ODK_NonceValues* nonce_values, + ODK_ClockValues* clock_values, + uint64_t system_time_seconds) { + if (core_message_size == NULL || nonce_values == NULL || + clock_values == NULL) { + return ODK_ERROR_CORE_MESSAGE; + } + + /* If the license has not been loaded, then this is release instead of a + * renewal. All releases use v15. */ + if (clock_values->timer_status == ODK_CLOCK_TIMER_STATUS_LICENSE_NOT_LOADED || + clock_values->timer_status == ODK_CLOCK_TIMER_STATUS_LICENSE_INACTIVE) { + nonce_values->api_major_version = ODK_FIRST_VERSION - 1; + } + if (nonce_values->api_major_version < ODK_FIRST_VERSION) { + *core_message_size = 0; + return OEMCrypto_SUCCESS; + } + + ODK_PreparedRenewalRequest renewal_request = {{0}, 0}; + /* First, we compute the time this request was made relative to the playback + * clock. */ + if (clock_values->time_of_first_decrypt == 0) { + /* It is OK to preemptively request a renewal before playback starts. + * We'll treat this as asking for a renewal at playback time 0. */ + renewal_request.playback_time = 0; + } else { + /* Otherwise, playback_time is relative to the first decrypt. */ + if (odk_sub_overflow_u64(system_time_seconds, + clock_values->time_of_first_decrypt, + &renewal_request.playback_time)) { + return ODK_ERROR_CORE_MESSAGE; + } + } + + /* Save time for this request so that we can verify the response. This makes + * all earlier requests invalid. If preparing this request fails, then all + * requests will be bad. */ + clock_values->time_of_renewal_request = renewal_request.playback_time; + + return ODK_PrepareRequest( + message, message_length, core_message_size, ODK_Renewal_Request_Type, + nonce_values, &renewal_request, sizeof(ODK_PreparedRenewalRequest)); +} + +OEMCryptoResult ODK_PrepareCoreProvisioningRequest( + uint8_t* message, size_t message_length, size_t* core_message_length, + const ODK_NonceValues* nonce_values, const uint8_t* device_id, + size_t device_id_length) { + if (core_message_length == NULL || nonce_values == NULL) { + return ODK_ERROR_CORE_MESSAGE; + } + ODK_PreparedProvisioningRequest provisioning_request = { + {0}, + 0, + {0}, + }; + if (device_id_length > sizeof(provisioning_request.device_id)) { + return ODK_ERROR_CORE_MESSAGE; + } + provisioning_request.device_id_length = device_id_length; + if (device_id) { + memcpy(provisioning_request.device_id, device_id, device_id_length); + } + return ODK_PrepareRequest(message, message_length, core_message_length, + ODK_Provisioning_Request_Type, nonce_values, + &provisioning_request, + sizeof(ODK_PreparedProvisioningRequest)); +} + +/* @@ parse response functions */ + +OEMCryptoResult ODK_ParseLicense( + const uint8_t* message, size_t message_length, size_t core_message_length, + bool initial_license_load, bool usage_entry_present, + const uint8_t* request_hash, ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, ODK_NonceValues* nonce_values, + ODK_ParsedLicense* parsed_license) { + if (message == NULL || request_hash == NULL || timer_limits == NULL || + clock_values == NULL || nonce_values == NULL || parsed_license == NULL) { + return ODK_ERROR_CORE_MESSAGE; + } + + ODK_LicenseResponse license_response = {{{0}}, parsed_license, {0}}; + const OEMCryptoResult err = ODK_ParseResponse( + message, message_length, core_message_length, ODK_License_Response_Type, + NULL, &license_response, sizeof(ODK_LicenseResponse)); + + if (err != OEMCrypto_SUCCESS) { + return err; + } + + /* We do not support future API version. Also, this function should not be + * used for legacy licenses. */ + if (license_response.request.core_message.nonce_values.api_major_version > + ODK_MAJOR_VERSION || + license_response.request.core_message.nonce_values.api_major_version < + ODK_FIRST_VERSION) { + return ODK_UNSUPPORTED_API; + } + + /* If the server sent us an older format, record the license's API version. */ + if (nonce_values->api_major_version > + license_response.request.core_message.nonce_values.api_major_version) { + nonce_values->api_major_version = + license_response.request.core_message.nonce_values.api_major_version; + nonce_values->api_minor_version = + license_response.request.core_message.nonce_values.api_minor_version; + } else if (nonce_values->api_minor_version > + license_response.request.core_message.nonce_values + .api_minor_version) { + nonce_values->api_minor_version = + license_response.request.core_message.nonce_values.api_minor_version; + } + /* If the license has a provider session token (pst), then OEMCrypto should + * have a usage entry loaded. The opposite is also an error. */ + if ((usage_entry_present && parsed_license->pst.length == 0) || + (!usage_entry_present && parsed_license->pst.length > 0)) { + return ODK_ERROR_CORE_MESSAGE; + } + + if (parsed_license->nonce_required) { + if (initial_license_load) { + if (nonce_values->nonce != + license_response.request.core_message.nonce_values.nonce || + nonce_values->session_id != + license_response.request.core_message.nonce_values.session_id) { + return OEMCrypto_ERROR_INVALID_NONCE; + } + } else { /* !initial_license_load */ + nonce_values->nonce = + license_response.request.core_message.nonce_values.nonce; + nonce_values->session_id = + license_response.request.core_message.nonce_values.session_id; + } + } + /* For v16, in order to be backwards compatible with a v15 license server, + * OEMCrypto stores a hash of the core license request and only signs the + * message body. Here, when we process the license response, we verify that + * the server has the same hash of the core request. */ + if (initial_license_load && parsed_license->nonce_required && + crypto_memcmp(request_hash, license_response.request_hash, + ODK_SHA256_HASH_SIZE)) { + return ODK_ERROR_CORE_MESSAGE; + } + *timer_limits = parsed_license->timer_limits; + /* And update the clock values state. */ + clock_values->timer_status = ODK_CLOCK_TIMER_STATUS_LICENSE_LOADED; + return OEMCrypto_SUCCESS; +} + +OEMCryptoResult ODK_ParseRenewal(const uint8_t* message, size_t message_length, + size_t core_message_length, + const ODK_NonceValues* nonce_values, + uint64_t system_time, + const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + uint64_t* timer_value) { + if (message == NULL || nonce_values == NULL || timer_limits == NULL || + clock_values == NULL) { + return ODK_ERROR_CORE_MESSAGE; + } + + ODK_RenewalResponse renewal_response = { + {{0}, 0}, + 0, + }; + const OEMCryptoResult err = ODK_ParseResponse( + message, message_length, core_message_length, ODK_Renewal_Response_Type, + nonce_values, &renewal_response, sizeof(ODK_RenewalResponse)); + + if (err != OEMCrypto_SUCCESS) { + return err; + } + + /* Reference: + * Doc: License Duration and Renewal (Changes for OEMCrypto v16) + * Section: Renewal Message + */ + /* If a renewal request is lost in transit, we should throw it out and create + * a new one. We use the timestamp to make sure we have the latest request. + */ + if (clock_values->time_of_renewal_request < + renewal_response.request.playback_time) { + return ODK_STALE_RENEWAL; + } + return ODK_ComputeRenewalDuration(timer_limits, clock_values, system_time, + renewal_response.renewal_duration_seconds, + timer_value); +} + +OEMCryptoResult ODK_ParseProvisioning( + const uint8_t* message, size_t message_length, size_t core_message_length, + const ODK_NonceValues* nonce_values, const uint8_t* device_id, + size_t device_id_length, ODK_ParsedProvisioning* parsed_response) { + if (message == NULL || nonce_values == NULL || device_id == NULL || + parsed_response == NULL) { + return ODK_ERROR_CORE_MESSAGE; + } + + ODK_ProvisioningResponse provisioning_response = {{{0}, 0, {0}}, + parsed_response}; + if (device_id_length > ODK_DEVICE_ID_LEN_MAX) { + return ODK_ERROR_CORE_MESSAGE; + } + + const OEMCryptoResult err = ODK_ParseResponse( + message, message_length, core_message_length, + ODK_Provisioning_Response_Type, nonce_values, &provisioning_response, + sizeof(ODK_ProvisioningResponse)); + + if (err != OEMCrypto_SUCCESS) { + return err; + } + + if (crypto_memcmp(device_id, provisioning_response.request.device_id, + device_id_length) != 0) { + return ODK_ERROR_CORE_MESSAGE; + } + + const uint8_t zero[ODK_DEVICE_ID_LEN_MAX] = {0}; + /* check bytes beyond device_id_length are 0 */ + if (crypto_memcmp(zero, + provisioning_response.request.device_id + device_id_length, + ODK_DEVICE_ID_LEN_MAX - device_id_length) != 0) { + return ODK_ERROR_CORE_MESSAGE; + } + + return OEMCrypto_SUCCESS; +} diff --git a/oemcrypto/odk/src/odk.gyp b/oemcrypto/odk/src/odk.gyp new file mode 100644 index 0000000..a16dec6 --- /dev/null +++ b/oemcrypto/odk/src/odk.gyp @@ -0,0 +1,24 @@ +# Copyright 2019 Google LLC. All rights reserved. This file and proprietary +# source code may only be used and distributed under the Widevine Master License +# Agreement. + +{ + 'targets': [ + { + 'target_name': 'odk', + 'type': 'static_library', + 'include_dirs': [ + '../include', + '../../include', + ], + 'includes' : [ + 'odk.gypi', + ], + 'direct_dependent_settings': { + 'include_dirs': [ + '../include', + ], + } + }, + ], +} diff --git a/oemcrypto/odk/src/odk.gypi b/oemcrypto/odk/src/odk.gypi new file mode 100644 index 0000000..ec29751 --- /dev/null +++ b/oemcrypto/odk/src/odk.gypi @@ -0,0 +1,17 @@ +# Copyright 2019 Google LLC. All rights reserved. This file and proprietary +# source code may only be used and distributed under the Widevine Master License +# Agreement. + +# These files are built into the ODK library on the device. They are also used +# by the server and by test cocde. These files should compile on C99 compilers. +{ + 'sources': [ + 'odk.c', + 'odk_overflow.c', + 'odk_serialize.c', + 'odk_timer.c', + 'odk_util.c', + 'serialization_base.c', + ], +} + diff --git a/oemcrypto/odk/src/odk_assert.h b/oemcrypto/odk/src/odk_assert.h new file mode 100644 index 0000000..6fda98b --- /dev/null +++ b/oemcrypto/odk/src/odk_assert.h @@ -0,0 +1,24 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#ifndef WIDEVINE_ODK_SRC_ODK_ASSERT_H_ +#define WIDEVINE_ODK_SRC_ODK_ASSERT_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#if (__STDC_VERSION__ >= 201112L) +#include +#define odk_static_assert static_assert +#else +#define odk_static_assert(msg, e) \ + enum { odk_static_assert = 1 / (!!((msg) && (e))) }; +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* WIDEVINE_ODK_SRC_ODK_ASSERT_H_ */ diff --git a/oemcrypto/odk/src/odk_endian.h b/oemcrypto/odk/src/odk_endian.h new file mode 100644 index 0000000..9cb12da --- /dev/null +++ b/oemcrypto/odk/src/odk_endian.h @@ -0,0 +1,29 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#ifndef WIDEVINE_ODK_SRC_ODK_ENDIAN_H_ +#define WIDEVINE_ODK_SRC_ODK_ENDIAN_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(__linux__) || defined(__ANDROID__) +#include +#define oemcrypto_htobe32 htobe32 +#define oemcrypto_be32toh be32toh +#define oemcrypto_htobe64 htobe64 +#define oemcrypto_be64toh be64toh +#else /* defined(__linux__) || defined(__ANDROID__) */ +uint32_t oemcrypto_htobe32(uint32_t u32); +uint32_t oemcrypto_be32toh(uint32_t u32); +uint64_t oemcrypto_htobe64(uint64_t u64); +uint64_t oemcrypto_be64toh(uint64_t u64); +#endif /* defined(__linux__) || defined(__ANDROID__) */ + +#ifdef __cplusplus +} +#endif + +#endif /* WIDEVINE_ODK_SRC_ODK_ENDIAN_H_ */ diff --git a/oemcrypto/odk/src/odk_overflow.c b/oemcrypto/odk/src/odk_overflow.c new file mode 100644 index 0000000..76c685f --- /dev/null +++ b/oemcrypto/odk/src/odk_overflow.c @@ -0,0 +1,36 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#include +#include + +int odk_sub_overflow_u64(uint64_t a, uint64_t b, uint64_t* c) { + if (a >= b) { + if (c) { + *c = a - b; + } + return 0; + } + return 1; +} + +int odk_add_overflow_u64(uint64_t a, uint64_t b, uint64_t* c) { + if (UINT64_MAX - a >= b) { + if (c) { + *c = a + b; + } + return 0; + } + return 1; +} + +int odk_add_overflow_ux(size_t a, size_t b, size_t* c) { + if (SIZE_MAX - a >= b) { + if (c) { + *c = a + b; + } + return 0; + } + return 1; +} diff --git a/oemcrypto/odk/src/odk_overflow.h b/oemcrypto/odk/src/odk_overflow.h new file mode 100644 index 0000000..b1e03ee --- /dev/null +++ b/oemcrypto/odk/src/odk_overflow.h @@ -0,0 +1,23 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#ifndef WIDEVINE_ODK_SRC_ODK_OVERFLOW_H_ +#define WIDEVINE_ODK_SRC_ODK_OVERFLOW_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int odk_sub_overflow_u64(uint64_t a, uint64_t b, uint64_t* c); +int odk_add_overflow_u64(uint64_t a, uint64_t b, uint64_t* c); +int odk_add_overflow_ux(size_t a, size_t b, size_t* c); + +#ifdef __cplusplus +} +#endif + +#endif /* WIDEVINE_ODK_SRC_ODK_OVERFLOW_H_ */ diff --git a/oemcrypto/odk/src/odk_serialize.c b/oemcrypto/odk/src/odk_serialize.c new file mode 100644 index 0000000..efd9475 --- /dev/null +++ b/oemcrypto/odk/src/odk_serialize.c @@ -0,0 +1,213 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +/* + * This code is auto-generated, do not edit + */ + +#include "odk_structs_priv.h" +#include "serialization_base.h" + +/* @ serialize */ + +/* @@ private serialize */ + +static void Pack_ODK_NonceValues(Message* msg, ODK_NonceValues const* obj) { + Pack_uint16_t(msg, &obj->api_minor_version); + Pack_uint16_t(msg, &obj->api_major_version); + Pack_uint32_t(msg, &obj->nonce); + Pack_uint32_t(msg, &obj->session_id); +} + +static void Pack_ODK_CoreMessage(Message* msg, ODK_CoreMessage const* obj) { + Pack_uint32_t(msg, &obj->message_type); + Pack_uint32_t(msg, &obj->message_length); + Pack_ODK_NonceValues(msg, &obj->nonce_values); +} + +static void Pack_OEMCrypto_KeyObject(Message* msg, + OEMCrypto_KeyObject const* obj) { + Pack_OEMCrypto_Substring(msg, &obj->key_id); + Pack_OEMCrypto_Substring(msg, &obj->key_data_iv); + Pack_OEMCrypto_Substring(msg, &obj->key_data); + Pack_OEMCrypto_Substring(msg, &obj->key_control_iv); + Pack_OEMCrypto_Substring(msg, &obj->key_control); +} + +static void Pack_ODK_TimerLimits(Message* msg, ODK_TimerLimits const* obj) { + Pack_bool(msg, &obj->soft_enforce_rental_duration); + Pack_bool(msg, &obj->soft_enforce_playback_duration); + Pack_uint64_t(msg, &obj->earliest_playback_start_seconds); + Pack_uint64_t(msg, &obj->rental_duration_seconds); + Pack_uint64_t(msg, &obj->total_playback_duration_seconds); + Pack_uint64_t(msg, &obj->initial_renewal_duration_seconds); +} + +static void Pack_ODK_ParsedLicense(Message* msg, ODK_ParsedLicense const* obj) { + /* hand-coded */ + if (obj->key_array_length > ODK_MAX_NUM_KEYS) { + SetStatus(msg, MESSAGE_STATUS_OVERFLOW_ERROR); + return; + } + Pack_OEMCrypto_Substring(msg, &obj->enc_mac_keys_iv); + Pack_OEMCrypto_Substring(msg, &obj->enc_mac_keys); + Pack_OEMCrypto_Substring(msg, &obj->pst); + Pack_OEMCrypto_Substring(msg, &obj->srm_restriction_data); + Pack_enum(msg, obj->license_type); + Pack_bool(msg, &obj->nonce_required); + Pack_ODK_TimerLimits(msg, &obj->timer_limits); + Pack_uint32_t(msg, &obj->key_array_length); + size_t i; + for (i = 0; i < (size_t)obj->key_array_length; i++) { + Pack_OEMCrypto_KeyObject(msg, &obj->key_array[i]); + } +} + +static void Pack_ODK_ParsedProvisioning(Message* msg, + ODK_ParsedProvisioning const* obj) { + Pack_enum(msg, obj->key_type); + Pack_OEMCrypto_Substring(msg, &obj->enc_private_key); + Pack_OEMCrypto_Substring(msg, &obj->enc_private_key_iv); + Pack_OEMCrypto_Substring(msg, &obj->encrypted_message_key); +} + +/* @@ odk serialize */ + +void Pack_ODK_PreparedLicenseRequest(Message* msg, + ODK_PreparedLicenseRequest const* obj) { + Pack_ODK_CoreMessage(msg, &obj->core_message); +} + +void Pack_ODK_PreparedRenewalRequest(Message* msg, + ODK_PreparedRenewalRequest const* obj) { + Pack_ODK_CoreMessage(msg, &obj->core_message); + Pack_uint64_t(msg, &obj->playback_time); +} + +void Pack_ODK_PreparedProvisioningRequest( + Message* msg, ODK_PreparedProvisioningRequest const* obj) { + Pack_ODK_CoreMessage(msg, &obj->core_message); + Pack_uint32_t(msg, &obj->device_id_length); + PackArray(msg, &obj->device_id[0], sizeof(obj->device_id)); +} + +/* @@ kdo serialize */ + +void Pack_ODK_LicenseResponse(Message* msg, ODK_LicenseResponse const* obj) { + Pack_ODK_PreparedLicenseRequest(msg, &obj->request); + Pack_ODK_ParsedLicense(msg, (const ODK_ParsedLicense*)obj->parsed_license); + PackArray(msg, &obj->request_hash[0], sizeof(obj->request_hash)); +} + +void Pack_ODK_RenewalResponse(Message* msg, ODK_RenewalResponse const* obj) { + Pack_ODK_PreparedRenewalRequest(msg, &obj->request); + Pack_uint64_t(msg, &obj->renewal_duration_seconds); +} + +void Pack_ODK_ProvisioningResponse(Message* msg, + ODK_ProvisioningResponse const* obj) { + Pack_ODK_PreparedProvisioningRequest(msg, &obj->request); + Pack_ODK_ParsedProvisioning( + msg, (const ODK_ParsedProvisioning*)obj->parsed_provisioning); +} + +/* @ deserialize */ + +/* @@ private deserialize */ + +static void Unpack_ODK_NonceValues(Message* msg, ODK_NonceValues* obj) { + Unpack_uint16_t(msg, &obj->api_minor_version); + Unpack_uint16_t(msg, &obj->api_major_version); + Unpack_uint32_t(msg, &obj->nonce); + Unpack_uint32_t(msg, &obj->session_id); +} + +static void Unpack_ODK_CoreMessage(Message* msg, ODK_CoreMessage* obj) { + Unpack_uint32_t(msg, &obj->message_type); + Unpack_uint32_t(msg, &obj->message_length); + Unpack_ODK_NonceValues(msg, &obj->nonce_values); +} + +static void Unpack_OEMCrypto_KeyObject(Message* msg, OEMCrypto_KeyObject* obj) { + Unpack_OEMCrypto_Substring(msg, &obj->key_id); + Unpack_OEMCrypto_Substring(msg, &obj->key_data_iv); + Unpack_OEMCrypto_Substring(msg, &obj->key_data); + Unpack_OEMCrypto_Substring(msg, &obj->key_control_iv); + Unpack_OEMCrypto_Substring(msg, &obj->key_control); +} + +static void Unpack_ODK_TimerLimits(Message* msg, ODK_TimerLimits* obj) { + Unpack_bool(msg, &obj->soft_enforce_rental_duration); + Unpack_bool(msg, &obj->soft_enforce_playback_duration); + Unpack_uint64_t(msg, &obj->earliest_playback_start_seconds); + Unpack_uint64_t(msg, &obj->rental_duration_seconds); + Unpack_uint64_t(msg, &obj->total_playback_duration_seconds); + Unpack_uint64_t(msg, &obj->initial_renewal_duration_seconds); +} + +static void Unpack_ODK_ParsedLicense(Message* msg, ODK_ParsedLicense* obj) { + Unpack_OEMCrypto_Substring(msg, &obj->enc_mac_keys_iv); + Unpack_OEMCrypto_Substring(msg, &obj->enc_mac_keys); + Unpack_OEMCrypto_Substring(msg, &obj->pst); + Unpack_OEMCrypto_Substring(msg, &obj->srm_restriction_data); + obj->license_type = (OEMCrypto_LicenseType)Unpack_enum(msg); + Unpack_bool(msg, &obj->nonce_required); + Unpack_ODK_TimerLimits(msg, &obj->timer_limits); + Unpack_uint32_t(msg, &obj->key_array_length); + if (obj->key_array_length > ODK_MAX_NUM_KEYS) { + SetStatus(msg, MESSAGE_STATUS_OVERFLOW_ERROR); + return; + } + uint32_t i; + for (i = 0; i < obj->key_array_length; i++) { + Unpack_OEMCrypto_KeyObject(msg, &obj->key_array[i]); + } +} + +static void Unpack_ODK_ParsedProvisioning(Message* msg, + ODK_ParsedProvisioning* obj) { + obj->key_type = (OEMCrypto_PrivateKeyType)Unpack_enum(msg); + Unpack_OEMCrypto_Substring(msg, &obj->enc_private_key); + Unpack_OEMCrypto_Substring(msg, &obj->enc_private_key_iv); + Unpack_OEMCrypto_Substring(msg, &obj->encrypted_message_key); +} + +/* @ kdo deserialize */ + +void Unpack_ODK_PreparedLicenseRequest(Message* msg, + ODK_PreparedLicenseRequest* obj) { + Unpack_ODK_CoreMessage(msg, &obj->core_message); +} + +void Unpack_ODK_PreparedRenewalRequest(Message* msg, + ODK_PreparedRenewalRequest* obj) { + Unpack_ODK_CoreMessage(msg, &obj->core_message); + Unpack_uint64_t(msg, &obj->playback_time); +} + +void Unpack_ODK_PreparedProvisioningRequest( + Message* msg, ODK_PreparedProvisioningRequest* obj) { + Unpack_ODK_CoreMessage(msg, &obj->core_message); + Unpack_uint32_t(msg, &obj->device_id_length); + UnpackArray(msg, &obj->device_id[0], sizeof(obj->device_id)); +} + +/* @@ odk deserialize */ + +void Unpack_ODK_LicenseResponse(Message* msg, ODK_LicenseResponse* obj) { + Unpack_ODK_PreparedLicenseRequest(msg, &obj->request); + Unpack_ODK_ParsedLicense(msg, obj->parsed_license); + UnpackArray(msg, &obj->request_hash[0], sizeof(obj->request_hash)); +} + +void Unpack_ODK_RenewalResponse(Message* msg, ODK_RenewalResponse* obj) { + Unpack_ODK_PreparedRenewalRequest(msg, &obj->request); + Unpack_uint64_t(msg, &obj->renewal_duration_seconds); +} + +void Unpack_ODK_ProvisioningResponse(Message* msg, + ODK_ProvisioningResponse* obj) { + Unpack_ODK_PreparedProvisioningRequest(msg, &obj->request); + Unpack_ODK_ParsedProvisioning(msg, obj->parsed_provisioning); +} diff --git a/oemcrypto/odk/src/odk_serialize.h b/oemcrypto/odk/src/odk_serialize.h new file mode 100644 index 0000000..f35f178 --- /dev/null +++ b/oemcrypto/odk/src/odk_serialize.h @@ -0,0 +1,49 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +/* + * This code is auto-generated, do not edit + */ +#ifndef WIDEVINE_ODK_SRC_ODK_SERIALIZE_H_ +#define WIDEVINE_ODK_SRC_ODK_SERIALIZE_H_ + +#include "odk_structs_priv.h" +#include "serialization_base.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* odk pack */ +void Pack_ODK_PreparedLicenseRequest(Message* msg, + const ODK_PreparedLicenseRequest* obj); +void Pack_ODK_PreparedRenewalRequest(Message* msg, + const ODK_PreparedRenewalRequest* obj); +void Pack_ODK_PreparedProvisioningRequest( + Message* msg, const ODK_PreparedProvisioningRequest* obj); + +/* odk unpack */ +void Unpack_ODK_LicenseResponse(Message* msg, ODK_LicenseResponse* obj); +void Unpack_ODK_RenewalResponse(Message* msg, ODK_RenewalResponse* obj); +void Unpack_ODK_ProvisioningResponse(Message* msg, + ODK_ProvisioningResponse* obj); + +/* kdo pack */ +void Pack_ODK_LicenseResponse(Message* msg, const ODK_LicenseResponse* obj); +void Pack_ODK_RenewalResponse(Message* msg, const ODK_RenewalResponse* obj); +void Pack_ODK_ProvisioningResponse(Message* msg, + const ODK_ProvisioningResponse* obj); + +/* kdo unpack */ +void Unpack_ODK_PreparedLicenseRequest(Message* msg, + ODK_PreparedLicenseRequest* obj); +void Unpack_ODK_PreparedRenewalRequest(Message* msg, + ODK_PreparedRenewalRequest* obj); +void Unpack_ODK_PreparedProvisioningRequest( + Message* msg, ODK_PreparedProvisioningRequest* obj); + +#ifdef __cplusplus +} /* extern "C" */ +#endif +#endif /* WIDEVINE_ODK_SRC_ODK_SERIALIZE_H_ */ diff --git a/oemcrypto/odk/src/odk_structs_priv.h b/oemcrypto/odk/src/odk_structs_priv.h new file mode 100644 index 0000000..3c5a502 --- /dev/null +++ b/oemcrypto/odk/src/odk_structs_priv.h @@ -0,0 +1,104 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#ifndef WIDEVINE_ODK_SRC_ODK_STRUCTS_PRIV_H_ +#define WIDEVINE_ODK_SRC_ODK_STRUCTS_PRIV_H_ + +#include + +#include "OEMCryptoCENCCommon.h" +#include "odk_structs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + ODK_License_Request_Type = 1, + ODK_License_Response_Type = 2, + ODK_Renewal_Request_Type = 3, + ODK_Renewal_Response_Type = 4, + ODK_Provisioning_Request_Type = 5, + ODK_Provisioning_Response_Type = 6, + + /* Reserve future message types to support forward compatibility. */ + ODK_Release_Request_Type = 7, + ODK_Release_Response_Type = 8, +} ODK_MessageType; + +typedef struct { + uint32_t message_type; + uint32_t message_length; + ODK_NonceValues nonce_values; +} ODK_CoreMessage; + +typedef struct { + ODK_CoreMessage core_message; +} ODK_PreparedLicenseRequest; + +typedef struct { + ODK_CoreMessage core_message; + uint64_t playback_time; +} ODK_PreparedRenewalRequest; + +typedef struct { + ODK_CoreMessage core_message; + uint32_t device_id_length; + uint8_t device_id[ODK_DEVICE_ID_LEN_MAX]; +} ODK_PreparedProvisioningRequest; + +typedef struct { + ODK_PreparedLicenseRequest request; + ODK_ParsedLicense* parsed_license; + uint8_t request_hash[ODK_SHA256_HASH_SIZE]; +} ODK_LicenseResponse; + +typedef struct { + ODK_PreparedRenewalRequest request; + uint64_t renewal_duration_seconds; +} ODK_RenewalResponse; + +typedef struct { + ODK_PreparedProvisioningRequest request; + ODK_ParsedProvisioning* parsed_provisioning; +} ODK_ProvisioningResponse; + +/* These are the sum of sizeof of each individual member of the request structs + */ +/* without any padding added by the compiler. Make sure they get updated when */ +/* request structs change. Refer to test suite OdkSizeTest in */ +/* ../test/odk_test.cpp for validations of each of the defined request sizes. */ +#define ODK_LICENSE_REQUEST_SIZE 20 +#define ODK_RENEWAL_REQUEST_SIZE 28 +#define ODK_PROVISIONING_REQUEST_SIZE 88 + +/* These are the possible timer status values. */ +#define ODK_CLOCK_TIMER_STATUS_UNDEFINED 0 /* Should not happen. */ +/* When the structure has been initialized, but no license is loaded. */ +#define ODK_CLOCK_TIMER_STATUS_LICENSE_NOT_LOADED 1 +/* After the license is loaded, before a successful decrypt. */ +#define ODK_CLOCK_TIMER_STATUS_LICENSE_LOADED 2 +/* After the license is loaded, if a renewal has also been loaded. */ +#define ODK_CLOCK_TIMER_STATUS_RENEWAL_LOADED 3 +/* The first decrypt has occurred and the timer is active. */ +#define ODK_CLOCK_TIMER_STATUS_ACTIVE 4 +/* The first decrypt has occurred and the timer is unlimited. */ +#define ODK_CLOCK_TIMER_STATUS_UNLIMITED 5 +/* The timer has transitioned from active to expired. */ +#define ODK_CLOCK_TIMER_STATUS_EXPIRED 6 +/* The license has been marked as inactive. */ +#define ODK_CLOCK_TIMER_STATUS_LICENSE_INACTIVE 7 + +/* A helper function for computing timer limits when a renewal is loaded. */ +OEMCryptoResult ODK_ComputeRenewalDuration(const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + uint64_t system_time_seconds, + uint64_t new_renewal_duration, + uint64_t* timer_value); + +#ifdef __cplusplus +} +#endif + +#endif /* WIDEVINE_ODK_SRC_ODK_STRUCTS_PRIV_H_ */ diff --git a/oemcrypto/odk/src/odk_timer.c b/oemcrypto/odk/src/odk_timer.c new file mode 100644 index 0000000..0fbcf18 --- /dev/null +++ b/oemcrypto/odk/src/odk_timer.c @@ -0,0 +1,502 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#include +#include + +#include "odk.h" +#include "odk_overflow.h" +#include "odk_structs_priv.h" + +/* Private function. Checks to see if the license is active. Returns + * ODK_TIMER_EXPIRED if the license is valid but inactive. Returns + * OEMCrypto_SUCCESS if the license is active. Returns + * OEMCrypto_ERROR_UNKNOWN_FAILURE on other errors. */ +static OEMCryptoResult ODK_LicenseActive(const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values) { + /* Check some basic errors. */ + if (clock_values == NULL || timer_limits == NULL) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + /* Check if the license has not been loaded yet. */ + if (clock_values->timer_status == ODK_CLOCK_TIMER_STATUS_UNDEFINED || + clock_values->timer_status == ODK_CLOCK_TIMER_STATUS_LICENSE_NOT_LOADED) { + return OEMCrypto_ERROR_UNKNOWN_FAILURE; + } + if (clock_values->status > kActive) { + return ODK_TIMER_EXPIRED; + } + return OEMCrypto_SUCCESS; +} + +/* Private function. Sets the timer_value to be the min(timer_value, new_value), + * with the convention that 0 means infinite. The convention that 0 means + * infinite is used for all Widevine license and duration values. */ +static void ComputeMinimum(uint64_t* timer_value, uint64_t new_value) { + if (timer_value == NULL) return; + if (new_value > 0) { + if (*timer_value == 0 || *timer_value > new_value) { + *timer_value = new_value; + } + } +} + +/* Private function. Check to see if the rental window restricts playback. If + * the rental enforcement is hard, or if this is the first playback, then we + * verify that system_time_seconds is within the rental window. If the + * enforcement is soft and we have already started playback, then there is no + * restriction. + * Return ODK_TIMER_EXPIRED if out of the window. + * Return ODK_TIMER_ACTIVE if within the window, and there is a hard limit. + * Return ODK_DISABLE_TIMER if no there should be no limit. + * Return other error on error. + * Also, if this function does compute a limit, the timer_value is reduced to + * obey that limit. If the limit is less restrictive than the current + * timer_value, then timer_value is not changed. */ +static OEMCryptoResult ODK_CheckRentalWindow( + const ODK_TimerLimits* timer_limits, ODK_ClockValues* clock_values, + uint64_t system_time_seconds, uint64_t* timer_value) { + if (clock_values == NULL || timer_limits == NULL || timer_value == NULL) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + /* If playback has already started, and rental duration enforcement is soft, + * then there is no restriction. */ + if (clock_values->time_of_first_decrypt > 0 && + timer_limits->soft_enforce_rental_duration) { + return ODK_DISABLE_TIMER; + } + + /* rental_clock = time since license signed. */ + uint64_t rental_clock = 0; + if (odk_sub_overflow_u64(system_time_seconds, + clock_values->time_of_license_signed, + &rental_clock)) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + /* Check if it is before license is valid. This is an unusual case. First + * playback may still work if it occurs after the rental window opens. */ + if (rental_clock < timer_limits->earliest_playback_start_seconds) { + return ODK_TIMER_EXPIRED; + } + /* If the rental duration is 0, there is no limit. */ + if (timer_limits->rental_duration_seconds == 0) { + return ODK_DISABLE_TIMER; + } + /* End of rental window, based on rental clock (not system time). */ + uint64_t end_of_rental_window = 0; + if (odk_add_overflow_u64(timer_limits->earliest_playback_start_seconds, + timer_limits->rental_duration_seconds, + &end_of_rental_window)) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + if (end_of_rental_window <= rental_clock) { + return ODK_TIMER_EXPIRED; + } + /* At this point system_time is within the rental window. */ + if (timer_limits->soft_enforce_rental_duration) { + /* For soft enforcement, we allow playback, and do not adjust the timer. */ + return ODK_DISABLE_TIMER; + } + uint64_t time_left = 0; + if (odk_sub_overflow_u64(end_of_rental_window, rental_clock, &time_left)) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + ComputeMinimum(timer_value, time_left); + return ODK_SET_TIMER; +} + +/* Private function. Check to see if the playback window restricts + * playback. This should only be called if playback has started, so that + * clock_values->time_of_first_decrypt is nonzero. + * Return ODK_TIMER_EXPIRED if out of the window. + * Return ODK_SET_TIMER if within the window, and there is a hard limit. + * Return ODK_DISABLE_TIMER if no limit. + * Return other error on error. + * Also, if this function does compute a limit, the timer_value is reduced to + * obey that limit. If the limit is less restrictive than the current + * timer_value, then timer_value is not changed. */ +static OEMCryptoResult ODK_CheckPlaybackWindow( + const ODK_TimerLimits* timer_limits, ODK_ClockValues* clock_values, + uint64_t system_time_seconds, uint64_t* timer_value) { + if (clock_values == NULL || timer_limits == NULL || timer_value == NULL) { + return OEMCrypto_ERROR_UNKNOWN_FAILURE; + } + /* if the playback duration is 0, there is no limit. */ + if (timer_limits->total_playback_duration_seconds == 0) { + return ODK_DISABLE_TIMER; + } + uint64_t end_of_playback_window = 0; + if (odk_add_overflow_u64(timer_limits->total_playback_duration_seconds, + clock_values->time_of_first_decrypt, + &end_of_playback_window)) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + if (end_of_playback_window <= system_time_seconds) { + return ODK_TIMER_EXPIRED; + } + /* At this point, system_time is within the total playback window. */ + if (timer_limits->soft_enforce_playback_duration) { + /* For soft enforcement, we allow playback, and do not adjust the timer. */ + return ODK_DISABLE_TIMER; + } + uint64_t time_left = 0; + if (odk_sub_overflow_u64(end_of_playback_window, system_time_seconds, + &time_left)) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + ComputeMinimum(timer_value, time_left); + return ODK_SET_TIMER; +} + +/* Update the timer status. If playback has already started, we use the given + * status. However, if playback has not yet started, then we expect a call to + * ODK_AttemptFirstPlayback in the future, and we need to signal to it that we + * have already computed the timer limit. */ +static void ODK_UpdateTimerStatusForRenewal(ODK_ClockValues* clock_values, + uint32_t new_status) { + if (clock_values == NULL) { + return; /* should not happen. */ + } + if (clock_values->timer_status == ODK_CLOCK_TIMER_STATUS_LICENSE_LOADED) { + /* Signal that the timer is already set. */ + clock_values->timer_status = ODK_CLOCK_TIMER_STATUS_RENEWAL_LOADED; + } else { + clock_values->timer_status = new_status; + } +} + +/* Private function, but accessed from odk.c so cannot be static. This checks to + * see if a renewal message should restart the playback timer and sets the value + * appropriately. */ +OEMCryptoResult ODK_ComputeRenewalDuration(const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + uint64_t system_time_seconds, + uint64_t new_renewal_duration, + uint64_t* timer_value) { + if (timer_limits == NULL || clock_values == NULL) { + return OEMCrypto_ERROR_INVALID_CONTEXT; /* should not happen. */ + } + /* If this is before the license was signed, something is odd. Return an + * error. */ + if (system_time_seconds < clock_values->time_of_license_signed) { + return OEMCrypto_ERROR_UNKNOWN_FAILURE; + } + + const OEMCryptoResult license_status = + ODK_LicenseActive(timer_limits, clock_values); + /* If the license is not active, then we cannot renew the license. */ + if (license_status != OEMCrypto_SUCCESS) { + return license_status; + } + + /* We start with the new renewal duration as the new timer limit. */ + uint64_t new_timer_value = new_renewal_duration; + + /* Then we factor in the rental window restrictions. This might decrease + * new_timer_value. */ + const OEMCryptoResult rental_status = ODK_CheckRentalWindow( + timer_limits, clock_values, system_time_seconds, &new_timer_value); + + /* If the rental status forbids playback, then we're done. */ + if ((rental_status != ODK_DISABLE_TIMER) && + (rental_status != ODK_SET_TIMER)) { + return rental_status; + } + + /* If playback has already started and it has hard enforcement, then check + * total playback window. */ + if (clock_values->time_of_first_decrypt > 0 && + !timer_limits->soft_enforce_playback_duration) { + /* This might decrease new_timer_value. */ + const OEMCryptoResult playback_status = ODK_CheckPlaybackWindow( + timer_limits, clock_values, system_time_seconds, &new_timer_value); + /* If the timer limits forbid playback in the playback window, then we're + * done. */ + if ((playback_status != ODK_DISABLE_TIMER) && + (playback_status != ODK_SET_TIMER)) { + return playback_status; + } + } + + /* If new_timer_value is infinite (represented by 0), then there are no + * limits, so we can return now. */ + if (new_timer_value == 0) { + clock_values->time_when_timer_expires = 0; + ODK_UpdateTimerStatusForRenewal(clock_values, + ODK_CLOCK_TIMER_STATUS_UNLIMITED); + return ODK_DISABLE_TIMER; + } + + /* If the caller gave us a pointer to store the new timer value. Fill it. */ + if (timer_value != NULL) { + *timer_value = new_timer_value; + } + if (odk_add_overflow_u64(system_time_seconds, new_timer_value, + &clock_values->time_when_timer_expires)) { + return OEMCrypto_ERROR_UNKNOWN_FAILURE; + } + ODK_UpdateTimerStatusForRenewal(clock_values, ODK_CLOCK_TIMER_STATUS_ACTIVE); + return ODK_SET_TIMER; +} + +/************************************************************************/ +/************************************************************************/ +/* Public functions, declared in odk.h. */ + +/* This is called when OEMCrypto opens a new session. */ +OEMCryptoResult ODK_InitializeSessionValues(ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + ODK_NonceValues* nonce_values, + uint32_t api_major_version, + uint32_t session_id) { + if (timer_limits == NULL || clock_values == NULL || nonce_values == NULL) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + /* Check that the API version passed in from OEMCrypto matches the version of + * this ODK library. */ + if (api_major_version != ODK_MAJOR_VERSION) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + timer_limits->soft_enforce_rental_duration = false; + timer_limits->soft_enforce_playback_duration = false; + timer_limits->earliest_playback_start_seconds = 0; + timer_limits->rental_duration_seconds = 0; + timer_limits->total_playback_duration_seconds = 0; + timer_limits->initial_renewal_duration_seconds = 0; + + ODK_InitializeClockValues(clock_values, 0); + + nonce_values->api_major_version = ODK_MAJOR_VERSION; + nonce_values->api_minor_version = ODK_MINOR_VERSION; + nonce_values->nonce = 0; + nonce_values->session_id = session_id; + + return OEMCrypto_SUCCESS; +} + +/* This is called when OEMCrypto generates a new nonce in + * OEMCrypto_GenerateNonce. */ +OEMCryptoResult ODK_SetNonceValues(ODK_NonceValues* nonce_values, + uint32_t nonce) { + if (nonce_values == NULL) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + /* Setting the nonce should only happen once per session. */ + if (nonce_values->nonce != 0) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + nonce_values->nonce = nonce; + return OEMCrypto_SUCCESS; +} + +/* This is called when OEMCrypto signs a license. */ +OEMCryptoResult ODK_InitializeClockValues(ODK_ClockValues* clock_values, + uint64_t system_time_seconds) { + if (clock_values == NULL) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + clock_values->time_of_license_signed = system_time_seconds; + clock_values->time_of_first_decrypt = 0; + clock_values->time_of_last_decrypt = 0; + clock_values->time_when_timer_expires = 0; + clock_values->timer_status = ODK_CLOCK_TIMER_STATUS_LICENSE_NOT_LOADED; + clock_values->status = kUnused; + return OEMCrypto_SUCCESS; +} + +/* This is called when OEMCrypto reloads a usage entry. */ +OEMCryptoResult ODK_ReloadClockValues(ODK_ClockValues* clock_values, + uint64_t time_of_license_signed, + uint64_t time_of_first_decrypt, + uint64_t time_of_last_decrypt, + enum OEMCrypto_Usage_Entry_Status status, + uint64_t system_time_seconds) { + if (clock_values == NULL) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + clock_values->time_of_license_signed = time_of_license_signed; + clock_values->time_of_first_decrypt = time_of_first_decrypt; + clock_values->time_of_last_decrypt = time_of_last_decrypt; + clock_values->time_when_timer_expires = 0; + clock_values->timer_status = ODK_CLOCK_TIMER_STATUS_LICENSE_NOT_LOADED; + clock_values->status = status; + return OEMCrypto_SUCCESS; +} + +/* This is called on the first playback for a session. */ +OEMCryptoResult ODK_AttemptFirstPlayback(uint64_t system_time_seconds, + const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + uint64_t* timer_value) { + if (clock_values == NULL || timer_limits == NULL) { + return OEMCrypto_ERROR_UNKNOWN_FAILURE; + } + /* All times are relative to when the license was signed. */ + uint64_t rental_time = 0; + if (odk_sub_overflow_u64(system_time_seconds, + clock_values->time_of_license_signed, + &rental_time)) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + if (rental_time < timer_limits->earliest_playback_start_seconds) { + clock_values->timer_status = ODK_TIMER_EXPIRED; + return ODK_TIMER_EXPIRED; + } + /* If the license is inactive or not loaded, then playback is not allowed. */ + OEMCryptoResult status = ODK_LicenseActive(timer_limits, clock_values); + if (status != OEMCrypto_SUCCESS) { + return status; + } + + /* We start with the initial renewal duration as the timer limit. */ + uint64_t new_timer_value = timer_limits->initial_renewal_duration_seconds; + /* However, if a renewal was loaded before this first playback, use the + * previously computed limit. */ + if (clock_values->timer_status == ODK_CLOCK_TIMER_STATUS_RENEWAL_LOADED) { + if (clock_values->time_when_timer_expires <= system_time_seconds) { + return ODK_TIMER_EXPIRED; + } + if (odk_sub_overflow_u64(clock_values->time_when_timer_expires, + system_time_seconds, &new_timer_value)) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + } + + /* Then we factor in the rental window restrictions. This might decrease + * new_timer_value. */ + status = ODK_CheckRentalWindow(timer_limits, clock_values, + system_time_seconds, &new_timer_value); + if ((status != ODK_DISABLE_TIMER) && (status != ODK_SET_TIMER)) { + return status; + } + + /* If playback has not already started, then this is the first playback. */ + if (clock_values->time_of_first_decrypt == 0) { + clock_values->time_of_first_decrypt = system_time_seconds; + clock_values->status = kActive; + } + + /* Similar to the rental window, we check the playback window + * restrictions. This might decrease new_timer_value. */ + status = ODK_CheckPlaybackWindow(timer_limits, clock_values, + system_time_seconds, &new_timer_value); + if ((status != ODK_DISABLE_TIMER) && (status != ODK_SET_TIMER)) { + return status; + } + + /* We know we are allowed to decrypt. The rest computes the timer duration. */ + clock_values->time_of_last_decrypt = system_time_seconds; + + /* If new_timer_value is infinite (represented by 0), then there are no + * limits, so we can return now. */ + if (new_timer_value == 0) { + clock_values->time_when_timer_expires = 0; + clock_values->timer_status = ODK_CLOCK_TIMER_STATUS_UNLIMITED; + return ODK_DISABLE_TIMER; + } + /* If the caller gave us a pointer to store the new timer value. Fill it. */ + if (timer_value) { + *timer_value = new_timer_value; + } + if (odk_add_overflow_u64(system_time_seconds, new_timer_value, + &clock_values->time_when_timer_expires)) { + return OEMCrypto_ERROR_UNKNOWN_FAILURE; + } + clock_values->timer_status = ODK_CLOCK_TIMER_STATUS_ACTIVE; + return ODK_SET_TIMER; +} + +/* This is called regularly during playback if OEMCrypto does not implement its + * own timer. */ +OEMCryptoResult ODK_UpdateLastPlaybackTime(uint64_t system_time_seconds, + const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values) { + OEMCryptoResult status = ODK_LicenseActive(timer_limits, clock_values); + if (status != OEMCrypto_SUCCESS) { + return status; + } + switch (clock_values->timer_status) { + case ODK_CLOCK_TIMER_STATUS_UNLIMITED: + break; + case ODK_CLOCK_TIMER_STATUS_ACTIVE: + /* Note: we allow playback at the time when the timer expires, but not + * after. This is not important for business cases, but it makes it + * easier to write tests. */ + if (clock_values->time_when_timer_expires > 0 && + system_time_seconds > clock_values->time_when_timer_expires) { + clock_values->timer_status = ODK_CLOCK_TIMER_STATUS_EXPIRED; + return ODK_TIMER_EXPIRED; + } + break; + default: /* Expired, error state, or never started. */ + return ODK_TIMER_EXPIRED; + } + clock_values->time_of_last_decrypt = system_time_seconds; + return OEMCrypto_SUCCESS; +} + +/* This is called from OEMCrypto_DeactivateUsageEntry. */ +OEMCryptoResult ODK_DeactivateUsageEntry(ODK_ClockValues* clock_values) { + if (clock_values == NULL) { + return OEMCrypto_ERROR_UNKNOWN_FAILURE; + } + if (clock_values->status == kUnused) { + clock_values->status = kInactiveUnused; + } else if (clock_values->status == kActive) { + clock_values->status = kInactiveUsed; + } + clock_values->timer_status = ODK_CLOCK_TIMER_STATUS_LICENSE_INACTIVE; + return OEMCrypto_SUCCESS; +} + +/* This is called when OEMCrypto loads a legacy v15 license, from + * OEMCrypto_LoadKeys. */ +OEMCryptoResult ODK_InitializeV15Values(ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + ODK_NonceValues* nonce_values, + uint32_t key_duration, + uint64_t system_time_seconds) { + if (timer_limits == NULL || clock_values == NULL || nonce_values == NULL) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + timer_limits->soft_enforce_playback_duration = false; + timer_limits->soft_enforce_rental_duration = false; + timer_limits->earliest_playback_start_seconds = 0; + timer_limits->rental_duration_seconds = 0; + timer_limits->total_playback_duration_seconds = 0; + timer_limits->initial_renewal_duration_seconds = key_duration; + + nonce_values->api_major_version = 15; + nonce_values->api_minor_version = 0; + if (key_duration > 0) { + clock_values->time_when_timer_expires = system_time_seconds + key_duration; + } else { + clock_values->time_when_timer_expires = 0; + } + clock_values->timer_status = ODK_CLOCK_TIMER_STATUS_LICENSE_LOADED; + return OEMCrypto_SUCCESS; +} + +/* This is called when OEMCrypto loads a legacy license renewal in + * OEMCrypto_RefreshKeys. */ +OEMCryptoResult ODK_RefreshV15Values(const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + const ODK_NonceValues* nonce_values, + uint64_t system_time_seconds, + uint32_t new_key_duration, + uint64_t* timer_value) { + if (timer_limits == NULL || clock_values == NULL || nonce_values == NULL) { + return OEMCrypto_ERROR_INVALID_CONTEXT; + } + if (nonce_values->api_major_version != 15) { + return OEMCrypto_ERROR_INVALID_NONCE; + } + if (clock_values->status > kActive) { + clock_values->timer_status = ODK_CLOCK_TIMER_STATUS_LICENSE_INACTIVE; + return ODK_TIMER_EXPIRED; + } + return ODK_ComputeRenewalDuration(timer_limits, clock_values, + system_time_seconds, new_key_duration, + timer_value); +} diff --git a/oemcrypto/odk/src/odk_util.c b/oemcrypto/odk/src/odk_util.c new file mode 100644 index 0000000..ff85a9c --- /dev/null +++ b/oemcrypto/odk/src/odk_util.c @@ -0,0 +1,34 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#include "odk_util.h" + +int crypto_memcmp(const void* in_a, const void* in_b, size_t len) { + if (len == 0) { + return 0; + } + + /* Only valid pointers are allowed. */ + if (in_a == NULL || in_b == NULL) { + return -1; + } + + const uint8_t* a = (const uint8_t*)in_a; + const uint8_t* b = (const uint8_t*)in_b; + uint8_t x = 0; + + for (size_t i = 0; i < len; i++) { + x |= a[i] ^ b[i]; + } + return x; +} + +bool ODK_NonceValuesEqual(const ODK_NonceValues* a, const ODK_NonceValues* b) { + if (a == NULL || b == NULL) { + return (a == b); + } + return (a->api_major_version == b->api_major_version && + a->api_minor_version == b->api_minor_version && + a->nonce == b->nonce && a->session_id == b->session_id); +} diff --git a/oemcrypto/odk/src/odk_util.h b/oemcrypto/odk/src/odk_util.h new file mode 100644 index 0000000..7de668e --- /dev/null +++ b/oemcrypto/odk/src/odk_util.h @@ -0,0 +1,28 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#ifndef WIDEVINE_ODK_SRC_ODK_UTIL_H_ +#define WIDEVINE_ODK_SRC_ODK_UTIL_H_ + +#include +#include + +#include "odk_structs.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* crypto_memcmp returns zero iff the |len| bytes at |a| and |b| are equal. It + * takes an amount of time dependent on |len|, but independent of the contents + * of |a| and |b|. Unlike memcmp, it cannot be used to order elements as the + * return value when a != b is undefined, other than being non-zero. */ +int crypto_memcmp(const void* a, const void* b, size_t len); + +bool ODK_NonceValuesEqual(const ODK_NonceValues* a, const ODK_NonceValues* b); + +#ifdef __cplusplus +} /* extern "C" */ +#endif +#endif /* WIDEVINE_ODK_SRC_ODK_UTIL_H_ */ diff --git a/oemcrypto/odk/src/serialization_base.c b/oemcrypto/odk/src/serialization_base.c new file mode 100644 index 0000000..4681b54 --- /dev/null +++ b/oemcrypto/odk/src/serialization_base.c @@ -0,0 +1,259 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#include "serialization_base.h" + +#include +#include +#include + +#include "OEMCryptoCENCCommon.h" +#include "odk_assert.h" +#include "odk_overflow.h" + +struct _Message { + uint8_t* base; + size_t capacity; + size_t size; /* bytes written */ + size_t read_offset; /* bytes read */ + MessageStatus status; +}; + +/* TODO(b/150776214): this can be removed once AllocateMessage gets cleaned up + */ +/* + * odk_static_assert(SIZE_OF_MESSAGE_STRUCT >= sizeof(struct _Message), + * "SIZE_OF_MESSAGE_STRUCT too small"); + */ + +bool ValidMessage(Message* message) { + if (message == NULL) { + return false; + } + if (message->status != MESSAGE_STATUS_OK) { + return false; + } + if (message->base == NULL) { + message->status = MESSAGE_STATUS_NULL_POINTER_ERROR; + return false; + } + if (message->size > message->capacity || + message->read_offset > message->size) { + message->status = MESSAGE_STATUS_OVERFLOW_ERROR; + return false; + } + return true; +} + +static void PackBytes(Message* message, const uint8_t* ptr, size_t count) { + if (count <= message->capacity - message->size) { + memcpy((void*)(message->base + message->size), (void*)ptr, count); + message->size += count; + } else { + message->status = MESSAGE_STATUS_OVERFLOW_ERROR; + } +} + +void Pack_enum(Message* message, int value) { + uint32_t v32 = value; + Pack_uint32_t(message, &v32); +} + +void Pack_bool(Message* message, const bool* value) { + if (!ValidMessage(message)) return; + uint8_t data[4] = {0}; + data[3] = *value ? 1 : 0; + PackBytes(message, data, sizeof(data)); +} + +void Pack_uint16_t(Message* message, const uint16_t* value) { + if (!ValidMessage(message)) return; + uint8_t data[2] = {0}; + data[0] = *value >> 8; + data[1] = *value >> 0; + PackBytes(message, data, sizeof(data)); +} + +void Pack_uint32_t(Message* message, const uint32_t* value) { + if (!ValidMessage(message)) return; + uint8_t data[4] = {0}; + data[0] = *value >> 24; + data[1] = *value >> 16; + data[2] = *value >> 8; + data[3] = *value >> 0; + PackBytes(message, data, sizeof(data)); +} + +void Pack_uint64_t(Message* message, const uint64_t* value) { + if (!ValidMessage(message)) return; + uint32_t hi = *value >> 32; + uint32_t lo = *value; + Pack_uint32_t(message, &hi); + Pack_uint32_t(message, &lo); +} + +void PackArray(Message* message, const uint8_t* base, size_t size) { + if (!ValidMessage(message)) return; + PackBytes(message, base, size); +} + +void Pack_OEMCrypto_Substring(Message* msg, const OEMCrypto_Substring* obj) { + uint32_t offset = obj->offset; + uint32_t length = obj->length; + Pack_uint32_t(msg, &offset); + Pack_uint32_t(msg, &length); +} + +static void UnpackBytes(Message* message, uint8_t* ptr, size_t count) { + if (count <= message->size - message->read_offset) { + memcpy((void*)ptr, (void*)(message->base + message->read_offset), count); + message->read_offset += count; + } else { + message->status = MESSAGE_STATUS_UNDERFLOW_ERROR; + } +} + +int Unpack_enum(Message* message) { + uint32_t v32; + Unpack_uint32_t(message, &v32); + return v32; +} + +void Unpack_bool(Message* message, bool* value) { + if (!ValidMessage(message)) return; + uint8_t data[4] = {0}; + UnpackBytes(message, data, sizeof(data)); + *value = (0 != data[3]); +} + +void Unpack_uint16_t(Message* message, uint16_t* value) { + if (!ValidMessage(message)) return; + uint8_t data[2] = {0}; + UnpackBytes(message, data, sizeof(data)); + *value = data[0]; + *value = *value << 8 | data[1]; +} + +void Unpack_uint32_t(Message* message, uint32_t* value) { + if (!ValidMessage(message)) return; + uint8_t data[4] = {0}; + UnpackBytes(message, data, sizeof(data)); + *value = data[0]; + *value = *value << 8 | data[1]; + *value = *value << 8 | data[2]; + *value = *value << 8 | data[3]; +} + +void Unpack_uint64_t(Message* message, uint64_t* value) { + if (!ValidMessage(message)) return; + uint32_t hi = 0; + uint32_t lo = 0; + Unpack_uint32_t(message, &hi); + Unpack_uint32_t(message, &lo); + *value = hi; + *value = *value << 32 | lo; +} + +void Unpack_OEMCrypto_Substring(Message* msg, OEMCrypto_Substring* obj) { + uint32_t offset = 0, length = 0; + Unpack_uint32_t(msg, &offset); + Unpack_uint32_t(msg, &length); + if (!ValidMessage(msg)) return; + /* Each substring should be contained within the message body, which is in the + * total message, just after the core message. The offset of a substring is + * relative to the message body. So we need to verify: + * 0 < offset and offset + length < message->capacity - message->size + * or offset + length + message->size < message->capacity + */ + size_t substring_end = 0; /* = offset + length; */ + size_t end = 0; /* = substring_end + message->size; */ + if (odk_add_overflow_ux(offset, length, &substring_end) || + odk_add_overflow_ux(substring_end, msg->size, &end) || + end > msg->capacity) { + msg->status = MESSAGE_STATUS_OVERFLOW_ERROR; + return; + } + obj->offset = offset; + obj->length = length; +} + +/* copy out */ +void UnpackArray(Message* message, uint8_t* address, size_t size) { + if (!ValidMessage(message)) return; + UnpackBytes(message, address, size); +} + +/* + * The message structure, which is separate from the buffer, + * is initialized to reference the buffer + */ +void InitMessage(Message* message, uint8_t* buffer, size_t capacity) { + if (message == NULL) return; + memset(message, 0, sizeof(Message)); + message->base = buffer; + message->capacity = capacity; + message->size = 0; + message->read_offset = 0; + message->status = MESSAGE_STATUS_OK; +} + +/* + * The message structure is in the first sizeof(Memory) bytes + * of the buffer + */ +Message* CreateMessage(uint8_t* buffer, size_t buffer_size) { + if (buffer == NULL || buffer_size < sizeof(Message)) return NULL; + Message* message = (Message*)buffer; + message->base = buffer + sizeof(Message); + message->capacity = buffer_size - sizeof(Message); + message->size = 0; + message->read_offset = 0; + message->status = MESSAGE_STATUS_OK; + return message; +} + +/* + * Set the message to an empty state + */ +void ResetMessage(Message* message) { + message->size = 0; + message->read_offset = 0; + message->status = MESSAGE_STATUS_OK; +} + +uint8_t* GetBase(Message* message) { + if (message == NULL) return NULL; + return message->base; +} + +size_t GetCapacity(Message* message) { + if (message == NULL) return 0; + return message->capacity; +} + +size_t GetSize(Message* message) { + if (message == NULL) return 0; + return message->size; +} + +void SetSize(Message* message, size_t size) { + if (message == NULL) return; + if (size > message->capacity) + message->status = MESSAGE_STATUS_OVERFLOW_ERROR; + else + message->size = size; +} + +MessageStatus GetStatus(Message* message) { return message->status; } + +void SetStatus(Message* message, MessageStatus status) { + message->status = status; +} + +size_t GetOffset(Message* message) { + if (message == NULL) return 0; + return message->read_offset; +} + +size_t SizeOfMessageStruct() { return sizeof(Message); } diff --git a/oemcrypto/odk/src/serialization_base.h b/oemcrypto/odk/src/serialization_base.h new file mode 100644 index 0000000..c99f4d1 --- /dev/null +++ b/oemcrypto/odk/src/serialization_base.h @@ -0,0 +1,96 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#ifndef WIDEVINE_ODK_SRC_SERIALIZATION_BASE_H_ +#define WIDEVINE_ODK_SRC_SERIALIZATION_BASE_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "OEMCryptoCENCCommon.h" + +#define SIZE_OF_MESSAGE_STRUCT 64 + +/* + * Description: + * Point |msg| to stack-array |blk|. + * |blk| is guaranteed large enough to hold a |Message| struct. + * |blk| cannot be used in the same scope as a variable name. + * |msg| points to valid memory in the same scope |AllocateMessage| is used. + * Parameters: + * msg: pointer to pointer to |Message| struct + * blk: variable name for stack-array + */ +#define AllocateMessage(msg, blk) \ + uint8_t blk[SIZE_OF_MESSAGE_STRUCT]; \ + *(msg) = (Message*)(blk) + +typedef struct _Message Message; + +typedef enum { + MESSAGE_STATUS_OK, + MESSAGE_STATUS_UNKNOWN_ERROR, + MESSAGE_STATUS_OVERFLOW_ERROR, + MESSAGE_STATUS_UNDERFLOW_ERROR, + MESSAGE_STATUS_PARSE_ERROR, + MESSAGE_STATUS_NULL_POINTER_ERROR, + MESSAGE_STATUS_API_VALUE_ERROR +} MessageStatus; + +bool ValidMessage(Message* message); + +void Pack_enum(Message* message, int value); +void Pack_bool(Message* message, const bool* value); +void Pack_uint16_t(Message* message, const uint16_t* value); +void Pack_uint32_t(Message* message, const uint32_t* value); +void Pack_uint64_t(Message* message, const uint64_t* value); +void PackArray(Message* message, const uint8_t* base, size_t size); +void Pack_OEMCrypto_Substring(Message* msg, const OEMCrypto_Substring* obj); + +int Unpack_enum(Message* message); +void Unpack_bool(Message* message, bool* value); +void Unpack_uint16_t(Message* message, uint16_t* value); +void Unpack_uint32_t(Message* message, uint32_t* value); +void Unpack_uint64_t(Message* message, uint64_t* value); +void UnpackArray(Message* message, uint8_t* address, + size_t size); /* copy out */ +void Unpack_OEMCrypto_Substring(Message* msg, OEMCrypto_Substring* obj); + +/* + * Create a message from a buffer. The message structure consumes the first + * sizeof(Message) bytes of the buffer. The caller is responsible for ensuring + * that the buffer remains allocated for the lifetime of the message. + */ +Message* CreateMessage(uint8_t* buffer, size_t buffer_size); + +/* + * Initialize a message structure to reference a separate buffer. The caller + * is responsible for ensuring that the buffer remains allocated for the + * lifetime of the message. + */ +void InitMessage(Message* message, uint8_t* buffer, size_t capacity); + +/* + * Reset an existing the message to an empty state + */ +void ResetMessage(Message* message); +uint8_t* GetBase(Message* message); +size_t GetCapacity(Message* message); +size_t GetSize(Message* message); +void SetSize(Message* message, size_t size); +MessageStatus GetStatus(Message* message); +void SetStatus(Message* message, MessageStatus status); +size_t GetOffset(Message* message); + +size_t SizeOfMessageStruct(); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* WIDEVINE_ODK_SRC_SERIALIZATION_BASE_H_ */ diff --git a/oemcrypto/odk/test/fuzzing/Android.bp b/oemcrypto/odk/test/fuzzing/Android.bp new file mode 100644 index 0000000..f0093f4 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/Android.bp @@ -0,0 +1,168 @@ +// Copyright 2020 Google LLC. All rights reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +cc_defaults { + name: "odk_fuzz_library_defaults", + srcs: [ + "odk_fuzz_helper.cpp", + ], + include_dirs: [ + "vendor/widevine/libwvdrmengine/oemcrypto/odk/test", + "vendor/widevine/libwvdrmengine/oemcrypto/odk/include", + "vendor/widevine/libwvdrmengine/oemcrypto/odk/src", + ], +} + +cc_fuzz { + name: "odk_license_request_fuzz", + srcs: [ + "odk_license_request_fuzz.cpp", + ], + fuzz_config: { + componentid: 611718, + }, + corpus: ["corpus/little_endian_64bit/license_request_corpus/*"], + static_libs: [ + "libwv_kdo", + "libwv_odk", + ], + defaults: ["odk_fuzz_library_defaults"], + proprietary: true, +} + +cc_fuzz { + name: "odk_renewal_request_fuzz", + srcs: [ + "odk_renewal_request_fuzz.cpp", + ], + fuzz_config: { + componentid: 611718, + }, + corpus: ["corpus/little_endian_64bit/renewal_request_corpus/*"], + static_libs: [ + "libwv_kdo", + "libwv_odk", + ], + defaults: ["odk_fuzz_library_defaults"], + proprietary: true, +} + +cc_fuzz { + name: "odk_provisioning_request_fuzz", + srcs: [ + "odk_provisioning_request_fuzz.cpp", + ], + fuzz_config: { + componentid: 611718, + }, + corpus: ["corpus/little_endian_64bit/provisioning_request_corpus/*"], + static_libs: [ + "libwv_kdo", + "libwv_odk", + ], + defaults: ["odk_fuzz_library_defaults"], + proprietary: true, +} + +cc_fuzz { + name: "odk_license_response_fuzz", + srcs: [ + "odk_license_response_fuzz.cpp", + ], + fuzz_config: { + componentid: 611718, + }, + corpus: ["corpus/little_endian_64bit/license_response_corpus/*"], + static_libs: [ + "libwv_kdo", + "libwv_odk", + ], + defaults: ["odk_fuzz_library_defaults"], + proprietary: true, +} + +cc_fuzz { + name: "odk_renewal_response_fuzz", + srcs: [ + "odk_renewal_response_fuzz.cpp", + ], + fuzz_config: { + componentid: 611718, + }, + corpus: ["corpus/little_endian_64bit/renewal_response_corpus/*"], + static_libs: [ + "libwv_kdo", + "libwv_odk", + ], + defaults: ["odk_fuzz_library_defaults"], + proprietary: true, +} + +cc_fuzz { + name: "odk_provisioning_response_fuzz", + srcs: [ + "odk_provisioning_response_fuzz.cpp", + ], + fuzz_config: { + componentid: 611718, + }, + corpus: ["corpus/little_endian_64bit/provisioning_response_corpus/*"], + static_libs: [ + "libwv_kdo", + "libwv_odk", + ], + defaults: ["odk_fuzz_library_defaults"], + proprietary: true, +} + +cc_fuzz { + name: "odk_license_response_fuzz_with_mutator", + srcs: [ + "odk_license_response_fuzz_with_mutator.cpp", + ], + fuzz_config: { + componentid: 611718, + }, + corpus: ["corpus/little_endian_64bit/license_response_corpus/*"], + static_libs: [ + "libwv_kdo", + "libwv_odk", + ], + defaults: ["odk_fuzz_library_defaults"], + proprietary: true, +} + +cc_fuzz { + name: "odk_renewal_response_fuzz_with_mutator", + srcs: [ + "odk_renewal_response_fuzz_with_mutator.cpp", + ], + fuzz_config: { + componentid: 611718, + }, + corpus: ["corpus/little_endian_64bit/renewal_response_corpus/*"], + static_libs: [ + "libwv_kdo", + "libwv_odk", + ], + defaults: ["odk_fuzz_library_defaults"], + proprietary: true, +} + +cc_fuzz { + name: "odk_provisioning_response_fuzz_with_mutator", + srcs: [ + "odk_provisioning_response_fuzz_with_mutator.cpp", + ], + fuzz_config: { + componentid: 611718, + }, + corpus: ["corpus/little_endian_64bit/provisioning_response_corpus/*"], + static_libs: [ + "libwv_kdo", + "libwv_odk", + ], + defaults: ["odk_fuzz_library_defaults"], + proprietary: true, +} \ No newline at end of file diff --git a/oemcrypto/odk/test/fuzzing/README.md b/oemcrypto/odk/test/fuzzing/README.md new file mode 100644 index 0000000..eb7da4f --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/README.md @@ -0,0 +1,19 @@ +# ODK Fuzzing + +## Objective + +* Run fuzzing on ODK and KDO serialize and deserialize APIs using google + supported fuzzer engines to find security vulnerabilities. Any issues found + by clusterfuzz will be reported to + [odk fuzz buganizer](https://b.corp.google.com/issues?q=componentid:425099%20status:open%20reporter:cluster-fuzz-googleplex@google.com). + +## Run fuzz target on local machine + +* In order to run fuzz target locally and see code coverage, save binary input + to be tested against fuzz target into a temporary corpus directory and + execute following commands + + ```shell + $ blaze build --config=asan-fuzzer //your:target + $ blaze-bin/your/target FULL_CORPUS_DIR + ``` diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_request_corpus/602c63d2f3d13ca3206cdf204cde24e7d8f4266c b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_request_corpus/602c63d2f3d13ca3206cdf204cde24e7d8f4266c new file mode 100644 index 0000000000000000000000000000000000000000..dc8cabcb1f324b0841ba2624c4e06bdf1d78c325 GIT binary patch literal 20 bcmZQzWMXDvWn<^yMC+6cPpi1x5hB literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_request_corpus/8cebdcc0161125a10e19c45f055051712873de25 b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_request_corpus/8cebdcc0161125a10e19c45f055051712873de25 new file mode 100644 index 0000000000000000000000000000000000000000..608e888d58863f67a23c07e34f765097056fea31 GIT binary patch literal 20 bcmZQzU|?imU=U$oVh~vSW4A2>1KTP96i5Sg literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/4e578d6c9628e832c099623b44f56d95aa37f94b b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/4e578d6c9628e832c099623b44f56d95aa37f94b new file mode 100644 index 0000000000000000000000000000000000000000..e319747664c3a072cb675e92f4c140a260b52d3e GIT binary patch literal 272 zcmZQ#5MTfS1r`PdMuyY9x_oQx7|%_vZN0{76TKI*#RUW4q{+YU|`?_v4NP+07yFkF~xw84*}4WBEJ9t literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/5b693511ef850e42c5ffded171794dbeb9460cc0 b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/5b693511ef850e42c5ffded171794dbeb9460cc0 new file mode 100644 index 0000000000000000000000000000000000000000..0cf77b5db8a598c0ccdd7237a2ad9668abc9d9ad GIT binary patch literal 282200 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 MxN`v%e^yS0HEaTf(O?rGGx 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%0Dcjl!*KS8ewM*< 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 b6B?vL%6cq8&Y^N8CA^O#>Oy2JB-j1}NHrbT literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/b6b865b095697164ad032c2f695ed828f5754749 b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/b6b865b095697164ad032c2f695ed828f5754749 new file mode 100644 index 0000000..225f87a --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/b6b865b095697164ad032c2f695ed828f5754749 @@ -0,0 +1 @@ +{"componentid":425099} diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/dba39b6cf6524e996397ddc1e08b928b5c92bb5d b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/license_response_corpus/dba39b6cf6524e996397ddc1e08b928b5c92bb5d new file mode 100644 index 0000000000000000000000000000000000000000..51f67eb6f45ff450ea3514757025dad75ad500d1 GIT binary patch literal 352 zcmZQ#5Mb~Pe)&9``_n9l%6I{+~@fMf(VX%sP# k9UuTQ*8xgPKq6fYJ$2x&lh?fYKA7^a&_^ z14@5@(2NF9ngdEZKxwGCj2Te=3MkzGrJ?3Cg3VR^mVGHhZK~n=nCS9&UbkJIMlP}4 QB4^hv`22Eu*y2c60K#}7$^ZZW literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/provisioning_request_corpus/53c26407b39c997143146a0dce8ff0ac11f565e1 b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/provisioning_request_corpus/53c26407b39c997143146a0dce8ff0ac11f565e1 new file mode 100644 index 0000000000000000000000000000000000000000..a2d65a82d61b8d3fa65e52c8ece687ad11b4b8fa GIT binary patch literal 88 zcmZQzU|?lnV2EH~Vi5Qh*1sLdjRf)~!!uJ-%QEv)LsE-N{PS`uy;Ccb@+%As41kiv F008%!4Ez89 literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/provisioning_request_corpus/fab3c99d604bab7b7bf5c54c5bd995fc98d4d96f b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/provisioning_request_corpus/fab3c99d604bab7b7bf5c54c5bd995fc98d4d96f new file mode 100644 index 0000000000000000000000000000000000000000..a4ded576fe810d7540ac36f2d1391e33d774e866 GIT binary patch literal 88 zcmZQzWMXDvWn<^yMC+6cQE@6%&_`l#-T_m6KOcR8m$^Ra4i{WB>!^MkXM= SrV7HtBrgbNY%FA8umJ!SD+jRv literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/provisioning_response_corpus/91e10d030fbdd3374e57a2720f09488f2b03ce69 b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/provisioning_response_corpus/91e10d030fbdd3374e57a2720f09488f2b03ce69 new file mode 100644 index 0000000000000000000000000000000000000000..0616a29b7b8d7c332d597f338c650b780aec2e0b GIT binary patch literal 204 zcmZQ#5McPXg1d4m8w0~CAdqB$fbh(e)UwRH)R5HT692rMO7GOlr2Gm40|SsEA^^}7 npot763``6HmE0@-0l8Cwd`X<mNjIgCII05V1g_y7O^ literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_request_corpus/97bf96be666434bfa93dbfb36b81baeefed14170 b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_request_corpus/97bf96be666434bfa93dbfb36b81baeefed14170 new file mode 100644 index 0000000000000000000000000000000000000000..4e6f216d56f5e652b0ee27cb42ee96cee2019eb8 GIT binary patch literal 76 zcmeZ&HYJV$2$G@n2`HTk5<~*5KsFAt@yd E0P{fz=l}o! literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_request_corpus/a7b0e7dca597331d7f051204096c9d01ba6d468e b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_request_corpus/a7b0e7dca597331d7f051204096c9d01ba6d468e new file mode 100644 index 0000000000000000000000000000000000000000..748c29cff8e661c14981c5078b2cb4a1b7170806 GIT binary patch literal 76 wcmeZ&HYJV$2$G@nAt)UW5<~*5KsFCY-aq8_W?*1n4OJEo01amf$^ZZW literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_response_corpus/38df40a320f60e955006aaa294b74d45a316e50f b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_response_corpus/38df40a320f60e955006aaa294b74d45a316e50f new file mode 100644 index 0000000000000000000000000000000000000000..4abb16a8b5bbb92fb8d3df63dd0eff3f8f8de061 GIT binary patch literal 148 zcmZQ#5MW5DXbe?gXJA+b1e2dmiDMuEz|^DD=ti&t1sEZMEI>+yfr&vNw6P+Efq`8C HB8(sb|K<=? literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_response_corpus/9962997b5ea87005276319cbfff67884846485cf b/oemcrypto/odk/test/fuzzing/corpus/little_endian_64bit/renewal_response_corpus/9962997b5ea87005276319cbfff67884846485cf new file mode 100644 index 0000000000000000000000000000000000000000..892a4edba65bf1e709cc7c507a1ac714561651fc GIT binary patch literal 148 zcmZQ#5MW5!JE?a$3j@O{ASix1C5{0I7#Ti(ss}NkfC`bq4aEQe literal 0 HcmV?d00001 diff --git a/oemcrypto/odk/test/fuzzing/corpus_generator/Android.bp b/oemcrypto/odk/test/fuzzing/corpus_generator/Android.bp new file mode 100644 index 0000000..e993971 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/corpus_generator/Android.bp @@ -0,0 +1,27 @@ +// Copyright 2020 Google LLC. All rights reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +// ---------------------------------------------------------------- +// Builds odk_corpus_generator shared library, which can be used with +// LD_PRELOAD command to generate corpus by intercepting oemcrypto +// unit tests. +// ---------------------------------------------------------------- +// Builds libwv_odk.so, The ODK shared Library (libwv_odk) is used +// by the OEMCrypto unit tests to generate corpus for ODK fuzz scrips. +cc_library_shared { + name: "libwv_odk_corpus_generator", + include_dirs: [ + "vendor/widevine/libwvdrmengine/oemcrypto/include", + "vendor/widevine/libwvdrmengine/oemcrypto/odk/include", + "vendor/widevine/libwvdrmengine/oemcrypto/odk/test", + ], + host_ldlibs: ["-ldl"], + srcs: [ + "odk_corpus_generator.c", + "odk_corpus_generator_helper.c", + ], + proprietary: true, + + owner: "widevine", +} diff --git a/oemcrypto/odk/test/fuzzing/corpus_generator/README.md b/oemcrypto/odk/test/fuzzing/corpus_generator/README.md new file mode 100644 index 0000000..f6df374 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/corpus_generator/README.md @@ -0,0 +1,79 @@ +# Objective + +The Idea behind the corpus generator code is to intercept OEMCrypto unit test +calls to odk APIs using LD_PRELOAD and read the data into corpus files which can +be fed as corpus to fuzzer scripts. + +LD_PRELOAD command needs to be run from cdm repository while running oemcrypto +unit tests. + +## Get OEMCrypto and Build OEMCrypto unit tests: + +* Install Pre-requisites + + ```shell + $ sudo apt-get install gyp ninja-build + ``` + +* download cdm source code (including ODK & OEMCrypto unit tests): + + ```shell + $ git clone sso://widevine-internal/cdm + ``` + +* We need to run odk as a dynamic library in order to use LD_PRELOAD, apply + patch from go/wvgerrit/95090 to locally cloned repo which has changes to run + odk as dynamic library: + + ```shell + $ cd /path/to/cdm/repo + $ git fetch origin 209721cc901745999e08e35466e74f708321267e + $ git cherry-pick FETCH_HEAD + ``` + +* Build OEMCrypto unit tests: + + ```shell + $ cd /path/to/cdm/repo + $ export PATH_TO_CDM_DIR=.. + $ gyp --format=ninja --depth=$(pwd) oemcrypto/oemcrypto_unittests.gyp + $ ninja -C out/Default/ + ``` + +## Capture corpus for odk fuzzer by intercepting OEMCrypto unit tests: + +When we run LD_PRELOAD command odk_corpus_generator.so gets preloaded before +oemcrypto_unittests and odk_corpus_generator has functions to intercept calls to +ODK request and response APIs. Each call to odk API from oemcrypto_unittests +gets intercepted and input to ODK de serialize response APIs and output from ODK +serialize request APIs is captured in binary format and stored into corpus files + +In order to run LD_PRELOAD command, we need to compile corpus generator shared +library and need to preload that before OEMCrypto unit tests run + +* Compile shared library + + ```shell + $ cd /path/to/cdm/repo + $ gyp --format=ninja --depth=$(pwd) oemcrypto/odk/test/fuzzing/corpus_generator/odk_fuzz_corpus_generator.gyp + $ ninja -C out/Default/ + ``` + +* Preload the shared library before running OEMCrypto unit tests + + ```shell + $ cd oemcrypto/odk/test/fuzzing/corpus + $ mkdir license_request_corpus license_response_corpus renewal_request_corpus renewal_response_corpus provisioning_request_corpus provisioning_response_corpus + $ cd /path/to/cdm/repo + $ LD_PRELOAD=out/Default/lib/libodk_corpus_generator.so ./out/Default/oemcrypto_unittests + ``` + +LD_PRELOAD command runs oemcrypto_unittests with odk_corpus_generator as +interceptor. We should see unit tests being executed. The corpus files in binary +format will be captured into `oemcrypto/odk/test/fuzzing/corpus` path. These +files can be used as input corpus for ODK request and response fuzzer scripts. + +The generated corpus files can be minimized using go/testcorpus#minimize and +uploaded into google3 repository under following directory under respective +corpus types +`fuzzing/corpus` diff --git a/oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator.c b/oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator.c new file mode 100644 index 0000000..5b36d89 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator.c @@ -0,0 +1,160 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +/* We must define this macro to get RTLD_NEXT definition from */ +#define _GNU_SOURCE + +#include + +#include "fuzzing/corpus_generator/odk_corpus_generator_helper.h" +#include "fuzzing/odk_fuzz_structs.h" +#include "odk_structs.h" + +OEMCryptoResult ODK_PrepareCoreLicenseRequest( + uint8_t* message, size_t message_length, size_t* core_message_length, + const ODK_NonceValues* nonce_values) { + OEMCryptoResult (*original_function)(uint8_t*, size_t, size_t*, + const ODK_NonceValues*); + original_function = dlsym(RTLD_NEXT, "ODK_PrepareCoreLicenseRequest"); + OEMCryptoResult oem_crypto_result = (*original_function)( + message, message_length, core_message_length, nonce_values); + char* file_name = GetFileName("license_request_corpus"); + + /* License Request format expected by fuzzer - [Core License Request] */ + AppendToFile(file_name, (const char*)message, *core_message_length); + free(file_name); + return oem_crypto_result; +} + +OEMCryptoResult ODK_ParseLicense( + const uint8_t* message, size_t message_length, size_t core_message_length, + bool initial_license_load, bool usage_entry_present, + const uint8_t* request_hash, ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, ODK_NonceValues* nonce_values, + ODK_ParsedLicense* parsed_license) { + struct ODK_ParseLicense_Args parse_license_args; + parse_license_args.nonce_values = *nonce_values; + memcpy(parse_license_args.request_hash, request_hash, ODK_SHA256_HASH_SIZE); + parse_license_args.timer_limits = *timer_limits; + parse_license_args.clock_values = *clock_values; + parse_license_args.usage_entry_present = usage_entry_present; + parse_license_args.initial_license_load = initial_license_load; + OEMCryptoResult (*original_function)( + const uint8_t*, size_t, size_t, bool, bool, const uint8_t*, + ODK_TimerLimits*, ODK_ClockValues*, ODK_NonceValues*, ODK_ParsedLicense*); + original_function = dlsym(RTLD_NEXT, "ODK_ParseLicense"); + OEMCryptoResult oem_crypto_result = (*original_function)( + message, message_length, core_message_length, initial_license_load, + usage_entry_present, request_hash, timer_limits, clock_values, + nonce_values, parsed_license); + char* file_name = GetFileName("license_response_corpus"); + + /* License Response format expected by fuzzer - [ODK_ParseLicense_Args][Core + */ + /* License Response] */ + AppendToFile(file_name, (const char*)&parse_license_args, + sizeof(struct ODK_ParseLicense_Args)); + AppendToFile(file_name, (const char*)message, core_message_length); + free(file_name); + return oem_crypto_result; +} + +OEMCryptoResult ODK_PrepareCoreRenewalRequest(uint8_t* message, + size_t message_length, + size_t* core_message_size, + ODK_NonceValues* nonce_values, + ODK_ClockValues* clock_values, + uint64_t system_time_seconds) { + OEMCryptoResult (*original_function)( + uint8_t*, size_t, size_t*, ODK_NonceValues*, ODK_ClockValues*, uint64_t); + original_function = dlsym(RTLD_NEXT, "ODK_PrepareCoreRenewalRequest"); + OEMCryptoResult oem_crypto_result = + (*original_function)(message, message_length, core_message_size, + nonce_values, clock_values, system_time_seconds); + char* file_name = GetFileName("renewal_request_corpus"); + + /* License Request format expected by fuzzer - [ODK_ClockValues][Core */ + /* License Request] */ + AppendToFile(file_name, (const char*)clock_values, sizeof(ODK_ClockValues)); + AppendToFile(file_name, (const char*)message, *core_message_size); + free(file_name); + return oem_crypto_result; +} + +OEMCryptoResult ODK_ParseRenewal(const uint8_t* message, size_t message_length, + size_t core_message_length, + const ODK_NonceValues* nonce_values, + uint64_t system_time, + const ODK_TimerLimits* timer_limits, + ODK_ClockValues* clock_values, + uint64_t* timer_value) { + struct ODK_ParseRenewal_Args parse_renewal_args; + parse_renewal_args.nonce_values = *nonce_values; + parse_renewal_args.clock_values = *clock_values; + parse_renewal_args.timer_limits = *timer_limits; + parse_renewal_args.system_time = system_time; + OEMCryptoResult (*original_function)( + const uint8_t*, size_t, size_t, const ODK_NonceValues*, uint64_t, + const ODK_TimerLimits*, ODK_ClockValues*, uint64_t*); + original_function = dlsym(RTLD_NEXT, "ODK_ParseRenewal"); + OEMCryptoResult oem_crypto_result = (*original_function)( + message, message_length, core_message_length, nonce_values, system_time, + timer_limits, clock_values, timer_value); + char* file_name = GetFileName("renewal_response_corpus"); + + /* Renewal Response format expected by fuzzer - [ODK_ParseRenewal_Args][Core + */ + /* Renewal Response] */ + AppendToFile(file_name, (const char*)&parse_renewal_args, + sizeof(struct ODK_ParseRenewal_Args)); + AppendToFile(file_name, (const char*)message, core_message_length); + free(file_name); + return oem_crypto_result; +} + +OEMCryptoResult ODK_PrepareCoreProvisioningRequest( + uint8_t* message, size_t message_length, size_t* core_message_length, + const ODK_NonceValues* nonce_values, const uint8_t* device_id, + size_t device_id_length) { + OEMCryptoResult (*original_function)(uint8_t*, size_t, size_t*, + const ODK_NonceValues*, const uint8_t*, + size_t); + original_function = dlsym(RTLD_NEXT, "ODK_PrepareCoreProvisioningRequest"); + OEMCryptoResult oem_crypto_result = + (*original_function)(message, message_length, core_message_length, + nonce_values, device_id, device_id_length); + char* file_name = GetFileName("provisioning_request_corpus"); + + /* Provisioning Request format expected by fuzzer - [Core Provisioning */ + /* Request] */ + AppendToFile(file_name, (const char*)message, *core_message_length); + free(file_name); + return oem_crypto_result; +} + +OEMCryptoResult ODK_ParseProvisioning( + const uint8_t* message, size_t message_length, size_t core_message_length, + const ODK_NonceValues* nonce_values, const uint8_t* device_id, + size_t device_id_length, ODK_ParsedProvisioning* parsed_response) { + struct ODK_ParseProvisioning_Args parse_provisioning_args; + parse_provisioning_args.nonce_values = *nonce_values; + memcpy(parse_provisioning_args.device_id, device_id, device_id_length); + parse_provisioning_args.device_id_length = device_id_length; + OEMCryptoResult (*original_function)(const uint8_t*, size_t, size_t, + const ODK_NonceValues*, const uint8_t*, + size_t, ODK_ParsedProvisioning*); + original_function = dlsym(RTLD_NEXT, "ODK_ParseProvisioning"); + OEMCryptoResult oem_crypto_result = (*original_function)( + message, message_length, core_message_length, nonce_values, device_id, + device_id_length, parsed_response); + char* file_name = GetFileName("provisioning_response_corpus"); + + /* Provisioning Response format expected by fuzzer - */ + /* [ODK_ParseProvisioning_Args][Core Provisioning Response] */ + AppendToFile(file_name, (const char*)&parse_provisioning_args, + sizeof(struct ODK_ParseProvisioning_Args)); + AppendToFile(file_name, (const char*)message, core_message_length); + free(file_name); + return oem_crypto_result; +} diff --git a/oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator_helper.c b/oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator_helper.c new file mode 100644 index 0000000..fa35dca --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator_helper.c @@ -0,0 +1,22 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ +#include "fuzzing/corpus_generator/odk_corpus_generator_helper.h" + +void AppendToFile(const char* file_name, const char* message, + const size_t message_size) { + FILE* fptr; + if ((fptr = fopen(file_name, "ab")) == NULL) { + printf("Error! opening file %s", file_name); + return; + } + fwrite(message, message_size, 1, fptr); + fclose(fptr); +} + +char* GetFileName(const char* directory) { + char* file_name; + file_name = malloc(150); + sprintf(file_name, "%s%s/%d", PATH_TO_CORPUS, directory, rand()); + return file_name; +} diff --git a/oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator_helper.h b/oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator_helper.h new file mode 100644 index 0000000..2dc6d51 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/corpus_generator/odk_corpus_generator_helper.h @@ -0,0 +1,19 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ +#ifndef WIDEVINE_ODK_TEST_FUZZING_CORPUS_GENERATOR_ODK_CORPUS_GENERATOR_HELPER_H_ +#define WIDEVINE_ODK_TEST_FUZZING_CORPUS_GENERATOR_ODK_CORPUS_GENERATOR_HELPER_H_ + +#define PATH_TO_CORPUS "./oemcrypto/odk/test/fuzzing/corpus/" + +#include +#include +#include + +void AppendToFile(const char* file_name, const char* message, + const size_t message_size); + +char* GetFileName(const char* directory); + +#endif /* WIDEVINE_ODK_TEST_FUZZING_CORPUS_GENERATOR_ODK_CORPUS_GENERATOR_HELPER_H_ \ + */ diff --git a/oemcrypto/odk/test/fuzzing/corpus_generator/odk_fuzz_corpus_generator.gyp b/oemcrypto/odk/test/fuzzing/corpus_generator/odk_fuzz_corpus_generator.gyp new file mode 100644 index 0000000..8acf1ac --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/corpus_generator/odk_fuzz_corpus_generator.gyp @@ -0,0 +1,33 @@ +# Copyright 2020 Google LLC. All rights reserved. This file and proprietary +# source code may only be used and distributed under the Widevine Master License +# Agreement. + +# Reference Link explaining flags for LD_PRELOAD: https://catonmat.net/simple-ld-preload-tutorial-part-two +{ + 'targets': [ + { + 'target_name': 'odk_corpus_generator', + 'type': 'shared_library', + 'cflags_cc': [ + '-g3', + '-O0', + '-fno-omit-frame-pointer', + '-Wall', + ], + 'include_dirs': [ + '../../../include', + '../../../test', + '../corpus_generator', + ], + 'ldflags': [ + '-fPIC', + ], + 'libraries': [ + '-ldl', + ], + 'sources': [ + 'odk_corpus_generator.c', + ], + } + ] +} diff --git a/oemcrypto/odk/test/fuzzing/odk_fuzz.gyp b/oemcrypto/odk/test/fuzzing/odk_fuzz.gyp new file mode 100644 index 0000000..58f00b0 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_fuzz.gyp @@ -0,0 +1,40 @@ +# Copyright 2019 Google LLC. All rights reserved. This file and proprietary +# source code may only be used and distributed under the Widevine Master License +# Agreement. + +#TODO(b/151858867): Fix File paths +{ + 'targets': [ + { + 'target_name': 'odk_fuzz', + 'type': 'executable', + 'includes': [ + '../src/odk.gypi', + '../kdo/oec_util.gypi', + ], + 'include_dirs': [ + '../../include', + '../include', + '../src', + '../kdo/include', + ], + 'cflags_cc': [ + '-std=c++11', + '-g3', + '-O0', + '-fsanitize=fuzzer,address,undefined', + '-fno-omit-frame-pointer', + ], + 'ldflags': [ + '-fPIC', + '-fsanitize=fuzzer,address,undefined', + ], + 'sources': [ + 'odk_fuzz.cpp', + ], + 'dependencies': [ + '../../../cdm/cdm.gyp:license_protocol' + ], + } + ] +} diff --git a/oemcrypto/odk/test/fuzzing/odk_fuzz_helper.cpp b/oemcrypto/odk/test/fuzzing/odk_fuzz_helper.cpp new file mode 100644 index 0000000..3e6f2e0 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_fuzz_helper.cpp @@ -0,0 +1,159 @@ +// Copyright 2020 Google LLC. All rights reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. +#include "fuzzing/odk_fuzz_helper.h" + +#include "odk.h" + +namespace oemcrypto_core_message { + +bool convert_byte_to_valid_boolean(const bool* in) { + const char* buf = reinterpret_cast(in); + for (int i = 0; i < sizeof(bool); i++) { + if (buf[i]) { + return true; + } + } + return false; +} + +void ConvertDataToValidBools(ODK_ParsedLicense* t) { + // Convert boolean flags in parsed_license to valid bytes to + // avoid errors from msan + t->nonce_required = convert_byte_to_valid_boolean(&t->nonce_required); + t->timer_limits.soft_enforce_playback_duration = + convert_byte_to_valid_boolean( + &t->timer_limits.soft_enforce_playback_duration); + t->timer_limits.soft_enforce_rental_duration = convert_byte_to_valid_boolean( + &t->timer_limits.soft_enforce_rental_duration); +} + +void ConvertDataToValidBools(ODK_PreparedRenewalRequest* t) {} + +void ConvertDataToValidBools(ODK_ParsedProvisioning* t) {} + +OEMCryptoResult odk_serialize_LicenseRequest( + const void* in, uint8_t* out, size_t* size, + const ODK_LicenseRequest& core_license_request, + const ODK_NonceValues* nonce_values) { + return ODK_PrepareCoreLicenseRequest(out, SIZE_MAX, size, nonce_values); +} + +OEMCryptoResult odk_serialize_RenewalRequest( + const void* in, uint8_t* out, size_t* size, + const ODK_RenewalRequest& core_renewal, ODK_NonceValues* nonce_values) { + ODK_ClockValues clock{}; + memcpy(&clock, in, sizeof(ODK_ClockValues)); + uint64_t system_time_seconds = core_renewal.playback_time_seconds; + return ODK_PrepareCoreRenewalRequest(out, SIZE_MAX, size, nonce_values, + &clock, system_time_seconds); +} + +OEMCryptoResult odk_serialize_ProvisioningRequest( + const void* in, uint8_t* out, size_t* size, + const ODK_ProvisioningRequest& core_provisioning, + const ODK_NonceValues* nonce_values) { + const std::string& device_id = core_provisioning.device_id; + return ODK_PrepareCoreProvisioningRequest( + out, SIZE_MAX, size, nonce_values, + reinterpret_cast(device_id.data()), device_id.size()); +} + +OEMCryptoResult odk_deserialize_LicenseResponse(const uint8_t* message, + size_t core_message_length, + ODK_ParseLicense_Args* a, + ODK_NonceValues* nonce_values, + ODK_ParsedLicense* parsed_lic) { + return ODK_ParseLicense(message, SIZE_MAX, core_message_length, + static_cast(a->initial_license_load), + static_cast(a->usage_entry_present), + a->request_hash, &a->timer_limits, &a->clock_values, + nonce_values, parsed_lic); +} + +OEMCryptoResult odk_deserialize_RenewalResponse( + const uint8_t* buf, size_t len, ODK_ParseRenewal_Args* a, + ODK_NonceValues* nonce_values, ODK_PreparedRenewalRequest* renewal_msg) { + /* Address Sanitizer doesn't like values other than 0 OR 1 for boolean + * variables. Input from fuzzer can be parsed and any random bytes can be + * assigned to boolean variables. Using the workaround to mitigate sanitizer + * errors in fuzzer code and converting random bytes to 0 OR 1. + * This has no negative security impact*/ + a->timer_limits.soft_enforce_playback_duration = + convert_byte_to_valid_boolean( + &a->timer_limits.soft_enforce_playback_duration); + a->timer_limits.soft_enforce_rental_duration = convert_byte_to_valid_boolean( + &a->timer_limits.soft_enforce_rental_duration); + uint64_t timer_value = 0; + OEMCryptoResult err = + ODK_ParseRenewal(buf, SIZE_MAX, len, nonce_values, a->system_time, + &a->timer_limits, &a->clock_values, &timer_value); + const bool is_parse_renewal_response_successful = + err == ODK_SET_TIMER || err == ODK_DISABLE_TIMER || + err == ODK_TIMER_EXPIRED || err == ODK_STALE_RENEWAL; + if (!is_parse_renewal_response_successful) { + return err; + } + // In order to capture playback_time information which is part of + // renewal_msg and will be later used in kdo_serialize_RenewalResponse in + // odk_kdo method, we call Unpack_ODK_PreparedRenewalRequest private method. + // playback_time cannot be captured from publicly exposed API + // ODK_ParseRenewal. + Message* msg = nullptr; + AllocateMessage(&msg, message_block); + InitMessage(msg, const_cast(buf), len); + SetSize(msg, len); + Unpack_ODK_PreparedRenewalRequest(msg, renewal_msg); + return OEMCrypto_SUCCESS; +} + +OEMCryptoResult odk_deserialize_ProvisioningResponse( + const uint8_t* buf, size_t len, ODK_ParseProvisioning_Args* a, + ODK_NonceValues* nonce_values, ODK_ParsedProvisioning* parsed_prov) { + return ODK_ParseProvisioning(buf, SIZE_MAX, len, nonce_values, a->device_id, + a->device_id_length, parsed_prov); +} + +bool kdo_serialize_LicenseResponse(const ODK_ParseLicense_Args* args, + const ODK_ParsedLicense& parsed_lic, + std::string* oemcrypto_core_message) { + const auto& nonce_values = args->nonce_values; + ODK_LicenseRequest core_request{nonce_values.api_minor_version, + nonce_values.api_major_version, + nonce_values.nonce, nonce_values.session_id}; + std::string core_request_sha_256( + reinterpret_cast(args->request_hash), ODK_SHA256_HASH_SIZE); + return serialize::CreateCoreLicenseResponse( + parsed_lic, core_request, core_request_sha_256, oemcrypto_core_message); +} + +bool kdo_serialize_RenewalResponse( + const ODK_ParseRenewal_Args* args, + const ODK_PreparedRenewalRequest& renewal_msg, + std::string* oemcrypto_core_message) { + const auto& nonce_values = args->nonce_values; + ODK_RenewalRequest core_request{ + nonce_values.api_minor_version, nonce_values.api_major_version, + nonce_values.nonce, nonce_values.session_id, renewal_msg.playback_time}; + return serialize::CreateCoreRenewalResponse( + core_request, args->timer_limits.initial_renewal_duration_seconds, + oemcrypto_core_message); +} + +bool kdo_serialize_ProvisioningResponse( + const ODK_ParseProvisioning_Args* args, + const ODK_ParsedProvisioning& parsed_prov, + std::string* oemcrypto_core_message) { + const auto& nonce_values = args->nonce_values; + if (args->device_id_length > sizeof(args->device_id)) { + return false; + } + ODK_ProvisioningRequest core_request{ + nonce_values.api_minor_version, nonce_values.api_major_version, + nonce_values.nonce, nonce_values.session_id, + std::string(reinterpret_cast(args->device_id), + args->device_id_length)}; + return serialize::CreateCoreProvisioningResponse(parsed_prov, core_request, + oemcrypto_core_message); +} +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/test/fuzzing/odk_fuzz_helper.h b/oemcrypto/odk/test/fuzzing/odk_fuzz_helper.h new file mode 100644 index 0000000..5672c00 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_fuzz_helper.h @@ -0,0 +1,206 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ +#ifndef WIDEVINE_ODK_TEST_FUZZING_ODK_FUZZ_HELPER_H_ +#define WIDEVINE_ODK_TEST_FUZZING_ODK_FUZZ_HELPER_H_ + +#include +#include + +#include "core_message_serialize.h" +#include "fuzzing/odk_fuzz_structs.h" +#include "odk_serialize.h" + +namespace oemcrypto_core_message { +bool convert_byte_to_valid_boolean(const bool* in); + +OEMCryptoResult odk_serialize_LicenseRequest( + const void* in, uint8_t* out, size_t* size, + const ODK_LicenseRequest& core_license_request, + const ODK_NonceValues* nonce_values); + +OEMCryptoResult odk_serialize_RenewalRequest( + const void* in, uint8_t* out, size_t* size, + const ODK_RenewalRequest& core_renewal, ODK_NonceValues* nonce_values); + +OEMCryptoResult odk_serialize_ProvisioningRequest( + const void* in, uint8_t* out, size_t* size, + const ODK_ProvisioningRequest& core_provisioning, + const ODK_NonceValues* nonce_values); + +OEMCryptoResult odk_deserialize_LicenseResponse(const uint8_t* message, + size_t core_message_length, + ODK_ParseLicense_Args* a, + ODK_NonceValues* nonce_values, + ODK_ParsedLicense* parsed_lic); + +OEMCryptoResult odk_deserialize_RenewalResponse( + const uint8_t* buf, size_t len, ODK_ParseRenewal_Args* a, + ODK_NonceValues* nonce_values, ODK_PreparedRenewalRequest* renewal_msg); + +OEMCryptoResult odk_deserialize_ProvisioningResponse( + const uint8_t* buf, size_t len, ODK_ParseProvisioning_Args* a, + ODK_NonceValues* nonce_values, ODK_ParsedProvisioning* parsed_prov); + +bool kdo_serialize_LicenseResponse(const ODK_ParseLicense_Args* args, + const ODK_ParsedLicense& parsed_lic, + std::string* oemcrypto_core_message); + +bool kdo_serialize_RenewalResponse( + const ODK_ParseRenewal_Args* args, + const ODK_PreparedRenewalRequest& renewal_msg, + std::string* oemcrypto_core_message); + +bool kdo_serialize_ProvisioningResponse( + const ODK_ParseProvisioning_Args* args, + const ODK_ParsedProvisioning& parsed_prov, + std::string* oemcrypto_core_message); + +/* Idea behind having three different functions is: */ +/* Only ODK_ParseLicense structure had fields which needed additional */ +/* procession. Having a single function with templated parameter T was */ +/* failing during compile time because other two structures doesn't have */ +/* fields that need additional processing. Hence to reduce code redundance and + */ +/* make us of common FuzzerMutateResponse across three response fuzzers, */ +/* three independent functions were defined and renewal and provisioning */ +/* functions would be empty as no additional processing is needed for them. */ +void ConvertDataToValidBools(ODK_ParsedLicense* t); + +void ConvertDataToValidBools(ODK_PreparedRenewalRequest* t); + +void ConvertDataToValidBools(ODK_ParsedProvisioning* t); + +/* Forward-declare the libFuzzer's mutator callback. Mark it weak so that */ +/* the program links successfully even outside of --config=asan-fuzzer */ +/* (apparently the only config in which LLVM uses our custom mutator). */ +extern "C" size_t LLVMFuzzerMutate(uint8_t* Data, size_t Size, size_t MaxSize) + __attribute__((weak)); + +template +size_t FuzzerMutateResponse(uint8_t* data, size_t size, size_t max_size, + const F& odk_deserialize_fun, + const G& kdo_serialize_fun) { + const size_t kArgsSize = sizeof(A); + const size_t kCoreResponseSize = sizeof(T); + const size_t kTotalResponseSize = kArgsSize + kCoreResponseSize; + + /* Deserializing data in order to make sure it deserializes properly. */ + /* Input byte array format: [function arguments][data to parse]. */ + std::shared_ptr _args(new A()); + A* args = _args.get(); + memcpy(args, data, kArgsSize); + ODK_NonceValues nonce_values = args->nonce_values; + args->nonce_values.api_major_version = ODK_MAJOR_VERSION; + const uint8_t* buf = data + kArgsSize; + T t = {}; + OEMCryptoResult result = + odk_deserialize_fun(buf, size - kArgsSize, args, &nonce_values, &t); + + /* If data doesn't deserialize successfully, We copy random bytes into */ + /* T and serialize using kdo function */ + /* which will create a valid oemcrypto core message using */ + /* nonce and request hash from function args. OEMCrypto core message acts as + */ + /* input to odk_kdo. */ + if (result != OEMCrypto_SUCCESS) { + if (max_size < kTotalResponseSize) { + return 0; + } + /* Initialize remaining bytes needed in data to zero. */ + if (size < kTotalResponseSize) { + memset(data + size, 0, kTotalResponseSize - size); + } + t = {}; + memcpy(&t, buf, kCoreResponseSize); + } + + /* Ask LLVM to run its usual mutations, hopefully giving us interesting */ + /* inputs. We copy deserialized data into pointer data, run mutations */ + /* and copy back the mutated data to args and t */ + memcpy(data + kArgsSize, &t, kCoreResponseSize); + LLVMFuzzerMutate(data, kTotalResponseSize, kTotalResponseSize); + memcpy(args, data, kArgsSize); + memcpy(&t, data + kArgsSize, kCoreResponseSize); + /* Convert boolean flags in parsed message to valid bytes to */ + /* avoid errors from msan. Only needed for parsed license. */ + ConvertDataToValidBools(&t); + /* Serialize the data after mutation. */ + std::string oemcrypto_core_message; + if (!kdo_serialize_fun(args, t, &oemcrypto_core_message)) { + return 0; + } + + /* Copy mutated and serialized oemcrypto_core_message to data */ + /* so that it acts as input to odk_kdo function. */ + memcpy(data + kArgsSize, oemcrypto_core_message.data(), + oemcrypto_core_message.size()); + return kArgsSize + oemcrypto_core_message.size(); +} + +/** + * Template arguments: + * A: struct holding function arguments + * T: odk deserialize output/kdo serialize input structure + * F: odk deserialize function + * G: kdo serialize function + * + * raw bytes -> F deserialize -> struct T -> G serialize -> raw bytes + */ +template +void odk_kdo(const F& odk_fun, const G& kdo_fun, const uint8_t* in, + const size_t size, const size_t args_size, uint8_t* out) { + T t = {}; + /* Input byte array format: [function arguments][data to parse] */ + if (size < args_size) { + return; + } + const uint8_t* buf = in + args_size; + std::shared_ptr _args(new A()); + A* args = _args.get(); + memcpy(args, in, args_size); + args->nonce_values.api_major_version = ODK_MAJOR_VERSION; + ODK_NonceValues nonce_values = args->nonce_values; + + OEMCryptoResult result = + odk_fun(buf, size - args_size, args, &nonce_values, &t); + if (result != OEMCrypto_SUCCESS) { + return; + } + std::string oemcrypto_core_message; + if (!kdo_fun(args, t, &oemcrypto_core_message)) { + return; + } +} + +/** + * Template arguments: + * T: kdo deserialize output/odk serialize input structure + * F: kdo deserialize function + * G: odk serialize function + * + * raw bytes -> F deserialize -> struct T -> G serialize -> raw bytes + */ +template +static void kdo_odk(const F& kdo_fun, const G& odk_fun, const uint8_t* in, + size_t size, const size_t clock_value_size, uint8_t* out) { + if (size <= clock_value_size) { + return; + } + /* Input byte array format: [Clock Values][data to parse]. */ + /* Only Renewal Request expects clock values to be present. */ + std::string input(reinterpret_cast(in) + clock_value_size, + size - clock_value_size); + T t = {}; + if (!kdo_fun(input, &t)) { + return; + } + ODK_NonceValues nonce_values = {t.api_minor_version, t.api_major_version, + t.nonce, t.session_id}; + OEMCryptoResult err = odk_fun(in, out, &size, t, &nonce_values); + if (OEMCrypto_SUCCESS != err) { + return; + } +} +} /* namespace oemcrypto_core_message */ +#endif /* WIDEVINE_ODK_TEST_FUZZING_ODK_FUZZ_HELPER_H_ */ diff --git a/oemcrypto/odk/test/fuzzing/odk_fuzz_structs.h b/oemcrypto/odk/test/fuzzing/odk_fuzz_structs.h new file mode 100644 index 0000000..c88de19 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_fuzz_structs.h @@ -0,0 +1,28 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ +#ifndef WIDEVINE_ODK_TEST_FUZZING_ODK_FUZZ_STRUCTS_H_ +#define WIDEVINE_ODK_TEST_FUZZING_ODK_FUZZ_STRUCTS_H_ + +#include "odk_structs.h" + +struct ODK_ParseLicense_Args { + ODK_NonceValues nonce_values; + uint8_t initial_license_load; + uint8_t usage_entry_present; + uint8_t request_hash[ODK_SHA256_HASH_SIZE]; + ODK_TimerLimits timer_limits; + ODK_ClockValues clock_values; +}; +struct ODK_ParseRenewal_Args { + ODK_NonceValues nonce_values; + uint64_t system_time; + ODK_TimerLimits timer_limits; + ODK_ClockValues clock_values; +}; +struct ODK_ParseProvisioning_Args { + ODK_NonceValues nonce_values; + size_t device_id_length; + uint8_t device_id[64]; +}; +#endif /* WIDEVINE_ODK_TEST_FUZZING_ODK_FUZZ_STRUCTS_H_ */ diff --git a/oemcrypto/odk/test/fuzzing/odk_license_request_fuzz.cpp b/oemcrypto/odk/test/fuzzing/odk_license_request_fuzz.cpp new file mode 100644 index 0000000..463c604 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_license_request_fuzz.cpp @@ -0,0 +1,22 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary + * source code may only be used and distributed under the Widevine Master + * License Agreement. + */ + +#include + +#include "core_message_deserialize.h" +#include "fuzzing/odk_fuzz_helper.h" + +namespace oemcrypto_core_message { +using oemcrypto_core_message::deserialize::CoreLicenseRequestFromMessage; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + std::vector out(size); + const size_t kClockValueSize = 0; + kdo_odk(CoreLicenseRequestFromMessage, + odk_serialize_LicenseRequest, data, size, + kClockValueSize, out.data()); + return 0; +} +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/test/fuzzing/odk_license_response_fuzz.cpp b/oemcrypto/odk/test/fuzzing/odk_license_response_fuzz.cpp new file mode 100644 index 0000000..12398fd --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_license_response_fuzz.cpp @@ -0,0 +1,20 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary + * source code may only be used and distributed under the Widevine Master + * License Agreement. + */ + +#include + +#include "fuzzing/odk_fuzz_helper.h" + +namespace oemcrypto_core_message { + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + const size_t kLicenseResponseArgsSize = sizeof(ODK_ParseLicense_Args); + std::vector out(size); + odk_kdo( + odk_deserialize_LicenseResponse, kdo_serialize_LicenseResponse, data, + size, kLicenseResponseArgsSize, out.data()); + return 0; +} +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/test/fuzzing/odk_license_response_fuzz_with_mutator.cpp b/oemcrypto/odk/test/fuzzing/odk_license_response_fuzz_with_mutator.cpp new file mode 100644 index 0000000..80c8ff3 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_license_response_fuzz_with_mutator.cpp @@ -0,0 +1,35 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary + * source code may only be used and distributed under the Widevine Master + * License Agreement. + */ + +#include + +#include "fuzzing/odk_fuzz_helper.h" + +namespace oemcrypto_core_message { + +// The custom mutator: Ensure that each input can be deserialized properly +// by ODK function after mutation. +extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data, size_t size, + size_t max_size, unsigned int seed) { + const size_t kLicenseResponseArgsSize = sizeof(ODK_ParseLicense_Args); + if (size < kLicenseResponseArgsSize) { + return 0; + } + + // Mutate input data and return mutated input size. + return FuzzerMutateResponse( + data, size, max_size, odk_deserialize_LicenseResponse, + kdo_serialize_LicenseResponse); +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + const size_t kLicenseResponseArgsSize = sizeof(ODK_ParseLicense_Args); + std::vector out(size); + odk_kdo( + odk_deserialize_LicenseResponse, kdo_serialize_LicenseResponse, data, + size, kLicenseResponseArgsSize, out.data()); + return 0; +} +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/test/fuzzing/odk_provisioning_request_fuzz.cpp b/oemcrypto/odk/test/fuzzing/odk_provisioning_request_fuzz.cpp new file mode 100644 index 0000000..984534e --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_provisioning_request_fuzz.cpp @@ -0,0 +1,22 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary + * source code may only be used and distributed under the Widevine Master + * License Agreement. + */ + +#include + +#include "core_message_deserialize.h" +#include "fuzzing/odk_fuzz_helper.h" + +namespace oemcrypto_core_message { +using oemcrypto_core_message::deserialize::CoreProvisioningRequestFromMessage; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + std::vector out(size); + const size_t kClockValueSize = 0; + kdo_odk(CoreProvisioningRequestFromMessage, + odk_serialize_ProvisioningRequest, data, + size, kClockValueSize, out.data()); + return 0; +} +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/test/fuzzing/odk_provisioning_response_fuzz.cpp b/oemcrypto/odk/test/fuzzing/odk_provisioning_response_fuzz.cpp new file mode 100644 index 0000000..90dc017 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_provisioning_response_fuzz.cpp @@ -0,0 +1,21 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary + * source code may only be used and distributed under the Widevine Master + * License Agreement. + */ + +#include + +#include "fuzzing/odk_fuzz_helper.h" + +namespace oemcrypto_core_message { + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + const size_t kProvisioningResponseArgsSize = + sizeof(ODK_ParseProvisioning_Args); + std::vector out(size); + odk_kdo( + odk_deserialize_ProvisioningResponse, kdo_serialize_ProvisioningResponse, + data, size, kProvisioningResponseArgsSize, out.data()); + return 0; +} +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/test/fuzzing/odk_provisioning_response_fuzz_with_mutator.cpp b/oemcrypto/odk/test/fuzzing/odk_provisioning_response_fuzz_with_mutator.cpp new file mode 100644 index 0000000..17787a4 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_provisioning_response_fuzz_with_mutator.cpp @@ -0,0 +1,38 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary + * source code may only be used and distributed under the Widevine Master + * License Agreement. + */ + +#include + +#include "fuzzing/odk_fuzz_helper.h" + +namespace oemcrypto_core_message { + +// The custom mutator: Ensure that each input can be deserialized properly +// by ODK function after mutation. +extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data, size_t size, + size_t max_size, unsigned int seed) { + const size_t kProvisioningResponseArgsSize = + sizeof(ODK_ParseProvisioning_Args); + if (size < kProvisioningResponseArgsSize) { + return 0; + } + + // Mutate input data and return mutated input size. + return FuzzerMutateResponse( + data, size, max_size, odk_deserialize_ProvisioningResponse, + kdo_serialize_ProvisioningResponse); +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + const size_t kProvisioningResponseArgsSize = + sizeof(ODK_ParseProvisioning_Args); + std::vector out(size); + odk_kdo( + odk_deserialize_ProvisioningResponse, kdo_serialize_ProvisioningResponse, + data, size, kProvisioningResponseArgsSize, out.data()); + return 0; +} +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/test/fuzzing/odk_renewal_request_fuzz.cpp b/oemcrypto/odk/test/fuzzing/odk_renewal_request_fuzz.cpp new file mode 100644 index 0000000..602b37a --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_renewal_request_fuzz.cpp @@ -0,0 +1,22 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary + * source code may only be used and distributed under the Widevine Master + * License Agreement. + */ + +#include + +#include "core_message_deserialize.h" +#include "fuzzing/odk_fuzz_helper.h" + +namespace oemcrypto_core_message { +using oemcrypto_core_message::deserialize::CoreRenewalRequestFromMessage; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + std::vector out(size); + const size_t kClockValueSize = sizeof(ODK_ClockValues); + kdo_odk(CoreRenewalRequestFromMessage, + odk_serialize_RenewalRequest, data, size, + kClockValueSize, out.data()); + return 0; +} +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/test/fuzzing/odk_renewal_response_fuzz.cpp b/oemcrypto/odk/test/fuzzing/odk_renewal_response_fuzz.cpp new file mode 100644 index 0000000..8d66908 --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_renewal_response_fuzz.cpp @@ -0,0 +1,20 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary + * source code may only be used and distributed under the Widevine Master + * License Agreement. + */ + +#include + +#include "fuzzing/odk_fuzz_helper.h" + +namespace oemcrypto_core_message { + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + const size_t kRenewalResponseArgsSize = sizeof(ODK_ParseRenewal_Args); + std::vector out(size); + odk_kdo( + odk_deserialize_RenewalResponse, kdo_serialize_RenewalResponse, data, + size, kRenewalResponseArgsSize, out.data()); + return 0; +} +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/test/fuzzing/odk_renewal_response_fuzz_with_mutator.cpp b/oemcrypto/odk/test/fuzzing/odk_renewal_response_fuzz_with_mutator.cpp new file mode 100644 index 0000000..0073c4e --- /dev/null +++ b/oemcrypto/odk/test/fuzzing/odk_renewal_response_fuzz_with_mutator.cpp @@ -0,0 +1,36 @@ +/* Copyright 2020 Google LLC. All rights reserved. This file and proprietary + * source code may only be used and distributed under the Widevine Master + * License Agreement. + */ + +#include + +#include "fuzzing/odk_fuzz_helper.h" + +namespace oemcrypto_core_message { + +// The custom mutator: Ensure that each input can be deserialized properly +// by ODK function after mutation. +extern "C" size_t LLVMFuzzerCustomMutator(uint8_t* data, size_t size, + size_t max_size, unsigned int seed) { + const size_t kRenewalResponseArgsSize = sizeof(ODK_ParseRenewal_Args); + if (size < kRenewalResponseArgsSize) { + return 0; + } + + // Mutate input data and return mutated input size. + return FuzzerMutateResponse( + data, size, max_size, odk_deserialize_RenewalResponse, + kdo_serialize_RenewalResponse); +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + const size_t kRenewalResponseArgsSize = sizeof(ODK_ParseRenewal_Args); + std::vector out(size); + odk_kdo( + odk_deserialize_RenewalResponse, kdo_serialize_RenewalResponse, data, + size, kRenewalResponseArgsSize, out.data()); + return 0; +} +} // namespace oemcrypto_core_message diff --git a/oemcrypto/odk/test/odk_core_message_test.cpp b/oemcrypto/odk/test/odk_core_message_test.cpp new file mode 100644 index 0000000..c824759 --- /dev/null +++ b/oemcrypto/odk/test/odk_core_message_test.cpp @@ -0,0 +1,37 @@ +// Copyright 2020 Google LLC. All rights reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "OEMCryptoCENCCommon.h" +#include "gtest/gtest.h" +#include "odk.h" +#include "third_party/absl/strings/escaping.h" + +namespace wvodk_test { +TEST(CoreMessageTest, RenwalRequest) { + std::string oem = + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrst" + "uvwxyzabcdefghijklmnopqrstuvwxyz"; + const uint8_t* buf = reinterpret_cast(oem.c_str()); + uint8_t* message = const_cast(buf); + size_t message_length = 88; + size_t core_message_length = 88; + uint16_t api_minor_version = 16; + uint16_t api_major_version = 16; + uint32_t nonce = 0; + uint32_t timer_status = 2; + uint64_t time = 10; + enum OEMCrypto_Usage_Entry_Status status = kInactiveUsed; + ODK_NonceValues nonce_values{api_minor_version, api_major_version, nonce}; + ODK_ClockValues clock_values{time, time, time, time, + time, timer_status, status}; + uint64_t system_time_seconds = 100; + EXPECT_EQ(OEMCrypto_SUCCESS, + ODK_PrepareCoreRenewalRequest(message, message_length, + &core_message_length, &nonce_values, + &clock_values, system_time_seconds)); + // All messages have at least a five 4-byte fields. + char* m = reinterpret_cast(message); + VLOG(0) << absl::BytesToHexString(std::string(m, core_message_length)); +} +} // namespace wvodk_test diff --git a/oemcrypto/odk/test/odk_test.cpp b/oemcrypto/odk/test/odk_test.cpp new file mode 100644 index 0000000..4e24895 --- /dev/null +++ b/oemcrypto/odk/test/odk_test.cpp @@ -0,0 +1,729 @@ +// Copyright 2019 Google LLC. All rights reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "odk.h" + +#include // TODO(b/147944591): use this one? Or odk_endian.h? + +#include +#include + +#include "OEMCryptoCENCCommon.h" +#include "core_message_deserialize.h" +#include "core_message_serialize.h" +#include "core_message_types.h" +#include "gtest/gtest.h" +#include "odk_structs_priv.h" +#include "odk_test_helper.h" + +namespace wvodk_test { + +namespace { + +using oemcrypto_core_message::ODK_LicenseRequest; +using oemcrypto_core_message::ODK_ProvisioningRequest; +using oemcrypto_core_message::ODK_RenewalRequest; + +using oemcrypto_core_message::deserialize::CoreLicenseRequestFromMessage; +using oemcrypto_core_message::deserialize::CoreProvisioningRequestFromMessage; +using oemcrypto_core_message::deserialize::CoreRenewalRequestFromMessage; + +using oemcrypto_core_message::serialize::CreateCoreLicenseResponse; +using oemcrypto_core_message::serialize::CreateCoreProvisioningResponse; +using oemcrypto_core_message::serialize::CreateCoreRenewalResponse; + +constexpr uint32_t kExtraPayloadSize = 128u; + +template +void ValidateRequest(uint32_t message_type, + const std::vector& extra_fields, + const F& odk_prepare_func, const G& kdo_parse_func) { + uint32_t message_size = 0; + uint16_t api_major_version = ODK_MAJOR_VERSION; + uint16_t api_minor_version = ODK_MINOR_VERSION; + uint32_t nonce = 0xdeadbeef; + uint32_t session_id = 0xcafebabe; + ODK_NonceValues nonce_values{api_minor_version, api_major_version, nonce, + session_id}; + std::vector total_fields = { + {ODK_UINT32, &message_type, "message_type"}, + {ODK_UINT32, &message_size, "message_size"}, + {ODK_UINT16, &api_minor_version, "api_minor_version"}, + {ODK_UINT16, &api_major_version, "api_major_version"}, + {ODK_UINT32, &nonce, "nonce"}, + {ODK_UINT32, &session_id, "session_id"}, + }; + + total_fields.insert(total_fields.end(), extra_fields.begin(), + extra_fields.end()); + for (auto& field : total_fields) { + message_size += ODK_FieldLength(field.type); + } + + // empty buf, expect core message length to be set correctly + size_t core_message_length = 0; + uint8_t* buf_empty = nullptr; + EXPECT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, + odk_prepare_func(buf_empty, &core_message_length, &nonce_values)); + EXPECT_EQ(core_message_length, message_size); + + // non-empty buf, expect core message length to be set correctly, and buf is + // filled with ODK_Field values appropriately + uint8_t* buf = new uint8_t[message_size]{}; + EXPECT_EQ(OEMCrypto_SUCCESS, + odk_prepare_func(buf, &core_message_length, &nonce_values)); + EXPECT_EQ(core_message_length, message_size); + + uint8_t* buf_expected = new uint8_t[message_size]{}; + size_t buf_len_expected = 0; + EXPECT_EQ(OEMCrypto_SUCCESS, ODK_IterFields(ODK_WRITE, buf_expected, SIZE_MAX, + &buf_len_expected, total_fields)); + EXPECT_EQ(buf_len_expected, message_size); + + EXPECT_NO_FATAL_FAILURE( + ODK_ExpectEqualBuf(buf_expected, buf, message_size, total_fields)); + + // odk kdo round-trip: deserialize from buf, then serialize it to buf2 + // expect them to be identical + T t = {}; + std::string oemcrypto_core_message(reinterpret_cast(buf), + message_size); + EXPECT_TRUE(kdo_parse_func(oemcrypto_core_message, &t)); + nonce_values.api_minor_version = t.api_minor_version; + nonce_values.api_major_version = t.api_major_version; + nonce_values.nonce = t.nonce; + nonce_values.session_id = t.session_id; + uint8_t* buf2 = new uint8_t[message_size]{}; + EXPECT_EQ(OEMCrypto_SUCCESS, + odk_prepare_func(buf2, &core_message_length, &nonce_values)); + EXPECT_EQ(core_message_length, message_size); + EXPECT_NO_FATAL_FAILURE( + ODK_ExpectEqualBuf(buf, buf2, message_size, total_fields)); + + delete[] buf; + delete[] buf_expected; + delete[] buf2; +} + +/** + * Template arguments: + * T: kdo input struct + * F: odk deserializer + * G: kdo serializer + */ +template +void ValidateResponse(ODK_CoreMessage* core_message, + const std::vector& extra_fields, + const F& odk_parse_func, const G& kdo_prepare_func) { + T t = {}; + t.api_minor_version = core_message->nonce_values.api_minor_version; + t.api_major_version = core_message->nonce_values.api_major_version; + t.nonce = core_message->nonce_values.nonce; + t.session_id = core_message->nonce_values.session_id; + + uint8_t* buf = nullptr; + uint32_t buf_size = 0; + ODK_BuildMessageBuffer(core_message, extra_fields, &buf, &buf_size); + + uint8_t* zero = new uint8_t[buf_size]{}; + size_t bytes_read = 0; + // zero-out input + EXPECT_EQ(OEMCrypto_SUCCESS, ODK_IterFields(ODK_READ, zero, buf_size, + &bytes_read, extra_fields)); + + // parse buf with odk + EXPECT_EQ(OEMCrypto_SUCCESS, odk_parse_func(buf, buf_size)); + + size_t size_out = 0; + ODK_IterFields(ODK_FieldMode::ODK_DUMP, buf, buf_size, &size_out, + extra_fields); + + // serialize odk output to oemcrypto_core_message + std::string oemcrypto_core_message; + EXPECT_TRUE(kdo_prepare_func(t, &oemcrypto_core_message)); + + // verify round-trip works + EXPECT_NO_FATAL_FAILURE(ODK_ExpectEqualBuf(buf, oemcrypto_core_message.data(), + buf_size, extra_fields)); + delete[] buf; + delete[] zero; +} + +TEST(OdkTest, SerializeFields) { + uint32_t x[] = {0, 1, 2}; + uint64_t y[] = {3LL << 32, 4LL << 32, 5LL << 32}; + OEMCrypto_Substring s = {.offset = 6, .length = 7}; + std::vector fields = { + {ODK_UINT32, &x[0], "x[0]"}, {ODK_UINT32, &x[1], "x[1]"}, + {ODK_UINT32, &x[2], "x[2]"}, {ODK_UINT64, &y[0], "y[0]"}, + {ODK_UINT64, &y[1], "y[1]"}, {ODK_UINT64, &y[2], "y[2]"}, + {ODK_SUBSTRING, &s, "s"}, + }; + uint8_t buf[1024] = {0}; + uint8_t buf2[1024] = {0}; + size_t bytes_read = 0, bytes_written = 0; + ODK_IterFields(ODK_WRITE, buf, SIZE_MAX, &bytes_read, fields); + std::vector fields2(fields.size()); + ODK_ResetOdkFields(&fields); + ODK_IterFields(ODK_READ, buf, bytes_read, &bytes_written, fields); + ODK_IterFields(ODK_WRITE, buf2, SIZE_MAX, &bytes_read, fields); + + EXPECT_NO_FATAL_FAILURE(ODK_ExpectEqualBuf(buf, buf2, bytes_read, fields)); +} + +TEST(OdkTest, SerializeFieldsStress) { + const int n = 1024; + std::vector fields(n); + std::srand(0); + size_t total_size = 0; + for (int i = 0; i < n; i++) { + fields[i].type = static_cast(std::rand() % + static_cast(ODK_NUMTYPES)); + fields[i].value = malloc(ODK_AllocSize(fields[i].type)); + fields[i].name = "stress"; + total_size += ODK_FieldLength(fields[i].type); + } + + uint8_t* buf = new uint8_t[total_size]{}; + for (int i = 0; i < total_size; i++) { + buf[i] = std::rand() & 0xff; + } + + size_t bytes_read = 0, bytes_written = 0; + uint8_t* buf2 = new uint8_t[total_size]{}; + ODK_IterFields(ODK_READ, buf, total_size, &bytes_read, fields); + EXPECT_EQ(bytes_read, total_size); + ODK_IterFields(ODK_WRITE, buf2, total_size, &bytes_written, fields); + EXPECT_EQ(bytes_written, total_size); + + EXPECT_NO_FATAL_FAILURE(ODK_ExpectEqualBuf(buf, buf2, total_size, fields)); + + // cleanup + for (int i = 0; i < n; i++) { + free(fields[i].value); + } + delete[] buf; + delete[] buf2; +} + +TEST(OdkTest, NullRequestTest) { + size_t core_message_length = 0; + ODK_NonceValues nonce_values{0}; + ODK_ClockValues clock_values{0}; + + // Assert that nullptr does not cause a core dump. + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, ODK_PrepareCoreLicenseRequest( + nullptr, 0uL, nullptr, &nonce_values)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_PrepareCoreLicenseRequest(nullptr, 0uL, &core_message_length, + nullptr)); + + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_PrepareCoreRenewalRequest(nullptr, 0uL, nullptr, &nonce_values, + &clock_values, 0uL)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_PrepareCoreRenewalRequest(nullptr, 0uL, &core_message_length, + nullptr, &clock_values, 0uL)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_PrepareCoreRenewalRequest(nullptr, 0uL, &core_message_length, + &nonce_values, nullptr, 0uL)); + + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_PrepareCoreProvisioningRequest( + nullptr, 0uL, &core_message_length, nullptr, nullptr, 0uL)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_PrepareCoreProvisioningRequest(nullptr, 0uL, nullptr, + &nonce_values, nullptr, 0uL)); + + // Null device id in provisioning request is ok + uint8_t message[ODK_PROVISIONING_REQUEST_SIZE] = {0}; + core_message_length = ODK_PROVISIONING_REQUEST_SIZE; + EXPECT_EQ(OEMCrypto_SUCCESS, + ODK_PrepareCoreProvisioningRequest( + message, ODK_PROVISIONING_REQUEST_SIZE, &core_message_length, + &nonce_values, nullptr, 0uL)); +} + +TEST(OdkTest, NullResponseTest) { + constexpr size_t message_size = 64; + uint8_t message[message_size] = {0}; + size_t core_message_length = message_size; + uint8_t request_hash[ODK_SHA256_HASH_SIZE] = {0}; + ODK_TimerLimits timer_limits{0}; + ODK_ParsedLicense parsed_license; + ODK_NonceValues nonce_values{0}; + ODK_ClockValues clock_values{0}; + + // Assert that nullptr does not cause a core dump. + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseLicense(message, message_size, core_message_length, true, + true, request_hash, &timer_limits, &clock_values, + &nonce_values, nullptr)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseLicense(message, message_size, core_message_length, true, + true, request_hash, &timer_limits, &clock_values, + nullptr, &parsed_license)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseLicense(message, message_size, core_message_length, true, + true, request_hash, &timer_limits, nullptr, + &nonce_values, &parsed_license)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseLicense(message, message_size, core_message_length, true, + true, request_hash, nullptr, &clock_values, + &nonce_values, &parsed_license)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseLicense(message, message_size, core_message_length, true, + true, nullptr, &timer_limits, &clock_values, + &nonce_values, &parsed_license)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseLicense(nullptr, message_size, core_message_length, true, + true, request_hash, &timer_limits, &clock_values, + &nonce_values, &parsed_license)); + + constexpr uint64_t system_time = 0; + uint64_t timer_value = 0; + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseRenewal(message, message_size, core_message_length, + &nonce_values, system_time, &timer_limits, nullptr, + &timer_value)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseRenewal(message, message_size, core_message_length, + &nonce_values, system_time, nullptr, &clock_values, + &timer_value)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseRenewal(message, message_size, core_message_length, + nullptr, system_time, &timer_limits, &clock_values, + &timer_value)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseRenewal(nullptr, message_size, core_message_length, + &nonce_values, system_time, &timer_limits, + &clock_values, &timer_value)); + + uint8_t device_id[ODK_DEVICE_ID_LEN_MAX] = {0}; + ODK_ParsedProvisioning parsed_response; + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseProvisioning(message, message_size, core_message_length, + &nonce_values, device_id, + ODK_DEVICE_ID_LEN_MAX, nullptr)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseProvisioning(message, message_size, core_message_length, + &nonce_values, nullptr, 0, &parsed_response)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseProvisioning(message, message_size, core_message_length, + nullptr, device_id, ODK_DEVICE_ID_LEN_MAX, + &parsed_response)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_ParseProvisioning(nullptr, message_size, core_message_length, + &nonce_values, device_id, + ODK_DEVICE_ID_LEN_MAX, &parsed_response)); +} + +TEST(OdkTest, PrepareCoreLicenseRequest) { + uint8_t license_message[ODK_LICENSE_REQUEST_SIZE] = {0}; + size_t core_message_length = sizeof(license_message); + ODK_NonceValues nonce_values{0}; + EXPECT_EQ(OEMCrypto_SUCCESS, ODK_PrepareCoreLicenseRequest( + license_message, sizeof(license_message), + &core_message_length, &nonce_values)); +} + +TEST(OdkTest, PrepareCoreLicenseRequestSize) { + uint8_t license_message[ODK_LICENSE_REQUEST_SIZE] = {0}; + size_t core_message_length = sizeof(license_message); + ODK_NonceValues nonce_values{0}; + // message length smaller than core message length + size_t core_message_length_invalid = core_message_length + 1; + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_PrepareCoreLicenseRequest( + license_message, sizeof(license_message), + &core_message_length_invalid, &nonce_values)); + // message length larger than core message length + uint8_t license_message_large[ODK_LICENSE_REQUEST_SIZE * 2] = {0}; + EXPECT_EQ(OEMCrypto_SUCCESS, + ODK_PrepareCoreLicenseRequest(license_message_large, + sizeof(license_message_large), + &core_message_length, &nonce_values)); +} + +TEST(OdkTest, PrepareCoreRenewalRequest) { + uint8_t renewal_message[ODK_RENEWAL_REQUEST_SIZE] = {0}; + size_t core_message_length = sizeof(renewal_message); + ODK_NonceValues nonce_values{0}; + ODK_ClockValues clock_values{0}; + constexpr uint64_t system_time_seconds = 10; + EXPECT_EQ(OEMCrypto_SUCCESS, + ODK_PrepareCoreRenewalRequest( + renewal_message, sizeof(renewal_message), &core_message_length, + &nonce_values, &clock_values, system_time_seconds)); +} + +TEST(OdkTest, PrepareCoreRenewalRequestTimer) { + uint8_t renewal_message[ODK_RENEWAL_REQUEST_SIZE] = {0}; + size_t core_message_length = sizeof(renewal_message); + ODK_NonceValues nonce_values{2, 16, 0, 0}; + constexpr uint64_t system_time_seconds = 10; + ODK_ClockValues clock_values_updated{0}; + // system time smaller than first decrypt time + clock_values_updated.time_of_first_decrypt = system_time_seconds + 1; + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_PrepareCoreRenewalRequest( + renewal_message, sizeof(renewal_message), &core_message_length, + &nonce_values, &clock_values_updated, system_time_seconds)); + clock_values_updated.time_of_first_decrypt = system_time_seconds - 1; + EXPECT_EQ(OEMCrypto_SUCCESS, + ODK_PrepareCoreRenewalRequest( + renewal_message, sizeof(renewal_message), &core_message_length, + &nonce_values, &clock_values_updated, system_time_seconds)); + // clock_values.time_of_renewal_request should get updated + EXPECT_EQ(system_time_seconds - clock_values_updated.time_of_first_decrypt, + clock_values_updated.time_of_renewal_request); +} + +TEST(OdkTest, PrepareCoreProvisioningRequest) { + uint8_t provisioning_message[ODK_PROVISIONING_REQUEST_SIZE] = {0}; + size_t core_message_length = sizeof(provisioning_message); + ODK_NonceValues nonce_values{0}; + uint8_t device_id[ODK_DEVICE_ID_LEN_MAX] = {0}; + EXPECT_EQ( + OEMCrypto_SUCCESS, + ODK_PrepareCoreProvisioningRequest( + provisioning_message, sizeof(provisioning_message), + &core_message_length, &nonce_values, device_id, sizeof(device_id))); +} + +TEST(OdkTest, PrepareCoreProvisioningRequestDeviceId) { + uint8_t provisioning_message[ODK_PROVISIONING_REQUEST_SIZE] = {0}; + size_t core_message_length = sizeof(provisioning_message); + ODK_NonceValues nonce_values{0}; + uint8_t device_id_invalid[ODK_DEVICE_ID_LEN_MAX + 1] = {0}; + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, + ODK_PrepareCoreProvisioningRequest( + provisioning_message, sizeof(provisioning_message), + &core_message_length, &nonce_values, device_id_invalid, + sizeof(device_id_invalid))); +} + +// Serialize and de-serialize license request +TEST(OdkTest, LicenseRequestRoundtrip) { + std::vector empty; + auto odk_prepare_func = [&](uint8_t* const buf, size_t* size, + ODK_NonceValues* nonce_values) { + return ODK_PrepareCoreLicenseRequest(buf, SIZE_MAX, size, nonce_values); + }; + auto kdo_parse_func = CoreLicenseRequestFromMessage; + ValidateRequest(ODK_License_Request_Type, empty, + odk_prepare_func, kdo_parse_func); +} + +TEST(OdkTest, RenewalRequestRoundtrip) { + constexpr uint64_t system_time_seconds = 0xBADDCAFE000FF1CE; + uint64_t playback_time = 0xCAFE00000000; + const uint64_t playback_start = system_time_seconds - playback_time; + const std::vector extra_fields = { + {ODK_UINT64, &playback_time, "playback_time"}, + }; + ODK_ClockValues clock_values = {0}; + clock_values.time_of_first_decrypt = playback_start; + auto odk_prepare_func = [&](uint8_t* const buf, size_t* size, + ODK_NonceValues* nonce_values) { + return ODK_PrepareCoreRenewalRequest(buf, SIZE_MAX, size, nonce_values, + &clock_values, system_time_seconds); + }; + auto kdo_parse_func = [&](const std::string& oemcrypto_core_message, + ODK_RenewalRequest* core_renewal_request) { + bool ok = CoreRenewalRequestFromMessage(oemcrypto_core_message, + core_renewal_request); + return ok; + }; + ValidateRequest(ODK_Renewal_Request_Type, extra_fields, + odk_prepare_func, kdo_parse_func); +} + +TEST(OdkTest, ProvisionRequestRoundtrip) { + uint32_t device_id_length = ODK_DEVICE_ID_LEN_MAX / 2; + uint8_t device_id[ODK_DEVICE_ID_LEN_MAX] = {0}; + memset(device_id, 0xff, device_id_length); + std::vector extra_fields = { + {ODK_UINT32, &device_id_length, "device_id_length"}, + {ODK_DEVICEID, device_id, "device_id"}, + }; + auto odk_prepare_func = [&](uint8_t* const buf, size_t* size, + const ODK_NonceValues* nonce_values) { + return ODK_PrepareCoreProvisioningRequest(buf, SIZE_MAX, size, nonce_values, + device_id, device_id_length); + }; + auto kdo_parse_func = + [&](const std::string& oemcrypto_core_message, + ODK_ProvisioningRequest* core_provisioning_request) { + bool ok = CoreProvisioningRequestFromMessage(oemcrypto_core_message, + core_provisioning_request); + return ok; + }; + ValidateRequest(ODK_Provisioning_Request_Type, + extra_fields, odk_prepare_func, + kdo_parse_func); +} + +TEST(OdkTest, ParseLicenseErrorNonce) { + ODK_LicenseResponseParams params; + ODK_SetDefaultLicenseResponseParams(¶ms); + uint8_t* buf = nullptr; + uint32_t buf_size = 0; + ODK_BuildMessageBuffer(&(params.core_message), params.extra_fields, &buf, + &buf_size); + // temporarily mess up with nonce + params.core_message.nonce_values.nonce = 0; + OEMCryptoResult err = ODK_ParseLicense( + buf, buf_size + kExtraPayloadSize, buf_size, params.initial_license_load, + params.usage_entry_present, params.request_hash, &(params.timer_limits), + &(params.clock_values), &(params.core_message.nonce_values), + &(params.parsed_license)); + EXPECT_EQ(OEMCrypto_ERROR_INVALID_NONCE, err); + delete[] buf; +} + +TEST(OdkTest, ParseLicenseErrorUsageEntry) { + ODK_LicenseResponseParams params; + ODK_SetDefaultLicenseResponseParams(¶ms); + uint8_t* buf = nullptr; + uint32_t buf_size = 0; + ODK_BuildMessageBuffer(&(params.core_message), params.extra_fields, &buf, + &buf_size); + params.usage_entry_present = false; + OEMCryptoResult err = ODK_ParseLicense( + buf, buf_size + kExtraPayloadSize, buf_size, params.initial_license_load, + params.usage_entry_present, params.request_hash, &(params.timer_limits), + &(params.clock_values), &(params.core_message.nonce_values), + &(params.parsed_license)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, err); + delete[] buf; +} + +TEST(OdkTest, ParseLicenseErrorRequestHash) { + ODK_LicenseResponseParams params; + ODK_SetDefaultLicenseResponseParams(¶ms); + uint8_t* buf = nullptr; + uint32_t buf_size = 0; + ODK_BuildMessageBuffer(&(params.core_message), params.extra_fields, &buf, + &buf_size); + // temporarily mess up with request hash + params.request_hash[0] = 0xff; + OEMCryptoResult err = ODK_ParseLicense( + buf, buf_size + kExtraPayloadSize, buf_size, params.initial_license_load, + params.usage_entry_present, params.request_hash, &(params.timer_limits), + &(params.clock_values), &(params.core_message.nonce_values), + &(params.parsed_license)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, err); + delete[] buf; +} + +TEST(OdkTest, ParseRenewalErrorTimer) { + ODK_RenewalResponseParams params; + ODK_SetDefaultRenewalResponseParams(¶ms); + uint8_t* buf = nullptr; + uint32_t buf_size = 0; + ODK_BuildMessageBuffer(&(params.core_message), params.extra_fields, &buf, + &buf_size); + params.clock_values.time_of_renewal_request = 0; + OEMCryptoResult err = ODK_ParseRenewal( + buf, buf_size, buf_size, &(params.core_message.nonce_values), + params.system_time, &(params.timer_limits), &(params.clock_values), + &(params.playback_timer)); + EXPECT_EQ(ODK_STALE_RENEWAL, err); + delete[] buf; +} + +TEST(OdkTest, ParsePrivisioningErrorDeviceId) { + ODK_ProvisioningResponseParams params; + ODK_SetDefaultProvisioningResponseParams(¶ms); + uint8_t* buf = nullptr; + uint32_t buf_size = 0; + ODK_BuildMessageBuffer(&(params.core_message), params.extra_fields, &buf, + &buf_size); + // temporarily mess up with device_id + params.device_id[0] = 0; + OEMCryptoResult err = ODK_ParseProvisioning( + buf, buf_size + 16, buf_size, &(params.core_message.nonce_values), + params.device_id, params.device_id_length, &(params.parsed_provisioning)); + EXPECT_EQ(ODK_ERROR_CORE_MESSAGE, err); + delete[] buf; +} + +// Serialize and de-serialize license response +TEST(OdkTest, LicenseResponseRoundtrip) { + ODK_LicenseResponseParams params; + ODK_SetDefaultLicenseResponseParams(¶ms); + // save a copy of params.request_hash as it will be zero out during the test + uint8_t request_hash_read[ODK_SHA256_HASH_SIZE]; + memcpy(request_hash_read, params.request_hash, sizeof(request_hash_read)); + auto odk_parse_func = [&](const uint8_t* buf, size_t size) { + return ODK_ParseLicense( + buf, size + kExtraPayloadSize, size, params.initial_license_load, + params.usage_entry_present, request_hash_read, &(params.timer_limits), + &(params.clock_values), &(params.core_message.nonce_values), + &(params.parsed_license)); + }; + const std::string request_hash_string( + reinterpret_cast(request_hash_read), + sizeof(request_hash_read)); + auto kdo_prepare_func = [&](const ODK_LicenseRequest& core_request, + std::string* oemcrypto_core_message) { + return CreateCoreLicenseResponse(params.parsed_license, core_request, + request_hash_string, + oemcrypto_core_message); + }; + ValidateResponse(&(params.core_message), + params.extra_fields, odk_parse_func, + kdo_prepare_func); +} + +TEST(OdkTest, RenewalResponseRoundtrip) { + ODK_RenewalResponseParams params; + ODK_SetDefaultRenewalResponseParams(¶ms); + const uint64_t playback_clock = params.playback_clock; + const uint64_t renewal_duration = params.renewal_duration; + auto odk_parse_func = [&](const uint8_t* buf, size_t size) { + OEMCryptoResult err = + ODK_ParseRenewal(buf, size, size, &(params.core_message.nonce_values), + params.system_time, &(params.timer_limits), + &(params.clock_values), &(params.playback_timer)); + + EXPECT_EQ(ODK_SET_TIMER, err); + EXPECT_EQ(renewal_duration, params.playback_timer); + EXPECT_EQ(params.clock_values.time_when_timer_expires, + params.system_time + params.playback_timer); + + return OEMCrypto_SUCCESS; + }; + auto kdo_prepare_func = [&](ODK_RenewalRequest& core_request, + std::string* oemcrypto_core_message) { + core_request.playback_time_seconds = playback_clock; + return CreateCoreRenewalResponse(core_request, renewal_duration, + oemcrypto_core_message); + }; + ValidateResponse(&(params.core_message), + params.extra_fields, odk_parse_func, + kdo_prepare_func); +} + +TEST(OdkTest, ProvisionResponseRoundtrip) { + ODK_ProvisioningResponseParams params; + ODK_SetDefaultProvisioningResponseParams(¶ms); + // save a copy of params.device_id as it will be zero out during the test + const uint32_t device_id_length = params.device_id_length; + uint8_t device_id[ODK_DEVICE_ID_LEN_MAX] = {0}; + memcpy(device_id, params.device_id, device_id_length); + + auto odk_parse_func = [&](const uint8_t* buf, size_t size) { + OEMCryptoResult err = ODK_ParseProvisioning( + buf, size + 16, size, &(params.core_message.nonce_values), device_id, + device_id_length, &(params.parsed_provisioning)); + return err; + }; + auto kdo_prepare_func = [&](ODK_ProvisioningRequest& core_request, + std::string* oemcrypto_core_message) { + core_request.device_id.assign(reinterpret_cast(device_id), + device_id_length); + return CreateCoreProvisioningResponse(params.parsed_provisioning, + core_request, oemcrypto_core_message); + }; + ValidateResponse(&(params.core_message), + params.extra_fields, odk_parse_func, + kdo_prepare_func); +} + +TEST(OdkSizeTest, LicenseRequest) { + uint8_t* message = nullptr; + size_t message_length = 0; + size_t core_message_length = 0; + uint16_t api_minor_version = ODK_MINOR_VERSION; + uint16_t api_major_version = 0; + uint32_t nonce = 0; + uint32_t session_id = 0; + ODK_NonceValues nonce_values{api_minor_version, api_major_version, nonce, + session_id}; + EXPECT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, + ODK_PrepareCoreLicenseRequest(message, message_length, + &core_message_length, &nonce_values)); + // the core_message_length should be appropriately set + EXPECT_EQ(ODK_LICENSE_REQUEST_SIZE, core_message_length); +} + +TEST(OdkSizeTest, RenewalRequest) { + uint8_t* message = nullptr; + size_t message_length = 0; + size_t core_message_length = 0; + uint16_t api_minor_version = ODK_MINOR_VERSION; + uint16_t api_major_version = ODK_MAJOR_VERSION; + uint32_t nonce = 0; + uint32_t session_id = 0; + ODK_ClockValues clock_values = {}; + clock_values.time_of_first_decrypt = 10; + clock_values.timer_status = ODK_CLOCK_TIMER_STATUS_LICENSE_LOADED; + uint64_t system_time_seconds = 15; + ODK_NonceValues nonce_values{api_minor_version, api_major_version, nonce, + session_id}; + EXPECT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, + ODK_PrepareCoreRenewalRequest(message, message_length, + &core_message_length, &nonce_values, + &clock_values, system_time_seconds)); + // the core_message_length should be appropriately set + EXPECT_EQ(ODK_RENEWAL_REQUEST_SIZE, core_message_length); +} + +TEST(OdkSizeTest, ReleaseRequest) { + uint8_t* message = nullptr; + size_t message_length = 0; + size_t core_message_length = 0; + uint16_t api_minor_version = ODK_MINOR_VERSION; + uint16_t api_major_version = 0; + uint32_t nonce = 0; + uint32_t session_id = 0; + ODK_ClockValues clock_values = {}; + clock_values.time_of_first_decrypt = 10; + clock_values.timer_status = ODK_CLOCK_TIMER_STATUS_LICENSE_INACTIVE; + uint64_t system_time_seconds = 15; + ODK_NonceValues nonce_values{api_minor_version, api_major_version, nonce, + session_id}; + EXPECT_EQ(OEMCrypto_SUCCESS, + ODK_PrepareCoreRenewalRequest(message, message_length, + &core_message_length, &nonce_values, + &clock_values, system_time_seconds)); + // Release requests do not have a core message. + EXPECT_GE(core_message_length, 0); +} + +TEST(OdkSizeTest, ProvisioningRequest) { + uint8_t* message = nullptr; + size_t message_length = 0; + size_t core_message_length = 0; + uint16_t api_minor_version = ODK_MINOR_VERSION; + uint16_t api_major_version = 0; + uint32_t nonce = 0; + uint32_t session_id = 0; + uint32_t device_id_length = 0; + ODK_NonceValues nonce_values{api_minor_version, api_major_version, nonce, + session_id}; + EXPECT_EQ(OEMCrypto_ERROR_SHORT_BUFFER, + ODK_PrepareCoreProvisioningRequest( + message, message_length, &core_message_length, &nonce_values, + nullptr, device_id_length)); + // the core_message_length should be appropriately set + EXPECT_EQ(ODK_PROVISIONING_REQUEST_SIZE, core_message_length); +} + +// Verify the version string contains the right version numbers. +TEST(OdkTest, CheckReleaseVersion) { + // Here are the version numbers. + std::string expected_version = std::to_string(ODK_MAJOR_VERSION) + "." + + std::to_string(ODK_MINOR_VERSION); + // Here is the version string. + EXPECT_NE(std::string(ODK_RELEASE_DATE).find(expected_version), + std::string::npos) + << "Version mismatch in odk_structs.h"; +} + +} // namespace + +} // namespace wvodk_test diff --git a/oemcrypto/odk/test/odk_test.gypi b/oemcrypto/odk/test/odk_test.gypi new file mode 100644 index 0000000..ecd720a --- /dev/null +++ b/oemcrypto/odk/test/odk_test.gypi @@ -0,0 +1,13 @@ +# Copyright 2019 Google LLC. All rights reserved. This file and proprietary +# source code may only be used and distributed under the Widevine Master License +# Agreement. + +{ + 'sources': [ + 'odk_test.cpp', + 'odk_test_helper.cpp', + 'odk_test_helper.h', + 'odk_timer_test.cpp', + ], +} + diff --git a/oemcrypto/odk/test/odk_test_helper.cpp b/oemcrypto/odk/test/odk_test_helper.cpp new file mode 100644 index 0000000..167d9e5 --- /dev/null +++ b/oemcrypto/odk/test/odk_test_helper.cpp @@ -0,0 +1,488 @@ +// Copyright 2019 Google LLC. All rights reserved. This file and proprietary +// source code may only be used and distributed under the Widevine Master +// License Agreement. + +#include "odk_test_helper.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "OEMCryptoCENCCommon.h" +#include "gtest/gtest.h" +#include "odk_structs.h" +#include "odk_structs_priv.h" + +namespace wvodk_test { + +void ODK_SetDefaultCoreFields(ODK_CoreMessage* core_message, + uint32_t message_type) { + ASSERT_TRUE(core_message != nullptr); + core_message->message_type = message_type; + core_message->message_length = 0; + core_message->nonce_values.api_minor_version = ODK_MINOR_VERSION; + core_message->nonce_values.api_major_version = ODK_MAJOR_VERSION; + core_message->nonce_values.nonce = 0xdeadbeef; + core_message->nonce_values.session_id = 0xcafebabe; +} + +void ODK_SetDefaultLicenseResponseParams(ODK_LicenseResponseParams* params) { + ODK_SetDefaultCoreFields(&(params->core_message), ODK_License_Response_Type); + params->initial_license_load = true; + params->usage_entry_present = true; + params->parsed_license = { + .enc_mac_keys_iv = {.offset = 0, .length = 1}, + .enc_mac_keys = {.offset = 2, .length = 3}, + .pst = {.offset = 4, .length = 5}, + .srm_restriction_data = {.offset = 6, .length = 7}, + .license_type = OEMCrypto_EntitlementLicense, + .nonce_required = true, + .timer_limits = + { + .soft_enforce_rental_duration = true, + .soft_enforce_playback_duration = false, + .earliest_playback_start_seconds = 10, + .rental_duration_seconds = 11, + .total_playback_duration_seconds = 12, + .initial_renewal_duration_seconds = 13, + }, + .key_array_length = 3, + .key_array = + { + { + .key_id = {.offset = 15, .length = 16}, + .key_data_iv = {.offset = 17, .length = 18}, + .key_data = {.offset = 19, .length = 20}, + .key_control_iv = {.offset = 21, .length = 22}, + .key_control = {.offset = 23, .length = 24}, + }, + { + .key_id = {.offset = 25, .length = 26}, + .key_data_iv = {.offset = 27, .length = 28}, + .key_data = {.offset = 29, .length = 30}, + .key_control_iv = {.offset = 31, .length = 32}, + .key_control = {.offset = 33, .length = 34}, + }, + { + .key_id = {.offset = 35, .length = 36}, + .key_data_iv = {.offset = 37, .length = 38}, + .key_data = {.offset = 39, .length = 40}, + .key_control_iv = {.offset = 41, .length = 42}, + .key_control = {.offset = 43, .length = 44}, + }, + }, + }; + memset(params->request_hash, 0xaa, sizeof(params->request_hash)); + params->extra_fields = { + {ODK_SUBSTRING, &(params->parsed_license.enc_mac_keys_iv), + ".enc_mac_keys_iv"}, + {ODK_SUBSTRING, &(params->parsed_license.enc_mac_keys), ".enc_mac_keys"}, + {ODK_SUBSTRING, &(params->parsed_license.pst), ".pst"}, + {ODK_SUBSTRING, &(params->parsed_license.srm_restriction_data), + ".srm_restriction_data"}, + {ODK_UINT32, &(params->parsed_license.license_type), ".license_type"}, + {ODK_UINT32, &(params->parsed_license.nonce_required), ".nonce_required"}, + {ODK_UINT32, + &(params->parsed_license.timer_limits.soft_enforce_rental_duration), + ".soft_enforce_rental_duration"}, + {ODK_UINT32, + &(params->parsed_license.timer_limits.soft_enforce_playback_duration), + ".soft_enforce_playback_duration"}, + {ODK_UINT64, + &(params->parsed_license.timer_limits.earliest_playback_start_seconds), + ".earliest_playback_start_seconds"}, + {ODK_UINT64, + &(params->parsed_license.timer_limits.rental_duration_seconds), + ".rental_duration_seconds"}, + {ODK_UINT64, + &(params->parsed_license.timer_limits.total_playback_duration_seconds), + ".total_playback_duration_seconds"}, + {ODK_UINT64, + &(params->parsed_license.timer_limits.initial_renewal_duration_seconds), + ".initial_renewal_duration_seconds"}, + {ODK_UINT32, &(params->parsed_license.key_array_length), + ".key_array_length"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[0].key_id), ".key_id"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[0].key_data_iv), + ".key_data_iv"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[0].key_data), + ".key_data"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[0].key_control_iv), + ".key_control_iv"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[0].key_control), + ".key_control"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[1].key_id), ".key_id"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[1].key_data_iv), + ".key_data_iv"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[1].key_data), + ".key_data"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[1].key_control_iv), + ".key_control_iv"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[1].key_control), + ".key_control"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[2].key_id), ".key_id"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[2].key_data_iv), + ".key_data_iv"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[2].key_data), + ".key_data"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[2].key_control_iv), + ".key_control_iv"}, + {ODK_SUBSTRING, &(params->parsed_license.key_array[2].key_control), + ".key_control"}, + {ODK_HASH, params->request_hash, ".request_hash"}, + }; +} + +void ODK_SetDefaultRenewalResponseParams(ODK_RenewalResponseParams* params) { + ODK_SetDefaultCoreFields(&(params->core_message), ODK_Renewal_Response_Type); + params->system_time = 0xfaceb00c; + params->playback_clock = 10; + params->playback_timer = 20; + params->renewal_duration = 300; + params->extra_fields = { + {ODK_UINT64, &(params->playback_clock), "playback_clock"}, + {ODK_UINT64, &(params->renewal_duration), "renewal_duration"}, + }; + params->timer_limits = { + .soft_enforce_rental_duration = false, + .soft_enforce_playback_duration = false, + .earliest_playback_start_seconds = 0, + .rental_duration_seconds = 1000, + .total_playback_duration_seconds = 2000, + .initial_renewal_duration_seconds = 300, + }; + params->clock_values = { + .time_of_license_signed = + params->system_time - params->playback_clock - 42, + .time_of_first_decrypt = params->system_time - params->playback_clock, + .time_of_last_decrypt = params->system_time - params->playback_clock, + .time_of_renewal_request = params->playback_clock, + .time_when_timer_expires = params->system_time + params->playback_timer, + .timer_status = ODK_CLOCK_TIMER_STATUS_ACTIVE, + .status = kActive, + }; +} + +void ODK_SetDefaultProvisioningResponseParams( + ODK_ProvisioningResponseParams* params) { + ODK_SetDefaultCoreFields(&(params->core_message), + ODK_Provisioning_Response_Type); + params->device_id_length = ODK_DEVICE_ID_LEN_MAX / 2; + memset(params->device_id, 0xff, params->device_id_length); + memset(params->device_id + params->device_id_length, 0, + ODK_DEVICE_ID_LEN_MAX - params->device_id_length); + params->parsed_provisioning = { + .enc_private_key = {.offset = 0, .length = 1}, + .enc_private_key_iv = {.offset = 2, .length = 3}, + .encrypted_message_key = {.offset = 4, .length = 5}, + }; + params->extra_fields = { + {ODK_UINT32, &(params->device_id_length), "device_id_length"}, + {ODK_DEVICEID, params->device_id, "device_id"}, + {ODK_UINT32, &(params->parsed_provisioning).key_type, "key_type"}, + {ODK_SUBSTRING, &(params->parsed_provisioning).enc_private_key, + "enc_private_key"}, + {ODK_SUBSTRING, &(params->parsed_provisioning).enc_private_key_iv, + "enc_private_key_iv"}, + {ODK_SUBSTRING, &(params->parsed_provisioning).encrypted_message_key, + "encrypted_message_key"}, + }; +} + +size_t ODK_FieldLength(ODK_FieldType type) { + switch (type) { + case ODK_UINT16: + return sizeof(uint16_t); + case ODK_UINT32: + return sizeof(uint32_t); + case ODK_UINT64: + return sizeof(uint64_t); + case ODK_SUBSTRING: + return sizeof(uint32_t) + sizeof(uint32_t); + case ODK_DEVICEID: + return ODK_DEVICE_ID_LEN_MAX; + case ODK_HASH: + return ODK_SHA256_HASH_SIZE; + default: + return SIZE_MAX; + } +} + +size_t ODK_AllocSize(ODK_FieldType type) { + if (type == ODK_SUBSTRING) { + return sizeof(OEMCrypto_Substring); + } + return ODK_FieldLength(type); +} + +OEMCryptoResult ODK_WriteSingleField(uint8_t* buf, const ODK_Field* field) { + if (buf == nullptr || field == nullptr || field->value == nullptr) { + return ODK_ERROR_CORE_MESSAGE; + } + switch (field->type) { + case ODK_UINT16: { + const uint16_t u16 = htobe16(*static_cast(field->value)); + memcpy(buf, &u16, sizeof(u16)); + break; + } + case ODK_UINT32: { + const uint32_t u32 = htobe32(*static_cast(field->value)); + memcpy(buf, &u32, sizeof(u32)); + break; + } + case ODK_UINT64: { + const uint64_t u64 = htobe64(*static_cast(field->value)); + memcpy(buf, &u64, sizeof(u64)); + break; + } + case ODK_SUBSTRING: { + OEMCrypto_Substring* s = static_cast(field->value); + const uint32_t off = htobe32(s->offset); + const uint32_t len = htobe32(s->length); + memcpy(buf, &off, sizeof(off)); + memcpy(buf + sizeof(off), &len, sizeof(len)); + break; + } + case ODK_DEVICEID: + case ODK_HASH: { + const size_t field_len = ODK_FieldLength(field->type); + const uint8_t* const id = static_cast(field->value); + memcpy(buf, id, field_len); + + break; + } + default: + return ODK_ERROR_CORE_MESSAGE; + } + return OEMCrypto_SUCCESS; +} + +OEMCryptoResult ODK_ReadSingleField(const uint8_t* buf, + const ODK_Field* field) { + if (buf == nullptr || field == nullptr || field->value == nullptr) { + return ODK_ERROR_CORE_MESSAGE; + } + switch (field->type) { + case ODK_UINT16: { + memcpy(field->value, buf, sizeof(uint16_t)); + uint16_t* u16p = static_cast(field->value); + *u16p = be16toh(*u16p); + break; + } + case ODK_UINT32: { + memcpy(field->value, buf, sizeof(uint32_t)); + uint32_t* u32p = static_cast(field->value); + *u32p = be32toh(*u32p); + break; + } + case ODK_UINT64: { + memcpy(field->value, buf, sizeof(uint64_t)); + uint64_t* u64p = static_cast(field->value); + *u64p = be64toh(*u64p); + break; + } + case ODK_SUBSTRING: { + OEMCrypto_Substring* s = static_cast(field->value); + uint32_t off = 0; + uint32_t len = 0; + memcpy(&off, buf, sizeof(off)); + memcpy(&len, buf + sizeof(off), sizeof(len)); + s->offset = be32toh(off); + s->length = be32toh(len); + break; + } + case ODK_DEVICEID: + case ODK_HASH: { + const size_t field_len = ODK_FieldLength(field->type); + uint8_t* const id = static_cast(field->value); + memcpy(id, buf, field_len); + break; + } + default: + return ODK_ERROR_CORE_MESSAGE; + } + return OEMCrypto_SUCCESS; +} + +OEMCryptoResult ODK_DumpSingleField(const uint8_t* buf, + const ODK_Field* field) { + if (buf == nullptr || field == nullptr || field->value == nullptr) { + return ODK_ERROR_CORE_MESSAGE; + } + switch (field->type) { + case ODK_UINT16: { + uint16_t val; + memcpy(&val, buf, sizeof(uint16_t)); + val = be16toh(val); + std::cerr << field->name << ": " << val << " = 0x" << std::hex << val + << "\n"; + break; + } + case ODK_UINT32: { + uint32_t val; + memcpy(&val, buf, sizeof(uint32_t)); + val = be32toh(val); + std::cerr << field->name << ": " << val << " = 0x" << std::hex << val + << "\n"; + break; + } + case ODK_UINT64: { + uint64_t val; + memcpy(&val, buf, sizeof(uint64_t)); + val = be64toh(val); + std::cerr << field->name << ": " << val << " = 0x" << std::hex << val + << "\n"; + break; + } + case ODK_SUBSTRING: { + uint32_t off = 0; + uint32_t len = 0; + memcpy(&off, buf, sizeof(off)); + memcpy(&len, buf + sizeof(off), sizeof(len)); + std::cerr << field->name << ": (off=" << off << ", len=" << len << ")\n"; + break; + } + case ODK_DEVICEID: + case ODK_HASH: { + const size_t field_len = ODK_FieldLength(field->type); + std::cerr << field->name << ": "; + for (size_t i = 0; i < field_len; i++) { + std::cerr << std::hex << std::setfill('0') << std::setw(2) + << static_cast(buf[i]); + } + std::cerr << "\n"; + break; + } + default: + return ODK_ERROR_CORE_MESSAGE; + } + std::cerr << std::dec; // Return to normal. + return OEMCrypto_SUCCESS; +} + +/* + * Parameters: + * [in] size_in: buffer size + * [out] size_out: bytes processed + */ +OEMCryptoResult ODK_IterFields(ODK_FieldMode mode, uint8_t* buf, + const size_t size_in, size_t* size_out, + const std::vector& fields) { + if (buf == nullptr || size_out == nullptr) { + return ODK_ERROR_CORE_MESSAGE; + } + size_t off = 0, off2 = 0; + for (size_t i = 0; i < fields.size(); i++) { + if (__builtin_add_overflow(off, ODK_FieldLength(fields[i].type), &off2) || + off2 > size_in) { + return ODK_ERROR_CORE_MESSAGE; + } + uintptr_t base = reinterpret_cast(buf); + if (__builtin_add_overflow(base, off, &base)) { + return ODK_ERROR_CORE_MESSAGE; + } + uint8_t* const buf_off = buf + off; + switch (mode) { + case ODK_WRITE: + ODK_WriteSingleField(buf_off, &fields[i]); + break; + case ODK_READ: + ODK_ReadSingleField(buf_off, &fields[i]); + break; + case ODK_DUMP: + ODK_DumpSingleField(buf_off, &fields[i]); + break; + default: + return ODK_ERROR_CORE_MESSAGE; + } + off = off2; + } + *size_out = off; + if (*size_out > size_in) { + return ODK_ERROR_CORE_MESSAGE; + } + return OEMCrypto_SUCCESS; +} + +void ODK_ExpectEqualBuf(const void* s1, const void* s2, size_t n, + const std::vector& fields) { + if (memcmp(s1, s2, n) != 0) { + const void* buffers[] = {s1, s2}; + for (int i = 0; i < 2; i++) { + char _tmp[] = "/tmp/fileXXXXXX"; + const int temp_fd = mkstemp(_tmp); + if (temp_fd >= 0) { + close(temp_fd); + } else { + std::cerr << "Failed to open temp file." << std::endl; + break; + } + std::string tmp(_tmp); + std::fstream out(tmp, std::ios::out | std::ios::binary); + out.write(static_cast(buffers[i]), n); + out.close(); + std::cerr << "buffer " << i << " dumped to " << tmp << std::endl; + size_t bytes_written; + uint8_t* buf = + const_cast(reinterpret_cast(buffers[i])); + ODK_IterFields(ODK_DUMP, buf, n, &bytes_written, fields); + } + FAIL(); + } +} + +void ODK_ResetOdkFields(std::vector* fields) { + if (fields == nullptr) { + return; + } + for (auto& field : *fields) { + if (field.value != nullptr) { + const size_t size = ODK_AllocSize(field.type); + memset(field.value, 0, size); + } + } +} + +void ODK_BuildMessageBuffer(ODK_CoreMessage* core_message, + const std::vector& extra_fields, + uint8_t** buf, uint32_t* buf_size) { + ASSERT_TRUE(core_message != nullptr); + ASSERT_TRUE(buf_size != nullptr); + std::vector total_fields = { + {ODK_UINT32, &(core_message->message_type), "message_type"}, + {ODK_UINT32, &(core_message->message_length), "message_size"}, + {ODK_UINT16, &(core_message->nonce_values.api_minor_version), + "api_minor_version"}, + {ODK_UINT16, &(core_message->nonce_values.api_major_version), + "api_major_version"}, + {ODK_UINT32, &(core_message->nonce_values.nonce), "nonce"}, + {ODK_UINT32, &(core_message->nonce_values.session_id), "session_id"}, + }; + + uint32_t header_size = 0; + for (auto& field : total_fields) { + header_size += ODK_FieldLength(field.type); + } + + total_fields.insert(total_fields.end(), extra_fields.begin(), + extra_fields.end()); + for (auto& field : total_fields) { + *buf_size += ODK_FieldLength(field.type); + } + // update message_size + *(reinterpret_cast(total_fields[1].value)) = *buf_size; + + *buf = new uint8_t[*buf_size]{}; + size_t bytes_written = 0; + // serialize ODK fields to message buffer + EXPECT_EQ(OEMCrypto_SUCCESS, ODK_IterFields(ODK_WRITE, *buf, SIZE_MAX, + &bytes_written, total_fields)); + EXPECT_EQ(bytes_written, *buf_size); +} + +} // namespace wvodk_test diff --git a/oemcrypto/odk/test/odk_test_helper.h b/oemcrypto/odk/test/odk_test_helper.h new file mode 100644 index 0000000..ad29e89 --- /dev/null +++ b/oemcrypto/odk/test/odk_test_helper.h @@ -0,0 +1,99 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary */ +/* source code may only be used and distributed under the Widevine Master */ +/* License Agreement. */ + +#ifndef WIDEVINE_ODK_TEST_ODK_TEST_HELPER_H_ +#define WIDEVINE_ODK_TEST_ODK_TEST_HELPER_H_ + +#include +#include +#include + +#include "odk_structs.h" +#include "odk_structs_priv.h" + +namespace wvodk_test { + +enum ODK_FieldType { + ODK_UINT16, + ODK_UINT32, + ODK_UINT64, + ODK_SUBSTRING, + ODK_DEVICEID, + ODK_HASH, + ODK_NUMTYPES, +}; + +enum ODK_FieldMode { + ODK_READ, + ODK_WRITE, + ODK_DUMP, +}; + +struct ODK_Field { + ODK_FieldType type; + void* value; + std::string name; +}; + +struct ODK_LicenseResponseParams { + ODK_CoreMessage core_message; + bool initial_license_load; + bool usage_entry_present; + uint8_t request_hash[ODK_SHA256_HASH_SIZE]; + ODK_TimerLimits timer_limits; + ODK_ClockValues clock_values; + ODK_ParsedLicense parsed_license; + std::vector extra_fields; +}; + +struct ODK_RenewalResponseParams { + ODK_CoreMessage core_message; + uint64_t system_time; + uint64_t playback_clock; + uint64_t renewal_duration; + ODK_TimerLimits timer_limits; + ODK_ClockValues clock_values; + uint64_t playback_timer; + std::vector extra_fields; +}; + +struct ODK_ProvisioningResponseParams { + ODK_CoreMessage core_message; + uint8_t device_id[ODK_DEVICE_ID_LEN_MAX]; + uint32_t device_id_length; + ODK_ParsedProvisioning parsed_provisioning; + std::vector extra_fields; +}; + +/* Default values in core_message for testing */ +void ODK_SetDefaultCoreFields(ODK_CoreMessage* core_message, + uint32_t message_type); +void ODK_SetDefaultLicenseResponseParams(ODK_LicenseResponseParams* params); +void ODK_SetDefaultRenewalResponseParams(ODK_RenewalResponseParams* params); +void ODK_SetDefaultProvisioningResponseParams( + ODK_ProvisioningResponseParams* params); + +size_t ODK_FieldLength(ODK_FieldType type); +size_t ODK_AllocSize(ODK_FieldType type); + +/* Copy ODK_Field to buf */ +OEMCryptoResult ODK_WriteSingleField(uint8_t* buf, const ODK_Field* field); +/* Load buf to ODK_Field */ +OEMCryptoResult ODK_ReadSingleField(const uint8_t* buf, const ODK_Field* field); +OEMCryptoResult ODK_DumpSingleField(const uint8_t* buf, const ODK_Field* field); +OEMCryptoResult ODK_IterFields(ODK_FieldMode mode, uint8_t* buf, + const size_t size_in, size_t* size_out, + const std::vector& fields); +void ODK_ExpectEqualBuf(const void* s1, const void* s2, size_t n, + const std::vector& fields); +void ODK_ResetOdkFields(std::vector* fields); + +/* Serialize core_message and extra_fields into buf */ +void ODK_BuildMessageBuffer(ODK_CoreMessage* core_message, + const std::vector& extra_fields, + uint8_t** buf, uint32_t* buf_size); + +} /* namespace wvodk_test */ + +#endif /* WIDEVINE_ODK_TEST_ODK_TEST_HELPER_H_ */ diff --git a/oemcrypto/odk/test/odk_timer_test.cpp b/oemcrypto/odk/test/odk_timer_test.cpp new file mode 100644 index 0000000..c7f00ce --- /dev/null +++ b/oemcrypto/odk/test/odk_timer_test.cpp @@ -0,0 +1,1236 @@ +/* Copyright 2019 Google LLC. All rights reserved. This file and proprietary + * source code may only be used and distributed under the Widevine Master + * License Agreement. + */ + +#include "OEMCryptoCENCCommon.h" +#include "gtest/gtest.h" +#include "odk.h" +#include "odk_structs_priv.h" + +namespace { + +// The rental clock starts when the request is signed. If any test fails +// because a time is off by exactly 1000, that is a strong indication that we +// confused rental and system clocks. The system clock should only be used +// internally on the device, because it might not be synchronized from one +// device to another. Rental clock is used in the license message from the +// server. +constexpr uint64_t kRentalClockStart = 1000u; + +// The renewal grace period is used by the server and the CDM layer to allow +// time between when the renewal is requested and when playback is cutoff if the +// renewal is not loaded. +constexpr uint64_t kGracePeriod = 5u; + +TEST(OdkTimerBasicTest, NullTest) { + // Assert that nullptr does not cause a core dump. + ODK_InitializeClockValues(nullptr, 0u); + ODK_ReloadClockValues(nullptr, 0u, 0u, 0u, kActive, 0u); + ODK_AttemptFirstPlayback(0u, nullptr, nullptr, nullptr); + ODK_UpdateLastPlaybackTime(0, nullptr, nullptr); + ASSERT_TRUE(true); +} + +TEST(OdkTimerBasicTest, Init) { + // Verify that basic initialization sets all of the fields. + ODK_ClockValues clock_values; + uint64_t time = 42; + ODK_InitializeClockValues(&clock_values, time); + EXPECT_EQ(clock_values.time_of_license_signed, time); + EXPECT_EQ(clock_values.time_of_first_decrypt, 0u); + EXPECT_EQ(clock_values.time_of_last_decrypt, 0u); + EXPECT_EQ(clock_values.time_when_timer_expires, 0u); + EXPECT_EQ(clock_values.timer_status, + ODK_CLOCK_TIMER_STATUS_LICENSE_NOT_LOADED); + EXPECT_EQ(clock_values.status, kUnused); +} + +TEST(OdkTimerBasicTest, Reload) { + // Verify that reloading clock values uses the same values + // for fields that can be saved, and sets others to 0. + ODK_ClockValues clock_values; + uint64_t time = 42u; + uint64_t lic_signed = 1u; + uint64_t first_decrypt = 2u; + uint64_t last_decrypt = 3u; + enum OEMCrypto_Usage_Entry_Status status = kInactiveUsed; + ODK_ReloadClockValues(&clock_values, lic_signed, first_decrypt, last_decrypt, + status, time); + EXPECT_EQ(clock_values.time_of_license_signed, lic_signed); + EXPECT_EQ(clock_values.time_of_first_decrypt, first_decrypt); + EXPECT_EQ(clock_values.time_of_last_decrypt, last_decrypt); + EXPECT_EQ(clock_values.time_when_timer_expires, 0u); + EXPECT_EQ(clock_values.timer_status, + ODK_CLOCK_TIMER_STATUS_LICENSE_NOT_LOADED); + EXPECT_EQ(clock_values.status, status); +} + +// All of the following test cases are derived from this base class. It +// simulates loading a license, attempting playback, and reloading a license. +class ODKTimerTest : public ::testing::Test { + public: + ODKTimerTest() { + // These are reasonable initial values for most tests. This is an unlimited + // license. Most of the tests below will add some restrictions by changing + // these values, and then verify that the ODK library behaves correctly. + timer_limits_.soft_enforce_rental_duration = true; + timer_limits_.soft_enforce_playback_duration = true; + timer_limits_.earliest_playback_start_seconds = 0u; + // A duration of 0 means infinite. + timer_limits_.rental_duration_seconds = 0u; + timer_limits_.total_playback_duration_seconds = 0u; + timer_limits_.initial_renewal_duration_seconds = 0u; + + // This is when we will attempt the first valid playback. + start_of_playback_ = + GetSystemTime(timer_limits_.earliest_playback_start_seconds + 10); + } + + protected: + void SetUp() override { + ::testing::Test::SetUp(); + // Start rental clock at kRentalClockStart. This happens when the license + // request is signed. + ODK_InitializeClockValues(&clock_values_, kRentalClockStart); + EXPECT_EQ(clock_values_.time_of_license_signed, kRentalClockStart); + } + + // Simulate loading or reloading a license in a new session. An offline + // license should have several of the clock_value's fields saved into the + // usage table. When it is reloaded those values should be reloaded. From + // these fields, the ODK function can tell if this is the first playback for + // the license, or just the first playback for this session. The key fields + // that should be saved are the status, and the times for license signed, and + // first and last playback times. + void ReloadLicense(uint64_t system_time) { + ODK_ClockValues old_clock_values = clock_values_; + // First clear out the old clock values. + memset(&clock_values_, 0, sizeof(clock_values_)); + // When the session is opened, the clock values are initialized. + ODK_InitializeClockValues(&clock_values_, 0); + // When the usage entry is reloaded, the clock values are reloaded. + ODK_ReloadClockValues(&clock_values_, + old_clock_values.time_of_license_signed, + old_clock_values.time_of_first_decrypt, + old_clock_values.time_of_last_decrypt, + old_clock_values.status, system_time); + EXPECT_EQ(clock_values_.timer_status, + ODK_CLOCK_TIMER_STATUS_LICENSE_NOT_LOADED); + // These shall not change: + EXPECT_EQ(clock_values_.time_of_license_signed, kRentalClockStart); + EXPECT_EQ(clock_values_.time_of_first_decrypt, + old_clock_values.time_of_first_decrypt); + EXPECT_EQ(clock_values_.time_of_last_decrypt, + old_clock_values.time_of_last_decrypt); + // ODK_ParseLicense sets the new timer state. + clock_values_.timer_status = ODK_CLOCK_TIMER_STATUS_LICENSE_LOADED; + } + + // Simulate loading or reloading a license, then verify that we are allowed + // playback from |start| to |stop|. If |cutoff| is not 0, then expect the + // timer to expire at that time. If |cutoff| is 0, expect the timer is not + // set. If you refer to the diagrams in "License Duration and Renewal", this + // tests a cyan bar with a green check mark. + // When nonzero |start|, |stop|, and |cutoff| are all system times. + void LoadAndAllowPlayback(uint64_t start, uint64_t stop, uint64_t cutoff) { + ReloadLicense(start); + AllowPlayback(start, stop, cutoff); + } + + // Verify that we are allowed playback from |start| to |stop|. If |cutoff| is + // not 0, then expect the timer to expire at that time. If |cutoff| is 0, + // expect the timer is not set. If you refer to the diagrams in "License + // Duration and Renewal", this tests a cyan bar with a green check mark. + // When nonzero |start|, |stop|, and |cutoff| are all system times. + void AllowPlayback(uint64_t start, uint64_t stop, uint64_t cutoff) { + ASSERT_LT(start, stop); + if (cutoff > 0) ASSERT_LE(stop, cutoff); + uint64_t timer_value; + const OEMCryptoResult result = ODK_AttemptFirstPlayback( + start, &timer_limits_, &clock_values_, &timer_value); + // After first playback, the license is active. + EXPECT_EQ(clock_values_.status, kActive); + EXPECT_EQ(clock_values_.time_when_timer_expires, cutoff); + if (cutoff > 0) { // If we expect the timer to be set. + EXPECT_EQ(result, ODK_SET_TIMER); + EXPECT_EQ(timer_value, cutoff - start); + } else { + EXPECT_EQ(result, ODK_DISABLE_TIMER); + } + CheckClockValues(start); + const uint64_t mid = (start + stop) / 2; + EXPECT_EQ(ODK_UpdateLastPlaybackTime(mid, &timer_limits_, &clock_values_), + OEMCrypto_SUCCESS); + CheckClockValues(mid); + EXPECT_EQ(ODK_UpdateLastPlaybackTime(stop, &timer_limits_, &clock_values_), + OEMCrypto_SUCCESS); + CheckClockValues(stop); + } + + // Simulate loading or reloading a license, then attempt to play from |start| + // to |cutoff|. Verify that we are allowed playback from |start| to |cutoff|, + // but playback is not allowed after |cutoff|. If you refer to the diagrams in + // "License Duration and Renewal", this tests a cyan bar with a black X. When + // nonzero |start|, and |cutoff| are all system times. + void LoadAndTerminatePlayback(uint64_t start, uint64_t cutoff) { + ReloadLicense(start); + TerminatePlayback(start, cutoff, cutoff + 10); + } + + // Attempt to play from |start| to |stop|. Verify that we are allowed playback + // from |start| to |cutoff|, but playback not allowed after |cutoff|. If you + // refer to the diagrams in "License Duration and Renewal", this tests a cyan + // bar with a black X. This assumes that |cutoff| is before |stop|. + // When nonzero |start|, |stop|, and |cutoff| are all system times. + void TerminatePlayback(uint64_t start, uint64_t cutoff, uint64_t stop) { + ASSERT_LT(start, cutoff); + ASSERT_LT(cutoff, stop); + AllowPlayback(start, cutoff, cutoff); + EXPECT_EQ( + ODK_UpdateLastPlaybackTime(cutoff + 1, &timer_limits_, &clock_values_), + ODK_TIMER_EXPIRED); + CheckClockValues(cutoff); + const uint64_t mid = (cutoff + stop) / 2; + EXPECT_EQ(ODK_UpdateLastPlaybackTime(mid, &timer_limits_, &clock_values_), + ODK_TIMER_EXPIRED); + // times do not change if playback was not allowed. + CheckClockValues(cutoff); + EXPECT_EQ(ODK_UpdateLastPlaybackTime(stop, &timer_limits_, &clock_values_), + ODK_TIMER_EXPIRED); + CheckClockValues(cutoff); + } + + // Verify that we are not allowed playback at the |start| time. If you refer + // to the diagrams in "License Duration and Renewal", this tests a cyan line + // followed by a black X. The parameter |start| is system time. + void ForbidPlayback(uint64_t start) { + ReloadLicense(start); + ODK_ClockValues old_clock_values = clock_values_; + uint64_t timer_value; + EXPECT_EQ(ODK_AttemptFirstPlayback(start, &timer_limits_, &clock_values_, + &timer_value), + ODK_TIMER_EXPIRED); + // These should not have changed. In particular, if the license was unused + // before, it should reamin unused. + EXPECT_EQ(clock_values_.time_of_license_signed, + old_clock_values.time_of_license_signed); + EXPECT_EQ(clock_values_.time_of_first_decrypt, + old_clock_values.time_of_first_decrypt); + EXPECT_EQ(clock_values_.time_of_last_decrypt, + old_clock_values.time_of_last_decrypt); + EXPECT_EQ(clock_values_.status, old_clock_values.status); + } + + // Verify that the clock values are correct. + void CheckClockValues(uint64_t time_of_last_decrypt) { + EXPECT_EQ(clock_values_.time_of_license_signed, kRentalClockStart); + EXPECT_EQ(clock_values_.time_of_first_decrypt, start_of_playback_); + EXPECT_EQ(clock_values_.time_of_last_decrypt, time_of_last_decrypt); + EXPECT_EQ(clock_values_.status, kActive); + } + + // Convert from rental time to system time. By "system time", we mean + // OEMCrypto's montonic clock. The spec does not specify a starting time for + // this clock. + uint64_t GetSystemTime(uint64_t rental_clock) { + return kRentalClockStart + rental_clock; + } + + // The end of the playback window. (using system clock) + // This is not useful if the playback duration is 0. + uint64_t EndOfPlaybackWindow() { + return start_of_playback_ + timer_limits_.total_playback_duration_seconds; + } + + // The end of the rental window. (using system clock) + // This is not useful if the rental duration is 0. + uint64_t EndOfRentalWindow() { + return GetSystemTime(timer_limits_.earliest_playback_start_seconds) + + timer_limits_.rental_duration_seconds; + } + + ODK_TimerLimits timer_limits_; + ODK_ClockValues clock_values_; + // The start of playback. This is set to the planned start at the beginning of + // the test. (using system clock) + uint64_t start_of_playback_; +}; + +TEST_F(ODKTimerTest, SimplePlayback) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 100, 0); +} + +// This tests that we are not allowed to start playback before the rental window +// opens. +TEST_F(ODKTimerTest, EarlyTest) { + timer_limits_.earliest_playback_start_seconds = 100u; + // This is earlier than we are allowed. + const uint64_t bad_start_time = + GetSystemTime(timer_limits_.earliest_playback_start_seconds - 10); + // An attempt to start playback before the timer starts is an error. + // We use the TIMER_EXPIRED error to mean both early or late. + ForbidPlayback(bad_start_time); + // And times were not updated: + EXPECT_EQ(clock_values_.status, kUnused); + // This is when we will attempt the first valid playback. + start_of_playback_ = + GetSystemTime(timer_limits_.earliest_playback_start_seconds + 10); + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 100, 0); +} + +// This runs the same test as above, but explicitly gives the ODK library a null +// pointer for the timer value. A null pointer is allowed for OEMCrypto +// implementations that do not manage their own timer. +TEST_F(ODKTimerTest, NullTimer) { + timer_limits_.earliest_playback_start_seconds = 100u; + // This is the earlier, invalid start time. + const uint64_t bad_start_time = + GetSystemTime(timer_limits_.earliest_playback_start_seconds - 10); + timer_limits_.rental_duration_seconds = 300; + timer_limits_.soft_enforce_rental_duration = false; + ReloadLicense(GetSystemTime(bad_start_time)); + + // If OEMCrypto passes in a null pointer, then the ODK should allow this. + uint64_t* timer_value_pointer = nullptr; + EXPECT_EQ(ODK_AttemptFirstPlayback(bad_start_time, &timer_limits_, + &clock_values_, timer_value_pointer), + ODK_TIMER_EXPIRED); + start_of_playback_ = + GetSystemTime(timer_limits_.earliest_playback_start_seconds + 10); + EXPECT_EQ(ODK_AttemptFirstPlayback(start_of_playback_, &timer_limits_, + &clock_values_, timer_value_pointer), + ODK_SET_TIMER); + CheckClockValues(start_of_playback_); +} + +/*****************************************************************************/ +// Note on Use Case tests. The test classes below correspond to the use cases +// in the doucment "License Duration and Renewal.". Each diagram in that +// document has a test class below to verify the use case is supported. +// +// In the document, we use realistic rental times in hours or days. In these +// tests, we will use round numbers so that it is easier to read. For example, +// instead of a seven day rental duration, we will use a 700 rental duration. +/*****************************************************************************/ + +/*****************************************************************************/ +// Streaming is the simplest use case. The user has three hours to watch the +// movie from the time of the rental. (See above for note on Use Case tests) +class ODKUseCase_Streaming : public ODKTimerTest { + public: + ODKUseCase_Streaming() { + // Rental duration = 3 hours hard. (use 300 for readability) + // Playback duration = 0 (unlimited) + timer_limits_.soft_enforce_rental_duration = false; + timer_limits_.rental_duration_seconds = 300; + timer_limits_.total_playback_duration_seconds = 0; + } +}; + +// Playback within rental duration. +TEST_F(ODKUseCase_Streaming, Case1) { + // Allow playback within the rental window. + LoadAndAllowPlayback(start_of_playback_, EndOfRentalWindow(), + EndOfRentalWindow()); +} + +// Playback exceeds rental duration. +TEST_F(ODKUseCase_Streaming, Case2) { + // Allow playback within the rental window, but not beyond. + LoadAndTerminatePlayback(start_of_playback_, EndOfRentalWindow()); +} + +// Playback with stops/restarts within rental duration, last one exceeds rental +// duration. +TEST_F(ODKUseCase_Streaming, Case3) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 50, + EndOfRentalWindow()); + LoadAndAllowPlayback(start_of_playback_ + 110, start_of_playback_ + 120, + EndOfRentalWindow()); + LoadAndTerminatePlayback(start_of_playback_ + 200, EndOfRentalWindow()); +} + +// Playback within rental duration, restart exceeds rental duration. +TEST_F(ODKUseCase_Streaming, Case4) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 50, + EndOfRentalWindow()); + ForbidPlayback(EndOfRentalWindow() + 10); +} + +// Initial playback exceeds rental duration. +TEST_F(ODKUseCase_Streaming, Case5) { + ForbidPlayback(EndOfRentalWindow() + 10); +} + +/*****************************************************************************/ +// Streaming Quick Start. The user must start watching within 30 seconds, and +// then has three hours to finish. (See above for note on Use Case tests) +class ODKUseCase_StreamingQuickStart : public ODKTimerTest { + public: + ODKUseCase_StreamingQuickStart() { + // Rental duration = 30 seconds, soft. + // Playback duration = 3 hours (use 300 for readability) + timer_limits_.soft_enforce_rental_duration = true; + timer_limits_.rental_duration_seconds = 30; + timer_limits_.total_playback_duration_seconds = 300; + timer_limits_.soft_enforce_playback_duration = false; + + // A valid start of playback time. + start_of_playback_ = + GetSystemTime(timer_limits_.rental_duration_seconds - 10); + } +}; + +// Playback starts within rental duration, continues within playback duration. +TEST_F(ODKUseCase_StreamingQuickStart, Case1) { + // As seen in the drawing, the playback window exceeds the rental window. + EXPECT_LE(EndOfRentalWindow(), EndOfPlaybackWindow()); + // Allow playback within the playback window. + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 100, + EndOfPlaybackWindow()); +} + +// Playback exceeds playback duration. +TEST_F(ODKUseCase_StreamingQuickStart, Case2) { + // Allow playback within the playback window, but not beyond. + LoadAndTerminatePlayback(start_of_playback_, EndOfPlaybackWindow()); +} + +// Playback with stops/restarts within playback duration, last one exceeds +// playback duration. +TEST_F(ODKUseCase_StreamingQuickStart, Case3) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 50, + EndOfPlaybackWindow()); + LoadAndAllowPlayback(start_of_playback_ + 110, start_of_playback_ + 120, + EndOfPlaybackWindow()); + LoadAndTerminatePlayback(start_of_playback_ + 200, EndOfPlaybackWindow()); +} + +// Initial playback exceeds rental duration. +TEST_F(ODKUseCase_StreamingQuickStart, Case4) { + ForbidPlayback(EndOfRentalWindow() + 10); +} + +/*****************************************************************************/ +// Seven Day / Two Day. The user must start watching within 7 days. Once +// started, the user has two days to finish the video. Playback is cutoff by the +// smaller of the two time limits. The first four cases start on day +// three. (See above for note on Use Case tests) +class ODKUseCase_SevenHardTwoHard_Start3 : public ODKTimerTest { + public: + ODKUseCase_SevenHardTwoHard_Start3() { + // Rental duration = 700, hard + // Playback duration = 200, hard + timer_limits_.rental_duration_seconds = 700; + timer_limits_.soft_enforce_rental_duration = false; + timer_limits_.total_playback_duration_seconds = 200; + timer_limits_.soft_enforce_playback_duration = false; + + start_of_playback_ = GetSystemTime(300); + } +}; + +// Playback within playback and rental duration. +TEST_F(ODKUseCase_SevenHardTwoHard_Start3, Case1) { + // As seen in the drawing, the playback window is within the rental window. + EXPECT_LT(EndOfPlaybackWindow(), EndOfRentalWindow()); + // Allow playback within the rental window. + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 100, + EndOfPlaybackWindow()); +} + +// Playback exceeds playback duration. +TEST_F(ODKUseCase_SevenHardTwoHard_Start3, Case2) { + // Allow playback within the playback window, but not beyond. + LoadAndTerminatePlayback(start_of_playback_, EndOfPlaybackWindow()); +} + +// Playback with stops/restarts within playback duration, last one exceeds +// playback duration. +TEST_F(ODKUseCase_SevenHardTwoHard_Start3, Case3) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 50, + EndOfPlaybackWindow()); + LoadAndAllowPlayback(start_of_playback_ + 75, start_of_playback_ + 100, + EndOfPlaybackWindow()); + LoadAndTerminatePlayback(start_of_playback_ + 125, EndOfPlaybackWindow()); +} + +// Restart exceeds playback duration. +TEST_F(ODKUseCase_SevenHardTwoHard_Start3, Case4) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 50, + EndOfPlaybackWindow()); + ForbidPlayback(EndOfPlaybackWindow() + 10); +} + +// The next four cases start on day six. +class ODKUseCase_SevenHardTwoHard_Start6 + : public ODKUseCase_SevenHardTwoHard_Start3 { + public: + ODKUseCase_SevenHardTwoHard_Start6() { + start_of_playback_ = GetSystemTime(600); + } +}; + +// Playback exceeds rental duration. +TEST_F(ODKUseCase_SevenHardTwoHard_Start6, Case5) { + // As seen in the drawing, the playback window is exceeds the rental window. + EXPECT_GT(EndOfPlaybackWindow(), EndOfRentalWindow()); + // Allow playback within the rental window. + LoadAndTerminatePlayback(start_of_playback_, EndOfRentalWindow()); +} + +// Playback with stops/restarts within playback duration, last one is terminated +// at the end of the rental duration. +TEST_F(ODKUseCase_SevenHardTwoHard_Start6, Case6) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 25, + EndOfRentalWindow()); + LoadAndAllowPlayback(start_of_playback_ + 50, start_of_playback_ + 75, + EndOfRentalWindow()); + // Allow playback that starts within rental window, but terminate at end of + // rental window. + LoadAndTerminatePlayback(start_of_playback_ + 90, EndOfRentalWindow()); +} + +// Playback within playback duration, restart exceeds rental duration. +TEST_F(ODKUseCase_SevenHardTwoHard_Start6, Case7) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 25, + EndOfRentalWindow()); + // Restart does not work after end of playback window. + ForbidPlayback(EndOfRentalWindow() + 10); +} + +// Playback exceeds rental and playback duration. +TEST_F(ODKUseCase_SevenHardTwoHard_Start6, Case8) { + LoadAndTerminatePlayback(start_of_playback_, EndOfRentalWindow()); +} + +// First playback cannot exceed rental duration. +TEST_F(ODKUseCase_SevenHardTwoHard_Start6, Case9) { + ForbidPlayback(EndOfRentalWindow() + 10); +} + +/*****************************************************************************/ +// Seven Day / Two Day. The user must start watching within 7 days. Once +// started, the user has two days to finish the video. Playback is cutoff by the +// rental duration time limits. The first four cases start on day three. (See +// above for note on Use Case tests) +class ODKUseCase_SevenHardTwoSoft_Start3 : public ODKTimerTest { + public: + ODKUseCase_SevenHardTwoSoft_Start3() { + // Rental duration = 700, hard + // Playback duration = 200, hard + timer_limits_.rental_duration_seconds = 700; + timer_limits_.soft_enforce_rental_duration = false; + timer_limits_.total_playback_duration_seconds = 200; + timer_limits_.soft_enforce_playback_duration = true; + + start_of_playback_ = GetSystemTime(300); + } +}; + +// Playback within playback and rental duration. +TEST_F(ODKUseCase_SevenHardTwoSoft_Start3, Case1) { + // As seen in the drawing, the playback window is within the rental window. + EXPECT_LT(EndOfPlaybackWindow(), EndOfRentalWindow()); + // Allow playback within the rental window. + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 100, + EndOfRentalWindow()); +} + +// Playback exceeds playback duration. +TEST_F(ODKUseCase_SevenHardTwoSoft_Start3, Case2) { + // Allow playback within the playback window, and a little after. + // Timer expires at end of rental window. + LoadAndAllowPlayback(start_of_playback_, EndOfPlaybackWindow() + 50, + EndOfRentalWindow()); +} + +// Playback with stops/restarts within playback duration, last one exceeds +// playback duration. +TEST_F(ODKUseCase_SevenHardTwoSoft_Start3, Case3) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 50, + EndOfRentalWindow()); + LoadAndAllowPlayback(start_of_playback_ + 75, start_of_playback_ + 100, + EndOfRentalWindow()); + LoadAndAllowPlayback(start_of_playback_ + 125, EndOfPlaybackWindow() + 50, + EndOfRentalWindow()); +} + +// Restart exceeds playback duration. +TEST_F(ODKUseCase_SevenHardTwoSoft_Start3, Case4) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 50, + EndOfRentalWindow()); + ForbidPlayback(EndOfPlaybackWindow() + 10); +} + +// The next four cases start on day six. +class ODKUseCase_SevenHardTwoSoft_Start6 + : public ODKUseCase_SevenHardTwoSoft_Start3 { + public: + ODKUseCase_SevenHardTwoSoft_Start6() { + start_of_playback_ = GetSystemTime(600); + } +}; + +// Playback exceeds rental duration. +TEST_F(ODKUseCase_SevenHardTwoSoft_Start6, Case5) { + // As seen in the drawing, the playback window is exceeds the rental window. + EXPECT_GT(EndOfPlaybackWindow(), EndOfRentalWindow()); + // Allow playback within the rental window. + LoadAndTerminatePlayback(start_of_playback_, EndOfRentalWindow()); +} + +// Playback with stops/restarts within playback duration, last one exceeds +// rental duration. +TEST_F(ODKUseCase_SevenHardTwoSoft_Start6, Case6) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 25, + EndOfRentalWindow()); + LoadAndAllowPlayback(start_of_playback_ + 50, start_of_playback_ + 75, + EndOfRentalWindow()); + LoadAndTerminatePlayback(start_of_playback_ + 90, EndOfRentalWindow()); +} + +// Playback within playback duration, restart exceeds rental duration. +TEST_F(ODKUseCase_SevenHardTwoSoft_Start6, Case7) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 25, + EndOfRentalWindow()); + // Restart does not work after end of playback window. + ForbidPlayback(EndOfRentalWindow() + 10); +} + +// Playback exceeds rental and playback duration. +TEST_F(ODKUseCase_SevenHardTwoSoft_Start6, Case8) { + LoadAndTerminatePlayback(start_of_playback_, EndOfRentalWindow()); +} + +// First playback cannot exceed rental duration. +TEST_F(ODKUseCase_SevenHardTwoSoft_Start6, Case9) { + ForbidPlayback(EndOfRentalWindow() + 10); +} + +/*****************************************************************************/ +// Seven Day / Two Day. The user must start watching within 7 days. Once +// started, the user has two days to finish the video. Playback is cutoff by the +// playback duration. The first four cases start on day three. (See above for +// note on Use Case tests) +class ODKUseCase_SevenSoftTwoHard_Start3 : public ODKTimerTest { + public: + ODKUseCase_SevenSoftTwoHard_Start3() { + // Rental duration = 700, hard + // Playback duration = 300, hard + timer_limits_.rental_duration_seconds = 700; + timer_limits_.soft_enforce_rental_duration = true; + timer_limits_.total_playback_duration_seconds = 200; + timer_limits_.soft_enforce_playback_duration = false; + + start_of_playback_ = GetSystemTime(300); + } +}; + +// Playback within playback and rental duration. +TEST_F(ODKUseCase_SevenSoftTwoHard_Start3, Case1) { + // As seen in the drawing, the playback window is within the rental window. + EXPECT_LT(EndOfPlaybackWindow(), EndOfRentalWindow()); + // Allow playback within the rental window. + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 100, + EndOfPlaybackWindow()); +} + +// Playback exceeds playback duration. +TEST_F(ODKUseCase_SevenSoftTwoHard_Start3, Case2) { + // Allow playback within the playback window, but not beyond. + LoadAndTerminatePlayback(start_of_playback_, EndOfPlaybackWindow()); +} + +// Playback with stops/restarts within playback duration, last one exceeds +// playback duration. +TEST_F(ODKUseCase_SevenSoftTwoHard_Start3, Case3) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 50, + EndOfPlaybackWindow()); + LoadAndAllowPlayback(start_of_playback_ + 75, start_of_playback_ + 100, + EndOfPlaybackWindow()); + LoadAndTerminatePlayback(start_of_playback_ + 125, EndOfPlaybackWindow()); +} + +// Restart exceeds playback duration. +TEST_F(ODKUseCase_SevenSoftTwoHard_Start3, Case4) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 50, + EndOfPlaybackWindow()); + ForbidPlayback(EndOfPlaybackWindow() + 10); +} + +// The next four cases start on day six. +class ODKUseCase_SevenSoftTwoHard_Start6 + : public ODKUseCase_SevenSoftTwoHard_Start3 { + public: + ODKUseCase_SevenSoftTwoHard_Start6() { + start_of_playback_ = GetSystemTime(600); + } +}; + +// Playback exceeds rental duration. +TEST_F(ODKUseCase_SevenSoftTwoHard_Start6, Case5) { + // As seen in the drawing, the playback window exceeds the rental window. + EXPECT_GT(EndOfPlaybackWindow(), EndOfRentalWindow()); + // Allow playback to continue beyond the rental window, but not beyond the + // playback window. + LoadAndTerminatePlayback(start_of_playback_, EndOfPlaybackWindow()); +} + +// Playback with stops/restarts within playback duration, last one exceeds +// rental duration. +TEST_F(ODKUseCase_SevenSoftTwoHard_Start6, Case6) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 25, + EndOfPlaybackWindow()); + LoadAndAllowPlayback(start_of_playback_ + 50, start_of_playback_ + 75, + EndOfPlaybackWindow()); + LoadAndTerminatePlayback(start_of_playback_ + 90, EndOfPlaybackWindow()); +} + +// Restart exceeds rental duration, playback exceeds playback duration. +TEST_F(ODKUseCase_SevenSoftTwoHard_Start6, Case7) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 25, + EndOfPlaybackWindow()); + LoadAndTerminatePlayback(EndOfRentalWindow() + 10, EndOfPlaybackWindow()); +} + +// Playback exceeds rental and playback duration. +TEST_F(ODKUseCase_SevenSoftTwoHard_Start6, Case8) { + LoadAndTerminatePlayback(start_of_playback_, EndOfPlaybackWindow()); +} + +// First playback cannot exceed rental duration. +TEST_F(ODKUseCase_SevenSoftTwoHard_Start6, Case9) { + ForbidPlayback(EndOfRentalWindow() + 10); +} + +/*****************************************************************************/ +// Seven Day / Two Day. The user must start watching within 7 days. Once +// started, the user has two days to finish the video. Playback is not cutoff, +// but restarts are not allowed after playback duration. The first four cases +// start on day three. (See above for note on Use Case tests) +class ODKUseCase_SevenSoftTwoSoft_Start3 : public ODKTimerTest { + public: + ODKUseCase_SevenSoftTwoSoft_Start3() { + // Rental duration = 700, hard + // Playback duration = 200, hard + timer_limits_.rental_duration_seconds = 700; + timer_limits_.soft_enforce_rental_duration = true; + timer_limits_.total_playback_duration_seconds = 200; + timer_limits_.soft_enforce_playback_duration = true; + + start_of_playback_ = GetSystemTime(300); + } +}; + +// Playback within playback and rental duration. +TEST_F(ODKUseCase_SevenSoftTwoSoft_Start3, Case1) { + // As seen in the drawing, the playback window is within the rental window. + EXPECT_LT(EndOfPlaybackWindow(), EndOfRentalWindow()); + // Allow playback within the rental window. + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 100, 0); +} + +// Playback exceeds playback duration. +TEST_F(ODKUseCase_SevenSoftTwoSoft_Start3, Case2) { + // Allow playback within the playback window, and beyond. No timer limit. + LoadAndAllowPlayback(start_of_playback_, EndOfPlaybackWindow() + 50, 0); +} + +// Playback with stops/restarts within playback duration, last one exceeds +// playback duration. +TEST_F(ODKUseCase_SevenSoftTwoSoft_Start3, Case3) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 50, 0); + LoadAndAllowPlayback(start_of_playback_ + 75, start_of_playback_ + 100, 0); + LoadAndAllowPlayback(start_of_playback_ + 125, EndOfPlaybackWindow() + 50, 0); +} + +// Restart exceeds playback duration. +TEST_F(ODKUseCase_SevenSoftTwoSoft_Start3, Case4) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 50, 0); + ForbidPlayback(EndOfPlaybackWindow() + 10); +} + +// The next four cases start on day six. +class ODKUseCase_SevenSoftTwoSoft_Start6 + : public ODKUseCase_SevenSoftTwoSoft_Start3 { + public: + ODKUseCase_SevenSoftTwoSoft_Start6() { + start_of_playback_ = GetSystemTime(600); + } +}; + +// Playback exceeds rental duration. +TEST_F(ODKUseCase_SevenSoftTwoSoft_Start6, Case5) { + // As seen in the drawing, the playback window exceeds the rental window. + EXPECT_GT(EndOfPlaybackWindow(), EndOfRentalWindow() + 25); + // Allow playback past the rental window, but within the playback window. + LoadAndAllowPlayback(start_of_playback_, EndOfRentalWindow() + 25, 0); +} + +// Playback with stops/restarts within playback duration, last one exceeds +// rental and playback duration. +TEST_F(ODKUseCase_SevenSoftTwoSoft_Start6, Case6) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 25, 0); + LoadAndAllowPlayback(start_of_playback_ + 50, start_of_playback_ + 75, 0); + LoadAndAllowPlayback(start_of_playback_ + 100, EndOfPlaybackWindow() + 50, 0); +} + +// Playback with stops/restarts within playback duration, last one exceeds +// playback duration. +TEST_F(ODKUseCase_SevenSoftTwoSoft_Start6, Case7) { + LoadAndAllowPlayback(start_of_playback_, start_of_playback_ + 25, 0); + // Allow playback to start after end of rental window, and continue after + // playback window. + LoadAndAllowPlayback(EndOfRentalWindow() + 10, EndOfPlaybackWindow() + 10, 0); + // But forbid restart after playback window. + ForbidPlayback(EndOfPlaybackWindow() + 20); +} + +// Playback exceeds rental and playback duration. +TEST_F(ODKUseCase_SevenSoftTwoSoft_Start6, Case8) { + LoadAndAllowPlayback(start_of_playback_, EndOfPlaybackWindow() + 100, 0); +} + +// First playback cannot exceed rental duration. +TEST_F(ODKUseCase_SevenSoftTwoSoft_Start6, Case9) { + ForbidPlayback(EndOfRentalWindow() + 10); +} + +class RenewalTest : public ODKTimerTest { + protected: + // Verify that a renewal can be processed and playback may continue from + // |start| to |stop|. If |cutoff| is not 0, then expect the timer to expire + // at that time. If |cutoff| is 0, expect the timer is not set. If you refer + // to the diagrams in "License Duration and Renewal", this tests a cyan bar + // with a green check mark. + void RenewAndContinue(uint64_t start, uint64_t stop, uint64_t cutoff, + uint64_t renewal_duration_seconds) { + uint64_t timer_value; + RenewAndContinue(start, stop, cutoff, renewal_duration_seconds, + &timer_value); + } + + // Verify that a renewal can be processed and playback may continue from + // |start| to |stop|. If |cutoff| is not 0, then expect the timer to expire + // at that time. If |cutoff| is 0, expect the timer is not set. If you refer + // to the diagrams in "License Duration and Renewal", this tests a cyan bar + // with a green check mark. + // This does the work of the function above, except it allows the caller to + // explicitly set the timer_value_pointer. The timer_value_pointer can be a + // nullptr. + void RenewAndContinue(uint64_t start, uint64_t stop, uint64_t cutoff, + uint64_t renewal_duration_seconds, + uint64_t* timer_value_pointer) { + ASSERT_LT(start, stop); + if (cutoff > 0) ASSERT_LE(stop, cutoff); + // We'll fake instantaneous renewal requests. Flight time not important. + clock_values_.time_of_renewal_request = start; + const OEMCryptoResult result = ODK_ComputeRenewalDuration( + &timer_limits_, &clock_values_, start, renewal_duration_seconds, + timer_value_pointer); + // After first playback, the license is active. + EXPECT_EQ(clock_values_.status, kActive); + EXPECT_EQ(clock_values_.time_when_timer_expires, cutoff); + if (cutoff > 0) { // If we expect the timer to be set. + EXPECT_EQ(result, ODK_SET_TIMER); + if (timer_value_pointer != nullptr) + EXPECT_EQ(*timer_value_pointer, cutoff - start); + } else { + EXPECT_EQ(result, ODK_DISABLE_TIMER); + } + EXPECT_EQ(ODK_UpdateLastPlaybackTime(start, &timer_limits_, &clock_values_), + OEMCrypto_SUCCESS); + CheckClockValues(start); + const uint64_t mid = (start + stop) / 2; + EXPECT_EQ(ODK_UpdateLastPlaybackTime(mid, &timer_limits_, &clock_values_), + OEMCrypto_SUCCESS); + CheckClockValues(mid); + EXPECT_EQ(ODK_UpdateLastPlaybackTime(stop, &timer_limits_, &clock_values_), + OEMCrypto_SUCCESS); + CheckClockValues(stop); + } + + // Verify that a renewal can be processed and attempt to play from |start| to + // after |cutoff|. Verify that we are allowed playback from |start| to + // |cutoff|, but playback not allowed after |cutoff|. If you refer to the + // diagrams in "License Duration and Renewal", this tests a cyan bar with a + // black X. + void RenewAndTerminate(uint64_t start, uint64_t cutoff, + uint64_t renewal_duration_seconds) { + RenewAndTerminate(start, cutoff, cutoff + 100, renewal_duration_seconds); + } + + // Verify that a renewal can be processed and attempt to play from |start| to + // |stop|. Verify that we are allowed playback from |start| to |cutoff|, but + // playback not allowed after |cutoff|. If you refer to the diagrams in + // "License Duration and Renewal", this tests a cyan bar with a black X. This + // assumes that |cutoff| is between |start| and |stop|. + void RenewAndTerminate(uint64_t start, uint64_t cutoff, uint64_t stop, + uint64_t renewal_duration_seconds) { + ASSERT_LT(start, cutoff); + ASSERT_LT(cutoff, stop); + RenewAndContinue(start, cutoff, cutoff, renewal_duration_seconds); + EXPECT_EQ( + ODK_UpdateLastPlaybackTime(cutoff + 1, &timer_limits_, &clock_values_), + ODK_TIMER_EXPIRED); + CheckClockValues(cutoff); + const uint64_t mid = (cutoff + stop) / 2; + EXPECT_EQ(ODK_UpdateLastPlaybackTime(mid, &timer_limits_, &clock_values_), + ODK_TIMER_EXPIRED); + CheckClockValues( + cutoff); // times do not change if playback was not allowed. + EXPECT_EQ(ODK_UpdateLastPlaybackTime(stop, &timer_limits_, &clock_values_), + ODK_TIMER_EXPIRED); + CheckClockValues(cutoff); + } + + // Verify that a renewal can be processed and playback may start from + // |start| to |stop|. If |cutoff| is not 0, then expect the timer to expire + // at that time. If |cutoff| is 0, expect the timer is not set. If you refer + // to the diagrams in "License Duration and Renewal", this tests a cyan bar + // with a green check mark. This is different from the previous functions, + // because the renewal is loaded before the first playback. + void RenewAndStart(uint64_t start, uint64_t stop, uint64_t cutoff, + uint64_t renewal_duration_seconds) { + ASSERT_LT(start, stop); + if (cutoff > 0) ASSERT_LE(stop, cutoff); + // We'll fake instantaneous renewal requests. Flight time not important. + clock_values_.time_of_renewal_request = start; + uint64_t timer_value; + const OEMCryptoResult result = + ODK_ComputeRenewalDuration(&timer_limits_, &clock_values_, start, + renewal_duration_seconds, &timer_value); + EXPECT_EQ(clock_values_.time_when_timer_expires, cutoff); + if (cutoff > 0) { // If we expect the timer to be set. + EXPECT_EQ(result, ODK_SET_TIMER); + EXPECT_EQ(timer_value, cutoff - start); + } else { + EXPECT_EQ(result, ODK_DISABLE_TIMER); + } + AllowPlayback(start, stop, cutoff); + } +}; + +// License with Renewal, limited by playback duration. (See above for note on +// Use Case tests) These tests are parameterized. If the parameter is 0, we +// limit the playback duration. If the parameter is 1, we limit the rental +// duration. The behavior is basically the same. +class ODKUseCase_LicenseWithRenewal + : public RenewalTest, + public ::testing::WithParamInterface { + public: + ODKUseCase_LicenseWithRenewal() { + // Either Playback or rental duration = 2 days hard + if (GetParam() == 0) { + timer_limits_.soft_enforce_rental_duration = false; + timer_limits_.rental_duration_seconds = 2000; + } else { + timer_limits_.soft_enforce_playback_duration = false; + timer_limits_.total_playback_duration_seconds = 2000; + } + timer_limits_.initial_renewal_duration_seconds = 50; + } + + void SetUp() override { + RenewalTest::SetUp(); + renewal_interval_ = + timer_limits_.initial_renewal_duration_seconds - kGracePeriod; + uint64_t next_renewal = start_of_playback_ + renewal_interval_; + // Allow playback within the initial renewal window. + LoadAndAllowPlayback(start_of_playback_, next_renewal, + next_renewal + kGracePeriod); + } + + uint64_t playback_end_restriction() { + if (GetParam() == 0) { + return EndOfRentalWindow(); + } else { + return EndOfPlaybackWindow(); + } + } + + protected: + // How long to wait before we load the next renewal. i.e. cutoff - + // kGracePeriod. This is because the renewal_duration includes the grace + // period. + uint64_t renewal_interval_; +}; + +// Playback within rental duration and renewal duration. +TEST_P(ODKUseCase_LicenseWithRenewal, Case1) { + uint64_t next_renewal = start_of_playback_ + renewal_interval_; + for (int i = 0; i < 4; i++) { + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndContinue( + current_renewal, // start: when renewal is loaded. + next_renewal, // stop: expect play allowed. + next_renewal + kGracePeriod, // cutoff: when timer expires. + timer_limits_.initial_renewal_duration_seconds); + } +} + +// Playback within rental duration, last renewal_interval_ exceeds renewal +// duration. +TEST_P(ODKUseCase_LicenseWithRenewal, Case2) { + uint64_t next_renewal = start_of_playback_ + renewal_interval_; + for (int i = 0; i < 4; i++) { + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndContinue( + current_renewal, // start: when renewal is loaded. + next_renewal, // stop: expect play allowed. + next_renewal + kGracePeriod, // cutoff: when timer expires. + timer_limits_.initial_renewal_duration_seconds); + } + // We attempt to continue playing beyond the renewal renewal_interval_. + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndTerminate(current_renewal, // start: when renewal is loaded. + next_renewal + kGracePeriod, // cutoff: when timer expires. + timer_limits_.initial_renewal_duration_seconds); +} + +// Playback interrupted by late renewal. +TEST_P(ODKUseCase_LicenseWithRenewal, Case3) { + uint64_t next_renewal = start_of_playback_ + renewal_interval_; + for (int i = 0; i < 4; i++) { + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndContinue( + current_renewal, // start: when renewal is loaded. + next_renewal, // stop: expect play allowed. + next_renewal + kGracePeriod, // cutoff: when timer expires. + timer_limits_.initial_renewal_duration_seconds); + } + // We attempt to continue playing beyond the renewal renewal_interval_. + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndTerminate(current_renewal, // start: when renewal is loaded. + next_renewal + kGracePeriod, // stop: when timer expires. + timer_limits_.initial_renewal_duration_seconds); + + const uint64_t late_renewal = next_renewal + kGracePeriod + 10; + next_renewal = late_renewal + renewal_interval_; + RenewAndContinue(late_renewal, // start: when renewal is loaded. + next_renewal, // stop: expect play allowed. + next_renewal + kGracePeriod, // cutoff: when timer expires. + timer_limits_.initial_renewal_duration_seconds); +} + +// Playback & restart within playback duration and renewal duration. Note: this +// simulates reloading a persistent license, and then reloading the last +// renewal. +TEST_P(ODKUseCase_LicenseWithRenewal, Case4) { + uint64_t next_renewal = start_of_playback_ + renewal_interval_; + for (int i = 0; i < 2; i++) { + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndContinue( + current_renewal, // start: when renewal is loaded. + next_renewal, // stop: expect play allowed. + next_renewal + kGracePeriod, // cutoff: when timer expires. + timer_limits_.initial_renewal_duration_seconds); + } + // Reload license after timer expired. + uint64_t reload_time = next_renewal + kGracePeriod + 100; + next_renewal = reload_time + renewal_interval_; + ReloadLicense(reload_time); + RenewAndStart(reload_time, next_renewal, next_renewal + kGracePeriod, + timer_limits_.initial_renewal_duration_seconds); + for (int i = 0; i < 2; i++) { + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndContinue( + current_renewal, // start: when renewal is loaded. + next_renewal, // stop: expect play allowed. + next_renewal + kGracePeriod, // cutoff: when timer expires. + timer_limits_.initial_renewal_duration_seconds); + } +} + +// Playback within renewal duration, but exceeds playback duration. +TEST_P(ODKUseCase_LicenseWithRenewal, Case5) { + uint64_t next_renewal = start_of_playback_ + renewal_interval_; + do { + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndContinue( + current_renewal, // start: when renewal is loaded. + next_renewal, // stop: expect play allowed. + next_renewal + kGracePeriod, // cutoff: when timer expires. + timer_limits_.initial_renewal_duration_seconds); + } while ((next_renewal + renewal_interval_ + kGracePeriod) < + playback_end_restriction()); + // Attempt playing beyond the playback window. + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndTerminate(current_renewal, // start: when renewal is loaded. + playback_end_restriction(), // stop: when timer expires. + timer_limits_.initial_renewal_duration_seconds); +} + +// Renewal duration increases over time. +TEST_P(ODKUseCase_LicenseWithRenewal, Case6) { + uint64_t next_renewal = start_of_playback_ + renewal_interval_; + do { + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndContinue( + current_renewal, // start: when renewal is loaded. + next_renewal, // stop: expect play allowed. + next_renewal + kGracePeriod, // cutoff: when timer expires. + renewal_interval_ + kGracePeriod); + // Renewal_Interval_ increases with each renewal. + renewal_interval_ += 100; + } while (next_renewal + renewal_interval_ + kGracePeriod < + playback_end_restriction()); + // Attempt playing beyond the playback window: + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndTerminate(current_renewal, // start: when renewal is loaded. + playback_end_restriction(), // cutoff: when timer expires. + renewal_interval_ + kGracePeriod); +} + +// Increasing renewal duration, playback exceeds last renewal. +// This is a mix between case 2 and case 6. +TEST_P(ODKUseCase_LicenseWithRenewal, Case7) { + uint64_t next_renewal = start_of_playback_ + renewal_interval_; + for (int i = 0; i < 4; i++) { + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndContinue( + current_renewal, // start: when renewal is loaded. + next_renewal, // stop: expect play allowed. + next_renewal + kGracePeriod, // cutoff: when timer expires. + renewal_interval_ + kGracePeriod); + // Renewal_Interval_ increases with each renewal. + renewal_interval_ += 100; + } + const uint64_t current_renewal = next_renewal; + next_renewal = current_renewal + renewal_interval_; + RenewAndTerminate(current_renewal, // start: when renewal is loaded. + next_renewal + kGracePeriod, // cutoff: when timer expires. + renewal_interval_ + kGracePeriod); +} + +// This is just like Case1, except we use a null pointer for the timer values in +// RenewAndContinue. It is not shown in the use case document. +TEST_P(ODKUseCase_LicenseWithRenewal, NullPointerTest) { + uint64_t next_renewal = start_of_playback_ + renewal_interval_; + const uint64_t start = next_renewal; + const uint64_t stop = start + renewal_interval_; + const uint64_t cutoff = stop + kGracePeriod; + const uint64_t renewal_duration_seconds = + timer_limits_.initial_renewal_duration_seconds; + uint64_t* timer_value_pointer = nullptr; + RenewAndContinue(start, stop, cutoff, renewal_duration_seconds, + timer_value_pointer); +} + +INSTANTIATE_TEST_CASE_P(RestrictRenewal, ODKUseCase_LicenseWithRenewal, + ::testing::Values(0, 1)); + +// Limited Duration License. (See above for notes on Use Case tests). The user +// has 15 minutes to begin watching the movie. If a renewal is not received, +// playback is terminated after 30 seconds. If a renewal is received, playback +// may continue for two hours from playback start. +class ODKUseCase_LimitedDurationLicense : public RenewalTest { + public: + ODKUseCase_LimitedDurationLicense() { + renewal_delay_ = 30u; + time_of_renewal_ = start_of_playback_ + renewal_delay_; + + timer_limits_.soft_enforce_rental_duration = true; + timer_limits_.rental_duration_seconds = 150; // 15 minutes. + timer_limits_.soft_enforce_playback_duration = false; + timer_limits_.total_playback_duration_seconds = 2000; // Two hours. + timer_limits_.initial_renewal_duration_seconds = + renewal_delay_ + kGracePeriod; + } + uint64_t renewal_delay_; + uint64_t time_of_renewal_; +}; + +// Playback started within rental window and continues. +TEST_F(ODKUseCase_LimitedDurationLicense, Case1) { + // Allow playback within the initial renewal window. + LoadAndAllowPlayback(start_of_playback_, time_of_renewal_, + time_of_renewal_ + kGracePeriod); + const uint64_t renewal_duration = 2000; // two hour renewal. + const uint64_t play_for_one_hour = GetSystemTime(1000); + RenewAndContinue(time_of_renewal_, // start: when renewal is loaded. + play_for_one_hour, // stop: expect play allowed. + EndOfPlaybackWindow(), // cutoff: when timer expires. + renewal_duration); +} + +// Playback started after rental duration. +TEST_F(ODKUseCase_LimitedDurationLicense, Case2) { + start_of_playback_ = EndOfRentalWindow() + 1; + ForbidPlayback(start_of_playback_); +} + +// Playback started within rental window but renewal not received. +TEST_F(ODKUseCase_LimitedDurationLicense, Case3) { + // Allow playback within the initial renewal window. + LoadAndTerminatePlayback(start_of_playback_, time_of_renewal_ + kGracePeriod); +} + +// Playback started within rental window, renewal is received, and playback +// continues. +TEST_F(ODKUseCase_LimitedDurationLicense, Case4) { + // Allow playback within the initial renewal window. + LoadAndAllowPlayback(start_of_playback_, time_of_renewal_, + time_of_renewal_ + kGracePeriod); + const uint64_t renewal_duration = 2000; // two hour renewal. + RenewAndTerminate(time_of_renewal_, // start: when renewal is loaded. + EndOfPlaybackWindow(), // stop: when timer expires. + renewal_duration); +} + +// Playback may be restarted. +TEST_F(ODKUseCase_LimitedDurationLicense, Case5) { + // Allow playback within the initial renewal window. + LoadAndAllowPlayback(start_of_playback_, time_of_renewal_, + time_of_renewal_ + kGracePeriod); + const uint64_t renewal_duration = 2000; // two hour renewal. + const uint64_t play_for_one_hour = GetSystemTime(1000); + RenewAndContinue(time_of_renewal_, // start: when renewal is loaded. + play_for_one_hour, // stop: expect play allowed. + EndOfPlaybackWindow(), // cutoff: when timer expires. + renewal_duration); + + const uint64_t reload_time = play_for_one_hour + 100; + ReloadLicense(reload_time); + // Simulate reloading the license, and then reloading the renewal, and then + // restarting playback. That is allowed, and playback shall be terminated at + // the end of the original playback window. + RenewAndStart(reload_time, EndOfPlaybackWindow(), EndOfPlaybackWindow(), + renewal_duration); + // But not one second more. + EXPECT_EQ(ODK_UpdateLastPlaybackTime(EndOfPlaybackWindow() + 1, + &timer_limits_, &clock_values_), + ODK_TIMER_EXPIRED); + CheckClockValues(EndOfPlaybackWindow()); +} + +// Verify that the backwards compatible function, ODK_InitializeV15Values, can +// set up the clock values and timer limits. +TEST_F(RenewalTest, V15Test) { + const uint32_t key_duration = 25; + ODK_NonceValues nonce_values; + const uint64_t license_loaded = GetSystemTime(10); + EXPECT_EQ( + ODK_InitializeV15Values(&timer_limits_, &clock_values_, &nonce_values, + key_duration, license_loaded), + OEMCrypto_SUCCESS); + const uint64_t later_on = GetSystemTime(200); + TerminatePlayback(license_loaded, license_loaded + key_duration, later_on); + const uint32_t new_key_duration = 100; + RenewAndTerminate(later_on, later_on + new_key_duration, new_key_duration); +} +} // namespace