Skip to main content

Implementing DRM Key Rotation with AWS MediaLive and Axinom Key Service

Overview​

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.

Architecture Summary​

Prerequisites​

  • 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​

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

# 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.