A self-contained Azure Media Service (AMS) end-to-end szenario with dynamic AES128 encryption

Microsoft Azure Media Services enables you to deliver Http-Live-Streaming (HLS) and Smooth Streams encrypted with AES128 (Advanced Encryption Standard using 128-bit encryption keys). To easily demonstrate a simple szenario for using AES123 encryption, I developed a self-contained solution that you can find in my GitHub repository. The use case is an intranet video portal to show training videos to employees. Depending on their role in a directory service (e.g. Active Directory) they can see different content. This demo can be started easily, without the need of setting up a directory service. Here you can see an overview of the architecture:

general_overview

 

I want to give a short description of what happens:

  1. An employee logs in to the CMS using corp credentials.
  2. The CMS will redirect the user to the ADFS (in this demo: a mockup using IdentityServer) for authentication.
  3. If the validation of the client credentials is successful, ADFS returns a bearer token and redirects the user back to the CMS.
  4. The user is now signed in to the intranet application. In this demo, this is a simple website showing the videos dedicated to this user. At this point, the video is encrypted.
  5. CMS requests a JWT Token from the Identity Provider (in this demo: a class in my CMS project (JWT Helper)). The Identity Provider checks the rights of the user and queries the database (in this demo: database mockup using a simple JSON file) for available videos for this user. A JWT Token is generated using the primary verification key of the video.
  6. JWT Token is returned.
  7. The video player shows the JWT token to Azure Media Key Services and gets the decryption key for the videos, if valid.
  8. The video fragments are decrypted dynamically in the browser.

A more detailed description of the components of the solution can be found in the GitHub Readme. In the following article, I will focus on how to do the encryption of the assets.

 

So, what do I have to do to encrypt my assets?

To encrypt an asset, you need to associate an content key with the asset, configure a authorization policy for the key and define a delivery policy for the asset:

  1. Content key
    Is used to dynamically encrypt your content using AES encryption when a stream is requested.
  2. Authorization Policy
    Or: how do you get the encryption key (from a service specified in the Delivery Policy), via a token or PlayReady?
    If token: TokenRestrictionTemplate
    - Authorization via  JWT or SWT (JSON Web Token or Simple Web Token)?
    - Who is the issuer of the token?
    - Who has access (audience)?
  3. Delivery Policy
    The Delivery Policy for an asset specifies how this asset will be delivered:
    - Key Acquisition URL (where can I get the encryption key for this asset? (AMS Key Services || PlayReady || Widevine)
    - Into which streaming protocol should your asset be packaged? (HLS, DASH, Smooth Streaming)
    - Should the asset be dynamically encrypted? And how? (envelope || common encryption)

 

In my demo, the AuthorizationPolicy is set to a token-based authorization. To get the encryption key, the player will request it from the Azure Media Services Key Delivery Service using a JWT Token:

flow

 

Got it! And how does it look like in code?  

 
private static async void SetupAESEncryptionAsync(CloudMediaContext context, IAsset encodedAsset,  string audience, Settings settings)
{
   //1.Create a content key and associate it with the encoded asset
   IContentKey key = CreateEnvelopeTypeContentKey(encodedAsset, context);

   //2.Configure the content keys authorization policy 
   string tokenTemplateString = await AddTokenRestrictedAuthorizationPolicy(context: context, contentKey: key, audience: audience, contentKeyIdentifierClaim: true,  issuer: settings.Issuer, primaryVerificationKey: settings.primaryVerificationKey);

   //3.Create Asset Delivery Policy (Dynamic or non-dynamic encryption)
   CreateAssetDeliveryPolicy(encodedAsset, key, context);
}

Let's step through this!

 

1.Create a content key and associate it with the encoded asset
You have to use ContentKeyType.EnvelopeEncryption in order to use AES-128 encryption.

[code highlight="13" language="csharp"]
static public IContentKey CreateEnvelopeTypeContentKey(IAsset asset, CloudMediaContext context)
{
//Check if there is already a content key associated with the asset
IContentKey contentKey = asset.ContentKeys.FirstOrDefault(k => k.ContentKeyType == ContentKeyType.EnvelopeEncryption);

// Create envelope encryption content key, Associate the key with the asset
if (contentKey == null)
{
contentKey = context.ContentKeys.Create(
keyId: Guid.NewGuid(),
contentKey: GetKeyBytes(16),
name: "ContentKey",
contentKeyType: ContentKeyType.EnvelopeEncryption);

asset.ContentKeys.Add(contentKey);
}

return contentKey;
}

2.Configure the Authorization Policy for the content key

ContentKeyDeliveryType.BaselineHttp specifies to use the AES key server from AMS.

[code highlight="6,7,23" language="csharp"]
public static async Task<string> AddTokenRestrictedAuthorizationPolicy(CloudMediaContext context, IContentKey contentKey, string issuer, string audience, bool contentKeyIdentifierClaim, byte[] primaryVerificationKey)
{
string tokenTemplateString = GenerateTokenRequirements(
issuer: issuer,
audience: audience,
contentKeyIdentifierClaim: contentKeyIdentifierClaim,
primaryVerificationKey: primaryVerificationKey);

IContentKeyAuthorizationPolicy policy = await context.ContentKeyAuthorizationPolicies.CreateAsync(name: "Token restricted authorization policy");
List<ContentKeyAuthorizationPolicyRestriction> restrictionList = new List<ContentKeyAuthorizationPolicyRestriction>
{
new ContentKeyAuthorizationPolicyRestriction
{
Name = "Token Authorization Policy",
KeyRestrictionType = (int)ContentKeyRestrictionType.TokenRestricted,
Requirements = tokenTemplateString
}
};

//You could have multiple options; BaselineHttp specifies that we use the AES key server from AMS
IContentKeyAuthorizationPolicyOption policyOption = context.ContentKeyAuthorizationPolicyOptions.CreateAsync(
name: "Token Authorization policy option",
deliveryType: ContentKeyDeliveryType.BaselineHttp,
restrictions: restrictionList,
keyDeliveryConfiguration: null).Result; // no key delivery data is needed for HLS

policy.Options.Add(policyOption);

// Add ContentKeyAutorizationPolicy to ContentKey
contentKey.AuthorizationPolicyId = policy.Id;
IContentKey updatedKey = await contentKey.UpdateAsync();

return tokenTemplateString;
}

3.Create Asset Delivery Policy (Dynamic or non-dynamic encryption)

[code highlight="3,14,15" language="csharp"]
static public async void CreateAssetDeliveryPolicy(IAsset asset, IContentKey key, CloudMediaContext context)
{
Uri keyAcquisitionUri = await key.GetKeyDeliveryUrlAsync(ContentKeyDeliveryType.BaselineHttp);

const string assetDeliveryPolicyName = "AssetDeliveryPolicy for HLS, SmoothStreaming and MPEG-DASH";
IAssetDeliveryPolicy assetDeliveryPolicy = context.AssetDeliveryPolicies
.Where(p => p.Name == assetDeliveryPolicyName)
.ToList().FirstOrDefault();

if (assetDeliveryPolicy == null)
{
assetDeliveryPolicy = await context.AssetDeliveryPolicies.CreateAsync(
name: assetDeliveryPolicyName,
policyType: AssetDeliveryPolicyType.DynamicEnvelopeEncryption,
deliveryProtocol: AssetDeliveryProtocol.SmoothStreaming | AssetDeliveryProtocol.HLS | AssetDeliveryProtocol.Dash,
configuration: new Dictionary<AssetDeliveryPolicyConfigurationKey, string> {{
AssetDeliveryPolicyConfigurationKey.EnvelopeBaseKeyAcquisitionUrl,
keyAcquisitionUri.AbsoluteUri }});

// Add AssetDelivery Policy to the asset
asset.DeliveryPolicies.Add(assetDeliveryPolicy);
}
}

In the TokenRestrictionTemplate you define three things:

  1. The type of the Token (JWT || SWT)
  2. The issuer of the Token. In my case the issuer is IdentityServer (ADFS-mockup in my solution), running locally ("https://localhost:5000/identity"). In a production scenario, this could be any IdentityProvider.
  3. The audience of the token - the people who are able to view the video. I defined two audiences in IdentityServer: staff and management. In a production scenario, this could be e.g. groups in your Active Directory.

 

[code highlight="3,11" language="csharp"]
static private string GenerateTokenRequirements(string issuer, string audience, bool contentKeyIdentifierClaim, byte[] primaryVerificationKey)
{
var template = new TokenRestrictionTemplate(TokenType.JWT)
{
PrimaryVerificationKey = new SymmetricVerificationKey(primaryVerificationKey),
Issuer = issuer,
Audience = audience
};

if (contentKeyIdentifierClaim)
template.RequiredClaims.Add(TokenClaim.ContentKeyIdentifierClaim);

//You can create a test token, useful to debug your final token if anything is not working as you want
string testToken = TokenRestrictionTemplateSerializer.GenerateTestToken(template);

return TokenRestrictionTemplateSerializer.Serialize(template);
}

You can specify if your token should include the ContentKeyIdentifierClaim. Here is a picture of a sample JWT token (grabbed using Fiddler), using https://jwt.io/ to take a closer look at the token of the Azure Media Player Sample with AES128 encryption sample with ContentKeyIdentifierClaim (urn:microsoft: ...):

 

jwt

 

 

The complete solution and code can be found on GitHub. Please reach out to me if you have any questions!
I'm also on Twitter: @blaujule.