Skip to main content

Implementing DRM Key Rotation with AWS MediaLive and Axinom Key Service

Overview​

If you already protect on‑demand (VOD) assets with Axinom DRM you know the drill: the encoder (or packaging stage) fetches one/multiple content key for the whole movie, the Entitlement Message (EM) carries that single KID/multiple KIDs, the player asks the Axinom License Service once and happily streams to the end.

A 24×7 live channel has very different risk‑ and availability requirements. If the entire broadcast were protected by a single, static key, an attacker who extracts that key could instantly decrypt every segment already aired and keep harvesting new content continuously as the stream proceeds.

Key rotation narrows that threat window by rolling in a fresh key at fresh interval, and the player transparently requests the matching license for each new key ID. If a key ever leaks, only a handful of minutes of content are vulnerable and operators can revoke that single key without touching the rest of the stream.

 VOD vs Live (Key‑Rotation) at a Glance​

 Workflow stage VOD Live w/ Rotation 
Encoder → Key ServiceSingle key request per assetContinuous Key requests (one per rotation interval)
Content key IDs storedNot necessary (Mostly you get enough time to extract the KeyID from the packaged asset)Persist every new KID in a datastore (DynamoDB) since reading the manifest every interval is impossible.
EM / TokenOne/multiple KID per EMRolling window of latest N KIDs per EM
Player licence requestsOn manifest load or if the license expiresEvery time the manifest advertises a new KID
Key revocation impactWhole assetA few minutes of content

Architecture Summary​

Video On Demand Flow​

Video On Demand Flow

Key Rotation Flow​

Key Rotation Flow

How Key Rotation Changes the Classic VOD DRM Flow​

Encoder behaviour​

  • VOD: Encoder makes a single Key request once the packaging starts.

  • Live: MediaLive calls SPEKE every rotation interval. Your Lambda proxy must be stateless except for persisting kid/timestamp so that later components can look them up.

State tracking​

Store each kid with a creation timestamp in a DB:

# in lambda_function.py 
keyId = xmldoc.getElementsByTagName('cpix:ContentKey')[0].getAttribute('kid')
dynamodb.put_item(
TableName='KeyMappings',
Item={
'key_id': {'S': keyId},
'created_at':{'N': str(int(datetime.utcnow().timestamp()))}
})

For VOD it is not mandatory to use this step as it is even possible to get the KeyIDs from the manifest.

Entitlement & token issuance​

The EM Service now queries the DB, sorts by created_at, and injects the latest N KIDs at every fresh interval into the content_keys_source.inline array in the Entitlement message before signing.

Key Injection

In VOD you would contact the EM Service once or based on the License expiration.

Player license flow​

  • VOD: Shaka (or any EME‑capable player) requests one license when the manifest first loads.

  • Live: Every time the manifest shows a segment encrypted with a new KID, the player calls the EM for a fresh token and requests a fresh license from the License Service.

Prerequisites for this demo​

  • Axinom DRM Tenant
  • AWS account with permissions to create MediaLive, MediaPackage, Lambda, API Gateway, IAM users and DynamoDB resources.
  • Node.js + NPM installed locally
  • (Optional) IIS or any web server to host your player and the token generator.

Setup​

This document provides a step-by-step guide for customers to implement DRM key rotation for a live streaming setup using:

  • AWS MediaLive (for encoding)
  • AWS MediaPackage (for output)
  • Lambda (as a SPEKE proxy)
  • DynamoDB (for tracking key IDs)
  • Axinom Key Service (for DRM integration)
  • Axinom License Service (for License generation)
  • Shaka Player (for playback)

The goal is to simulate a production-ready setup that supports DRM key rotation, entitlements, and playback validation.

You can find the implementation of the below setup in our github repository.

Create a Lambda Function​

This Lambda function will accept CPIX requests from MediaLive, extract key_id, store it in DynamoDB, and forward to Axinom Key Service.

note

This demo uses SPEKE 1.0 (https://key-server-management.axprod.net/api/Speke) because it illustrates a single-key workflow to keep the example simple.

# lambda_function.py
import json
import requests
import boto3
from xml.dom import minidom
from datetime import datetime

# Initialize DynamoDB
dynamodb = boto3.client('dynamodb')

def lambda_handler(event, context):
requestBody = event['body']
requestBody = requestBody.lstrip() # remove leading whitespace

try:
xmldoc = minidom.parseString(requestBody)
keyId = xmldoc.getElementsByTagName('cpix:ContentKey')[0].getAttribute('kid')

# Store Key ID and timestamp in DynamoDB
dynamodb.put_item(
TableName='KeyMappings',
Item={
'key_id': {'S': keyId},
'created_at': {'N': str(int(datetime.utcnow().timestamp()))}
}
)
except Exception as e:
print("Error:", e)

# Forward to Axinom Key Service
headers = {
'Authorization': 'Basic <Base64(TenantID:ManagementKey)>',
'Content-Type': 'text/xml'
}
response = requests.post(
'https://key-server-management.axprod.net/api/Speke',
headers=headers,
data=requestBody
)

return {
"statusCode": response.status_code,
"body": response.text
}

Setup API Gateway​

note
  • Integration Type should be selected as Lambda function and then select the above created Lambda function.

In MediaLive, the API Gateway URL will be your SPEKE URL:

https://<api-id>.execute-api.<region>.amazonaws.com/<API-Gateway-Name>

Configure Media Live channel.​

  1. Before configuring MediaPackage, it is necessary to create an IAM role that allows MediaPackage to call the API Gateway. You can follow the steps mentioned here

  2. You need to create a media package channel with Key rotation enabled. You can follow the Media Package Setup. When you enable Package encryption, you will see a Additional configuration section. Expand it and set the desired interval for the key rotation.

  3. Now you can create the Media Live channel as mentioned here

Setup the DynamoDB.​

  • Open AWS DynamoDB console and create a new table.
  • Table Name: Key Mappings
  • Primary Key: key_id
  • Add attribute: created_at

Setup a IAM user to access the DynamoDB.​

  • Open the AWS IAM user console.
  • Click Create User.
  • Provide a name for the user. Click Next.
  • On the 'Set permissions' page, select Attach policies directly.
  • From the Permissions policies, select the AmazonDynamoDBReadOnlyAccess policy
  • Then click create user.

Build Entitlement Service & License Service Message Generator​

Retrieve keys from Dynamo DB​

The following code snippet extracts the keyIDs from the DynamoDB. In this snippet, you will have to provide credentials of the above created IAM User

const { DynamoDBClient, ScanCommand } = require("@aws-sdk/client-dynamodb");

const client = new DynamoDBClient({
region: "<Your Region>",
credentials: {
accessKeyId: "<Your Access Key ID>",
secretAccessKey: "<Your Access Key>"
} });
//Give credentials of the IAM user created to access the DynamoDB.

async function getKeyMappings() {
const command = new ScanCommand({ TableName: "<Table Name>" });
const result = await client.send(command);
return result.Items.map(item => ({
keyId: item.key_id.S,
createdat: item.createdat.N,
}));
}

Entitlement Service​

The following code snippet created the Entitlement message JSON with the latest 10 keyIDs. If you want all the keys, simply you can remove the key sorting.

function generateEntitlementMessage(keys, expirationTime, nowTime) {
let message = {
"type": "entitlement_message",
"version": 2,
"license": {
"start_datetime": nowTime,
"expiration_datetime": expirationTime,
},
"content_keys_source": {
"inline": [
]
},
}
const sortedKeys = keys.sort((a, b) => a.createdat - b.createdat);
// Take last 10 (latest)
const latest10Keys = sortedKeys.slice(-10);

// push it into the inline keys
latest10Keys.forEach(key => {
let inlineKey = {
id: key.keyId
};

message.content_keys_source.inline.push(inlineKey);
});

return message;
}

Token Generator​

Below snippet shows how the token generation happen by signing the Entitlement message created with the above function. To sign the EM, the function will need "Communication key" and the "Communication Key ID".

async function generateToken(validFrom, validTo) {
const entitlement = await getEntitlementForResource();

if (!entitlement) {
throw new Error("Entitlement not found");
}

if (!secrets.communicationKey) {
throw new Error("Missing or invalid 'communicationKey' in secrets.");
}

const envelope = {
version: 2,
com_key_id: secrets.communicationKeyId,
message: entitlement,
begin_date: validFrom.toISOString(),
expiration_date: validTo.toISOString(),
};

const communicationKeyAsBuffer = Buffer.from(secrets.communicationKey, "base64");
const token = jwt.sign(envelope, communicationKeyAsBuffer, {
algorithm: "HS256",
noTimestamp: true,
});

return token;
}

Set Up the Shaka Player​

You need to setup the Shaka player to request the license from the License server. Below you can find the sample code.

    var videoElement = document.getElementById('video');
const videoContainer = document.getElementById('videoContainer');
shaka.polyfill.installAll();

player = new shaka.Player(video);

const ui = new shaka.ui.Overlay(player, videoContainer, video);

const controls = ui.getControls();

const protection = {
drm: {
servers: {
'com.widevine.alpha': 'https://drm-widevine-licensing.axprod.net/AcquireLicense',
},
advanced: {
'com.microsoft.playready': {
audioRobustness: 'SW_SECURE_CRYPTO',
videoRobustness: 'SW_SECURE_CRYPTO',
},
},
},
}

player.configure(protection);

player.getNetworkingEngine().registerRequestFilter(async function (type, request) {
if (type === shaka.net.NetworkingEngine.RequestType.LICENSE) {
const token = await tokenGeneratorForPlayer();
request.headers['X-AxDRM-Message'] = token;
}
});
player.load('<Manifest URL from the media Package>')

How the Player Handles Token Refreshing​

When the player sees a new segment with a new key ID, the player will invoke the "tokenGeneratorForPlayer()" function. So the function will always call the token Generator service and pass the signed License Service Message to the player. Below is the "tokenGeneratorForPlayer()" function:

    async function tokenGenerator() {
const now = new Date();
const expires = new Date();
expires.setDate(now.getDate() + 2);

const response = await fetch('http://localhost:3000/token', { //Change the Token Generator URL as needed
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
validFrom: now.toISOString(),
validTo: expires.toISOString()
})
});

if (!response.ok) {
throw new Error('Failed to fetch license token');
}
const tokenResponse = await response.json();

return tokenResponse.token;
}

Testing​

  • Open the player and try the playback.
  • You will see the video plays without an interruption and in the console -> Network Tab, you will see frequent AcquiredLicense requests from the player according to the key rotation interval.