Why this matters

If you are a founder, product manager, or a first-time streaming CTO, key delivery is the layer where content protection either works or quietly fails you: a mistake here either kills playback start for your whole audience or hands your content key to somewhere it can be copied. You probably will not write the license server yourself — a multi-DRM vendor or cloud provides it — but designing the entitlement check around it, surviving a premiere spike, and keeping the key from leaking are on you. By the end you will be able to explain how a player gets the key, draw the boundary between your entitlement service and the license server, and name out loud the three places the content key must never appear. This is the direct sequel to CENC, CTR, and CBCS: there we encrypted the bytes once; here we deliver the key that opens them.

The one problem key delivery solves

Start with the job, because the whole design only makes sense from it. In CENC, CTR, and CBCS we encrypted the catalog with a symmetric key — the content encryption key (CEK): one secret 128-bit number that both locks and unlocks the bytes. The encrypted segments sit on the CDN, open for anyone to download; anyone can steal them. They stay useless exactly as long as the thief has no key. So all of content protection comes down to one question: how do you hand the content key to a million legitimate devices and to no pirate?

There are two naive answers, and both break. Put the key next to the video on the CDN, and you have left the key in the lock. Bake the key into the player app, and that is a little better, but anyone who cracks the app extracts the key and decrypts the whole catalog. Protection requires the key to be delivered live, per playback, individually to each device, and to arrive wrapped so that only that device's trusted hardware can unwrap it. The system that does this is the license server, and its answer is a license: a small message carrying the content key locked to one device, plus the rules of what that device is allowed to do.

Hold on to the split from the CENC article: the encryption layer is shared, the license layer is per-system. The encrypted bytes are the same for everyone (one cbcs package). But key delivery differs across Widevine, PlayReady, and FairPlay — and that is what this article is about.

The license-acquisition flow, step by step

In the browser the whole exchange runs through one standard — Encrypted Media Extensions (EME), a W3C Recommendation that lets the player negotiate with a protection system without knowing its internals. Walk the flow step by step; it repeats almost verbatim in native apps and on smart TVs.

First, the player begins loading a protected stream and hits a marker that says "this is encrypted here." The browser fires an encrypted event, handing the player initialization data — a small block that, among other things, names the key it needs.

Second, the player asks the browser for a MediaKeys object (via requestMediaKeySystemAccess) and opens a key session (MediaKeySession) — this "represents the lifetime of a license and its keys." On that session the player calls generateRequest(), passing the decryption module that initialization data.

Third, the decryption module — the Content Decryption Module (CDM), the trusted device component that both stores and applies keys — assembles a license request. This is not just "give me the key with this ID": the request is signed by the device's identity, so the server can be sure it is talking to real, un-cracked hardware. The browser hands this request to the player through a message event.

Fourth, and this is the key architectural moment: your JavaScript page transports the license request, not the browser directly. The player takes the request bytes and sends them to the license-server URL with an ordinary network call (fetch/XHR). Because the transport is your code, you can attach anything to it: a sign-in token, a subscription ID, a session marker. This is the hook that all of entitlement hangs on — we will return to it.

Fifth, the license server checks the request — device identity, integrity, rights — and, if everything is clean, pulls the content key from secure storage, wraps it for this specific CDM, and returns a license. The player passes it back to the module through update(). Now, and only now, the key lands inside the CDM, decryption happens right before decoding, and the video plays. A keystatuseschange event fires — "the key is usable."

The thing to take away: the content key never leaves into your code or onto the network in the clear at any step. The request is device-signed; the license is device-encrypted; the unwrapping happens inside the CDM. Your app shuffles sealed envelopes and never sees what is inside.

License-acquisition sequence: from the encrypted event in the player through the CDM, app, and license server to the key inside the decryption module. Figure 1. The license-acquisition flow. The player catches encrypted, the CDM assembles a device-signed request, your app forwards it to the license server (attaching an entitlement token), the server returns the key wrapped for the device, and update() places it inside the CDM. The clear content key never leaves.

Here is the same flow as minimal browser code — note that the app code only ferries bytes, never reading them (values are fake):

// The player saw an encrypted stream
video.addEventListener('encrypted', async (event) => {
  const session = mediaKeys.createSession('temporary'); // session type — see below
  session.addEventListener('message', async (e) => {
    // e.message is the license request the CDM built. We do not look inside.
    const res = await fetch('https://license.example.com/widevine', {
      method: 'POST',
      headers: { 'Authorization': 'Bearer eyJfake.entitlement.token' }, // the entitlement hook
      body: e.message
    });
    const license = await res.arrayBuffer();
    await session.update(license); // the key goes into the CDM, not into our code
  });
  await session.generateRequest(event.initDataType, event.initData);
});

Three dialects of one handshake

The shape of the exchange — request, check, wrapped key — is the same in all three systems. What differs is the names and message formats. Knowing them helps, because they show up in vendor dashboards and logs. (The map of which system runs on which device is the subject of The Three DRM Systems; we take it as given here.)

Google Widevine. The decryption module assembles an encrypted, device-signed license request and sends it to the Widevine license server, which returns a license with the key. Device identity is set at the factory: the root of trust is the keybox, and Google's provisioning server issues the device a certificate. On L1 devices, keys and decrypted frames live only in a hardware trusted execution environment (TEE), invisible to the host CPU — so the Widevine key on a good device never leaves the hardware at all.

Microsoft PlayReady. The client forms a license challenge (an XML message); the key identifier (KID) attached to it names the key needed; the license server generates a content key for that KID and returns a license bound to the device. Each client has unique proof that authenticates it to the server.

Apple FairPlay. The most recognizable dialect. The device creates a Server Playback Context (SPC) — an encrypted request that only this device could assemble and only for this session. The SPC goes to the license server, where the Key Security Module (KSM) decrypts it, checks the certificate hash against Apple's Application Certificate, pulls the content key from storage, and wraps it into a Content Key Context (CKC) — the response returned to the app. In the HLS world this exchange is also marked up in the manifest: the EXT-X-KEY tag (an HLS feature, RFC 8216) carries METHOD=SAMPLE-AES, KEYFORMAT="com.apple.streamingkeydelivery", and a URI with the skd:// scheme that tells the player where to go for the key.

System What the client sends Server module What comes back Devices covered
Widevine License request (device-signed) Widevine license server License with key Android, Chrome, many TVs
PlayReady License challenge (XML) + KID PlayReady license server License bound to device Windows, Xbox, many TVs
FairPlay SPC (Server Playback Context) KSM (Key Security Module) CKC (Content Key Context) Apple devices only

Different words, one handshake: the device proves who it is; the server hands back a key only that device can unwrap. To make one workflow serve all three (the baseline modern pattern), you need a multi-DRM service that speaks all three dialects from one package — covered in Multi-DRM: One Workflow, Every Device.

The license proxy: where rights live, and why not in the player

Return to step four — the one where your app transports the license request. This is not an implementation detail; it is the main control point in all of content protection. The license server knows how to issue a key; deciding who gets one is not its job. Your business logic decides that: is the subscription active, is the title in the plan, is the concurrent-stream limit exceeded, is the region allowed. That logic is called the entitlement service, and it must live on the server.

The clean, proven pattern is a license proxy: to the player it looks like the license server, but it actually sits in front of the real one. The player sends the license request to the proxy with no secret attached; the proxy checks rights (by sign-in token, cookie, or header), mints a fresh, short-lived entitlement token, and forwards the request along with the token to the actual license server. The server issues the key only if the token is valid.

Why this, and not "token straight from the player to the license server"? Because of where secrets must live. The communication key between your entitlement service and the license service cannot go into the client: if it leaks, an adversary starts minting valid entitlement tokens on your behalf and pulling keys for any content. So the entitlement logic runs on the server and never travels to the browser. Tokens are short-lived (seconds or minutes) and single-use, so they cannot be replayed. A bonus of the proxy: token lifetime becomes a purely server-side concern, and silent start failures from expired tokens disappear — the player sees an endpoint that always answers.

Entitlement boundary: the player sends a request to the license proxy, which checks rights on the server, mints a short-lived token, and forwards the request to the license server. Figure 2. The license proxy and entitlement service. The "who is allowed" logic lives on the server, not in the player. The proxy checks subscription, region, and stream limit, mints a fresh single-use token, and forwards the request to the license server. The proxy ↔ license-server communication key is never handed to the client.

This is also why the rights check cannot be "shortcut" on the client for speed. Any check in a JavaScript player is bypassed by anyone who opens developer tools. The license server is the last door to the key; server logic must hold it, not a client the user fully controls.

Where each secret lives — and who must never see it

Gather every secret onto one map, because this is the practical takeaway of the article. A protected stream has several different secrets, and each has its own place beyond which it must not travel.

The content key (CEK) — the 128-bit number itself that opens the bytes. It lives in three places and nowhere else: in the key store (a key server, typically over a KMS or a hardware HSM), which the packager visits for it at encryption time through the secure exchange CPIX (Content Protection Information Exchange Format, a DASH-IF specification; SPEKE from AWS is built on it); in the license server, which pulls it to wrap for a device; and, for the instant of playback, inside the device's CDM/TEE. That is all. It is not in the manifest, not in the open files on the CDN, not in the JavaScript player, not in the app logs.

The key identifier (default_KID) — the public name of the key, not the key. Like a locker number, it is safely placed in the manifest and in the tenc box (per ISO/IEC 23001-7) so the player knows which key to ask for. Confusing the key's name with the key is a common source of needless fear: naming the key in the open is safe.

Device identity — the factory certificate and private keys in secure hardware (Widevine's keybox, FairPlay's Application Certificate). They never leave the device; the trust that lets the server be sure it is talking to a real CDM rests on them.

The entitlement token — the short-lived pass between the player and your backend, described above. It lives for seconds; it is single-use.

Who must never see the content key in the clear, ever: your JavaScript player, the CDN, the logs, analytics, and any human in operations. The rule is simple — the clear content key exists only inside protected boundaries (KMS/HSM, license server, the device CDM); between them it moves only wrapped. Breaking that rule is the number-one way to lose a catalog.

The content key's journey: out of KMS or HSM over CPIX to the packager, and through the license server wrapped into the device CDM; the zones where the clear key must never appear. Figure 3. Where the content key lives. It is clear only in three protected places: the key store (KMS/HSM), the license server, and the device CDM. It goes to the packager over CPIX/SPEKE and to the device wrapped in a license. The manifest, CDN, player, and logs never see the clear key.

Session versus persistent license: online and offline

So far the license has lived exactly as long as the video plays. That is not always so, and the choice is set by the key-session type in EME — it decides whether the license outlives the tab and whether the module writes anything to disk. There are three types.

temporary — session (non-persistent). The license and keys live in memory and die with the tab. This is the default for online playback: close it, and on the next start the player fetches the license again. Nothing is written to disk; the attack surface is minimal.

persistent-license — persistent. The module saves the license to disk, bound to the origin, so it survives a restart. This is the offline-download mechanism: the user loads a film for the road, and the stored license lets it play with no network. More capable — and more demanding: an offline license carries its own validity window, and a secret now sits on disk to which rules must be applied more strictly.

persistent-usage-record — usage record. The license is not stored, but the module saves a record of what was played and for how long, to report later. Used rarely and narrowly, for analytics and rights reconciliation.

Session type (EME) Survives close? Writes to disk? What it is for
temporary No No Normal online playback
persistent-license Yes The license (key) Offline download and viewing
persistent-usage-record Yes Only a viewing record Reporting and rights reconciliation

What a license actually allows beyond "yes" — the rental window, the offline term, the resolution cap, the HDCP requirement — is the license policy, the subject of its own article, License Policy: Rentals, Offline, Output Control, and Rights. Here one thing matters: the session type decides where and how long the key lives, and a persistent license is a deliberate choice to write a secret to disk for the sake of offline.

Key rotation for live streams

For VOD, one content key can serve a whole film. For a premium live event — a match, a premiere — the stakes are higher, and key rotation appears: the content key changes at regular intervals, so one leaked key opens only a short stretch rather than the whole stream. The player transparently requests a license for each new key identifier. In the key exchange this is marked by CPIX: the ContentKeyPeriod element defines the period over which a specific key is valid.

Do the math out loud, because a scale trap hides here. Say a two-hour match is running and the key rotates every 60 seconds:

Key periods = 2 hours × 60 min × 60 s ÷ 60 s = 120 keys per event

Naive: each device fetches a new license for every period.
Audience peak of 500,000 viewers:
  license requests = 500,000 × 120 = 60,000,000 per event
  — and each rotation hits the license server in a synchronized spike.

Hierarchical license: one response carries a batch of keys ahead.
  license requests ≈ 500,000 (one per viewer, plus rare top-ups)
  — about 120× less load on the license server.

Two engineering conclusions follow. First: frequent rotation without thoughtful delivery is a self-inflicted DDoS on your license server — 500,000 devices synchronously asking for a new key every minute. Second, and this is the fix: hierarchical keys — the license hands over the current and future keys in one response, so the device does not run for a license on every rotation. Modern stacks (for example AWS Elemental MediaPackage with key rotation, or Unified Streaming with castLabs for high-frequency rotation) pack several keys into one license response and spread requests over time so the server stays up. The premiere spike as a load mode is covered in Live-Event Delivery and the Premiere Spike; here the rule is enough: the more frequent the rotation, the more it matters to deliver keys in batches rather than one at a time.

Common mistake: a limit with no check, and a secret in the client

The most expensive mistake at this layer is shipping "protection" that anyone who opens developer tools can bypass. It happens in two forms.

The first is a rights check on the client. A team puts the concurrent-stream limit or the geo-check in the JavaScript player because it is faster that way. But the client is fully under the user's control: the check is switched off, and the license server dutifully hands over the key. The cure is the rule from the middle of this article: the license request must pass through a server-side entitlement service (the proxy), and the "issue the key or not" decision is made by the server, not the client.

The second is a secret in the client or the manifest. The content key is placed next to the video; the communication key with the license server is baked into the app; the entitlement token is made long-lived and reusable. Any one of the three turns protection into a stage prop. The cure is the secret map: the clear content key only inside KMS/HSM, the license server, and the CDM; tokens short and single-use; the manifest carries only the public key name (default_KID), not the key itself. You verify this with one grep over what actually ships to the client: if the content key or an eternal token is visible there, the catalog is already unprotected — no one has just noticed yet.

Where Fora Soft fits in

Key delivery is the layer where the wrong architecture either drops playback start for the whole audience at peak or quietly hands the content key to somewhere it can be copied, and both failures surface only in production. Fora Soft has built video-streaming, OTT/Internet TV, e-learning, and telemedicine software since 2005 — 625+ shipped projects for 400+ clients — and key delivery runs through all of it: a license proxy with a server-side entitlement service that keeps subscription, region, and stream limit out of the client's reach; key exchange over CPIX/SPEKE between the key store and the packager; key rotation with hierarchical delivery so a premiere spike does not become a self-inflicted DDoS on the license server; and a careful secret boundary across which the clear content key never travels beyond the KMS, the license server, and the device CDM. When a media company needs protection that holds at both premiere scale and studio audit, this key-delivery engineering is what we bring.

What to read next

Call to action

References

  1. Encrypted Media Extensions (EME) — W3C Recommendation, 2017-09-18. The controlling standard for browser key delivery. Defines the encryptedMediaKeys/MediaKeySessiongenerateRequest()message event → update() flow; the Content Decryption Module (CDM) model; and the three session types — temporary, persistent-license, persistent-usage-record. Confirms that the application (not the browser) transports the license request, and that a session "represents the lifetime of a license and its keys." Tier 1 (official standard). https://www.w3.org/TR/encrypted-media/ (accessed 2026-06-17)
  2. ISO/IEC 23001-7:2023 — Common Encryption (CENC), 4th edition — ISO/IEC, 2023-08. Defines default_KID as the 128-bit identifier (name) of the content key in the tenc box, and the model in which DRM systems locate the key by KID while "how each system protects and locates the key is left to the DRM-specific method." The basis for "the key's name is public, the key is not." Tier 1 (official standard). https://www.iso.org/standard/84637.html (accessed 2026-06-17)
  3. HTTP Live Streaming — RFC 8216 — IETF, 2017-08. Defines the EXT-X-KEY and EXT-X-SESSION-KEY tags (§4.3.2.4, §4.3.4.5): METHOD (AES-128, SAMPLE-AES), the key/license URI, IV, KEYFORMAT. This is the key-delivery mechanism in HLS; for FairPlay, KEYFORMAT="com.apple.streamingkeydelivery" with a skd:// URI. Tier 1 (official standard). https://www.rfc-editor.org/rfc/rfc8216 (accessed 2026-06-17)
  4. DASH-IF: Content Protection Information Exchange Format (CPIX) — DASH Industry Forum. Defines the schema for exchanging keys and rights between the DRM system / key store and the packager (multi-key, multi-DRM), including ContentKeyPeriod for key rotation in live. The container basis for the encryption side's "fetch the key from the store." Tier 2 (issuing-body guideline). https://dashif.org/CPIX/ (accessed 2026-06-17)
  5. Secure Packager and Encoder Key Exchange (SPEKE) API Specification — Amazon Web Services. A CPIX-based key-exchange protocol between the packager/encoder and the key/DRM server, with authentication and a request format; shows how the packager obtains the content key from the store in practice. Tier 4 (vendor engineering). https://docs.aws.amazon.com/speke/latest/documentation/what-is-speke.html (accessed 2026-06-17)
  6. FairPlay Streaming (Overview + Programming Guide / FPS SDK) — Apple. First-party source for the FairPlay dialect: the device creates an SPC (Server Playback Context), the KSM (Key Security Module) on the server checks the certificate hash against the Application Certificate, pulls the content key, and returns a CKC (Content Key Context). Tier 3 (first-party vendor). https://developer.apple.com/streaming/fps/ (accessed 2026-06-17)
  7. Widevine DRM — Overview, Provisioning and License Server — Google. First-party source for the Widevine dialect: the CDM assembles a device-signed license request; the provisioning server issues the device certificate; the keybox is the root of trust; on L1, keys and decrypted frames never leave the hardware TEE. Tier 3 (first-party vendor). https://developers.google.com/widevine/drm/overview (accessed 2026-06-17)
  8. PlayReady License Acquisition / License Server — Microsoft Learn. First-party source for the PlayReady dialect: the client sends a license challenge, the server generates a content key for the KID and returns a license bound to the device; each client has unique proof for authentication. Tier 3 (first-party vendor). https://learn.microsoft.com/en-us/playready/ (accessed 2026-06-17)
  9. DRM Best Practices / DRM Beyond the Player (License Server's Perspective) — Axinom. Engineering first-party source on the license proxy and entitlement service: the proxy "stands in as the license server" for the player, checks rights on the server, mints a fresh short-lived token, and forwards the request; the proxy ↔ license-server communication key "cannot be included in the client." Tier 4 (vendor engineering). https://docs.axinom.com/blog/drm-beyond-the-player (accessed 2026-06-17)
  10. Understanding key rotation behavior — AWS Elemental MediaPackage — Amazon Web Services. Engineering first-party source on key rotation in live: changing the content key by interval, delivering current/future keys, and reducing how often devices request a license. Confirms hierarchical delivery as the way to take scale. Tier 4 (vendor engineering). https://docs.aws.amazon.com/mediapackage/latest/ug/drm-content-key-rotation.html (accessed 2026-06-17)

Source note (per §4.3.2): the license-acquisition flow, the session types, and the CDM role trace to the tier-1 primary source — W3C EME (ref. 1); key identification via default_KID to ISO/IEC 23001-7:2023 (ref. 2); HLS key delivery (EXT-X-KEY, skd://) to IETF RFC 8216 (ref. 3). The encryption-side key exchange is tier-2 CPIX from DASH-IF (ref. 4) and its SPEKE implementation (ref. 5). The three systems' dialects are first-party vendor sources: FairPlay SPC/CKC/KSM (ref. 6), Widevine (ref. 7), PlayReady (ref. 8). The license-proxy pattern and the "communication key not in the client" rule are from Axinom engineering (ref. 9); key rotation and hierarchical delivery from AWS (ref. 10). Where popular guides advise checking rights on the client for speed, the article follows the server-side proxy pattern and flags the bypass risk.