Skip to content

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();
}

Clone this wiki locally