Repository navigation
VtsHalDrmTargetTest.cpp
Ulrond edited this page Jun 18, 2026
·
1 revision
/*
* VtsHalDrmTargetTest.cpp
*
* Vendor-layer conformance test for the RDK DRM HAL (com.rdk.hal.drm),
* interface version 0.1.0.0.
*
* Binder backend: libbinder C++ (the rdk-halif-aidl generated bindings link
* `binder` + `utils`; this is NOT the NDK / libbinder_ndk backend). Builds as a
* downstream consumer of libdrm-vcurrent-cpp.so plus the linux_binder SDK.
*
* Parameterized over the registered scheme instance name:
* - "clearkey": software, deterministic, key response fabricated locally
* (ClearKey is designed for that). CI / x86 sim.
* - "widevine": target silicon; provisioning required; key exchange goes to a
* REAL (test) license server -- response is NOT fabricated.
*
* Run:
* ./VtsHalDrmTargetTest (clearkey default)
* VTS_DRM_INSTANCES=clearkey,widevine \
* VTS_WV_LICENSE_URL=https://<test-proxy>/wv ./VtsHalDrmTargetTest
*/
#include <com/rdk/hal/drm/IDrmFactory.h>
#include <com/rdk/hal/drm/IDrmPlugin.h>
#include <com/rdk/hal/drm/ICryptoPlugin.h>
#include <com/rdk/hal/drm/CryptoSchemes.h>
#include <com/rdk/hal/drm/DecryptArgs.h>
#include <com/rdk/hal/drm/KeyRequest.h>
#include <com/rdk/hal/drm/KeyType.h>
#include <com/rdk/hal/drm/KeyValue.h>
#include <com/rdk/hal/drm/Mode.h>
#include <com/rdk/hal/drm/Pattern.h>
#include <com/rdk/hal/drm/SecurityLevel.h>
#include <com/rdk/hal/drm/SubSample.h>
#include <com/rdk/hal/drm/Uuid.h>
#include <binder/IServiceManager.h>
#include <binder/ProcessState.h>
#include <utils/String16.h>
#include <utils/String8.h>
#include <utils/StrongPointer.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cstdlib>
#include <string>
#include <vector>
namespace drm = ::com::rdk::hal::drm;
using ::android::defaultServiceManager;
using ::android::IBinder;
using ::android::interface_cast;
using ::android::sp;
using ::android::String16;
using ::android::binder::Status;
// ---------------------------------------------------------------------------
// Scheme identity (standard system IDs from the DASH-IF / W3C registry).
// ---------------------------------------------------------------------------
static drm::Uuid MakeUuid(const std::array<uint8_t, 16>& bytes) {
drm::Uuid u;
u.uuid = bytes; // Uuid.aidl: byte[16] uuid -> std::array<uint8_t,16>
return u;
}
static const drm::Uuid kClearKeyUuid = MakeUuid(
{{0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02,
0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b}});
static const drm::Uuid kWidevineUuid = MakeUuid(
{{0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce,
0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed}});
static bool IsClearKey(const std::string& i) { return i == "clearkey"; }
static drm::Uuid UuidFor(const std::string& i) {
return IsClearKey(i) ? kClearKeyUuid : kWidevineUuid;
}
// ---------------------------------------------------------------------------
// ClearKey test vector. KID/KEY are arbitrary test values that must match the
// keys used when packaging the test content (e.g. with Shaka Packager).
// ---------------------------------------------------------------------------
static const std::vector<uint8_t> kCkKeyId =
{0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f};
static const std::vector<uint8_t> kCkKey =
{0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f};
static std::string B64Url(const std::vector<uint8_t>& in) {
static const char* t =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
std::string out; size_t i = 0;
for (; i + 2 < in.size(); i += 3) {
out += t[(in[i] >> 2) & 0x3f];
out += t[((in[i] << 4) | (in[i + 1] >> 4)) & 0x3f];
out += t[((in[i + 1] << 2) | (in[i + 2] >> 6)) & 0x3f];
out += t[in[i + 2] & 0x3f];
}
if (i < in.size()) {
out += t[(in[i] >> 2) & 0x3f];
if (i + 1 < in.size()) {
out += t[((in[i] << 4) | (in[i + 1] >> 4)) & 0x3f];
out += t[(in[i + 1] << 2) & 0x3f];
} else {
out += t[(in[i] << 4) & 0x3f];
}
}
return out;
}
// The ClearKey "license response": JWK Set mapping kid -> key. Local stand-in
// for the headend on the ClearKey path.
static std::vector<uint8_t> FabricateClearKeyResponse() {
std::string json =
"{\"keys\":[{\"kty\":\"oct\",\"kid\":\"" + B64Url(kCkKeyId) +
"\",\"k\":\"" + B64Url(kCkKey) + "\"}],\"type\":\"temporary\"}";
return std::vector<uint8_t>(json.begin(), json.end());
}
// ---------------------------------------------------------------------------
// Platform buffer seam. DecryptArgs carries source/destinationBufferHandle as
// opaque AVBuffer handles: non-secure -> ashmem/dma-buf; secure -> SoC secure
// allocator (gst-svp-ext: gst_svp_allocate_sec_mem / svp_allocate_secure_buffers).
// Left open: the one genuinely platform-specific piece of this test.
// ---------------------------------------------------------------------------
struct TestBuffers { int64_t srcHandle = 0; int64_t dstHandle = 0; bool secure = false; };
static TestBuffers AllocBuffers(bool secure, const std::vector<uint8_t>& cipher) {
TestBuffers b; b.secure = secure; (void)cipher;
// TODO(SoC): fill src with cipher bytes; return the integer handles the HAL expects.
return b;
}
static void FreeBuffers(TestBuffers&) { /* TODO(SoC): release via same allocator */ }
static std::vector<uint8_t> ReadOut(const TestBuffers&, size_t) { return {}; }
// ---------------------------------------------------------------------------
// Fixture
// ---------------------------------------------------------------------------
class DrmHalTest : public ::testing::TestWithParam<std::string> {
protected:
std::string instance_;
sp<drm::IDrmFactory> factory_;
sp<drm::IDrmPlugin> drm_;
void SetUp() override {
instance_ = GetParam();
String16 name(("com.rdk.hal.drm.IDrmFactory/" + instance_).c_str());
ASSERT_TRUE(defaultServiceManager()->isDeclared(name))
<< "HAL.DRM.1: factory instance not declared: " << instance_;
sp<IBinder> binder = defaultServiceManager()->waitForService(name);
ASSERT_NE(binder, nullptr) << "no IDrmFactory/" << instance_;
factory_ = interface_cast<drm::IDrmFactory>(binder);
ASSERT_NE(factory_, nullptr);
Status st = factory_->createDrmPlugin(UuidFor(instance_),
String16("vts.drm.test"), &drm_);
ASSERT_TRUE(st.isOk()) << "createDrmPlugin: " << st.toString8().c_str();
ASSERT_NE(drm_, nullptr);
}
std::vector<uint8_t> OpenAndProvisionKeys(drm::SecurityLevel level) {
std::vector<uint8_t> sid;
EXPECT_TRUE(drm_->openSession(level, &sid).isOk());
EXPECT_FALSE(sid.empty());
std::vector<uint8_t> initData = kCkKeyId; // WV: real PSSH from asset
std::vector<drm::KeyValue> opt;
drm::KeyRequest req;
Status st = drm_->getKeyRequest(sid, initData, String16("video/mp4"),
drm::KeyType::STREAMING, opt, &req);
EXPECT_TRUE(st.isOk()) << "getKeyRequest: " << st.toString8().c_str();
EXPECT_FALSE(req.request.empty());
std::vector<uint8_t> response =
IsClearKey(instance_) ? FabricateClearKeyResponse()
: AcquireFromTestLicenseServer(req);
drm::KeySetId keySetId;
st = drm_->provideKeyResponse(sid, response, &keySetId);
EXPECT_TRUE(st.isOk()) << "provideKeyResponse: " << st.toString8().c_str();
return sid;
}
// Widevine only: forward the opaque challenge to the test license server and
// return the body verbatim. Deliberately NOT a synthesizer -- attestation is
// part of what we validate.
std::vector<uint8_t> AcquireFromTestLicenseServer(const drm::KeyRequest& req) {
const char* url = std::getenv("VTS_WV_LICENSE_URL");
(void)url; (void)req; // TODO(integrator): HTTP POST req.request -> raw body
ADD_FAILURE() << "wire VTS_WV_LICENSE_URL to a Widevine test server";
return {};
}
};
// 1. Discovery & capabilities (HAL.DRM.1 / HAL.DRM.2)
TEST_P(DrmHalTest, FactoryAdvertisesScheme) {
drm::CryptoSchemes schemes;
ASSERT_TRUE(factory_->getSupportedCryptoSchemes(&schemes).isOk());
EXPECT_FALSE(schemes.uuids.empty());
EXPECT_FALSE(schemes.mimeTypes.empty());
EXPECT_NE(std::find(schemes.uuids.begin(), schemes.uuids.end(), UuidFor(instance_)),
schemes.uuids.end());
}
TEST_P(DrmHalTest, MandatoryProperties) {
String16 v;
EXPECT_TRUE(drm_->getPropertyString(String16("vendor"), &v).isOk());
EXPECT_GT(v.size(), 0u);
EXPECT_TRUE(drm_->getPropertyString(String16("version"), &v).isOk());
EXPECT_TRUE(drm_->getPropertyString(String16("description"), &v).isOk());
}
// 2. Provisioning
TEST_P(DrmHalTest, ProvisioningStateReadable) {
if (IsClearKey(instance_)) GTEST_SKIP() << "clearkey needs no provisioning";
std::vector<uint8_t> id;
Status st = drm_->getPropertyByteArray(String16("deviceUniqueId"), &id);
EXPECT_TRUE(st.isOk()) << "deviceUniqueId unreadable -- check keybox provisioning";
EXPECT_FALSE(id.empty());
}
// 3. Session lifecycle
TEST_P(DrmHalTest, OpenCloseSession) {
std::vector<uint8_t> sid;
ASSERT_TRUE(drm_->openSession(drm::SecurityLevel::SW_SECURE_CRYPTO, &sid).isOk());
EXPECT_FALSE(sid.empty());
EXPECT_TRUE(drm_->closeSession(sid).isOk());
}
TEST_P(DrmHalTest, CloseInvalidSessionFails) {
std::vector<uint8_t> bogus = {0xde, 0xad, 0xbe, 0xef};
EXPECT_FALSE(drm_->closeSession(bogus).isOk());
}
TEST_P(DrmHalTest, SecurityLevelHonoursRequest) {
std::vector<uint8_t> sid;
ASSERT_TRUE(drm_->openSession(drm::SecurityLevel::SW_SECURE_CRYPTO, &sid).isOk());
drm::SecurityLevel got;
ASSERT_TRUE(drm_->getSecurityLevel(sid, &got).isOk());
EXPECT_LE(static_cast<int>(got), static_cast<int>(drm::SecurityLevel::HW_SECURE_ALL));
drm_->closeSession(sid);
}
// 4. Full key flow (HAL.DRM.4)
TEST_P(DrmHalTest, KeyRequestResponseRoundTrip) {
drm::SecurityLevel level = IsClearKey(instance_)
? drm::SecurityLevel::SW_SECURE_CRYPTO : drm::SecurityLevel::HW_SECURE_CRYPTO;
std::vector<uint8_t> sid = OpenAndProvisionKeys(level);
ASSERT_FALSE(sid.empty());
std::vector<drm::KeyValue> status;
ASSERT_TRUE(drm_->queryKeyStatus(sid, &status).isOk());
EXPECT_FALSE(status.empty());
EXPECT_TRUE(drm_->removeKeys(sid).isOk());
drm_->closeSession(sid);
}
// 5. Decrypt via ICryptoPlugin bound to the keyed session (HAL.DRM.5)
TEST_P(DrmHalTest, DecryptSubsampleCtr) {
drm::SecurityLevel level = IsClearKey(instance_)
? drm::SecurityLevel::SW_SECURE_CRYPTO : drm::SecurityLevel::HW_SECURE_CRYPTO;
std::vector<uint8_t> sid = OpenAndProvisionKeys(level);
ASSERT_FALSE(sid.empty());
sp<drm::ICryptoPlugin> crypto;
std::vector<uint8_t> cryptoInit;
ASSERT_TRUE(factory_->createCryptoPlugin(UuidFor(instance_), cryptoInit, &crypto).isOk());
ASSERT_NE(crypto, nullptr);
ASSERT_TRUE(crypto->setMediaDrmSession(sid).isOk());
bool secure = false;
crypto->requiresSecureDecoderComponent(String16("video/mp4"), &secure);
std::vector<uint8_t> cipher = /* packaged CENC sample bytes */ {};
TestBuffers buf = AllocBuffers(secure, cipher);
drm::DecryptArgs args;
args.secure = secure;
args.keyId = kCkKeyId; // WV: KID from the PSSH
args.iv = std::vector<uint8_t>(16, 0x00); // per-sample IV from senc
args.mode = drm::Mode::AES_CTR; // cenc; AES_CBC for cbcs
args.pattern = drm::Pattern{0, 0}; // 0/0 cenc; e.g. 1/9 cbcs
args.subSamples = { drm::SubSample{32, static_cast<int32_t>(cipher.size())} };
args.sourceBufferHandle = buf.srcHandle;
args.destinationBufferHandle = buf.dstHandle;
int32_t bytesDecrypted = 0;
Status st = crypto->decrypt(args, &bytesDecrypted);
EXPECT_TRUE(st.isOk()) << "decrypt: " << st.toString8().c_str();
if (!secure && IsClearKey(instance_)) {
std::vector<uint8_t> out = ReadOut(buf, cipher.size());
// EXPECT_EQ(out, expectedPlaintext); // bit-exact once buffers are wired
} else {
EXPECT_GE(bytesDecrypted, 0); // secure: host can't read plaintext
}
FreeBuffers(buf);
drm_->closeSession(sid);
}
// 6. Secure-decoder policy per level
TEST_P(DrmHalTest, SecureDecoderRequirement) {
bool reqLow = true, reqHigh = false;
drm_->requiresSecureDecoder(String16("video/mp4"),
drm::SecurityLevel::SW_SECURE_CRYPTO, &reqLow);
drm_->requiresSecureDecoder(String16("video/mp4"),
drm::SecurityLevel::HW_SECURE_ALL, &reqHigh);
if (IsClearKey(instance_)) EXPECT_FALSE(reqLow);
else EXPECT_TRUE(reqHigh);
}
// 7. Session accounting doesn't leak
TEST_P(DrmHalTest, SessionCountReturnsToBaseline) {
std::vector<std::vector<uint8_t>> sids;
for (int i = 0; i < 3; ++i) {
std::vector<uint8_t> s;
if (drm_->openSession(drm::SecurityLevel::SW_SECURE_CRYPTO, &s).isOk())
sids.push_back(s);
}
for (auto& s : sids) EXPECT_TRUE(drm_->closeSession(s).isOk());
}
// ---------------------------------------------------------------------------
static std::vector<std::string> Instances() {
const char* env = std::getenv("VTS_DRM_INSTANCES");
std::string s = env ? env : "clearkey";
std::vector<std::string> out; size_t p = 0, c;
while ((c = s.find(',', p)) != std::string::npos) { out.push_back(s.substr(p, c - p)); p = c + 1; }
out.push_back(s.substr(p));
return out;
}
INSTANTIATE_TEST_SUITE_P(PerScheme, DrmHalTest, ::testing::ValuesIn(Instances()),
[](const auto& info) { return info.param; });
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
android::ProcessState::self()->startThreadPool();
return RUN_ALL_TESTS();
}