feat(drm): license a track only when its own KID has no key

A vault key for the track's KID now stands in for the licence on the client and the server (PlayReady included). decrypt_verified licenses any other header KID the file uses.
This commit is contained in:
imSp4rky
2026-11-12 18:03:16 -07:00
parent 16fbf5d17a
commit 7f91a1d36b
15 changed files with 343 additions and 66 deletions
@@ -56,7 +56,15 @@ async def _run_batch(monkeypatch, tracks, cdm_type, calls):
return session
def fake_single(
service, title, track, pssh_str, drm_type, request, sources=None, refusal=None, vault_only=False, shared=None
service,
title,
track,
pssh_str,
drm_type,
request,
sources=None,
refusal=None,
**_,
):
calls.append((pssh_str, drm_type))
return {"00" * 16: "key"}
@@ -132,3 +132,12 @@ def test_a_refused_server_cdm_licence_logs_one_warning(caplog):
assert svc.proxy_license(b"chal", track, "widevine") == b"chal"
assert [r.levelname for r in caplog.records if r.levelname in ("WARNING", "ERROR")] == ["WARNING"]
def test_the_kid_dl_needs_goes_to_the_server():
svc = _service("widevine", {"keys": {KID.hex: "k"}, "drm_type": "widevine"})
track = SimpleNamespace(id="vid", drm=[_Widevine()])
svc.get_widevine_license(challenge=b"", title=None, track=track, kid=KID)
assert svc.client.posts[0]["kid"] == KID.hex
@@ -115,12 +115,25 @@ async def test_batch_reports_vault_keys_from_the_init_segment_pssh(monkeypatch):
session = SessionEntry(session_id="sess", service_tag="SVC", service_instance=SimpleNamespace())
session.tracks = {"vid": track}
probed: list = []
async def validated(sid, req):
return session
def fake_single(
service, title, track, pssh_str, drm_type, request, sources=None, refusal=None, vault_only=False, shared=None
service,
title,
track,
pssh_str,
drm_type,
request,
sources=None,
refusal=None,
vault_only=False,
shared=None,
track_kid=None,
):
probed.append(track_kid)
if pssh_str == "init":
sources[CDM_KID.hex] = "sqlite"
return {CDM_KID.hex: "v"}
@@ -147,6 +160,66 @@ async def test_batch_reports_vault_keys_from_the_init_segment_pssh(monkeypatch):
assert payload["keys"] == {"vid": {CDM_KID.hex: "v"}}
assert payload["vault_keys"] == [CDM_KID.hex]
assert session.served_keys == {CDM_KID.hex: ("v", "sqlite")}
assert probed == [CDM_KID, CDM_KID]
async def test_batch_asks_again_when_a_cached_vault_answer_lacks_the_track_kid(monkeypatch):
"""Renditions that share a header: a vault answer for one track's KID does not serve a track with another KID."""
kids = {"a": KID, "b": CDM_KID, "c": CDM_KID}
session = SessionEntry(session_id="sess", service_tag="SVC", service_instance=SimpleNamespace())
session.tracks = {
tid: SimpleNamespace(id=tid, drm=[_WV("shared", [KID, CDM_KID])], get_key_id=lambda init, k=kid: k)
for tid, kid in kids.items()
}
asked: list = []
async def validated(sid, req):
return session
def fake_single(
service, title, track, pssh_str, drm_type, request, sources=None, refusal=None, track_kid=None, **_
):
asked.append(track_kid)
if track_kid == KID:
sources[KID.hex] = "sqlite"
return {KID.hex: "v"}
return {KID.hex: "v", CDM_KID.hex: "c"}
monkeypatch.setattr(handlers, "get_validated_session", validated)
monkeypatch.setattr(handlers, "server_cdm_allowed", lambda request, tag: True)
monkeypatch.setattr(handlers, "ensure_track_drm", lambda track, session=None, init_data=None: None)
monkeypatch.setattr(handlers, "find_title_for_track", lambda tid, session: SimpleNamespace())
monkeypatch.setattr(handlers, "detect_cdm_type_for_service", lambda tag, cfg: "widevine")
monkeypatch.setattr(handlers, "resolve_device_name", lambda *a, **k: "dev")
monkeypatch.setattr(handlers, "fetch_init_segment", lambda track, sess: b"init")
monkeypatch.setattr(handlers, "handle_single_server_cdm", fake_single)
monkeypatch.setattr(handlers.config, "serve", {"users": {}}, raising=False)
resp = await handlers.session_license_handler(
{"mode": "server_cdm", "track_ids": list(kids), "drm_type": "widevine"}, "sess", None
)
payload = json.loads(resp.body)
assert asked == [KID, CDM_KID]
assert payload["keys"]["c"] == {KID.hex: "v", CDM_KID.hex: "c"}
@pytest.mark.parametrize("drm", [["drm"], []], ids=["with-drm", "clear"])
def test_the_server_reads_the_head_of_a_one_file_track_with_drm(drm):
from unshackle.core.tracks import Video
fetched: list = []
track = SimpleNamespace(
id="v",
data={},
descriptor=Video.Descriptor.URL,
drm=drm,
get_init_segment=lambda session: fetched.append(session) or b"head",
)
head = handlers.fetch_init_segment(track, "svc-session")
assert (head, fetched) == ((b"head", ["svc-session"]) if drm else (None, []))
async def test_server_tells_a_remote_vault_only_when_it_served_the_pair(monkeypatch, session):
@@ -27,6 +27,8 @@ from unshackle.core.drm import PlayReady
pytestmark = pytest.mark.unit
REAL_CHECK_VAULTS = handlers.check_vaults
KID_A = UUID("161219ec3df64e0eadbb3827c0ccc46d")
KID_B = UUID("43de497c6d1945f49acc2951696ec628")
@@ -161,3 +163,59 @@ async def test_uncovered_kid_reaches_the_client(server_env, fake_playready, monk
)
assert any(KID_B.hex in r["message"] for r in buf.since(0)), "client gets no reason for the missing key"
class _VaultsHolding:
"""Server vaults that hold a content key for ``held`` only."""
def __init__(self, *held: UUID):
self.held = held
self.vaults = [SimpleNamespace(name="v1")]
def get_key(self, kid):
return ("k" + kid.hex[:4], self.vaults[0]) if kid in self.held else (None, None)
@pytest.mark.parametrize(
("track_kid", "found"), [(None, None), (KID_A, {KID_A.hex}), (KID_B, None)], ids=["every-kid", "held", "not-held"]
)
def test_check_vaults_answers_for_the_track_kid(monkeypatch, track_kid, found):
monkeypatch.setattr(handlers, "load_server_vaults", lambda name: _VaultsHolding(KID_A))
hit = handlers.check_vaults([KID_A, KID_B], "SVC", track_kid=track_kid)
assert (set(hit[0]) if hit else None) == found
@pytest.mark.parametrize("track_kid", [KID_A, None], ids=["track-kid-held", "track-kid-unknown"])
def test_a_playready_track_takes_the_vault_key_only_for_its_known_kid(server_env, monkeypatch, track_kid):
monkeypatch.setattr(handlers, "check_vaults", REAL_CHECK_VAULTS)
monkeypatch.setattr(handlers, "load_server_vaults", lambda name: _VaultsHolding(KID_A))
licensed: list = []
monkeypatch.setattr(
PlayReady,
"get_content_keys",
lambda self, cdm, certificate, licence: licensed.append(self) or self.content_keys.update({KID_A: "cdm"}),
)
pro_b64 = _kidless_pro_b64()
track = SimpleNamespace(
id="vid", drm=[PlayReady(pssh=RealPSSH(base64.b64decode(pro_b64)), kid=KID_A, pssh_b64=pro_b64)]
)
sources: dict = {}
keys = handlers.handle_single_server_cdm(
SimpleNamespace(), SimpleNamespace(), track, pro_b64, "playready", None, sources, track_kid=track_kid
)
assert bool(licensed) is (track_kid is None)
assert sources == ({KID_A.hex: "v1"} if track_kid else {})
assert set(keys) == {KID_A.hex}
def test_the_client_kid_counts_only_when_the_track_drm_names_it():
track = SimpleNamespace(drm=[SimpleNamespace(kids=[KID_A])])
assert handlers.client_track_kid(KID_A.hex, track) == KID_A
assert handlers.client_track_kid(KID_B.hex, track) is None
assert handlers.client_track_kid("not-hex", track) is None
assert handlers.client_track_kid(None, track) is None
@@ -397,7 +397,15 @@ def licence_env(monkeypatch):
vault = {}
def fake_single(
service, title, track, pssh_str, drm_type, request, sources=None, refusal=None, vault_only=False, shared=None
service,
title,
track,
pssh_str,
drm_type,
request,
sources=None,
refusal=None,
**_,
):
calls.append((track.id, pssh_str))
if pssh_str in vault:
@@ -134,7 +134,15 @@ async def test_batch_shares_full_bundle_per_pssh(monkeypatch):
bundle_b = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": "k_b"}
def fake_single(
service, title, track, pssh_str, drm_type, request, sources=None, refusal=None, vault_only=False, shared=None
service,
title,
track,
pssh_str,
drm_type,
request,
sources=None,
refusal=None,
**_,
):
calls.append(pssh_str)
return dict(bundle_a) if pssh_str == pssh_a else dict(bundle_b)