Лицензионные серверы DRM и доставка ключей

Автор: Николай СапуновОбновлено: август 202616 мин чтения
Содержание статьи +

TL;DR

Зашифрованное видео бесполезно без ключа, и весь смысл защиты контента в том, чтобы доставить этот ключ нужному устройству на время одного воспроизведения – и больше никуда. Делает это лицензионный сервер: плеер собирает запрос лицензии через браузерный стандарт Encrypted Media Extensions (EME, рекомендация W3C), сервер проверяет права и возвращает ключ, упакованный так, что распаковать его может только доверенный модуль расшифровки внутри устройства. У трёх систем DRM свои диалекты этого обмена – у Widevine запрос лицензии, у PlayReady challenge, у FairPlay пара SPC/CKC, – но форма одна: запрос → проверка → ключ. Эта статья простыми словами разбирает поток получения лицензии, роль лицензионного прокси и сервиса прав, ротацию ключей для прямых эфиров, разницу между сессионной и постоянной лицензией и – главное – где живёт каждый секрет и кто не должен видеть его никогда.

Почему это важно

Если вы основатель, продакт-менеджер или впервые отвечаете за стриминг как CTO, доставка ключей – тот слой, где защита контента либо работает, либо тихо вас подводит: ошибка здесь либо роняет старт воспроизведения у всей аудитории, либо отдаёт ваш контент-ключ туда, откуда его скопируют. Сам сервер лицензий вы, скорее всего, не пишете – его даёт multi-DRM-вендор или облако, – но спроектировать вокруг него проверку прав, выдержать пик премьеры и не дать ключу утечь обязаны вы. Прочитав статью, вы сможете объяснить, как плеер получает ключ, нарисовать границу между вашим сервисом прав и сервером лицензий и назвать вслух три места, где контент-ключ появляться не должен никогда. Это прямое продолжение CENC, CTR и CBCS: там мы зашифровали байты один раз; здесь доставляем ключ, который их открывает.

Какую одну проблему решает доставка ключей

Начнём с задачи, потому что вся конструкция понятна только из неё. В статье CENC, CTR и CBCS мы зашифровали каталог симметричным ключом – контент-ключом (content encryption key, CEK): одно секретное 128-битное число, которым байты и запираются, и отпираются. Зашифрованные сегменты лежат на CDN открыто для скачивания; украсть их может кто угодно. Бесполезны они ровно до тех пор, пока у вора нет ключа. Значит, вся защита сводится к одному вопросу: как передать контент-ключ миллиону легальных устройств и ни одному пиратскому.

Наивных решений два, и оба ломаются. Положить ключ рядом с видео на CDN – всё равно что оставить ключ в замке. Зашить ключ в приложение плеера – чуть лучше, но любой, кто вскроет приложение, достанет ключ и расшифрует весь каталог. Защита требует, чтобы ключ доставлялся вживую, по одному воспроизведению, индивидуально каждому устройству, и приходил завёрнутым так, что развернуть его умеет только доверенное «железо» именно этого устройства. Систему, которая это делает, и называют лицензионным сервером (license server), а её ответ – лицензией: маленьким сообщением, несущим контент-ключ, запертый под конкретное устройство, плюс правила, на что это устройство имеет право.

Держите в голове разделение из статьи про CENC: слой шифрования общий, слой лицензии – свой для каждой системы. Зашифрованные байты одни на всех (одна упаковка по cbcs). А вот доставка ключа у Widevine, PlayReady и FairPlay устроена по-разному, и именно ей посвящена эта статья.

Поток получения лицензии, шаг за шагом

В браузере весь обмен идёт через один стандарт – Encrypted Media Extensions (EME), рекомендацию W3C, которая даёт плееру договариваться с системой защиты, не зная её внутренностей. Разберём поток по шагам; он почти дословно повторяется и в нативных приложениях, и на смарт-ТВ.

Во-первых, плеер начинает грузить защищённый поток и натыкается на пометку «здесь зашифровано». Браузер поднимает событие encrypted, передавая плееру initialization data – небольшой блок, который, помимо прочего, называет идентификатор нужного ключа.

Во-вторых, плеер просит у браузера объект MediaKeys (через requestMediaKeySystemAccess) и открывает сессию ключей (MediaKeySession) – она представляет «жизнь одной лицензии и её ключей». На этой сессии плеер вызывает generateRequest(), передавая модулю расшифровки ту самую init data.

В-третьих, модуль расшифровки – Content Decryption Module (CDM), доверенный компонент устройства, который и хранит, и применяет ключи, – собирает запрос лицензии. Это не просто «дай ключ по такому-то ID»: запрос подписан личностью устройства, так что сервер может убедиться, что говорит с настоящим, непровзломанным «железом». Браузер отдаёт этот запрос плееру через событие message.

В-четвёртых, и это ключевой момент архитектуры: запрос лицензии передаёт ваша JavaScript-страница, а не браузер напрямую. Плеер берёт байты запроса и шлёт их на URL сервера лицензий обычным сетевым вызовом (fetch/XHR). Раз транспорт – ваш код, вы можете приложить к нему что угодно: токен входа, идентификатор подписки, маркер сеанса. Это и есть крючок, на котором держится вся проверка прав, – к нему вернёмся.

В-пятых, сервер лицензий проверяет запрос – личность устройства, целостность, права – и, если всё чисто, достаёт контент-ключ из защищённого хранилища, заворачивает его под этот конкретный CDM и возвращает лицензию. Плеер передаёт её обратно модулю через update(). Теперь, и только теперь, ключ оказывается внутри CDM, расшифровка идёт прямо перед декодированием, и видео играет. Поднимается событие keystatuseschange – «ключ годен».

Главное, что стоит вынести: наружу, в ваш код и по сети, контент-ключ в открытом виде не выходит ни на одном шаге. Запрос подписан устройством; лицензия зашифрована под устройство; распаковка происходит внутри CDM. Ваше приложение перекладывает запечатанные конверты и не видит, что внутри.

Рисунок 1. Поток получения лицензии. Плеер ловит `encrypted`, CDM собирает подписанный устройством запрос, ваше приложение пересылает его на сервер лицензий (приложив токен прав), сервер возвращает ключ, завёрнутый под устройство, а `update()` кладёт его внутрь CDM. Открытый контент-ключ наружу не выходит.

Вот тот же поток минимальным кодом в браузере – обратите внимание, что код приложения только перевозит байты, никогда их не читая (значения вымышлены):

// Плеер увидел зашифрованный поток
video.addEventListener('encrypted', async (event) => {
  const session = mediaKeys.createSession('temporary'); // тип сессии — см. ниже
  session.addEventListener('message', async (e) => {
    // e.message — запрос лицензии, собранный CDM. Внутрь мы не смотрим.
    const res = await fetch('https://license.example.com/widevine', {
      method: 'POST',
      headers: { 'Authorization': 'Bearer eyJfake.entitlement.token' }, // крючок прав
      body: e.message
    });
    const license = await res.arrayBuffer();
    await session.update(license); // ключ уходит внутрь CDM, не в наш код
  });
  await session.generateRequest(event.initDataType, event.initData);
});

Три диалекта одного рукопожатия

Форма обмена – запрос, проверка, завёрнутый ключ – у всех трёх систем одна. Различаются названия и форматы сообщений. Знать их полезно, потому что они встретятся вам в дашбордах вендоров и логах. (Карта «какая система на каком устройстве» – тема статьи Три системы DRM; здесь берём её как данность.)

Google Widevine. Модуль расшифровки собирает зашифрованный, подписанный устройством запрос лицензии и шлёт его на сервер лицензий Widevine; тот возвращает лицензию с ключом. Личность устройства закладывается на заводе: корень доверия – keybox, а провижининг-сервер Google выдаёт устройству сертификат. На устройствах уровня L1 ключи и расшифрованные кадры живут только в аппаратной доверенной среде (TEE) и не видны основному процессору – поэтому ключ Widevine на хорошем устройстве не покидает «железо» вообще.

Microsoft PlayReady. Клиент формирует license challenge (XML-сообщение), приложенный к нему идентификатор ключа (KID) называет нужный ключ; сервер лицензий генерирует контент-ключ под этот KID и возвращает лицензию, привязанную к устройству. У каждого клиента – уникальное доказательство, аутентифицирующее его перед сервером.

Apple FairPlay. Самый узнаваемый диалект. Устройство создаёт Server Playback Context (SPC) – зашифрованный запрос, который сумело собрать только это устройство и только для этой сессии. SPC уходит на сервер лицензий, где модуль Key Security Module (KSM) расшифровывает его, сверяет хеш сертификата с сертификатом приложения (Application Certificate) от Apple, достаёт контент-ключ из хранилища и заворачивает его в Content Key Context (CKC) – ответ, который и возвращается приложению. В мире HLS этот обмен ещё и размечается в манифесте: тег EXT-X-KEY (формат HLS, RFC 8216) несёт METHOD=SAMPLE-AES, KEYFORMAT="com.apple.streamingkeydelivery" и URI со схемой skd://, указывающий плееру, куда идти за ключом.

СистемаЧто шлёт клиентМодуль на сервереЧто возвращаетсяКакие устройства
WidevineЗапрос лицензии (подписан устройством)Сервер лицензий WidevineЛицензия с ключомAndroid, Chrome, многие ТВ
PlayReadyLicense challenge (XML) + KIDСервер лицензий PlayReadyЛицензия под устройствоWindows, Xbox, многие ТВ
FairPlaySPC (Server Playback Context)KSM (Key Security Module)CKC (Content Key Context)Только устройства Apple

Разные слова, одно рукопожатие: устройство доказывает, кто оно; сервер выдаёт ключ, который сможет развернуть только это устройство. Чтобы один воркфлоу обслуживал все три (а это базовый современный паттерн), нужен multi-DRM-сервис, говорящий на всех трёх диалектах из одной упаковки, – он разобран в Multi-DRM: один воркфлоу, любое устройство.

Лицензионный прокси: где живут права и почему не в плеере

Вернёмся к четвёртому шагу – тому, где запрос лицензии везёт ваше приложение. Это не деталь реализации, а главная точка контроля во всей защите. Сервер лицензий умеет выдать ключ; решать, кому его выдавать, – не его работа. Решает это ваша бизнес-логика: активна ли подписка, входит ли тайтл в план, не превышен ли лимит одновременных потоков, разрешён ли регион. Эта логика называется сервисом прав (entitlement service), и жить она обязана на сервере.

Чистый и проверенный паттерн – лицензионный прокси: для плеера он выглядит как сервер лицензий, но на деле стоит перед настоящим. Плеер шлёт запрос лицензии на прокси без приложенного секрета; прокси проверяет права (по токену входа, куке или заголовку), выпускает свежий короткоживущий токен прав и пересылает запрос вместе с токеном уже настоящему серверу лицензий. Тот выдаёт ключ, только если токен верен.

Почему так, а не «токен прямо из плеера на сервер лицензий»? Из-за того, где обязаны жить секреты. Ключ связи между вашим сервисом прав и сервисом лицензий нельзя класть в клиент: утечёт – и злоумышленник начнёт штамповать валидные токены прав от вашего имени и тянуть ключи на любой контент. Поэтому логика прав исполняется на сервере и в браузер не уезжает. Токены делаются короткоживущими (секунды или минуты) и одноразовыми, чтобы их нельзя было переиспользовать. Бонус прокси-схемы: время жизни токена становится чисто серверной заботой, и тихие сбои старта из-за протухших токенов исчезают – плеер видит эндпоинт, который всегда отвечает.

Рисунок 2. Лицензионный прокси и сервис прав. Логика «кому можно» живёт на сервере, не в плеере. Прокси проверяет подписку, регион и лимит потоков, выпускает свежий одноразовый токен и пересылает запрос на сервер лицензий. Ключ связи прокси ↔ сервер лицензий клиенту не отдаётся никогда.

Это и причина, по которой проверку прав нельзя «срезать» на клиенте ради скорости. Любая проверка в JavaScript-плеере обходится тем, кто откроет инструменты разработчика. Сервер лицензий – последняя дверь к ключу; держать её должна серверная логика, а не клиент, которому пользователь полностью управляет.

Где живёт каждый секрет – и кто не должен видеть его никогда

Соберём все секреты на одной карте, потому что это и есть практический итог статьи. У защищённого потока несколько разных секретов, и у каждого – своё место, дальше которого ему хода нет.

Контент-ключ (CEK) – само 128-битное число, открывающее байты. Он живёт в трёх местах и больше нигде: в хранилище ключей (key server, как правило поверх KMS или аппаратного модуля HSM), куда упаковщик ходит за ним при шифровании через защищённый обмен CPIX (Content Protection Information Exchange Format, спецификация DASH-IF; на нём же построен SPEKE от AWS); в сервере лицензий, который достаёт его, чтобы завернуть под устройство; и, на миг воспроизведения, внутри CDM/TEE устройства. Всё. В манифесте его нет, в открытых файлах на CDN нет, в JavaScript-плеере нет, в логах приложения нет.

Идентификатор ключа (default_KID) – публичное имя ключа, а не сам ключ. Как номер ячейки в камере хранения, его спокойно кладут в манифест и в бокс tenc (по ISO/IEC 23001-7), чтобы плеер знал, какой ключ просить. Путать имя ключа с ключом – частый источник страхов на ровном месте: называть ключ в открытую безопасно.

Личность устройства – заводской сертификат и приватные ключи в защищённом «железе» (keybox у Widevine, сертификат приложения у FairPlay). Они не покидают устройство; на них держится доверие, позволяющее серверу убедиться, что он говорит с настоящим CDM.

Токен прав – короткоживущий пропуск между плеером и вашим бэкендом, разобранный выше. Живёт секунды; одноразовый.

Кто не должен видеть контент-ключ в открытом виде, никогда: ваш JavaScript-плеер, CDN, логи, аналитика и любой человек в эксплуатации. Правило простое – открытый контент-ключ существует только внутри защищённых границ (KMS/HSM, сервер лицензий, CDM устройства); между ними он перемещается лишь завёрнутым. Нарушение этого правила и есть способ номер один потерять каталог.

Рисунок 3. Где живёт контент-ключ. Открытым он бывает только в трёх защищённых местах: хранилище (KMS/HSM), сервер лицензий и CDM устройства. К упаковщику он идёт по CPIX/SPEKE, к устройству – завёрнутым в лицензию. Манифест, CDN, плеер и логи открытого ключа не видят никогда.

Сессионная против постоянной лицензии: онлайн и офлайн

До сих пор лицензия у нас «жила» ровно столько, сколько играет видео. Так бывает не всегда, и выбор задаётся типом сессии ключей в EME – он решает, переживает ли лицензия закрытие вкладки и пишет ли модуль что-либо на диск. Типов три.

temporary – сессионная (временная). Лицензия и ключи живут в памяти и умирают вместе с вкладкой. Это умолчание для онлайн-воспроизведения: закрыл – при следующем запуске плеер берёт лицензию заново. Ничего на диск не пишется, поверхность атаки минимальна.

persistent-license – постоянная. Лицензию модуль сохраняет на диск, привязав к источнику (origin), чтобы пережить перезапуск. Это механизм офлайн-скачивания: пользователь грузит фильм в дорогу, и сохранённая лицензия даёт играть его без сети. Сильнее по возможностям – и по требованиям: офлайн-лицензия несёт собственное окно действия, а на диске лежит секрет, к которому правила надо применять строже.

persistent-usage-record – запись использования. Лицензия не хранится, но модуль сохраняет на диск запись о том, что и сколько воспроизводилось, – чтобы отчитаться позже. Применяется редко и точечно, под аналитику и сверку прав.

Тип сессии (EME)Переживает закрытие?Пишет на диск?Зачем нужен
temporaryНетНетОбычное онлайн-воспроизведение
persistent-licenseДаЛицензию (ключ)Офлайн-скачивание и просмотр
persistent-usage-recordДаТолько запись о просмотреОтчётность и сверка прав

Что именно лицензия разрешает помимо «да» – окно аренды, офлайн-срок, потолок разрешения, требование HDCP, – это уже политика лицензии, и ей посвящена отдельная статья Политика лицензий: аренда, офлайн, output control и права. Здесь нам важно одно: тип сессии решает, где и сколько живёт ключ, и постоянная лицензия – это сознательный выбор записать секрет на диск ради офлайна.

Ротация ключей для прямых эфиров

У VOD один контент-ключ может обслуживать весь фильм. У премиум-эфира – матча, премьеры – ставки выше, и появляется ротация ключей: контент-ключ меняется через равные интервалы, так что один утёкший ключ открывает лишь короткий отрезок, а не весь эфир. Плеер прозрачно запрашивает лицензию под каждый новый идентификатор ключа. В обмене ключами это размечает CPIX: элемент ContentKeyPeriod задаёт период, на котором действует конкретный ключ.

Считаем вслух, потому что здесь прячется ловушка масштаба. Пусть идёт двухчасовой матч, ключ ротируется каждые 60 секунд:

Периодов ключа = 2 часа × 60 мин × 60 с ÷ 60 с = 120 ключей за эфир

Наивно: каждое устройство берёт новую лицензию на каждый период.
Пик аудитории 500 000 зрителей:
  запросов лицензий = 500 000 × 120 = 60 000 000 за эфир
  — и каждая ротация бьёт по серверу лицензий синхронным всплеском.

Иерархическая лицензия: один ответ несёт пачку ключей вперёд.
  запросов лицензий ≈ 500 000 (по одной на зрителя, плюс редкие добор)
  — в ~120 раз меньше нагрузки на сервер лицензий.

Отсюда два инженерных вывода. Первый: частая ротация без продуманной доставки сама создаёт DDoS по вашему серверу лицензий – 500 тысяч устройств, синхронно просящих новый ключ каждую минуту. Второй, и это решение: иерархические ключи – лицензия отдаёт сразу текущие и будущие ключи в одном ответе, так что устройство не бегает за лицензией на каждую ротацию. Современные связки (например, AWS Elemental MediaPackage с ротацией ключей, или Unified Streaming с castLabs для высокочастотной ротации) кладут несколько ключей в один лицензионный ответ и разводят запросы во времени, чтобы не ронять сервер. Пик премьеры как режим нагрузки разобран в Доставка живых событий и всплеск премьеры; здесь достаточно правила: чем чаще ротация, тем важнее доставлять ключи пачками, а не по одному.

Частая ошибка: лимит без проверки и секрет в клиенте

Самая дорогая ошибка этого слоя – поставить «защиту», которую обходит любой, кто откроет инструменты разработчика. Случается она в двух видах.

Первый – проверка прав на клиенте. Команда делает лимит одновременных потоков или гео-проверку в JavaScript-плеере, потому что так быстрее. Но клиент полностью под контролем пользователя: проверку отключают, и сервер лицензий покорно отдаёт ключ. Лекарство – правило из середины статьи: запрос лицензии обязан пройти через серверный сервис прав (прокси), и решение «выдавать ключ или нет» принимает сервер, а не клиент.

Второй – секрет в клиенте или в манифесте. Контент-ключ кладут рядом с видео; ключ связи с сервером лицензий зашивают в приложение; токен прав делают долгоживущим и многоразовым. Любое из трёх превращает защиту в декорацию. Лекарство – карта секретов: открытый контент-ключ только внутри KMS/HSM, сервера лицензий и CDM; токены короткие и одноразовые; в манифесте – лишь публичное имя ключа (default_KID), но не он сам. Проверяется это одним грепом по тому, что реально уходит на клиент: если там виден контент-ключ или вечный токен – каталог уже не защищён, просто пока этого никто не заметил.

Где здесь Фора Софт

Доставка ключей – слой, где неверная архитектура либо роняет старт у всей аудитории на пике, либо тихо отдаёт контент-ключ туда, откуда его скопируют, и оба провала всплывают только в продакшене. Фора Софт с 2005 года создаёт ПО для видеостриминга, OTT/Internet TV, e-learning и телемедицины – 250+ выпущенных проектов для 400+ клиентов, – и доставка ключей проходит через всю эту работу: лицензионный прокси с серверным сервисом прав, который держит подписку, регион и лимит потоков вне досягаемости клиента; обмен ключами по CPIX/SPEKE между хранилищем и упаковщиком; ротация ключей с иерархической доставкой, чтобы пик премьеры не превратился в самоналоженный DDoS по серверу лицензий; и аккуратная граница секретов, на которой открытый контент-ключ не выходит за пределы KMS, сервера лицензий и CDM устройства. Когда медиакомпании нужна защита, которая держит и масштаб премьеры, и аудит студии, именно эту инженерию доставки ключей мы и приносим.

Ключевые выводы

  • Зашифрованное видео бесполезно без ключа; вся защита – это доставить ключ на одно воспроизведение и никуда больше.
  • В браузере поток лицензии идёт по EME (W3C): запрос → проверка → ключ, завёрнутый под устройство.
  • Три диалекта: запрос лицензии Widevine, challenge PlayReady, пара SPC/CKC у FairPlay – форма одна.
  • Проверку прав делает серверный прокси; в клиент логика и ключ связи не уезжают никогда.
  • Открытый контент-ключ живёт только в KMS/HSM, сервере лицензий и CDM устройства – не в манифесте и не в плеере.
  • Постоянная лицензия включает офлайн; ротация ключей защищает эфиры, но требует иерархической доставки.

Что почитать дальше

Строите такую систему?

Подберём параметры кодирования под ваш контент и посчитаем стоимость доставки до старта разработки.