EZDRM Cloudformation Template JSON & Setup

EZDRM Cloudformation Template JSON & Setup

Here is a Cloudformation template that can be used with AWS Cloudformation service to create a SPEKE server and also setup the licence server with EZDRM.

You need to create a key_server.py file with this contents.

import base64

from key_server_common import ClientResponseBuilder, ServerResponseBuilder


def server_handler(event, context):
	host = event['headers']['Host']
	stage = event['requestContext']['stage']
	client_url_prefix = "https://%s/%s" % (host, stage)
	body = event['body']
	if event['isBase64Encoded']:
		body = base64.b64decode(body)
	r = ServerResponseBuilder(client_url_prefix, body)
	return r.get_response()

def client_handler(event, context):
	content_id = event["pathParameters"]["content_id"]
	kid = event["pathParameters"]["kid"]
	r = ClientResponseBuilder(content_id, kid)
	return r.get_response()

And a key_server_common.py file with the contents.

import base64
import hashlib
import xml.etree.ElementTree as ET
import xml.etree.ElementTree as EM
import urllib.request



# The official system ids are documented here:-
# http://dashif.org/identifiers/protection/
HLS_AES_128_SYSTEM_ID = '81376844-f976-481e-a84e-cc25d39b0b33'		# this is not an official system id
HLS_SAMPLE_AES_SYSTEM_ID = '94ce86fb-07ff-4f43-adb8-93d2fa968ca2'
DASH_CENC_SYSTEM_ID = 'edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'
PLAYREADY_SYSTEM_ID = '9a04f079-9840-4286-ab92-e65be0885f95'

WIDEVINE_PSSH_BOX = "AAAAanBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAEoIARIQeSIcblaNbb7Dji6sAtKZzRoNd2lkZXZpbmVfdGVzdCIfa2V5LWlkOmVTSWNibGFOYmI3RGppNnNBdEtaelE9PSoCU0QyAA=="

# Generated from Microsoft's PlayReady Test Server
# http://playready.directtaps.net
PLAYREADY_PROTECTION_HEADER = 'CgMAAAEAAQAAAzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4ATwBXAGoAaAB0AHIAMwB1ADkAawArAHIAZABvADEASQBMAFkAMAByAGEAdwA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBCADMAQQA2AEEAMwB4AG0AdABkAEkAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAOgAvAC8AcABsAGEAeQByAGUAYQBkAHkALgBkAGkAcgBlAGMAdAB0AGEAcABzAC4AbgBlAHQALwBwAHIALwBzAHYAYwAvAHIAaQBnAGgAdABzAG0AYQBuAGEAZwBlAHIALgBhAHMAbQB4AD8AUABsAGEAeQBSAGkAZwBoAHQAPQAxACYAYQBtAHAAOwBhAG0AcAA7AGEAbQBwADsAVQBzAGUAUwBpAG0AcABsAGUATgBvAG4AUABlAHIAcwBpAHMAdABlAG4AdABMAGkAYwBlAG4AcwBlAD0AMQA8AC8ATABBAF8AVQBSAEwAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA=='
PLAYREADY_PSSH_BOX = 'AAADMHBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAxAKAwAAAQABAAADPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgBPAFcAagBoAHQAcgAzAHUAOQBrACsAcgBkAG8AMQBJAEwAWQAwAHIAYQB3AD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEIAMwBBADYAQQAzAHgAbQB0AGQASQA9ADwALwBDAEgARQBDAEsAUwBVAE0APgA8AEwAQQBfAFUAUgBMAD4AaAB0AHQAcAA6AC8ALwBwAGwAYQB5AHIAZQBhAGQAeQAuAGQAaQByAGUAYwB0AHQAYQBwAHMALgBuAGUAdAAvAHAAcgAvAHMAdgBjAC8AcgBpAGcAaAB0AHMAbQBhAG4AYQBnAGUAcgAuAGEAcwBtAHgAPwBQAGwAYQB5AFIAaQBnAGgAdAA9ADEAJgBhAG0AcAA7AGEAbQBwADsAYQBtAHAAOwBVAHMAZQBTAGkAbQBwAGwAZQBOAG8AbgBQAGUAcgBzAGkAcwB0AGUAbgB0AEwAaQBjAGUAbgBzAGUAPQAxADwALwBMAEEAXwBVAFIATAA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A'
PLAYREADY_CONTENT_KEY = 'p3dWaHARtL97MpT7TE916w=='

KEY_STRING = 'uFTnkSlVE6K5yf3c'

def get_client_url(client_url_prefix, content_id, kid):
	return "%s/client/%s/%s" % (client_url_prefix, content_id, kid)


def get_digest(*args):
	m = hashlib.md5()
	for arg in args:
		m.update(arg.encode('utf-8'))
	return m.digest()



class ServerResponseBuilder:
	def __init__(self, client_url_prefix, request_body):
		self.error_message = ""
		self.client_url_prefix = client_url_prefix

		ET.register_namespace("cpix", "urn:dashif:org:cpix")
		ET.register_namespace("pskc", "urn:ietf:params:xml:ns:keyprov:pskc")
		ET.register_namespace("speke", "urn:aws:amazon:com:speke")
		self.root = ET.fromstring(request_body)

	def fill_request(self):
		content_id = self.root.get("id")
		use_playready_content_key = False
		for drm_system in self.root.findall("./{urn:dashif:org:cpix}DRMSystemList/{urn:dashif:org:cpix}DRMSystem"):
			system_id = drm_system.get("systemId")
			if system_id.lower() == HLS_SAMPLE_AES_SYSTEM_ID:
				mst = "1"
			else:
				mst = "0"
	
		for content_key in self.root.findall("./{urn:dashif:org:cpix}ContentKeyList/{urn:dashif:org:cpix}ContentKey"):
			kid = content_key.get("kid")
			iv = content_key.get("explicitIV")
			data = ET.SubElement(content_key, "{urn:dashif:org:cpix}Data")
			secret = ET.SubElement(data, "{urn:ietf:params:xml:ns:keyprov:pskc}Secret")
			plain_value = ET.SubElement(secret, "{urn:ietf:params:xml:ns:keyprov:pskc}PlainValue")
			if use_playready_content_key:
				plain_value.text = PLAYREADY_CONTENT_KEY
			else:
				plain_value.text = base64.b64encode(get_digest(KEY_STRING, content_id, kid)).decode('utf-8')
			#if iv is None and system_ids.get(HLS_SAMPLE_AES_SYSTEM_ID, False) == kid:
			content_key.set('explicitIV', base64.b64encode(get_digest(KEY_STRING, content_id, kid)).decode('utf-8'))
		with urllib.request.urlopen('http://cpix.ezdrm.com/aws.aspx?m=' + mst + '&k=' + kid + '&u=[YOUR EZDRM EMAIL]&p=[YOUR EZDRM PASSWORD]&c=' + content_id) as response:
			html = response.read()
			EM.register_namespace("cpix", "urn:dashif:org:cpix")
			EM.register_namespace("pskc", "urn:ietf:params:xml:ns:keyprov:pskc")
			self.moot = EM.fromstring(html)
	def get_response(self):
		self.fill_request()
		if self.error_message:
			return {"isBase64Encoded": False, "statusCode": 500, "headers": {"Content-Type": "text/plain"}, "body": self.error_message}
		return {"isBase64Encoded": False, "statusCode": 200, "headers": {"Content-Type": "application/xml", "Speke-User-Agent": "AWSElementalMockKeyServer"}, "body": ET.tostring(self.moot).decode('utf-8')}


class ClientResponseBuilder:
	def __init__(self, content_id, kid):
		self.error_message = ""
		self.content_id = content_id
		self.kid = kid

	def get_response(self):
		key = get_digest(KEY_STRING, self.content_id, self.kid)
		key_base64 = base64.b64encode(key).decode('utf-8')
		if self.error_message:
			return {"isBase64Encoded": False, "statusCode": 500, "headers": {"Content-Type": "text/plain"}, "body": self.error_message}
		return {"isBase64Encoded": True, "statusCode": 200, "headers": {"Content-Type": "application/octet-stream"}, "body": key_base64}

Make sure you change the values [YOUR EZDRM EMAIL] [YOUR EZDRM PASSWORD] with your EZDRM details.

You will need to zip this two files and call the zip key_server.zip it is important to keep the correct naming convetntions as the Cloudformation templates params reference them you will need to upload the key_server.zip to one of your buckets.

You can then use this json template.

{
	"Description" : "This is a sample template for lambda-backed custom resource. Runtime for Lambda function is python",
	"Parameters" : {
		"S3BucketName" : {
			"Type" : "String",
			"Default" : ""
		}
	},
	"Resources":{
		"EzDRMServerLambdaFunction" : {
			"Type" : "AWS::Lambda::Function",
			"Properties" : {
				"Handler" : "key_server.server_handler",
				"Code" : {
					"S3Bucket" : { "Ref" : "S3BucketName" },
					"S3Key" : "key_server.zip"
				},
				"Timeout" : "10",
				"MemorySize" : "256",
				"Role": { "Fn::GetAtt": ["LambdaExecutionRole", "Arn"]},
				"Runtime" : "python3.6"
			}
		},
		"EzDRMServerResource": {
			"Type": "AWS::ApiGateway::Resource",
			"Properties": {
				"RestApiId": {"Ref": "EzDRMRestApi"},
				"ParentId": {"Fn::GetAtt": ["EzDRMRestApi", "RootResourceId"]},
				"PathPart": "copyProtection"
			}
		},
		"EzDRMClientLambdaFunction" : {
			"Type" : "AWS::Lambda::Function",
			"Properties" : {
				"Handler" : "key_server.client_handler",
				"Code" : {
					"S3Bucket" : { "Ref" : "S3BucketName" },
					"S3Key" : "key_server.zip"
				},
				"Timeout" : "10",
				"MemorySize" : "256",
				"Role": { "Fn::GetAtt": ["LambdaExecutionRole", "Arn"]},
				"Runtime" : "python3.6"
			}
		},
		"EzDRMClientResource": {
			"Type": "AWS::ApiGateway::Resource",
			"Properties": {
				"RestApiId": {"Ref": "EzDRMRestApi"},
				"ParentId": {"Fn::GetAtt": ["EzDRMRestApi", "RootResourceId"]},
				"PathPart": "client"
			}
		},
		"EzDRMClientContentIdResource": {
			"Type": "AWS::ApiGateway::Resource",
			"Properties": {
				"RestApiId": {"Ref": "EzDRMRestApi"},
				"ParentId": {"Ref": "EzDRMClientResource"},
				"PathPart": "{content_id}"
			}
		},
		"EzDRMClientKidResource": {
			"Type": "AWS::ApiGateway::Resource",
			"Properties": {
				"RestApiId": {"Ref": "EzDRMRestApi"},
				"ParentId": {"Ref": "EzDRMClientContentIdResource"},
				"PathPart": "{kid}"
			}
		},
		"EzDRMRestApi": {
			"Type": "AWS::ApiGateway::RestApi",
			"Properties": {
				"Description": "EzDRM Server API",
				"Name": "EzDRMRestAPI"
			}
		},
		"EzDRMServerApiMethod" : {
			"Type" : "AWS::ApiGateway::Method",
			"Properties" : {
				"AuthorizationType" : "AWS_IAM",
				"HttpMethod" : "POST",
				"Integration": {
					"Type": "AWS_PROXY",
					"IntegrationHttpMethod": "POST",
					"Uri": {"Fn::Join": ["",
						["arn:aws:apigateway:", {"Ref": "AWS::Region"}, ":lambda:path/2015-03-31/functions/", {"Fn::GetAtt": ["EzDRMServerLambdaFunction", "Arn"]}, "/invocations"]
					]},
					"IntegrationResponses": [{
						"StatusCode": 200,
						"ResponseParameters": {
							"method.response.header.SpEzDRM-User-Agent": "integration.response.header.SpEzDRM-User-Agent"
						}
					}, {
						"StatusCode": 500,
						"SelectionPattern": "Error.*"
					}]
				},
				"ResourceId" : {"Ref": "EzDRMServerResource"},
				"RestApiId" : {"Ref": "EzDRMRestApi"},
				"MethodResponses": [{
					"StatusCode": 200,
					"ResponseParameters": {
						"method.response.header.SpEzDRM-User-Agent": false
					}
				},{
					"StatusCode": 500
				}]
			}
		},
		"EzDRMClientApiMethod" : {
			"Type" : "AWS::ApiGateway::Method",
			"Properties" : {
				"AuthorizationType" : "NONE",
				"HttpMethod" : "GET",
				"Integration": {
					"Type": "AWS_PROXY",
					"IntegrationHttpMethod": "POST",
					"Uri": {"Fn::Join": ["",
						["arn:aws:apigateway:", {"Ref": "AWS::Region"}, ":lambda:path/2015-03-31/functions/", {"Fn::GetAtt": ["EzDRMClientLambdaFunction", "Arn"]}, "/invocations"]
					]},
					"IntegrationResponses": [{
						"StatusCode": 200
					}, {
						"StatusCode": 500,
						"SelectionPattern": "Error.*"
					}]
				},
				"ResourceId" : {"Ref": "EzDRMClientKidResource"},
				"RestApiId" : {"Ref": "EzDRMRestApi"},
				"MethodResponses": [{
					"StatusCode": 200
				},{
					"StatusCode": 500
				}]
			}
		},
		"EzDRMApiStage": {
			"DependsOn": ["ApiGatewayAccount"],
			"Type": "AWS::ApiGateway::Stage",
			"Properties": {
				"DeploymentId": {"Ref": "EzDRMApiDeployment"},
				"MethodSettings": [{
					"DataTraceEnabled": true,
					"HttpMethod": "*",
					"LoggingLevel": "INFO",
					"ResourcePath": "/*"
				}],
				"RestApiId": {"Ref": "EzDRMRestApi"},
				"StageName": "LATEST"
			}
		},
		"EzDRMApiDeployment": {
			"Type": "AWS::ApiGateway::Deployment",
			"DependsOn": ["EzDRMServerApiMethod", "EzDRMClientApiMethod"],
			"Properties": {
				"RestApiId": {"Ref": "EzDRMRestApi"},
				"StageName": "EzDRMStage"
			}
		},
		"LambdaExecutionRole": {
			"Type": "AWS::IAM::Role",
			"Properties": {
				"AssumeRolePolicyDocument": {
					"Version": "2012-10-17",
					"Statement": [{
						"Effect": "Allow",
						"Principal": { "Service": ["lambda.amazonaws.com"] },
						"Action": ["sts:AssumeRole"]
					}]
				},
				"ManagedPolicyArns": ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]
			}
		},

		"EzDRMServerLambdaPermission": {
			"Type": "AWS::Lambda::Permission",
			"Properties": {
				"Action": "lambda:invokeFunction",
				"FunctionName": {"Fn::GetAtt": ["EzDRMServerLambdaFunction", "Arn"]},
				"Principal": "apigateway.amazonaws.com",
				"SourceArn": {"Fn::Join": ["", ["arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", {"Ref": "EzDRMRestApi"}, "/*"]]}
			}
		},
		"EzDRMClientLambdaPermission": {
			"Type": "AWS::Lambda::Permission",
			"Properties": {
				"Action": "lambda:invokeFunction",
				"FunctionName": {"Fn::GetAtt": ["EzDRMClientLambdaFunction", "Arn"]},
				"Principal": "apigateway.amazonaws.com",
				"SourceArn": {"Fn::Join": ["", ["arn:aws:execute-api:", {"Ref": "AWS::Region"}, ":", {"Ref": "AWS::AccountId"}, ":", {"Ref": "EzDRMRestApi"}, "/*"]]}
			}
		},

		"ApiGatewayCloudWatchLogsRole": {
			"Type": "AWS::IAM::Role",
			"Properties": {
				"AssumeRolePolicyDocument": {
					"Version": "2012-10-17",
					"Statement": [{
						"Effect": "Allow",
						"Principal": { "Service": ["apigateway.amazonaws.com"] },
						"Action": ["sts:AssumeRole"]
					}]
				},
				"Policies": [{
					"PolicyName": "ApiGatewayLogsPolicy",
					"PolicyDocument": {
						"Version": "2012-10-17",
						"Statement": [{
							"Effect": "Allow",
							"Action": [
								"logs:CreateLogGroup",
								"logs:CreateLogStream",
								"logs:DescribeLogGroups",
								"logs:DescribeLogStreams",
								"logs:PutLogEvents",
								"logs:GetLogEvents",
								"logs:FilterLogEvents"
							],
							"Resource": "*"
						}]
					}
				}]
			}
		},

		"ApiGatewayAccount": {
			"Type": "AWS::ApiGateway::Account",
			"Properties": {
				"CloudWatchRoleArn": {"Fn::GetAtt": ["ApiGatewayCloudWatchLogsRole", "Arn"] }
			}
		}
	},
	"Outputs": {
		"RootUrl": {
			"Description": "Root URL of the API gateway",
			"Value": {"Fn::Join": ["", ["https://", {"Ref": "EzDRMRestApi"}, ".execute-api.", {"Ref": "AWS::Region"}, ".amazonaws.com"]]}
		}
	}
}

When you run the template you will see this screen.

Enter the bucket name you uploaded to key_server.zip file to.

You can then encode your video through AWS or use the S3Bubble dashboard.

We use this within our themes to create hollywood level drm you can see a working example here https://wpott.tv/content/drm-example/

If you are using the S3Bubble themes you will need to add your details in the theme under the drm section.

Apple Fairplay

  • Media Url: https://000000000000.cloudfront.net/drm/master.m3u8
  • Keysystem: com.apple.fps.1_0
  • License Uri: https://fps.ezdrm.com/api/licenses/000000000000 // You need to ask ezdrm to store this for you
  • Certificate Uri: https://000000000000.cloudfront.net/fairplay.cer // You can store this in one of your buckets

For fairplay you must first request access at this link https://developer.apple.com/contact/fps/.

When your access is approved you will receive and email like this.

Thank you for your interest in FairPlay Streaming (FPS). Your request has been approved.

The FPS Deployment Package can be downloaded from the Downloads for Apple Developers page at: https://developer.apple.com/download/more/?=FPS

The FPS Deployment Package contains the D Function and specification along with instructions about how to generate the FairPlay Streaming Certificate and Application Secret key (ASk).

To avoid misplacing your ASk, FairPlay Streaming Certificate or private key, we recommend you don’t create your certificates until you have a Key Server Module (KSM) that has been fully tested using the verify_ckc tool and test vectors as instructed in the FairPlay Streaming Programing Guide.

The FPS Credentials that you will be generating consist of a private key, the ASk and the FPS Certificate. The private key is generated first in the process. If you are asked to provide a pass phrase for the private key, keep the pass phrase in a safe and secure place because if the pass phrase is forgotten the private key is essentially lost. You need to keep all of these items in a safe and secure place because if you loose one of them, your system will not work. If the ASk is compromised, you will no longer be able to protect your content with FairPlay Streaming. Please don’t loose or mishandle the FPS Credentials.

Many companies now have processes or systems for where and how company specific credentials like these are stored.

You must save a copy of the ASk and store it securely. If the ASk is compromised, you will no longer be able to protect your content with FairPlay Streaming. Only one (1) ASk is allocated to your team. The value will not be provided again and cannot be retrieved at a later time.

The ASk and FairPlay Streaming Certificate that will be generated are used together to secure the Content Key in the FPS protocol. Please refer to “FairPlay Streaming Programming Guide: Programming the Key Security Module” to understand how the ASk is used in your FPS implementation.

You will now need to follow the link and tutorials to sign your certs.

Widevine

  • Media Url: https://000000000000.cloudfront.net/drm/master.mpd // This needs to be in dash format
  • Keysystem: com.widevine.alpha
  • License Uri: https://widevine-dash.ezdrm.com/widevine-php/widevine-foreignkey.php?pX=000000 // You can find the pX id in your Widevine account https://www.ezdrm.com/html/Members/DRM/Google/widevine/my_widevine_account.asp it is the last 6 digits of your Widevine Profile ID

Playready

Leave a comment