Solution Design Considerations in Using PlayReady License Delivery Service of Azure Media Services


Microsoft Azure Media Services is a scalable and robust platform for building online video solutions. This has been demonstrated by a few solutions such as NBC Sports/Sochi Olympics solution built on Azure Media Services.

If a video solution is for premium contents, it is generally required by content providers that a desired level of DRM protection is used to protect premium contents. With 14 years of investment and experiences, Microsoft PlayReady is the trusted leader in DRM market and is often the DRM technology designated by content providers.

Azure Media Services has been adding PlayReady support over the years. In addition to providing PlayReady protection packaging in transcoding workflow, Azure Media Services has also provided PlayReady License Server as a Service, which is essentially PlayReady license server in Azure cloud.

In this blog, some PlayReady-based DRM design considerations and best practices is presented in the context of Azure Media Services License Delivery Service. The same design considerations apply to PlayReady DRM solutions in general.

It is assumed that readers have some background in PlayReady and DRM concept, especially PlayReady license server and the basic end-to-end DRM scenarios.

Logically, a PlayReady DRM subsystem can be divided into five components:

  1. Key generation and management
  2. PlayReady license server
  3. PlayReady packaging workflow for VOD or dynamic encryption for live
  4. Secure token service (STS) and its dependent systems such as customer billing and entitlement check.
  5. PlayReady client and player.

In a PlayReady DRM solution, it is critical to understand the flow of the following critical data:

  • Key ID
  • Key seed
  • Content key (encryption key)
  • Symmetric hash key
  • Token
  • Claim
  • PlayReady license

The five DRM components and the flow of key ID, key seed, content key, hash key, token and claims are described in the following diagram.




Note that in this diagram, we have excluded user authentication subsystem for simplicity since it does not matter what kind of identity provider your player application uses. For example, many player applications use the user's local cable service provider's subscriber login credential for authentication. Or online service providers may choose to use their own custom identity providers. What is critical is for a video player application to to obtain the security taken from the token issuer (STS) after authentication.

Next, we will discuss each component and how they work together in a DRM solution.


Key Generation and Management

In a DRM solution using PlayReady, it is always essential to have a consistent way to generate key ID and content key and manage them properly. This is more often handled by a Key Management System (KMS).

While key ID is almost always a GUID and can easily be generated, there are two approaches in creating content keys:

  1. Using key seed and the appropriate API to generate content key for a given key ID and key seed. In this case, all you need to maintain is key seeds and you can generate a content key for any given key ID with a key seed.
  2. Directly generate content key randomly with cryptographic strength using API without using a pre-specified key seed. In this case, you need to maintain all the mapping between key ID and content key.

Below is the C# code for generating key seed and content key using Azure Media Services .NET SDK.

Utility for generating cryptographically strong random bytes:


        public static byte[] GenerateCryptographicallyStrongRandomBytes(int length)


            byte[] bytes = new byte[length];

            //This type implements the IDisposable interface. When you have finished using the type, you should dispose of it either directly or indirectly. To dispose of the type directly, call its Dispose method in a try/catch block. To dispose of it indirectly, use a language construct such as using (in C#)

            using (var rng = new System.Security.Cryptography.RNGCryptoServiceProvider())




            return bytes;



Code for generating key seed:

        public static string GeneratePlayReadyKeySeed()


            byte[] bytes = GenerateCryptographicallyStrongRandomBytes(30); 

            return Convert.ToBase64String(bytes);


Code for generating PlayReady content key from a given key ID and key seed:

        public static string GetPlayReadyContentKeyFromKeyIdKeySeed(string keyIdString, string keySeedB64)


            Guid keyId = new Guid(keyIdString);

            byte[] keySeed = Convert.FromBase64String(keySeedB64);

            byte[] contentKey = CommonEncryption.GeneratePlayReadyContentKey(keySeed, keyId);

            string contentKeyB64 = Convert.ToBase64String(contentKey);

            return contentKeyB64;



Directly generating PlayReady content key without a specific key ID or key seed:

        public static string GeneratePlayReadyContentKey()


            byte[] bytes = GenerateCryptographicallyStrongRandomBytes(16);

            return Convert.ToBase64String(bytes);



Key ID, key seed and content key can be generated and used in multiple ways. For example, you can use a fixed/single key seed for all the assets of the same genre while a distinct key ID is used for each individual asset in this genre. The advantage of this approach is, with a fixed key seed, you can “compute” the content key for an encrypted asset as soon as its key ID is given. Or, the other extreme case, for each asset, randomly generate a key ID and a content key, without using key seed. In this case, to keep track of key ID and corresponding content key, you really need to maintain a mapping table between the two.

The following table provides a summary of the different approaches in generating key IDs and content keys.


In the above table,

  • Fixed: means a specific key, key seed or content key is reused more than once for encrypting/decrypting more than one assets.
  • Generated: for key ID, it means a newly generated unique GUID is used for each distinct asset. For key seed or content key, it means randomly generated content key or key seed with cryptographic strength. Each is used only once for a single asset.

While cases 1, 2, 3, 5, 6, and 7 are valid options, cases 4 and 8 are not allowed because a key ID can map to only a single content key. Case 2 is the most useful since you have better control. You can deduce content key for any key ID as long as you have key seed.

It is worthwhile to point out that Azure Media Services never needs or stores key seed which should be a secret customers owns to themselves.


PlayReady License Delivery Service

We know that setting up a PlayReady license server is not a trivial task. For example, the author has a blog on things that can go wrong when working on PlayReady license server.

The good news with Azure Media Services PlayReady License Delivery Service is that you no longer need to go through the process of securing PlayReady commercial licenses and setting up a PlayReady license server. You can use AMS .NET API to configure PlayReady License Delivery Service. Note: Make sure you are using AMS .NET API v or later.

In general, a PlayReady license contains the following three parts:

  1. (Encrypted) content key;
  2. Right(s), e.g. at least a play right, and
  3. (Optionally), restriction(s), such as Output Protection (OP).

An IContentKey has 1-to-1 mapping with its key ID, content key and License Acquisition URL (LA_URL). A set of IContentKeys can share the same IContentKeyAuthorizationPolicy. For example, for contents in a specific genre (say, kids movies), a single IContentKeyAuthoirzationPolicy is defined and all of its IContentKeys share the same IContentKeyAuthorizationPolicy. And of course all contents within this genre can share the same key seed.

An IContentKeyAuthorizationPolicy can have one or more IContentKeyAuthorizationPolicyOption, each of which contains one or more ContentKeyAuthorizationPolicyRestriction. Each ContentKeyAuthorizationPolicyRestriction contains a license restriction.

These relationships can be best described by the following diagram.


ContentKeyAuthorizationPolicyRestriction.KeyRestrictionType can have 3 different values:

  1. ContentKeyRestrictionType.Open
  2. ContentKeyRestrictionType.IPRestricted
  3. ContentKeyRestrictionType.TokenRestricted

While the first two are pretty simple, the 3rd one is the more practical case, so we are going to focus on the 3rd case.

Naturally, after configuring license delivery services for some PlayReady protected assets, we would like to map out all the content keys (IcontentKey), their corresponding policy (IcontentKeyAuthorizationPolicy), policy options (IcontentKeyAuthorizationPolicyOption) and restrictions (ContentKeyAuthorizationPolicyRestriction), as well as any assets (IAsset) protected by each content key.

The following code does exactly this.

public static void ListContentKeys(CloudMediaContext objCloudMediaContext)


            string FORMAT1 = "\t{0,-25} = {1,-60}";

            string FORMAT2 = "\t\t{0,-25} = {1,-60}";

            string FORMAT3 = "\t\t\t{0,-25} = {1,-60}";

            string CLASS = "***[CLASS]***";

            IList<IAsset> objIList_IAsset;

            string url;


            //get content key-Assets dictionary

            Dictionary<string, IList<IAsset>> objDictionary = CollectKeyIdAssets(objCloudMediaContext);


            foreach (IContentKey objIContentKey in objCloudMediaContext.ContentKeys.OrderByDescending(k => k.LastModified)


                switch (objIContentKey.ContentKeyType)


                    case ContentKeyType.CommonEncryption:

                        Console.WriteLine("{0} IContentKey:", CLASS);

                        Console.WriteLine(FORMAT1, "Id", objIContentKey.Id);                                                                                                             //key ID with WAMS prefix: nb:kid:UUID:{KID-GUID}

                        Console.WriteLine(FORMAT1, "AuthorizationPolicyId", objIContentKey.AuthorizationPolicyId);                                               //AuthNPolicyID

                        Console.WriteLine(FORMAT1, "Name", objIContentKey.Name);                                                                                               //key name

                        Console.WriteLine(FORMAT1, "ProtectionKeyId", objIContentKey.ProtectionKeyId);                                                               //X.509 cert thumbprint used to encrypt for storage

                        Console.WriteLine(FORMAT1, "ProtectionKeyType", objIContentKey.ProtectionKeyType.ToString());

                        Console.WriteLine(FORMAT1, "GetClearKeyValue()", Convert.ToBase64String(objIContentKey.GetClearKeyValue()));        //content key

                        Console.WriteLine(FORMAT1, "ContentKeyType", objIContentKey.ContentKeyType);                                                             //CommonEncryption (for PlayReady protection)

                        Console.WriteLine(FORMAT1, "Created", objIContentKey.Created.ToString("MM-dd-yyyy HH:mm:ss"));

                        Console.WriteLine(FORMAT1, "LastModified", objIContentKey.LastModified.ToString("MM-dd-yyyy HH:mm:ss"));

                        Console.WriteLine(FORMAT1, "GetkeyDeliveryUrl()", objIContentKey.GetKeyDeliveryUrl(ContentKeyDeliveryType.PlayReadyLicense).OriginalString);  //LAURL

                        //ContentKeyAuthorizationPolicy, ContentKeyAuthorizationPolicyOption, ContentKeyAuthorizationOptionRestriction

                        var policies = objCloudMediaContext.ContentKeyAuthorizationPolicies.Where(p => p.Id == objIContentKey.AuthorizationPolicyId);


                        foreach (var policy in policies)


                            Console.WriteLine("{0} IContentKeyAuthorizationPolicy:", CLASS);

                            Console.WriteLine(FORMAT1, "Id", policy.Id);

                            Console.WriteLine(FORMAT1, "Name", policy.Name);

                            IList<IContentKeyAuthorizationPolicyOption> objIList_option = policy.Options;

                            foreach(var option in objIList_option)


                                Console.WriteLine(FORMAT1, "Options", string.Format("{0} IContentKeyAuthorizationPolicyOption:", CLASS));

                                Console.WriteLine(FORMAT2, "Name", option.Name);

                                Console.WriteLine(FORMAT2, "KeyDeliveryConfiguration", FormatXmlString(option.KeyDeliveryConfiguration));

                                Console.WriteLine(FORMAT2, "KeyDeliveryType", option.KeyDeliveryType);

                                List<ContentKeyAuthorizationPolicyRestriction> objList_restriction = option.Restrictions;

                                foreach (var restriction in objList_restriction)


                                    //KeyRestrictionType: Open, IPRestricted, TokenRestricted

                                    Console.WriteLine(FORMAT2, "Restrictions",  string.Format("{0} ContentKeyAuthorizationPolicyRestriction:", CLASS));

                                    Console.WriteLine(FORMAT3, "Name", restriction.Name);

                                    Console.WriteLine(FORMAT3, "KeyRestrictionType", restriction.KeyRestrictionType);

                                    Console.WriteLine(FORMAT3, "Requirements", FormatXmlString(restriction.Requirements));





                        //list IAssets with this IContentKey

                        if (objDictionary.ContainsKey(objIContentKey.Id))    //for failed job, a content key may not map to an existing IAsset


                            objIList_IAsset = objDictionary[objIContentKey.Id];

                            foreach (IAsset objIAsset in objIList_IAsset)


                                url = GetOrignUrl(objIAsset, objCloudMediaContext);

                                Console.WriteLine("{0} IAsset:", CLASS);

                                Console.WriteLine(FORMAT1, "Id", objIAsset.Id);

                                Console.WriteLine(FORMAT1, "Name", objIAsset.Name);

                                Console.WriteLine(FORMAT1, "LastModified",  objIAsset.LastModified.ToString("MM/dd/yyyy"));

                                Console.WriteLine(FORMAT1, "OriginUrl", url);





                            Console.WriteLine("ContentKey: {0} does not have any associated IAsset", objIContentKey.Id);










        //Collect all key ID's and their corresponding IAssets and put in a dictionary for lookup

        public static Dictionary<string, IList<IAsset>> CollectKeyIdAssets(CloudMediaContext objCloudMediaContext)


            Dictionary<string, IList<IAsset>> objDictionary = new Dictionary<string, IList<IAsset>>();

            IList<IAsset> objIList_IAsset;

            IList<IContentKey> objIList_IContentKey;

            var assets = objCloudMediaContext.Assets.ToList().OrderByDescending(a => a.LastModified);      //sort by date

            foreach (IAsset objIAsset in assets)


                objIList_IContentKey = objIAsset.ContentKeys;

                if (objIList_IContentKey.Count > 0)


                    foreach (IContentKey objIContentKey in objIList_IContentKey)


                        if (!objDictionary.ContainsKey(objIContentKey.Id))


                            objIList_IAsset = new List<IAsset>();

                           objDictionary.Add(objIContentKey.Id, objIList_IAsset);


                        objIList_IAsset = objDictionary[objIContentKey.Id];





            return objDictionary;



         private static string FormatXmlString(string xmlString)


            if (string.IsNullOrEmpty(xmlString))


                return xmlString;




                System.Xml.Linq.XElement element = System.Xml.Linq.XElement.Parse(xmlString);

                return element.ToString();



        public static string GetOrignUrl(IAsset objIAsset, CloudMediaContext objCloudMediaContext)


            ILocator objILocator = null;

            string url = string.Empty;


            var assetLocators = objIAsset.Locators.ToList();

            foreach (var assetLocator in assetLocators)


                //It must be origin

                if (assetLocator.Type == LocatorType.OnDemandOrigin)


                    //It must not expire for the next 5 minutes:

                    if (assetLocator.ExpirationDateTime > DateTime.UtcNow.AddMinutes(5))


                        //Use it:

                        objILocator = assetLocator;






            if (objILocator != null)


                var theManifest = from f in objILocator.Asset.AssetFiles where f.Name.EndsWith(".ism") select f;

                //Cast the reference to a true IAssetFile type.

                IAssetFile objIAssetFile = theManifest.First();

                url = string.Format("{0}{1}/manifest", objILocator.Path, objIAssetFile.Name);


            return url;



Below is the portion of the output of the above code for a single IContentKey:

***[CLASS]*** IContentKey:

        Id                                = nb:kid:UUID:42b3ddc1-2b93-4813-b50b-af1ec3c9c771

        AuthorizationPolicyId     = nb:ckpid:UUID:d7d1dfa4-1374-4d52-81cb-e73a1c35b759

        Name                           = KID_42b3ddc1-2b93-4813-b50b-af1ec3c9c771

        ProtectionKeyId             = 7D9BB04D9D0A4A24800CADBFEF232689E048F69C

        ProtectionKeyType         = X509CertificateThumbprint

        GetClearKeyValue()       = 6TomBxJMgINoV1HID1WBng==

        ContentKeyType            = CommonEncryption

        Created                        = 07-25-2014 20:34:18

        LastModified                 = 07-25-2014 20:34:20

        GetkeyDeliveryUrl()      =

***[CLASS]*** IContentKeyAuthorizationPolicy:

        Id                                =  nb:ckpid:UUID:d7d1dfa4-1374-4d52-81cb-e73a1c35b759

        Name                           = Deliver Common Content Key with no restrictions

        Options                        = ***[CLASS]*** IContentKeyAuthorizationPolicyOption:

                Name                              = My Policy Option

                KeyDeliveryConfiguration  = <PlayReadyLicenseResponseTemplate xmlns:i="" 




                                                                          <ContentKey i:type="ContentEncryptionKeyFromHeader" />





                                                               <ResponseCustomData>WAMS-SecureKeyDelivery, Time = 07-25-2014 20:34:19</ResponseCustomData>


                KeyDeliveryType               = PlayReadyLicense

                Restrictions                      = ***[CLASS]*** ContentKeyAuthorizationPolicyRestriction:

                        Name                           = TokenRestricted

                        KeyRestrictionType        = 1

                        Requirements               = <TokenRestriction issuer="" audience="urn:test">


                                                                          <VerificationKey type="Symmetric"

                                                                                                  IsPrimary="true" />

                                                                         <VerificationKey type="Symmetric"


                                                                                                  IsPrimary="false" />



                                                                               <Claim type="urn:microsoft:azure:mediaservices:contentkeyidentifier" />



***[CLASS]*** IAsset:

        Id                              = nb:cid:UUID:96cb5ac1-d233-4cf9-832f-3c174e680957

        Name                        = PlayReady Protected Smooth Asset

        LastModified              = 07/25/2014

        OriginUrl                   =


Secure Token Service (STS) and Symmetric Hash Key

The token generated for a player consists of a clear-text token string and its Hash-based Message Authentication Code (HMAC) hash by using the SHA256 hash function. The key (a byte array) used for computing the hash is of length 32 bytes and is called symmetric key since both signing and verification use the same key.

The symmetric key is used by both PlayReady License Delivery Service for authorization verification and STS to generate a token. For a given specific key ID, and a chosen symmetric key,

  • In PlayReady License Delivery Service, the symmetric key is used to generate the Requirements property of ContentKeyAuthorizationPolicyRestriction in a IContentKeyAuthorizationPolicyOption which belongs to a  IContentKeyAuthorizationPolicy. The same symmetric key will also be used for verification of the token submitted by a player in its PlayReady license request.
  • In Secure Token Service, when we generate the hash of a clear text token, we need to use a symmetric key to initialize an instance of System.Security.Cryptography.HMACSHA256 class.
  • Based on the key ID in client manifest, a player (PlayReady client) will request the corresponding token from STS. The STS generates a token based on the given key ID and the symmetric key and provides the token to the player so that the player can submit the token with its PlayReady license request for a PlayReady protected content with a specific key ID. The token is then used by PlayReady License Delivery Service for authorization check before issuing a PlayReady license.

This flow is described in the lower half of the first figure.

The code for generating a token (HMAC hash) from a symmetric key and clear text token string.

        /// <summary>

        /// Compute HMAC hash using SHA256 function

        /// </summary>

        /// <param name="symmetricKey">Base64 string of byte[32], generated by RNGCryptoServiceProvider</param>

        /// <param name="unsignedToken">Clear-text string to be hashed</param>

        /// <returns></returns>

        public static string Hash(string symmetricKey, string unsignedToken)


            string signature = string.Empty;


            byte[] symmetricKeyData = Convert.FromBase64String(symmetricKey);

            using (var signatureAlgorithm = new System.Security.Cryptography.HMACSHA256(symmetricKeyData))


                signature = System.Web.HttpUtility.UrlEncode(Convert.ToBase64String(signatureAlgorithm.ComputeHash(Encoding.ASCII.GetBytes(unsignedToken))));


            return signature;



Utility for generating symmetric hash keys:

        public static string GenerateSymmetricHashKey()


            byte[] bytes = GenerateCryptographicallyStrongRandomBytes(32); 

            return Convert.ToBase64String(bytes);



PlayReady Packaging of Content with Given Key ID and Content Key

Once you are provided a key ID and corresponding content key, regardless whether key seed is used, how and where you encrypt (PlayReady package) a smooth streaming or DASH asset is independent from PlayReady License Delivery Service. The same applies to where you host your assets: As PlayReady license server can be in a totally different domain from origin, media origin server can be hosted anywhere, Azure Media Services or others. In other words, PlayReady License Delivery Service is physically separate from media storage and origin.

Microsoft Azure Media Services provides a rich set of transcoding workflow capability inside Azure. In particular, you can find how to create and submit PlayReady protection task info from Protecting Smooth Streaming and MPEG DASH with PlayReady.

In case you choose on-your-premise transcoding approach using tools like IIS Transform Manager, details can be found in the author’s blog: How to add PlayReady protection in a Transform Manager Job Template?

For a given key ID-content key pair, you need to update the PlayReady protection task configuration XML file (programmatically) with the key ID and content key. You can find sample code for this in the following method at the MSDN article: Protecting Smooth Streaming and MPEG DASH with PlayReady.

public static void UpdatePlayReadyConfigurationXMLFile(string keyDeliveryServiceUriStr)


PlayReady Client and License Acquisition

There are various PlayReady clients for corresponding client platforms:

To play a PlayReady protected video, a player application (PlayReady client) needs to go through the following flow before video
decryption can begin:

  1. Get authenticated;
  2. Perform entitlement check;
  3. Request client manifest from origin/CDN;
  4. Retrieve key ID from protection header in client manifest;
  5. Retrieve token from Secure Token Service (STS) using the key ID;
  6. Request PlayReady license, by submitting the token.

In the license acquisition request inside player, you have to request token first and include the token in a HTTP header named “Authorization”, as shown below:

objHttpWebRequest.Headers["Authorization"] = token;

where token is the Hash-based Message Authentication Code (HMAC) by using the SHA256 hash function as below


The symmetrickey value is the same one as defined in ContentKeyAuthorizationPolicyRestriction.Requirements in an IContentKeyAuthorizationPolicyOption:

<TokenRestriction issuer='' audience = 'urn:test' >


      <VerificationKey type='Symmetric' value='IRPQMJ006zlzV/Y1gbyoKJPKwLGOCAO7M5/17gfh4XU=' IsPrimary='true' />

      <VerificationKey type='Symmetric' value='WRPQMJ006zlzV/Y1gbyoKJPKwLGOCAO7M5/17gfh4XU=' IsPrimary='false' />



      <Claim type='urn:microsoft:azure:mediaservices:contentkeyidentifier' />




In this blog, we have defined the five building blocks of a PlayReady DRM solution in the context of PlayReady License Delivery Service in Microsoft Azure Media Services. We discussed the design considerations and some implementation aspects of each building block.

A DRM solution includes the following tasks:

  1. Build a Secure Token Service (STS) which generates HMAC hashed token for a given key ID. STS also provides symmetric key to the application configuring PlayReady License Delivery Service.
  2. Prepare key ID and content key;
  3. Configure License Delivery Service for given key ID-content key pair and a symmetric key.
  4. Content protection: Once you have the key ID-content key pair, configuring license delivery service and packaging content for PlayReady protection are completely separate.
    • Update protection task preset
    • Run PlayReady protection job.
  5. In the license acquisition request inside player, you have to request token first and include the token in a HTTP header named “Authorization”.



Comments (1)

  1. Yavuz says:

    Hi William, thanks for the article.

    I want to learn deep how a playready client (ie a silverlight application decrypting encryped lie channel) protect the keys? When we capture the traffic we can see the license tag in fiddler, is it possible to find the key from that string? Or how silverlight or playready prevent such operations?

    Another point, is it possible to find AES key in memory dump with a programme suc as aeskeyfind? Againg if not how silverlight or playready client protect this key?


Skip to main content