DRM HAL Vendor Conformance Test - rdkcentral/rdk-halif-aidl GitHub Wiki
A guide to testing a vendor's com.rdk.hal.drm implementation directly, below
OCDM and the media pipeline, using a VTS-style binder client.
Applies to interface version 0.1.0.0 (drm/current).
The DRM HAL is the seam between the RDK middleware and the SoC vendor's DRM library (the Widevine/PlayReady implementation running in or around the TEE). A test that drives the HAL directly isolates the vendor implementation from everything above it ā OCDM, the GStreamer decryptor, Rialto, and the application. If a frame decrypts here, every failure higher up is wiring rather than crypto.
This mirrors the approach noted in the HAL documentation: the same Android VTS methodology can be applied early in SoC bring-up, because this interface is a near-identical port of Android's DRM/Crypto plugin model.
| Area | Interface surface | Requirement |
|---|---|---|
| Discovery & capabilities |
IDrmFactory.getSupportedCryptoSchemes(), factory registration name |
HAL.DRM.1 / HAL.DRM.2 |
| Mandatory properties | IDrmPlugin.getPropertyString("vendor"/"version"/"description") |
ā |
| Provisioning |
getPropertyByteArray("deviceUniqueId"), getProvisionRequest / provideProvisionResponse
|
ā |
| Session lifecycle |
openSession, getSecurityLevel, closeSession
|
HAL.DRM.3 |
| Key flow |
getKeyRequest ā provideKeyResponse ā queryKeyStatus ā removeKeys
|
HAL.DRM.4 |
| Decryption |
IDrmFactory.createCryptoPlugin, ICryptoPlugin.setMediaDrmSession, ICryptoPlugin.decrypt
|
HAL.DRM.5 |
| Secure-decoder policy |
requiresSecureDecoder, ICryptoPlugin.requiresSecureDecoderComponent
|
ā |
| Session accounting | getNumberOfSessions |
ā |
The test is parameterized over the registered scheme instance name so a single binary serves both bring-up and CI:
-
clearkeyā software, deterministic, no network. The "license response" is a ClearKey JWK Set fabricated locally (ClearKey is designed for this), so this configuration runs in CI and on an x86 simulation. It validates the test harness and the pipeline logic. -
widevineā runs on target silicon. Provisioning must be present, and the key exchange goes through a real (test) Widevine license server ā the response is not fabricated, because device attestation is part of what is being validated. Secure-decode/SVP expectations are asserted at the hardware security levels.
āāāāāāāāāāāāāāāā same VtsHalDrmTargetTest binary āāāāāāāāāāāāāāāā
ā ā
clearkey ā SW_SECURE_CRYPTO ā local JWK response ā CI / x86 sim ā
widevine ā HW_SECURE_CRYPTO ā test license server ā target silicon ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
The standard system IDs used to select the scheme:
| Scheme | UUID |
|---|---|
| ClearKey | 1077efec-c0b2-4d02-ace3-3c1e52e2fb4b |
| Widevine | edef8ba9-79d6-4ace-a3c8-27dcd51d21ed |
provideKeyResponse is the single point where content keys enter the HAL.
openSession(securityLevel) ā sessionId
getKeyRequest(sessionId, initData,
"video/mp4", STREAMING, {}) ā KeyRequest{ request, requestType, defaultUrl }
ā
āā clearkey: fabricate JWK Set locally { "keys":[{ "kty":"oct","kid":..,"k":.. }],"type":"temporary" }
āā widevine: POST request blob to the (test) license server, take the body verbatim
ā
provideKeyResponse(sessionId, response) ā keySetId
createCryptoPlugin(uuid, initData) ā ICryptoPlugin
setMediaDrmSession(sessionId) // bind crypto context to the keyed session
decrypt(DecryptArgs{ ... }) // per-sample CENC decryption
For clearkey, the KID/key pair in the fabricated response must match the keys
used when packaging the test content (e.g. with Shaka Packager). For widevine,
initData is the CENC PSSH from the packaged asset.
Everything in the test is portable except two hooks, which are genuinely SoC-specific and left open on purpose:
-
Buffer allocation.
DecryptArgs.sourceBufferHandle/destinationBufferHandleare opaque AVBuffer handles. Non-secure (ClearKey) is plain ashmem/dma-buf; the secure path uses the SoC secure allocator (viagst-svp-ext, e.g.gst_svp_allocate_sec_mem/svp_allocate_secure_buffers). This is where the SVP backend meets the test. -
License acquisition (Widevine only). A plain forwarder: POST the opaque challenge to the test license server / proxy and return the raw body. It must not synthesize a license.
The check in the decrypt test is asymmetric, and must be:
- Non-secure (ClearKey): read the destination buffer back and assert bit-exact plaintext against the known key/IV.
- Secure / SVP (Widevine at HW levels): the host cannot read the buffer. Assert the secure handle is valid and let the downstream HW decode (pipeline test) confirm correctness.
A test that attempts to read plaintext on the secure path is itself a defect.
For Widevine L1, no license will issue until the device is provisioned (the
keybox in the SoC secure store). The most common bring-up failure is license
acquisition failing before decrypt is ever reached because the board carries
no/expired keybox. The ProvisioningStateReadable test reads deviceUniqueId
as an early signal; confirm with your SoC vendor whether the reference board has
a production or test keybox.
Excerpts from the full test (VtsHalDrmTargetTest.cpp, committed alongside this
page). These use the libbinder C++ backend ā namespace com::rdk::hal::drm,
android::sp<>, android::String16, android::binder::Status,
defaultServiceManager() ā matching what rdk-halif-aidl generates.
namespace drm = ::com::rdk::hal::drm;
using ::android::defaultServiceManager;
using ::android::interface_cast;
using ::android::sp;
using ::android::String16;
using ::android::binder::Status;
String16 name(("com.rdk.hal.drm.IDrmFactory/" + instance_).c_str()); // e.g. ".../clearkey"
ASSERT_TRUE(defaultServiceManager()->isDeclared(name)); // HAL.DRM.1
sp<drm::IDrmFactory> factory =
interface_cast<drm::IDrmFactory>(defaultServiceManager()->waitForService(name));
ASSERT_NE(factory, nullptr);
sp<drm::IDrmPlugin> plugin;
Status st = factory->createDrmPlugin(UuidFor(instance_), String16("vts.drm.test"), &plugin);
ASSERT_TRUE(st.isOk()) << st.toString8().c_str();Note the C++-backend call conventions: out-params are passed by pointer as the
last argument, every method returns android::binder::Status, AIDL String
maps to String16, byte[] to std::vector<uint8_t>, and Uuid (byte[16])
to std::array<uint8_t,16>.
provideKeyResponse is where content keys enter the HAL. On ClearKey the
response is fabricated locally; on Widevine it is the verbatim body from a real
test license server.
std::vector<uint8_t> sid;
plugin->openSession(drm::SecurityLevel::SW_SECURE_CRYPTO, &sid);
drm::KeyRequest req;
std::vector<drm::KeyValue> opt;
plugin->getKeyRequest(sid, /*initData=*/kid, String16("video/mp4"),
drm::KeyType::STREAMING, opt, &req); // req.request -> license server
std::vector<uint8_t> response =
IsClearKey(instance_) ? FabricateClearKeyResponse() // local headend stand-in
: AcquireFromTestLicenseServer(req); // real, forwarded only
drm::KeySetId keySetId;
plugin->provideKeyResponse(sid, response, &keySetId); // <-- keys now loadedThe ClearKey response is just a JWK Set mapping KID to key ā the KID/key here must match what was used to package the test content:
static std::vector<uint8_t> FabricateClearKeyResponse() {
std::string json =
"{\"keys\":[{\"kty\":\"oct\",\"kid\":\"" + B64Url(kCkKeyId) +
"\",\"k\":\"" + B64Url(kCkKey) + "\"}],\"type\":\"temporary\"}";
return {json.begin(), json.end()};
}sp<drm::ICryptoPlugin> crypto;
factory->createCryptoPlugin(UuidFor(instance_), /*initData=*/{}, &crypto);
crypto->setMediaDrmSession(sid); // bind crypto ctx to keyed session
bool secure = false;
crypto->requiresSecureDecoderComponent(String16("video/mp4"), &secure);
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{/*clear*/32, /*enc*/(int32_t)cipher.size()} };
args.sourceBufferHandle = buf.srcHandle; // SoC buffer seam (see below)
args.destinationBufferHandle = buf.dstHandle;
int32_t bytesDecrypted = 0;
ASSERT_TRUE(crypto->decrypt(args, &bytesDecrypted).isOk());
if (!secure && IsClearKey(instance_)) {
// non-secure: read the buffer back, assert bit-exact plaintext
} else {
// secure/SVP: host cannot read plaintext; assert handle validity + downstream decode
EXPECT_GE(bytesDecrypted, 0);
}rdk-halif-aidl is a two-stage CMake build against an in-tree Android Binder
port (the linux_binder SDK). It does not use Android Soong / aidl_interface,
and the generated C++ uses the libbinder C++ backend (links binder and
utils) ā not the NDK backend / libbinder_ndk. The conformance test is a
downstream consumer of the generated libdrm-vcurrent-cpp.so plus that SDK.
./build_binder.sh # Android Binder AIDL toolchain + binder librariesThe generated C++ under drm/current/{src,include} is gitignored toolchain
output and is not present in a fresh clone ā regenerate it first.
./build_interfaces.sh drm # regenerate drm/current/{src,include}
./build_modules.sh drm # compile -> out/target/lib/halif/libdrm-vcurrent-cpp.soCross-compile for target with the usual compiler env vars:
CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ ./build_interfaces.sh drmThe test's own CMakeLists.txt mirrors the module pattern: include the binder
SDK headers and the DRM include/, point at the binder + halif lib dirs, and
link binder, utils, drm-vcurrent-cpp and gtest.
cmake_minimum_required(VERSION 3.16)
project(VtsHalDrmTargetTest LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
add_executable(VtsHalDrmTargetTest VtsHalDrmTargetTest.cpp)
target_include_directories(VtsHalDrmTargetTest PRIVATE
"${BINDER_SDK_DIR}/include/binder_sdk"
"${HALIF_INCLUDE_DIR}" # drm/current/include
)
target_link_directories(VtsHalDrmTargetTest PRIVATE
"${BINDER_SDK_DIR}/lib/binder"
"${HALIF_LIB_DIR}" # out/target/lib/halif
)
target_link_libraries(VtsHalDrmTargetTest PRIVATE
binder utils drm-vcurrent-cpp gtest gtest_main
)cmake -S . -B build \
-DBINDER_SDK_DIR=/path/to/out/target \
-DBINDER_SDK_INCLUDE_DIR=/path/to/out/build \
-DHALIF_LIB_DIR=/path/to/out/target/lib/halif \
-DHALIF_INCLUDE_DIR=/path/to/drm/current/include
cmake --build buildBackend note. Because the bindings are the libbinder C++ backend, the test uses
defaultServiceManager()/getService(String16("com.rdk.hal.drm.IDrmFactory/<instance>")), thecom::rdk::hal::drmnamespace,android::binder::Status, andProcessState::self()->startThreadPool()ā not the NDKAServiceManager_*/::ndk::ScopedAStatus/aidl::APIs.
# default: clearkey only (deterministic / x86 sim)
./build/VtsHalDrmTargetTest
# target silicon: both schemes, real Widevine test server
VTS_DRM_INSTANCES=clearkey,widevine \
VTS_WV_LICENSE_URL=https://<your-test-proxy>/wv \
./build/VtsHalDrmTargetTestThis repo wires nothing into CI ā its own checks are on-demand shell scripts
(tests/smoke_test.sh, tests/fake-yocto/) that assert the libraries build.
The conformance test is an additional consumer binary: run it by hand on target,
or wire it into your integration rig / HPK certification flow. There is no
Android-style VTS runner or atest in this repository.
This page covers rung 1. The full integration ladder:
- HAL / VTS (this page) ā vendor DRM HAL in isolation.
-
OCDM ā
opencdm_gstreamer_session_decrypt()through the OCDM-Widevine / OCDM-Clearkey plugin, in-place (HD) path. -
Pipeline ā full GStreamer pipeline (Rialto
rialtoserveron target), HD in-place then UHD / SVP secure path. - Application ā real player via EME end to end, including HDCP enforcement.
Run bottom-up: a pass at rung n means rung nā1 is solid.
- Interface definition:
drm/0.1.0.0 -
IDrmFactory.aidl,IDrmPlugin.aidl,ICryptoPlugin.aidl - RDK SVP design specification (in-place vs handle-based decryption)
-
gst-svp-ext(secure buffer abstraction) - https://github.com/rdkcentral/rdk-halif-aidl/wiki/VtsHalDrmTargetTest.cpp